Documentation
¶
Overview ¶
Package authz is a Zanzibar-style relationship-based authorization engine: relation tuples (object#relation@subject) evaluated against a fixed, non-user-editable namespace by a recursive Check. Tuples are written only out-of-band; the Store interface backs either in-memory (tests) or Postgres.
Index ¶
- Constants
- Variables
- func Migrate(db *gorm.DB) error
- func ServicePrincipalID(tenantID string) string
- type Engine
- type MemoryStore
- func (s *MemoryStore) Delete(_ context.Context, t Tuple) error
- func (s *MemoryStore) ReadByObjectRelation(_ context.Context, objType, objID, relation string) ([]Tuple, error)
- func (s *MemoryStore) ReadBySubject(_ context.Context, subjType, subjID string) ([]Tuple, error)
- func (s *MemoryStore) Write(_ context.Context, t Tuple) error
- type Namespace
- type PostgresStore
- func (s *PostgresStore) Delete(ctx context.Context, t Tuple) error
- func (s *PostgresStore) ReadByObjectRelation(ctx context.Context, objType, objID, relation string) ([]Tuple, error)
- func (s *PostgresStore) ReadBySubject(ctx context.Context, subjType, subjID string) ([]Tuple, error)
- func (s *PostgresStore) Write(ctx context.Context, t Tuple) error
- type RelationDef
- type RelationTuple
- type Rewrite
- type RewriteKind
- type Store
- type Tuple
- type TypeDef
Constants ¶
const ( TypeUser = "user" // humans + service principals (unified subject) TypeSystem = "system" // singleton root, object id == SystemObjectID TypeTenant = "tenant" // a memory-system workspace TypeDocument = "document" // a stored document )
Object types.
const ( RelAdmin = "admin" RelOwner = "owner" // personal-tenant owner: a manager (⊆ manager) without system-admin reach (⊄ admin) RelManager = "manager" RelMember = "member" RelViewer = "viewer" RelEditor = "editor" RelSystem = "system" // tenant -> system parent edge RelTenant = "tenant" // document -> tenant parent edge )
Relation names.
const ( // SystemObjectID is the singleton system object's id; global admins hold // system:memory#admin@user:<id>. SystemObjectID = "memory" // Wildcard is the subject id matching every subject of its type; a user:* // tuple grants the relation to any user (public access, e.g. common-pool read). Wildcard = "*" // ServicePrincipalPrefix is the subject-id prefix for a tenant's service // principal — the unified subject an API key resolves to with no explicit // subject_id. Full id: prefix + tenant UUID (see ServicePrincipalID). ServicePrincipalPrefix = "svc:" )
const DefaultMaxDepth = 16
DefaultMaxDepth caps Check recursion. The real graph is shallow (doc editor -> tenant member -> tenant admin -> system admin = depth 3); the cap is a misconfiguration backstop, the visited-set is the primary cycle guard.
Variables ¶
var ( // ErrDepthExceeded is returned (fail closed) when evaluation exceeds MaxDepth; // distinguishable so callers can tell a resource deny from a hard deny. ErrDepthExceeded = errors.New("authz: check depth limit exceeded") // ErrUnknownRelation is returned when a Check names a type/relation not in the // namespace (a caller bug), rather than silently allowing or denying. ErrUnknownRelation = errors.New("authz: unknown object type or relation") )
Functions ¶
func Migrate ¶
Migrate creates/updates the relation_tuples table and indexes; safe to call repeatedly.
func ServicePrincipalID ¶
ServicePrincipalID returns the service-principal subject id for a tenant: ServicePrincipalPrefix + tenant UUID. Single source of truth for the "svc:<tenant_id>" convention.
Types ¶
type Engine ¶
type Engine struct {
// MaxDepth is the recursion cap (NewEngine: DefaultMaxDepth; tests may lower it).
MaxDepth int
// contains filtered or unexported fields
}
Engine evaluates Zanzibar-style checks against a Store and fixed Namespace. Concurrency-safe if the Store is.
func (*Engine) Check ¶
func (e *Engine) Check(ctx context.Context, objType, objID, relation, subjType, subjID string) (bool, error)
Check reports whether subjType:subjID holds relation on objType:objID, resolving rewrite rules recursively. Subject is always a concrete principal (no subject relation). Depth-bounded, cycle-safe, fails closed.
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is an in-process, goroutine-safe Store for fast unit tests; not for production.
func NewMemoryStore ¶
func NewMemoryStore() *MemoryStore
NewMemoryStore returns an empty in-memory store.
func (*MemoryStore) ReadByObjectRelation ¶
func (*MemoryStore) ReadBySubject ¶
type Namespace ¶
Namespace is the fixed, non-user-editable authorization model; DefaultNamespace returns the single canonical instance.
func DefaultNamespace ¶
func DefaultNamespace() Namespace
DefaultNamespace returns the fixed memory-system authorization model (design D1):
type user type system admin: [user] type tenant system: [system] # parent edge (seeded at tenant create) admin: [user] or admin from system # tenant admins ∪ global admins owner: [user] # personal-tenant owner (⊆ manager, ⊄ admin) manager: [user] or admin or owner # admins AND owners are managers member: [user] or manager # managers are members viewer: [user:*] or member # wildcard enables public read type document tenant: [tenant] # parent edge (set at document create) viewer: [user] or editor or viewer from tenant # editors read too editor: [user] or member from tenant
Inclusion chains: system#admin ⊆ tenant#admin ⊆ tenant#manager ⊆ tenant#member ⊆ tenant#viewer, and tenant#owner ⊆ tenant#manager (owner ⊄ admin: an owner is a full manager of their own tenant but is NOT a system admin). document#editor ⊆ document#viewer. No rewrite cycle (owner is a `this`-only leaf; the deepest chain still runs viewer → editor → member-from-tenant → …#admin → system#admin, no back-edge to viewer). manager gains one extra union branch (computed(owner)), but that branch is a shallow leaf, so the deepest Check chain (system admin reading a doc, via the admin branch) is unchanged at depth 5 — well under DefaultMaxDepth (16).
type PostgresStore ¶
type PostgresStore struct {
// contains filtered or unexported fields
}
PostgresStore is the production GORM-backed Store, persisting to relation_tuples (model.go). Writes are idempotent via ON CONFLICT DO NOTHING on the composite-PK.
func NewPostgresStore ¶
func NewPostgresStore(db *gorm.DB) *PostgresStore
NewPostgresStore returns a Store over db; caller must have run Migrate on it.
func (*PostgresStore) ReadByObjectRelation ¶
func (*PostgresStore) ReadBySubject ¶
type RelationDef ¶
RelationDef defines one relation within a type: the direct subject specs its `this` clause allows (documentation only — Check does not enforce subject typing) and the union of rewrite rules defining membership.
type RelationTuple ¶
type RelationTuple struct {
ObjectType string `gorm:"primaryKey;column:object_type;type:text;not null;index:idx_relation_tuples_object,priority:1"`
ObjectID string `gorm:"primaryKey;column:object_id;type:text;not null;index:idx_relation_tuples_object,priority:2"`
Relation string `gorm:"primaryKey;column:relation;type:text;not null;index:idx_relation_tuples_object,priority:3"`
SubjectType string `gorm:"primaryKey;column:subject_type;type:text;not null;index:idx_relation_tuples_subject,priority:1"`
SubjectID string `gorm:"primaryKey;column:subject_id;type:text;not null;index:idx_relation_tuples_subject,priority:2"`
SubjectRelation string `gorm:"primaryKey;column:subject_relation;type:text;not null;default:''"`
}
RelationTuple is the GORM row backing the Postgres tuple store. The six-column tuple is the composite PK, doubling as the uniqueness constraint (a tuple is present or absent; no duplicates). Two secondary indexes serve Check's hot reads:
- idx_relation_tuples_object (object_type, object_id, relation) -> forward expansion / this + tuple_to_userset
- idx_relation_tuples_subject (subject_type, subject_id) -> reverse lookup / ReadBySubject
SubjectRelation is "" for direct (user/wildcard) subjects, the relation name for usersets.
func (RelationTuple) TableName ¶
func (RelationTuple) TableName() string
TableName returns the Postgres table name.
type Rewrite ¶
type Rewrite struct {
Kind RewriteKind
// Relation is the sibling relation to evaluate (RewriteComputedUserset).
Relation string
// Tupleset is the parent-edge relation read on the object; ComputedRelation
// is evaluated on each referenced parent (RewriteTupleToUserset).
Tupleset string
ComputedRelation string
}
Rewrite is a single child of a relation's union rewrite rule.
type RewriteKind ¶
type RewriteKind int
RewriteKind identifies one child of a relation's userset-rewrite union.
const ( // RewriteThis: direct tuples on (object, relation), including a user:* wildcard. RewriteThis RewriteKind = iota // RewriteComputedUserset: subjects with Relation on the SAME object (sibling // relation, e.g. "member" includes "admin"). RewriteComputedUserset // RewriteTupleToUserset: follow a parent edge — read (object, Tupleset) tuples // for parent objects, then eval ComputedRelation on each (e.g. doc editor == // "member from tenant"). RewriteTupleToUserset )
type Store ¶
type Store interface {
// Write persists t. If an identical tuple already exists it is a no-op.
Write(ctx context.Context, t Tuple) error
// Delete removes t. Deleting an absent tuple is a no-op.
Delete(ctx context.Context, t Tuple) error
// ReadByObjectRelation returns every tuple on (objType, objID, relation),
// including wildcard and userset subjects.
ReadByObjectRelation(ctx context.Context, objType, objID, relation string) ([]Tuple, error)
// ReadBySubject returns every tuple whose subject is (subjType, subjID),
// regardless of subject relation.
ReadBySubject(ctx context.Context, subjType, subjID string) ([]Tuple, error)
}
Store is the tuple persistence contract Check depends on. Small by design: Check needs only the two reads; Write/Delete serve out-of-band writers. Writes are idempotent — re-writing an existing tuple is a no-op, never an error.
type Tuple ¶
type Tuple struct {
ObjectType string
ObjectID string
Relation string
SubjectType string
SubjectID string
SubjectRelation string
}
Tuple is a single relation tuple:
<ObjectType>:<ObjectID>#<Relation>@<SubjectType>:<SubjectID>[#<SubjectRelation>]
SubjectRelation is "" for a direct subject (concrete user or user:* wildcard) and non-empty when the subject is a userset (type:id#relation): "everyone who has <SubjectRelation> on <SubjectType>:<SubjectID>". Plain value type; the GORM row lives in model.go.
func (Tuple) IsUserset ¶
IsUserset reports whether the subject is a userset (type:id#relation) vs. a concrete/wildcard subject.
func (Tuple) IsWildcard ¶
IsWildcard reports whether the subject is the public wildcard (id "*", no subject relation).
type TypeDef ¶
type TypeDef struct {
Name string
Relations map[string]RelationDef
}
TypeDef is the set of relations defined on an object type.