This directory contains the Go runtime module (github.com/pthm/melange/melange) for performing permission checks against PostgreSQL. It has zero external dependencies (stdlib only), making it lightweight for production use.
go get github.com/pthm/melange/melangeThe melange client provides a Checker API for evaluating permissions against the authorization model and tuples stored in PostgreSQL. It works with *sql.DB, *sql.Tx, or *sql.Conn, allowing permission checks to participate in transactions and see uncommitted changes.
package main
import (
"context"
"database/sql"
"github.com/pthm/melange/melange"
_ "github.com/lib/pq"
)
func main() {
db, _ := sql.Open("postgres", "postgres://localhost/mydb")
checker := melange.NewChecker(db)
// Define subject and object
user := melange.Object{Type: "user", ID: "123"}
repo := melange.Object{Type: "repository", ID: "456"}
// Check permission
allowed, err := checker.Check(context.Background(), user, "can_read", repo)
if err != nil {
panic(err)
}
if !allowed {
// Handle unauthorized access
}
}Implement SubjectLike and ObjectLike interfaces on your domain types:
type User struct {
ID int64
Name string
}
func (u User) FGASubject() melange.Object {
return melange.Object{Type: "user", ID: fmt.Sprint(u.ID)}
}
type Repository struct {
ID int64
Name string
}
func (r Repository) FGAObject() melange.Object {
return melange.Object{Type: "repository", ID: fmt.Sprint(r.ID)}
}
// Use directly in checks
allowed, err := checker.Check(ctx, user, "can_read", repo)Enable caching to reduce database load for repeated checks:
cache := melange.NewCache(melange.WithTTL(time.Minute))
checker := melange.NewChecker(db, melange.WithCache(cache))
// First check hits the database
allowed, _ := checker.Check(ctx, user, "can_read", repo)
// Subsequent checks hit the cache
allowed, _ = checker.Check(ctx, user, "can_read", repo)Find all objects a subject can access:
// With pagination
ids, cursor, err := checker.ListObjects(ctx, user, "can_read", "repository",
melange.PageOptions{Limit: 100})
// Get next page
ids, cursor, err = checker.ListObjects(ctx, user, "can_read", "repository",
melange.PageOptions{Limit: 100, After: cursor})
// Or fetch all at once (auto-paginates)
allIDs, err := checker.ListObjectsAll(ctx, user, "can_read", "repository")Find all subjects that have access to an object:
// Who can read repository 456?
userIDs, cursor, err := checker.ListSubjects(ctx, repo, "can_read", "user",
melange.PageOptions{Limit: 100})
// Or fetch all
allUserIDs, err := checker.ListSubjectsAll(ctx, repo, "can_read", "user")Permission checks work within transactions and see uncommitted changes:
tx, _ := db.BeginTx(ctx, nil)
defer tx.Rollback()
// Insert new data
tx.ExecContext(ctx, `INSERT INTO org_members ...`)
// Checker on transaction sees the uncommitted row
checker := melange.NewChecker(tx)
allowed, _ := checker.Check(ctx, user, "member", org) // allowed == true
tx.Commit()Bypass database checks for testing or admin tools:
// Always allow (for admin tools or testing)
checker := melange.NewChecker(nil, melange.WithDecision(melange.DecisionAllow))
// Always deny (for testing unauthorized paths)
checker := melange.NewChecker(nil, melange.WithDecision(melange.DecisionDeny))
// Context-based overrides
checker := melange.NewChecker(db, melange.WithContextDecision())
ctx := melange.WithDecisionContext(ctx, melange.DecisionAllow)
allowed, _ := checker.Check(ctx, user, "can_read", repo) // always truePass temporary tuples that only affect a single check:
tuples := []melange.ContextualTuple{
{
Subject: melange.Object{Type: "user", ID: "123"},
Relation: "editor",
Object: melange.Object{Type: "document", ID: "456"},
},
}
allowed, err := checker.CheckWithContextualTuples(ctx, user, "can_edit", doc, tuples)allowed, err := checker.Check(ctx, user, "can_read", repo)
if err != nil {
if melange.IsNoTuplesTableErr(err) {
// melange_tuples view needs to be created
} else if melange.IsMissingFunctionErr(err) {
// Run 'melange migrate' to install SQL functions
}
return err
}Use Must for internal invariants where unauthorized access is a programmer error:
// Panics if check fails or errors
checker.Must(ctx, user, "can_write", repo)| File | Description |
|---|---|
melange.go |
Core types: Object, Relation, Querier, interfaces |
checker.go |
Checker API for permission checks and listing |
cache.go |
In-memory cache with optional TTL |
decision.go |
Decision overrides and context support |
errors.go |
Sentinel errors and error helpers |
validator.go |
Request validation interface |