authz

package
v1.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: AGPL-3.0 Imports: 7 Imported by: 0

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

View Source
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.

View Source
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.

View Source
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:"
)
View Source
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

View Source
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

func Migrate(db *gorm.DB) error

Migrate creates/updates the relation_tuples table and indexes; safe to call repeatedly.

func ServicePrincipalID

func ServicePrincipalID(tenantID string) string

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 NewEngine

func NewEngine(store Store) *Engine

NewEngine returns an Engine over store with the default namespace and depth cap.

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) Delete

func (s *MemoryStore) Delete(_ context.Context, t Tuple) error

func (*MemoryStore) ReadByObjectRelation

func (s *MemoryStore) ReadByObjectRelation(_ context.Context, objType, objID, relation string) ([]Tuple, error)

func (*MemoryStore) ReadBySubject

func (s *MemoryStore) ReadBySubject(_ context.Context, subjType, subjID string) ([]Tuple, error)

func (*MemoryStore) Write

func (s *MemoryStore) Write(_ context.Context, t Tuple) error

type Namespace

type Namespace struct {
	Types map[string]TypeDef
}

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).

func (Namespace) Relation

func (n Namespace) Relation(objType, relation string) (RelationDef, bool)

Relation returns the definition of objType#relation, or ok=false if absent.

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) Delete

func (s *PostgresStore) Delete(ctx context.Context, t Tuple) error

func (*PostgresStore) ReadByObjectRelation

func (s *PostgresStore) ReadByObjectRelation(ctx context.Context, objType, objID, relation string) ([]Tuple, error)

func (*PostgresStore) ReadBySubject

func (s *PostgresStore) ReadBySubject(ctx context.Context, subjType, subjID string) ([]Tuple, error)

func (*PostgresStore) Write

func (s *PostgresStore) Write(ctx context.Context, t Tuple) error

type RelationDef

type RelationDef struct {
	Name           string
	DirectSubjects []string
	Rewrites       []Rewrite
}

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

func (t Tuple) IsUserset() bool

IsUserset reports whether the subject is a userset (type:id#relation) vs. a concrete/wildcard subject.

func (Tuple) IsWildcard

func (t Tuple) IsWildcard() bool

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL