authzed-codegen

module
v1.5.0 Latest Latest
Warning

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

Go to latest
Published: May 9, 2026 License: MIT

README

authzed-codegen

Type-safe Go bindings for AuthZED / SpiceDB schemas. Each definition block in a .zed file becomes a .gen.go with typed constructors, relation writers, and per-permission Check / Lookup methods over the runtime engine in pkg/authz/.

Example

Given a schema:

definition menusvc/order {
    relation creator: menusvc/user | menusvc/customer
    relation belongs_company: menusvc/company
    permission write = creator + creator->manage + belongs_company->manage
}

The codegen produces typed bindings:

order := menusvc.Order("o-1")
user  := menusvc.User("u-1")

if err := order.CreateCreatorRelations(ctx, menusvc.OrderCreatorObjects{
    User: []menusvc.User{user},
}); err != nil {
    return err
}

ok, err := order.CheckWrite(ctx, menusvc.CheckOrderWriteInputs{
    User: []menusvc.User{user},
})

Each method dispatches through the authz.Engine interface; the SpiceDB client lives in pkg/authz/spicedb/.

Install

go install github.com/danhtran94/authzed-codegen/cmd/authzed-codegen@latest

Usage

authzed-codegen --output <out-dir> <schema.zed>

One .gen.go is emitted per definition block, grouped by namespace (menusvc/order<out-dir>/menusvc/order.gen.go). See example/ for a complete schema and its generated output.

Schema Support

