Documentation
¶
Overview ¶
Package postgres is the authz.Datastore on Postgres, via pgx. Reads run in a repeatable-read, read-only transaction so one resolution sees one snapshot; writes run in one transaction so a WriteRelationships lands whole or not at all, and every write records what it changed in the change log at a revision that follows commit order.
The datastore stores tuples and their changelog, and keeps its Index beside them when asked (WithIndex): the userset closure and the permission sets, maintained by a Hook inside the write transaction, or afterwards from the changelog (WithAsyncIndex). Tx hands the index the transaction behind a Reader so it can query its own tables at the same snapshot. Migrations create every table; see Migrate.
A write can also run inside a transaction the application owns, so a grant commits with the row it protects or not at all: TransactIn takes the application's pgx.Tx and does everything Transact does except begin and commit.
Index ¶
- Constants
- Variables
- func Check(ctx context.Context, pool *pgxpool.Pool) error
- func Migrate(ctx context.Context, pool *pgxpool.Pool) error
- func Tx(r authz.Reader) (pgx.Tx, bool)
- type Datastore
- func (d *Datastore) CreateAll(ctx context.Context, rels []authz.Relationship) error
- func (d *Datastore) Index() authz.Index
- func (d *Datastore) Transact(ctx context.Context, fn func(authz.Writer) error) error
- func (d *Datastore) TransactIn(ctx context.Context, tx pgx.Tx, fn func(authz.Writer) error) error
- func (d *Datastore) View(ctx context.Context, fn func(authz.Reader) error) error
- type Drift
- type Hook
- type Index
- func (x *Index) Catchup(ctx context.Context) (int, error)
- func (x *Index) Follow(ctx context.Context, interval time.Duration, errs chan<- error)
- func (x *Index) HasPermission(ctx context.Context, r authz.Reader, resource authz.ObjectRef, ...) (bool, error)
- func (x *Index) Materialized(resourceType, permission string) bool
- func (x *Index) NestedRelationships(ctx context.Context, r authz.Reader, q authz.RelationshipQuery) ([]authz.Relationship, error)
- func (x *Index) NestedResourceIDs(ctx context.Context, r authz.Reader, subjectType string, subjectIDs []string, ...) ([]string, error)
- func (x *Index) NestedResourceIDsAmong(ctx context.Context, r authz.Reader, subjectType string, subjectIDs []string, ...) ([]string, error)
- func (x *Index) Reindex(ctx context.Context) error
- func (x *Index) ResourcesWithPermission(ctx context.Context, r authz.Reader, resourceType, permission string, ...) ([]string, error)
- func (x *Index) ResourcesWithPermissionAmong(ctx context.Context, r authz.Reader, resourceType, permission string, ...) ([]string, error)
- func (x *Index) SubjectsWithPermission(ctx context.Context, r authz.Reader, resource authz.ObjectRef, ...) ([]string, error)
- func (x *Index) Verify(ctx context.Context) (Drift, error)
- type Migrator
- type Option
Constants ¶
const DefaultNestingBudget = 100_000
DefaultNestingBudget bounds how many closure rows one change may add.
const MigrationTable = "public.authz_migration"
MigrationTable records the applied versions. It sits outside the authz schema so that any runner can create it before the schema exists, and it is qualified because a role named authz puts the authz schema first in its search path once the first migration has created it; an unqualified name would then resolve to a different table than the one the first run made.
Variables ¶
var ( // ErrNestingTooLarge is returned when one change would add more closure // rows than the budget; a synchronous write is rolled back. ErrNestingTooLarge = errors.New("postgres: userset nesting exceeds the index budget") // ErrNotPostgres is returned when the index is asked to answer for a // Reader that did not come from this datastore. ErrNotPostgres = errors.New("postgres: the index needs this datastore's reader") // ErrNotMaterializable is returned by New for a permission whose rewrite // the permission sets cannot represent. ErrNotMaterializable = materialize.ErrNotMaterializable )
var ErrSchemaVersion = errors.New("authz: database schema version does not match the library")
ErrSchemaVersion is Check's answer when the database is not at SchemaVersion: migrations are pending, or the code is older than the database.
var Migrations embed.FS
Migrations is every table the datastore and its index need, as numbered goose-annotated SQL: one file today, creating the relationships, the change log, the userset closure and the permission sets. Migrator applies it. A host that runs goose itself hands it to its own provider under MigrationTable; a host with another tool has the files.
var SchemaVersion = mustSchemaVersion()
SchemaVersion is the version this code expects the database at: the number of the last migration in Migrations. Check compares the database with it.
Functions ¶
func Check ¶
Check fails with ErrSchemaVersion unless the database is at SchemaVersion. Run it at boot beside engine.Service.ValidateStored, so a deploy whose migrations did not run stops before its first query fails.
Types ¶
type Datastore ¶
type Datastore struct {
// contains filtered or unexported fields
}
Datastore is the Postgres-backed store, with its Index when one was asked for.
func New ¶
New wraps a pool. The authz schema must exist (Migrate); the engine finds the index through Datastore.Index.
func (*Datastore) CreateAll ¶
CreateAll implements authz.BulkCreator: one COPY into the relationships, one Change, the hooks once, one commit.
func (*Datastore) Index ¶
Index returns the datastore's index, or nil when New was not asked for one. It is the engine's way in (authz.Indexed), and the way to Verify, Reindex, Catchup or Follow it.
func (*Datastore) Transact ¶
Transact runs fn in a read-committed transaction of its own, records the change it produced, runs the hooks, and commits if everything returned nil.
func (*Datastore) TransactIn ¶
TransactIn is Transact inside a transaction the caller owns: fn runs on a Writer over tx, then the change is recorded and the hooks run, all in tx, and the caller commits or rolls back. It is how a grant lands in the same transaction as the application row it protects:
tx, err := pool.Begin(ctx)
defer tx.Rollback(ctx)
// ... insert the row ...
err = ds.TransactIn(ctx, tx, func(w authz.Writer) error {
return svc.WriteRelationshipsIn(ctx, w, updates)
})
// ... commit
Call it last, just before the commit: the change log's lock is taken here and held until tx ends, and every other write that changed something waits on it. AI: the change log and the hooks belong to the write, not to the commit, so they run here rather than in Transact; a Writer handed out without them would leave every index and mirror behind.
type Hook ¶
Hook runs inside a write transaction, after the relationships and the change log rows are written and before commit, with the Change the transaction produced. Returning an error rolls the whole write back.
type Index ¶
type Index struct {
// contains filtered or unexported fields
}
Index is the datastore's index (authz.Index): the userset closure, after Zanzibar's Leopard, and the permission sets, after AuthZed's Materialize, in two tables beside the relationships. It is maintained in SQL inside every write transaction (WithIndex) or afterwards from the changelog (WithAsyncIndex), and answers each question with one query. Datastore.Index returns it.
func (*Index) Catchup ¶
Catchup applies every change after the follower's cursor, each in its own transaction, and reports how many it applied. It is one pass; Follow loops. An asynchronous index (WithAsyncIndex) is kept in step by one of them.
func (*Index) Follow ¶
Follow runs Catchup every interval until ctx ends. Errors are sent on errs when there is room and never stop the loop.
func (*Index) HasPermission ¶
func (x *Index) HasPermission(ctx context.Context, r authz.Reader, resource authz.ObjectRef, permission string, subject authz.SubjectRef) (bool, error)
HasPermission implements authz.PermissionIndex. AI: it starts from the few sets that grant the permission on the one resource and tests the subject against each, never from every set the subject is in: a subject granted ninety thousand resources made the other order two hundred times slower.
func (*Index) Materialized ¶
Materialized implements authz.PermissionIndex.
func (*Index) NestedRelationships ¶
func (x *Index) NestedRelationships(ctx context.Context, r authz.Reader, q authz.RelationshipQuery) ([]authz.Relationship, error)
NestedRelationships implements authz.NestingIndex.
func (*Index) NestedResourceIDs ¶
func (x *Index) NestedResourceIDs(ctx context.Context, r authz.Reader, subjectType string, subjectIDs []string, subjectRelation, resourceType, relation string) ([]string, error)
NestedResourceIDs implements authz.NestingIndex.
func (*Index) NestedResourceIDsAmong ¶
func (x *Index) NestedResourceIDsAmong(ctx context.Context, r authz.Reader, subjectType string, subjectIDs []string, subjectRelation, resourceType, relation string, among []string) ([]string, error)
NestedResourceIDsAmong implements authz.NestingIndex.
func (*Index) Reindex ¶
Reindex rebuilds every index table from the relationships and sets the follower's cursor to the latest revision. It is the repair tool; Verify says whether it is needed.
func (*Index) ResourcesWithPermission ¶
func (x *Index) ResourcesWithPermission(ctx context.Context, r authz.Reader, resourceType, permission string, subject authz.SubjectRef) ([]string, error)
ResourcesWithPermission implements authz.PermissionIndex.
func (*Index) ResourcesWithPermissionAmong ¶
func (x *Index) ResourcesWithPermissionAmong(ctx context.Context, r authz.Reader, resourceType, permission string, subject authz.SubjectRef, among []string) ([]string, error)
ResourcesWithPermissionAmong implements authz.PermissionIndex. AI: it is HasPermission over many resources at once, resource-first for the same reason: the granting sets of every resource in the batch, each tested for the subject, never the subject's every set.
func (*Index) SubjectsWithPermission ¶
func (x *Index) SubjectsWithPermission(ctx context.Context, r authz.Reader, resource authz.ObjectRef, permission, subjectType, subjectRelation string) ([]string, error)
SubjectsWithPermission implements authz.PermissionIndex.
func (*Index) Verify ¶
Verify reads every relationship and every index row at one snapshot, derives what the index should hold with the materialize package, and reports how the maintained rows differ. Nothing is changed. AI: the derivation is a second implementation of the index, in Go, so a fault in the SQL maintenance cannot hide from it. It is the consistency check to run after a suspected fault, an index reconfiguration, or in a test after concurrent writes.
type Migrator ¶
type Migrator struct {
// contains filtered or unexported fields
}
Migrator applies Migrations to one database with goose and records what ran in MigrationTable. Up and Down are what a deploy step calls; Version is what Check reads.
func NewMigrator ¶
NewMigrator builds a Migrator on the pool. Close it when done; the pool stays open.
type Option ¶
type Option func(*Datastore)
Option configures New.
func WithAsyncIndex ¶
func WithAsyncIndex() Option
WithAsyncIndex maintains the index from the changelog after commit, through Index.Follow or Index.Catchup, instead of inside the write transaction. Reads then trail writes by the follower's lag, the trade AuthZed Materialize makes.
func WithHook ¶
WithHook registers a Hook. Hooks run in registration order, after the index when there is one.
func WithIndex ¶
WithIndex keeps the index beside the tuples, maintained inside every write transaction so reads are never behind: the userset closure, and the permission sets of the named permissions, written type#permission, or of every permission the schema lets a set represent when none are named. A named permission the sets cannot represent fails New with ErrNotMaterializable.
func WithNestingBudget ¶
WithNestingBudget overrides DefaultNestingBudget for the index.
func WithNestingIndex ¶
func WithNestingIndex() Option
WithNestingIndex keeps the userset closure alone, with no permission sets. It needs no schema.