Construct Status
Union (+), arrow (->)
Wildcard relations (type:*) ✓ — Wildcards sub-struct on <Rel>Objects; sibling Read<Rel><Type>Wildcard read methods
Intersection (&), exclusion (-)
Caveats (with <caveat>) ✓ — typed <Pascal>Args per caveat; nested Caveats sub-struct on <Rel>Objects and Check<Perm>Inputs; multi-caveat-per-permission supported
Expiration (with expiration) ✓ — per-tuple TTL via Expirations sub-struct on <Rel>Objects; auto-switches to OPERATION_TOUCH; combines with caveats
Sub-relation references (foo#bar) ✓ — typed userset write field (<TypeName><PascalSubRel>) on <Rel>Objects; userset Check input field; SubRelation on metadata struct

Parsing delegates to github.com/authzed/spicedb/pkg/schemadsl/compiler — any schema SpiceDB accepts will parse. The codegen layer is narrower; rejected constructs surface schema-relative errors before any output is written. Rationale: docs/ADR-001-parser-migration.md.

Caveats

Relations and allowed types declared with <caveat> generate a typed <CaveatPascal>Args struct per caveat (one per namespace) plus a Caveats sub-struct on the relation's <Rel>Objects and the permission's Check<Perm>Inputs. Scalar fields (*string, *int, *bool, *float64) are pointer-typed so callers can defer individual parameters to check time; container fields ([]string, []byte, map) stay direct (nil = unset).

caveat extsvc/tenant_match(tenant string) {
    tenant == "acme"
}

definition extsvc/folder {
    relation tenanted_viewer: extsvc/user with extsvc/tenant_match
    permission tenanted_browse = tenanted_viewer
}

Pre-bind the policy at write time (caveat travels with the tuple):

folder.CreateTenantedViewerRelations(ctx, extsvc.FolderTenantedViewerObjects{
    User: []extsvc.User{user},
    Caveats: extsvc.FolderTenantedViewerCaveats{
        User: &extsvc.TenantMatchArgs{Tenant: new("acme")},
    },
})

Or defer all binding to check time (write attaches the caveat name with no pre-context; check supplies the value):

folder.CreateTenantedViewerRelations(ctx, extsvc.FolderTenantedViewerObjects{
    User: []extsvc.User{user},
    // Caveats omitted — write-time pre-context is nil
})

ok, err := folder.CheckTenantedBrowse(ctx, extsvc.CheckFolderTenantedBrowseInputs{
    User: []extsvc.User{user},
    Caveats: extsvc.CheckFolderTenantedBrowseCaveats{
        TenantMatch: &extsvc.TenantMatchArgs{Tenant: new("acme")},
    },
})

Per-key precedence is per SpiceDB's wire model: write-time values win on collision, unbound keys fall through to check-time. Permissions reaching 2+ distinct caveats are supported — Check<Perm>Caveats gets one field per unique caveat, the generated method merges all non-nil entries into one wire Context. Cross-caveat parameter-name collisions (two caveats declaring the same key) are detected at codegen and emit a clear error.

Lookup<Perm><Type>Resources and Lookup<Perm><Type>Subjects thread caveat context through too — for caveat-reaching permissions, both methods accept a Caveats argument (positional for Subjects, on the existing input struct for Resources) and route through LookupResourcesWithCaveat / LookupSubjectsWithCaveat. CONDITIONAL_PERMISSION results are filtered out of the returned slice, matching Check<Perm>'s collapse-to-deny semantics.

See docs/spec-002-caveat-codegen.md and docs/spec-003-write-time-caveat-codegen.md.

Expiration

Schemas declaring use expiration at the top can mark relations with with expiration. Tuples carry per-tuple TTL via OptionalExpiresAt; SpiceDB filters expired entries server-side from Check / Lookup / Read. The codegen surfaces a *time.Time field per expiring allowed type on a new Expirations sub-struct (parallel to Wildcards and Caveats):

use expiration

definition extsvc/folder {
    relation expiring_viewer: extsvc/user with expiration
    permission expiring_browse = expiring_viewer
}
expiresAt := time.Now().Add(1 * time.Hour)
folder.CreateExpiringViewerRelations(ctx, extsvc.FolderExpiringViewerObjects{
    User: []extsvc.User{user},
    Expirations: extsvc.FolderExpiringViewerExpirations{
        User: &expiresAt,
    },
})

Combined with caveats — relation gated: extsvc/user with extsvc/tenant_match and expiration — both Caveats and Expirations sub-structs are populated independently. The codegen routes through CreateRelationsWithExpiration (auto-switching to OPERATION_TOUCH because un-garbage-collected expired tuples may collide on tuple identity). See docs/spec-004-expiration-codegen.md.

Read with Metadata

Read<Rel><Type>Relations returns []<Rel><Type>Relation — a typed metadata struct per tuple carrying the subject ID alongside the caveat name, decoded caveat context, and expiration timestamp:

type FolderTenantedViewerUserRelation struct {
    ID            extsvc.User
    CaveatName    string         // "" when no caveat is attached
    CaveatContext map[string]any // nil when no caveat or empty pre-context
    ExpiresAt     *time.Time     // nil when no per-tuple TTL
}

The metadata fields are nil/empty for plain relations; they populate from SpiceDB's Relationship.OptionalCaveat and Relationship.OptionalExpiresAt for trait-bearing tuples. Use cases — admin/audit UIs that need to surface "user X has access via tenant=acme until 2026-Q4" without bypassing the codegen.

For callers that just want the IDs (matching the pre-v1.4.0 shape):

rels, _ := folder.ReadViewerUserRelations(ctx)
users := authz.IDsOf(rels)  // []User

authz.IDsOf is a generic helper; type inference resolves the typed slice from the single positional argument.

Wildcard reads return the same metadata struct alongside the presence bool:

meta, isWildcard, err := folder.ReadGuestUserWildcard(ctx)
if isWildcard && meta.ExpiresAt != nil {
    // public-for-everyone-until-timestamp pattern
}

See docs/spec-005-read-with-metadata.md for the full Engine surface and constraints (no auto-decoded <Caveat>Args, slice materialization vs streaming, wildcard split discipline).

Sub-relation References

Schemas declaring relation X: Type#SubRelation grant access via inheritance — anyone reaching Type#SubRelation (typically a permission or relation on the target type) is implicitly granted on the resource. The codegen surfaces userset writes as a typed field on <Rel>Objects:

definition extsvc/team {
    relation owner: extsvc/user
    relation manager: extsvc/user
    permission admin = owner + manager
}

definition extsvc/folder {
    relation collab: extsvc/team#admin
    permission collab_view = collab
}
// Grant team t1's admin set as a collaborator. SpiceDB stores
// (folder:f1, collab, team:t1#admin) — the wire keeps the team ID
// as the anchor; user resolution happens at Check time.
folder.CreateCollabRelations(ctx, extsvc.FolderCollabObjects{
    TeamAdmin: []extsvc.Team{team},
})

Common-case Check (does user u1 have access?) — SpiceDB walks the userset chain server-side:

// u1 must be owner or manager of t1 for this Check to grant.
ok, _ := folder.CheckCollabView(ctx, extsvc.CheckFolderCollabViewInputs{
    TeamAdmin: []extsvc.Team{team},  // userset-as-subject input
})

Permissions reaching userset allowed types expose userset input fields on Check<Perm>Inputs. The userset-as-subject Check (rare case) matches the literal userset reference — useful for "does this group itself have permission?" admin/audit tooling. Direct-subject Check (the common case) walks the chain transparently when the schema includes both branches.

Read-side rows surface a SubRelation field on the metadata struct — empty for direct subjects, non-empty for userset references. Mixed schemas (relation viewer: user | team#admin) produce distinct Read methods per subject type (ReadViewerUserRelations and ReadViewerTeamRelations); each returns disjoint rows.

See docs/spec-006-sub-relation-references.md for the wire-level walkthrough, the rare-case Check semantics (literal-match vs chain-walking), and the deferred Lookup-with-userset-results work.

Behavior Notes

  • Permission chains. Check<Permission>Inputs exposes the full set of input types reachable through arrow expressions in referenced permissions, including cross-definition arrows. Cycles (permission p = p + q) exit non-zero with cycle detected.
  • Wildcards. Create<Rel>Relations accepts Wildcards{User: true} regardless of which permissions reference the relation. AuthZED's guidance is to grant wildcards only on read-side relations (e.g. viewer) to avoid universal write access; the codegen does not enforce this — callers own the discipline.

Verification

Round-trip the fixture (regression bar for the codegen itself):

go run ./cmd/authzed-codegen --output example/authzed example/schema.zed
git diff --quiet example/authzed/

End-to-end tests exercise the generated stubs against a real SpiceDB container via testcontainers-go. The harness lives in pkg/authz/spicedbtest/; the test packages are example/authzed/{bookingsvc,menusvc,extsvc} and pkg/authz/spicedb/.

go test ./pkg/authz/spicedb/... ./example/authzed/...

Tests skip cleanly when Docker is unavailable.

License

MIT — see LICENSE.

Directories

Path Synopsis
cmd
authzed-codegen command
example
internal
pkg
authz/spicedbtest
Package spicedbtest spins up an ephemeral SpiceDB Docker container, loads a schema, and exposes a ready-to-use spicedb.Engine for tests.
Package spicedbtest spins up an ephemeral SpiceDB Docker container, loads a schema, and exposes a ready-to-use spicedb.Engine for tests.

Jump to

Keyboard shortcuts

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