Skip to content

Tags: authorizerdev/authorizer

Tags

2.3.0

Toggle 2.3.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(grpc): auth interceptor, authctx principal, and client metadata …

…helpers (#636)

* feat(grpc): auth interceptor, authctx principal, and client metadata helpers

- Add gRPC auth interceptor that reads authorizer.v1.public proto
  annotations and rejects unauthenticated calls before handlers run;
  deny-by-default for unlisted services
- Attach validated caller identity to context.Context via internal/authctx
  (Principal carries profile, admin flag, and raw token data)
- Add internal/grpcsrv/client helpers (WithBearerToken, WithAuthorizerURL,
  WithAdminSecret, WithCookies) for pure-gRPC callers
- Mark Revoke and AdminLogin as public in proto (RFC 7009 / admin entry point)
- Add make proto-check and Buf CI step to fail when gen/ is stale
- Wire TokenProvider into gRPC server for interceptor token validation
- Fix Couchbase audit log secondary index to use created_at field
- Fix ScyllaDB audit log queries: remove ALLOW FILTERING on indexed columns
  and wait for materialized-view index build after bootstrap
- Document gRPC auth metadata in docs/grpc-rest-api-spec.md §2.3
- Update AGENTS.md with full Makefile reference; point CLAUDE.md at AGENTS.md

* fix(grpc): address post-review security and robustness findings

- auth interceptor: guard Session cookie-only path on publicServiceName
  to prevent a future same-named method on another service inheriting it
- admin_token: reject header auth when AdminSecret is unconfigured (empty)
  as a defense-in-depth measure against accidental open deployments
- cassandradb: replace fixed 5s sleep with a probe-based retry loop
  (up to 30s) for ScyllaDB async secondary index builds, matching the
  Couchbase retry pattern already in the codebase
- tests: add TestAuth_NilTokenProviderFailsClosed, TestAuth_SessionOnlyAcceptsPublicService,
  and four IsSuperAdmin unit tests covering the new empty-secret guard

2.3.0-rc.9

Toggle 2.3.0-rc.9's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(api)!: serve all auth ops on gRPC/REST + flatten response envelo…

…pe (#635)

* build(docker): expose gRPC port 9091

gRPC server binds to --host:9091 but the image only EXPOSEd 8080/8081
and compose only published 8080, so external gRPC clients could not
reach AuthorizerService/AuthorizerAdminService without manual port
mapping.

Publish 9091 in docker-compose and document it in the Dockerfile EXPOSE comment.

* feat(api)!: serve all auth ops on gRPC/REST, flatten envelope

Move the 10 remaining auth operations (login, magic_link_login,
verify_email, resend_verify_email, verify_otp, resend_otp,
forgot_password, reset_password, update_profile, deactivate_account)
into the transport-agnostic service layer so GraphQL, gRPC, and REST
share one implementation; GraphQL resolvers become thin wrappers.

Public RPC responses now return the bare domain message (AuthResponse,
User, Meta) instead of a per-RPC wrapper, making gRPC/REST byte-identical
to GraphQL. Consolidate the proto into a single authorizer.v1 package and
regenerate Go, OpenAPI, and the Go/Python/TS client stubs.

Fix the REST gateway dropping cookies: session/MFA cookies are emitted as
set-cookie gRPC metadata, now promoted to real Set-Cookie headers via an
outgoing header matcher (previously surfaced as Grpc-Metadata-Set-Cookie
and ignored by browsers). Admin login/logout/session cookies benefit too.

BREAKING CHANGE: /v1 REST and gRPC responses for signup/login/verify_email/
verify_otp/session are no longer wrapped under `auth` (read top-level
access_token/user/...); profile is no longer under `user`; meta no longer
under `meta`. Regenerated clients expose the flat types.

* fix(api): address review on PR #635

- UpdateProfile no longer silently disables MFA. is_multi_factor_auth_enabled
  is now `optional bool` so a partial update that omits it leaves MFA
  unchanged; a non-optional proto3 bool defaulted to false on every call and
  turned MFA off. Handler maps the *bool through; proto regenerated.
- Wire AuthenticatorProvider into the service layer in the integration test
  harness; without it any TOTP path through the service nil-panicked. Add
  TOTP-through-service coverage (passcode, recovery code, invalid) and a
  regression test that a partial UpdateProfile preserves MFA.
- Restore sanitized error strings lost in the migration: verify_otp passcode/
  recovery-code validation faults and magic_link_login update-user failure no
  longer surface the raw internal error; resend_otp keeps its response body.

* fix(ci): handle intentional proto break + flat profile in smoke test

- smoke test (mcp_stdio) read the old {user:{…}} Profile wrapper; Profile now
  returns the flat User object, so parse email at the top level.
- buf breaking gate: the v1 package consolidation and response-envelope
  flatten are intentional, reviewed breaks. Allow them only when a maintainer
  applies the `proto-breaking-approved` label; the gate still hard-fails on
  every unlabeled PR so accidental breaks are caught.

* ci(buf): re-run breaking gate on label changes

2.3.0-rc.8

Toggle 2.3.0-rc.8's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(api): AuthorizerAdmin service (gRPC + REST) + module-wide lint g…

…ate (#631)

* chore(admin-api): scaffold AuthorizerAdminService proto

* chore(admin-api): define AdminProvider interface + requireSuperAdmin

* chore(admin-api): register AdminHandler on shared gRPC server

* chore(admin-api): forward admin-secret header through gateway + transport

* feat(admin-api): admin auth + meta over gRPC + REST

Add AuthorizerAdminService with AdminLogin/AdminLogout/AdminSession/AdminMeta
RPCs (gRPC + REST via grpc-gateway), migrate logic into internal/service
(admin_auth.go + AdminProvider interface), refactor the GraphQL resolvers to
thin adapters, and add transport-agnostic admin cookie builders. Remaining
admin ops are stubbed (admin_stubs.go) so *provider satisfies AdminProvider
during the staged migration.

* feat(admin-api): users over gRPC + REST

- migrate Users, User, UpdateUser, DeleteUser, VerificationRequests
  from GraphQL resolvers into the transport-agnostic service layer
- expose them on AuthorizerAdminService (gRPC + REST gateway)
- resolvers become thin adapters; super-admin auth enforced via
  requireSuperAdmin in the service layer
- add proto messages + projections, reusing User and projectUser

* feat(admin-api): access ops over gRPC + REST

- migrate RevokeAccess, EnableAccess, InviteMembers from GraphQL resolvers
  into the transport-agnostic service layer (admin_access.go)
- expose them on AuthorizerAdminService (gRPC + REST gateway)
- resolvers become thin adapters; super-admin auth enforced via
  requireSuperAdmin in the service layer
- add proto messages + InviteMembers projection, reusing projectUser
- distinct RevokeAccessRequest/EnableAccessRequest messages to satisfy
  buf STANDARD one-message-per-RPC lint

* feat(admin-api): webhook ops over gRPC + REST

* feat(admin-api): email template ops over gRPC + REST

- migrate add/update/delete/list email-template resolvers to service layer
- add AddEmailTemplate/UpdateEmailTemplate/DeleteEmailTemplate/EmailTemplates
  RPCs with REST mappings under /v1/admin/*
- thin GraphQL resolver adapters delegate to the shared service
- update preserves existing subject/template/design when only one field changes

* feat(admin-api): audit logs over gRPC + REST

- migrate audit_logs resolver to the shared service layer
- add AuditLogs RPC with REST mapping at /v1/admin/audit_logs
- mirror ListAuditLogRequest filters (action, actor, resource, time range)
- centralize audit filter keys as constants

* feat(admin-api): FGA admin ops over gRPC + REST

* test(admin-api): admin surface smoke coverage (REST + gRPC + fail-closed)

* chore(lint): resolve golangci-lint issues across the module

Make `golangci-lint run ./...` pass cleanly so it can gate in CI.

Fixes (first-party): check or explicitly discard previously-unchecked errors
(errcheck) including go/defer fire-and-forget wraps; fix a context leak in the
mongodb provider (govet lostcancel); drop ineffectual assignments; remove an
unused jwks helper; print-style error formatting (SA1006); comment/error-string
format (ST1005/ST1020/ST1021); receiver-name and quickfix cleanups.

Config policy (.golangci.yml): disable ST1003 (renaming long-standing exported
identifiers like DbType*/HttpStatus is a breaking change) and ST1000 (package
doc comments on internal packages are churn without value). Scope-exclude the
GORM-derived sqlite dialect (errcheck/staticcheck/unused) and NoSQL driver
SDK-deprecation churn (SA1019). Correctness checks remain fully enforced.

* ci: add golangci-lint job to CI

* chore: update claude.md

* feat(observability): record transport protocol in audit logs + metrics

Every API operation is now attributable to the protocol it was served over
(graphql, grpc, rest):

- Add RequestMetadata.Protocol, set per-transport (MetaFromGin=graphql;
  MetaFromGRPC=grpc, or rest when the grpc-gateway marks the request via a new
  x-authorizer-transport=rest metadata pair).
- Metrics: new authorizer_api_operations_total{protocol,operation,status}
  counter, recorded centrally by a gRPC Metrics interceptor (covers grpc+rest)
  and the GraphQL operation middleware (graphql).
- Audit: audit.Event.Protocol folded into the existing Metadata JSON column by
  LogEvent (no audit-log schema change); set on all admin service audit events
  and tagged graphql on the public GraphQL resolver audit events.

Also renames admin_grpc_test.go -> admin_auth_grpc_test.go for consistency with
the per-domain admin_<domain>_grpc_test.go test files.

* test(admin-api): REST coverage for users + access ops

* test(admin-api): REST coverage for audit + fga ops; GraphQL AuditLogs test

* test(admin-api): REST coverage for webhook + email-template ops

* test(admin-api): REST test harness + auth/meta REST coverage

2.3.0-rc.7

Toggle 2.3.0-rc.7's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(storage): silence GORM logger in legacy-uniqueness cleanup (#632)

clearLegacyColumnUniqueness probes information_schema, which errors on
databases without that catalog (sqlite). The SQL provider uses GORM's
default logger, which writes such errors to os.Stdout — and the MCP server
speaks JSON-RPC over stdio, so the stray line corrupted the stream and
broke `make smoke` (TestReleaseSmoke/mcp_stdio: "unexpected end of JSON
input").

Run the cleanup through a discarding GORM logger; actionable failures are
already surfaced via the zerolog logger (stderr). No behaviour change to
the migration itself.

2.3.0-rc.6

Toggle 2.3.0-rc.6's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(storage): clear legacy unique email/phone objects name-agnostical…

…ly (< 2.3.0 upgrades) (#629)

* fix(storage): handle idx_-named unique constraints on email/phone

A v1 gorm:"uniqueIndex" can be stored as a UNIQUE *constraint* named
idx_<table>_<col> whose backing index cannot be dropped with DROP INDEX
("constraint ... requires it"), and GORM's GetIndexes does not surface
constraint-backed indexes — so the previous index-only path missed it and
AutoMigrate still aborted with SQLSTATE 42704 (seen in the field on
authorizer_otps.phone_number).

Add idx_<table>_<col> to the constraint-name drop list so it is removed
via DROP CONSTRAINT. Extend the migration test to seed all three real
forms: a *_key constraint, an idx_-named constraint, and a standalone
unique index.

* fix(storage): drop legacy unique email/phone objects name-agnostically

The previous approach enumerated known constraint names (<table>_<col>_key,
uni_<table>_<col>, idx_<table>_<col>). Any other name — e.g. a custom
constraint from a hand-rolled v1 migration — still slipped through and
aborted startup with the GORM "DROP CONSTRAINT uni_<table>_<col>" /
SQLSTATE 42704 failure.

Replace the name guessing with a catalog-driven sweep: read the actual
single-column UNIQUE constraint names on users/otps email & phone_number
from information_schema.table_constraints (the same source GORM's
MigrateColumnUnique reads to decide a column is unique) and DROP CONSTRAINT
each by its real name, then drop any standalone single-column UNIQUE index.
Guards every < 2.3.0 upgrader regardless of how the constraint was named.

Best-effort and non-fatal; sqlite (no information_schema, unaffected) and
fresh installs are no-ops. Test now also seeds a custom-named constraint.

2.3.0-rc.5

Toggle 2.3.0-rc.5's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(storage): drop stale unique email/phone constraints on upgrade (#628

)

* fix(storage): drop stale unique email/phone constraints on upgrade

v1 declared email and phone_number UNIQUE on authorizer_users and
authorizer_otps; v2 keeps non-unique indexes and enforces uniqueness in
the application layer. On an upgraded SQL database GORM AutoMigrate emits
DROP CONSTRAINT "uni_<table>_<col>" — a name that does not match what the
DB created — and aborts with SQLSTATE 42704, so the server fails to start
("failed to create storage provider").

Clear the legacy uniqueness before AutoMigrate for all four columns. The
old form is a constraint (<table>_<col>_key / uni_<table>_<col>) on some
v1 releases and a standalone unique index (idx_<table>_<col>) on others,
so handle both: drop the named constraints, then drop any single-column
UNIQUE index on email/phone_number via GetIndexes (name-agnostic, leaves
the current non-unique indexes intact). Non-fatal; fresh installs are
unaffected. Generalises the prior phone_number-only handling.

* test(storage): cover email/phone uniqueness and stale-constraint migration

Two SQL provider tests:

- TestProviderEmailPhoneUpdatesAndUniqueness (all SQL backends): a user can
  update their own email and phone number seamlessly, duplicates across
  different users are still rejected (app-layer uniqueness), and OTPs key
  independently on email and phone.
- TestStaleUniqueConstraintMigration (Postgres): seeds a legacy UNIQUE
  constraint on users.email and a legacy UNIQUE index on otps.phone_number,
  then asserts startup drops them (no SQLSTATE 42704 abort), the search index
  is preserved as non-unique, and a post-migration phone update succeeds.

Honors TEST_DBS; defaults to sqlite locally, Postgres path runs in CI.

2.3.0-rc.4

Toggle 2.3.0-rc.4's commit message

Verified

This commit was signed with the committer’s verified signature.
lakhansamani Lakhan Samani
chore: update docker and release

2.3.0-rc.3

Toggle 2.3.0-rc.3's commit message

Verified

This commit was signed with the committer’s verified signature.
lakhansamani Lakhan Samani
chore: add smoke test

2.3.0-rc.2

Toggle 2.3.0-rc.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(authz): replace bespoke FGA with embedded OpenFGA ReBAC engine (#…

…625)

* feat(authz): replace bespoke FGA with embedded OpenFGA ReBAC engine

Remove the not-yet-rolled-out Resource/Scope/Policy/Permission engine
(#607/#610/#611) and replace it with an OpenFGA-backed ReBAC engine.

- AuthorizationEngine SPI (internal/authorization/engine) with an embedded
  OpenFGA implementation (memory/sqlite/postgres/mysql datastores) plus
  external-mode flag scaffolding.
- GraphQL: admin _fga_write_model/_fga_get_model/_fga_write_tuples/
  _fga_delete_tuples/_fga_read_tuples and runtime fga_check/fga_batch_check/
  fga_list_objects (runtime principal pinned to the token subject).
- required_relations on session/validate_session/validate_jwt_token;
  coarse roles/scope gating unchanged.
- Dashboard FGA admin UI: authorization model editor, relationship tuples,
  access tester.
- Standardize the SQLite driver on modernc.org/sqlite via a local GORM
  dialect so the embedded OpenFGA SQL datastore links without a duplicate
  database/sql "sqlite" registration.

Flags: --authorization-engine, --fga-mode, --fga-store, --fga-store-url,
--fga-external-url.

* docs(authz): OpenFGA migration plan, agentic-auth design, enterprise model

Add design docs for the OpenFGA migration and the agentic-authorization
program, and update the v2 roadmap.

- FGA_OPENFGA_MIGRATION_PLAN.md: phased plan, locked decisions, deployment
  modes (single-node / HA / serverless), implementation status.
- ENTERPRISE_AUTHZ_MODEL.md: OpenFGA model patterns (role grants,
  user-specific overrides, exclusions, hierarchy) with a worked example.
- AGENTIC_DELEGATION_DESIGN.md: RFC 8693 token exchange, act claim,
  attenuation, audit delegation chain, revocation.
- FGA_IMPLEMENTATION_AGENTS.md: program execution plan.
- ROADMAP_V2.md: agentic authorization track; corrected FGA/audit status.

* feat(authz): add _fga_list_users/_fga_expand and trust-gated subject on FGA checks

- Admin introspection ops `_fga_list_users` and `_fga_expand` (super-admin
  gated). These reveal the access graph (who-can-access / why), so they are
  admin-only rather than end-user facing.
- Optional, trust-gated `user` on `fga_check`/`fga_batch_check`/
  `fga_list_objects`: a super-admin may query an explicit subject; an ordinary
  end-user token stays pinned to its own subject and a client-supplied `user`
  is rejected (prevents enumerating another user's access). Centralized in
  resolveFgaSubject; M2M/client-credentials callers to be allowed in Phase 2.
- Engine SPI: ListUsers and Expand methods on AuthorizationEngine.

* test(authz): close FGA coverage gaps

Add tests for previously-uncovered surface:
- _fga_delete_tuples (removes a tuple; non-admin rejected)
- _fga_get_model (returns active model; non-admin rejected)
- trust gate enforced per decision op: fga_list_objects and fga_batch_check
  reject an ordinary user supplying another subject (not only fga_check)
- session query honors required_relations (separate wiring of the same
  helper as validate_session)

* fix(authz): _fga_get_model returns model id; cover validate_jwt_token relations

- engine.ReadModel now returns (id, dsl): _fga_get_model previously returned an
  empty FgaModel.id while _fga_write_model returned one. Populate it from the
  active OpenFGA model id.
- Add a validate_jwt_token required_relations test (the third entry point of
  the shared enforceRequiredRelations helper); re-logs in for a fresh
  access token since session ops in earlier subtests rotate the original.

* refactor(authz): drop --authorization-engine flag; enable FGA via store config

The two-engine selector (--authorization-engine=policy|fga) was a vestige of
the SPI design — the policy engine was removed entirely, leaving only OpenFGA.
FGA is now enabled by configuring a store: --fga-store (embedded) or
--fga-external-url (external). With neither set the engine is not constructed
and the fga_* resolvers fail closed, identical to the previous default.

- Remove the AuthorizationEngine config field and CLI flag.
- --fga-store defaults to "" (set it to enable embedded FGA).
- Update stale comments/schema descriptions referencing the removed flag.

* refactor(authz): drop external-mode + dead old-engine flags; embed-only FGA

Authorizer embeds OpenFGA in-process — it IS the engine. Trim the FGA config
surface to what's actually used:

- Remove --fga-mode and --fga-external-url: external-OpenFGA-service mode was a
  non-functional stub (logged a warning, started no engine). HA/serverless use
  the embedded engine + an external SQL store (postgres/mysql), not a separate
  service. The AuthorizationEngine SPI still allows adding an external client
  later if a real need arises.
- Remove three dead flags left from the old policy engine, with zero consumers
  after its removal: --authorization-cache-ttl, --include-permissions-in-token,
  --authorization-log-all-checks.

FGA is now enabled solely by --fga-store (+ --fga-store-url). Build + full
SQLite suite green.

* fix(dashboard): correct FGA "not enabled" message; polish empty states

The "not enabled" empty state referenced the removed --authorization-engine=fga
flag. Rewrite it as a helpful empty state: correct enable command (--fga-store)
with copy-to-clipboard, store options (memory/sqlite/postgres/mysql), and a docs
link, styled to the dashboard's blue accent. Also replace the bare "No Tuples"
empty state with guidance on what a tuple is and how to grant the first one.

* feat(authz): FGA reuses the main database; --fga-store is now an override

When the main database is OpenFGA-compatible (sqlite/postgres/mysql/mariadb),
FGA derives its store from --database-url automatically — no extra flags, with
OpenFGA's tables living in the main DB (as the old engine did). --fga-store /
--fga-store-url become overrides, required only when the main DB is unsupported
(mongodb, dynamodb, cassandra, couchbase, arangodb, sqlserver) or to use a
dedicated store.

- config.FGAStoreConfig() resolves the store (explicit override > main-DB
  derivation > disabled); unit-tested across the matrix.
- Migrations run on boot for SQL stores (idempotent, goose-locked → HA-safe).
- Dashboard "not enabled" copy updated to explain auto-reuse + the override.

Verified: a SQLite-configured instance auto-enables FGA (reused_main_db=true)
with no --fga-store and no driver-registration panic.

* test(authz): FGA disabled (and instance works) for unsupported DBs without store

- config: for every database OpenFGA can't use (mongodb, dynamodb, cassandra,
  scylla, couchbase, arangodb, sqlserver, libsql, cockroachdb, yugabyte,
  planetscale), FGAStoreConfig returns disabled when --fga-store/--fga-store-url
  are unset; an explicit --fga-store still enables it.
- integration: validate_session without required_relations succeeds when no FGA
  engine is configured — the instance works normally without FGA.

* feat(dashboard): visual authorization-model builder (no DSL needed)

Replace the raw-DSL-only model editor with a visual builder that generates
OpenFGA DSL under the hood, plus a "DSL (advanced)" escape hatch:

- ModelBuilder: add/edit types, relations and permissions via forms — direct
  assignment (chips), unions, and inheritance ("X from Y"), no DSL knowledge
  needed.
- modelDsl.ts: generateDsl / parseDsl (best-effort) / validateModel /
  plain-English summarize + 3 starter templates (document sharing, folder
  inheritance, org/team/project). Verified round-trip; advanced constructs
  (and / but not / conditions) keep the user in DSL mode.
- Model page: Builder <-> DSL tabs, template chips, live "what this model
  means" summary, clearer intro copy. Loads an existing model into the builder
  when representable, else opens DSL.

* feat(dashboard): guided 3-step FGA flow, worked examples, collapsible nav

Turn the three Authorization pages into a clear guided workflow:

- AuthSteps: a shared, clickable stepper (1 Define model → 2 Grant access →
  3 Test access) shown on each page, with done/current/upcoming states. Steps
  stay deep-linkable so admins can jump directly.
- Each page now leads with "Step N · <title>", a concrete worked Example
  callout (document-sharing running example), and a "Next →" link to continue.
- "RBAC — your roles" model template generated from the instance's configured
  roles (fetched via admin _env), with role-name sanitization. Round-trip
  verified.
- Sidebar: the Authorization group is now collapsible (chevron, aria-expanded),
  default-open when on an authorization route.

* refactor(dashboard): replace fragile model builder with react-arborist tree

The hand-rolled form builder was fragile (delete bug, cluttered layout). Replace
it with a robust master-detail tree editor:

- react-arborist tree shows types -> relations (expand/collapse, keyboard nav,
  per-node add/delete, selection); a detail pane edits the selected node's name,
  assignable types, and computed terms. Builder | DSL stays as two tabs.
- All model edits go through pure, unit-tested mutation helpers in modelDsl.ts
  (add/delete/rename type & relation, add/remove assignable & computed) — this
  eliminates the in-place-mutation delete bug at the source. Verified by a
  standalone mutation test.
- Removed the bespoke ModelBuilder.tsx.

* feat(dashboard): simple example-driven model editor with a full example catalog

Replace the confusing tree/builder + Builder/DSL sub-tabs with one simple,
example-driven editor:

- A catalog of 9 ready-to-use OpenFGA model examples (raw DSL, so they use the
  full language): document sharing, folder hierarchy, organizations & teams,
  RBAC roles, groups, block list (exclusion), multi-tenant SaaS, GitHub-style
  repos, and time-bound access (conditions) — plus a dynamic "Your roles"
  example. Each card shows a description; clicking loads it into the editor.
- One DSL editor + a live plain-English summary + Save. No tree, no builder,
  no model sub-tabs. CRUD is load/edit/save.
- All 9 examples validated against the OpenFGA DSL transformer (the same one
  the backend uses on save). Removed react-arborist and ModelTree.tsx.

* fix(dashboard): Authorization nav no longer looks disabled

The collapsible group header was styled as a faded uppercase section label
(text-gray-400, uppercase), which read as a disabled item. Style it like a
normal nav entry (text-sm, gray-700, blue-50 when active).

* feat(dashboard): FGA docs links, grant-pattern examples, accurate stepper

- DocsLinks: links to OpenFGA / ReBAC concepts, modeling guide, DSL reference,
  and relationship tuples — shown on the Model and Grant-access pages.
- Grant-access page: "Common grant patterns" cards (direct, assign a role,
  grant a whole role via role#assignee, public user:*, and grant-on-a-folder so
  all resources inherit) that prefill the form, plus a tip on avoiding a tuple
  per object id.
- Model page: switching to an example now confirms if there are unsaved changes
  and shows a toast; a note explains there is one active model and saving makes
  a new immutable version active.
- Stepper now marks a step done only when actually complete (model saved /
  tuples exist), so step 1 isn't checked when no model exists.

* docs(dashboard): explain model versioning in the model editor

Add an "About model versions" info panel: one active model, saving creates a
new immutable version, earlier versions are retained, OpenFGA models are
append-only (a version can't be deleted individually), and separate models
need separate stores.

* feat(fga): add guarded reset for the authorization model

OpenFGA models are append-only — individual versions cannot be deleted.
Reset is the only way to remove a model and all its past versions and
start fresh.

- engine: add Reset() to the AuthorizationEngine SPI; OpenFGA impl deletes
  the store (model + all versions + tuples) and creates a new empty one
- graphql: add _fga_reset mutation, super-admin gated and audited
  (admin.fga_reset). Refused while any relationship tuples still exist so
  live grants are never dropped silently — callers must delete tuples first
- dashboard: "Danger zone" on the model page. Disabled with a link to the
  Grant access page while tuples exist; otherwise a typed-confirmation
  dialog (type RESET) before wiping
- test: TestOpenFGAEngine_Reset covers store rotation, model clearing,
  tuple removal, and engine reuse

* feat(fga): empty-model state + Prometheus metrics for FGA resolvers

- Add engine.ErrNoModel sentinel; ReadModel returns it on a fresh store so
  callers treat "no model yet" as an empty state, not a failure. FgaGetModel
  maps it to an empty model for the dashboard's starting view. Fail-closed is
  unchanged — Check/BatchCheck/ListObjects still deny on a model-less store.
- Add authorizer_fga_checks_total, authorizer_fga_check_duration_seconds and
  authorizer_fga_operations_total, recorded across the FGA resolvers. Only
  low-cardinality constant labels are ever used as label values.
- Tests: ErrNoModel sentinel (engine), empty-model GraphQL state + metric
  recording (integration), metric helpers (unit).

* feat(dashboard): friendly FGA model builder, example modals, tester subject

- Step 1 is now two-mode: a roles × permissions matrix (RbacBuilder, the
  default for non-developers) that generates a standard OpenFGA RBAC model,
  plus the Advanced (DSL) editor. No syntax to learn to define a model.
- Example catalogs (model examples and grant patterns) moved into modal
  popups so the editor and the add-tuple form stay the focus.
- Tester gains a User (subject) field so a super-admin can check any subject;
  result copy reflects the checked subject. Server already gates the override
  to admins.
- Grant page guards against writing tuples before a model exists, and only
  blocks on a genuine no-model error — never on a transient failure.
- Drop the dead _env.ROLES / AdminRolesQuery fetch.
- Add vitest + modelDsl.test.ts unit coverage (rbacModel, parse, summarize,
  example catalog).

* feat(fga): _admin_meta query + seed model builder from configured roles

- Add admin-only _admin_meta query (AdminMeta type) returning the configured
  roles / default_roles / protected_roles. Super-admin gated; the non-deprecated
  replacement for the role bits of _env (deprecated in v2).
- Dashboard model builder seeds its roles × permissions matrix from the real
  configured roles via _admin_meta, falling back to a generic set. The builder
  mounts only after the roles fetch settles so it never locks in the fallback.
- Test: admin_meta_test.go (super-admin gated, returns configured roles).

* docs(fga): ReBAC hierarchy guide, concentric examples, user-id convention

- Add docs/fga-rebac-guide.md: app vs FGA roles, identifying subjects by
  user:<id> (not names), org→project→resource hierarchy (grant once, inherit
  everywhere), and fine-grained grants that coexist with inheritance.
- Add "Org → project → resource" and "Company roles (RBAC)" model examples;
  make both concentric (editor implies viewer; permissions reference the next
  more-powerful one) per OpenFGA's concentric-relationships guidance.
- Add hierarchy_test.go proving inheritance from one org-level grant, scoped
  fine-grained grants, and concentric view, all keyed by user:<id>.
- Grant form nudges admins to use the user's id, not a name.

* fix(fga): align all shipped models with OpenFGA best practices + validate in CI

Reviewed every shipped model against openfga/agent-skills (the official
OpenFGA modeling rules):

- Folder hierarchy example: chain owner down (`owner from parent_folder`) so a
  folder owner can edit its documents — was the documented "parent role
  forgotten on child types" anti-pattern; rename parent → parent_folder per
  the naming convention; add folder can_view.
- Organizations & teams example: add can_view so apps check a permission, not
  the member relation directly.
- Model editor placeholder: concentric (editor implies viewer) instead of
  independent viewer/editor unioned in can_view.
- Add examples_validation_test.go: extracts every DSL from the dashboard
  catalog, the editor placeholder, and docs/fga-rebac-guide.md and writes each
  through the real embedded engine — the in-repo equivalent of
  `fga model validate`, so a malformed example can never ship.

* docs(specs): v1→v2 migration tool design spec

* fix(dashboard): user:<id> examples everywhere + grant-form alignment

- Replace every user:alice example, placeholder and grant-pattern prefill with
  the user:<id> / user:<user-id> convention the docs recommend — names aren't
  unique or stable; point admins at the Users page for the id.
- Fix the Grant access form alignment: the id hint under the User column made
  it taller than the other columns in the items-end grid; the hint is now a
  full-width row below the inputs so all fields and the Add button align.

* feat(dashboard): generic RBAC seed with instance roles as suggestions, id-only examples

- The model builder now always starts from the standard admin/editor/viewer
  matrix; the instance's configured roles are offered as one-click suggestion
  chips instead of being forced in as the seed (app roles like "user" make
  poor object-scoped FGA roles).
- Grant-pattern prefill uses folder:<folder-id>; ReBAC guide examples now use
  numeric object ids (organization:101, project:201, resource:301) — objects,
  like users, are identified by id, never by name. role:* objects stay keyed
  by role name by design.

* feat(fga)!: check_permissions + list_permissions public API, one resolver per file

BREAKING (branch-only, never released): replaces fga_check, fga_batch_check
and fga_list_objects.

- Public surface is now exactly two operations:
  - check_permissions(checks: [{relation, object, contextual_tuples?}], user?)
    → results echo each pair with allowed (a single check is a batch of one).
  - list_permissions(relation, object_type, user?) → objects.
- Subject trust gate (resolveFgaSubject): defaults to the caller's token
  subject; an explicit `user` (bare id normalized to user:<id>) is honored
  only for super-admins or when it equals the caller's own subject — anything
  else is rejected, never silently ignored.
- Resolvers restructured one-per-file: fga.go (shared helpers + gate),
  check_permissions.go, list_permissions.go, fga_write_model.go,
  fga_get_model.go, fga_write_tuples.go, fga_delete_tuples.go,
  fga_read_tuples.go, fga_list_users.go, fga_expand.go, fga_reset.go.
- Dashboard: Access Tester page removed (the wizard is now 2 steps); per-user
  verification moved to Users table → "View Permissions" modal, which calls
  list_permissions with an explicit subject under the admin session.
- Metrics labels: check_permissions / list_permissions.
- Integration tests rewritten, including a new self-specification case
  (non-admin passing their own subject is honored).

* docs: point openfga-modeling skill reference at the new permission APIs

* docs(fga): note exact-string self-match semantics in the trust gate

* fix(fga): actionable error when a tuple doesn't match the model

Adding a tuple whose relation or object type isn't in the active model
surfaced OpenFGA's raw gRPC error ("rpc error: code = Code(2000) desc =
Invalid tuple ..."), which read as "can't add grant access".

- Map tuple-validation errors in _fga_write_tuples/_fga_delete_tuples to a
  friendly message that keeps OpenFGA's reason and points at Step 1; raw
  error stays in the debug log. Covered by an integration test (also asserts
  no gRPC internals leak).
- Grant-pattern modal now states tuples must match YOUR model; the folder
  pattern notes it needs a folder type.

* docs!: move design specs and guides to the authorizer-docs repo

All program design docs (FGA migration plan, agentic delegation design,
enterprise authz model, implementation agents, migration-tool spec) and the
ReBAC guide now live in the authorizer-docs repo under specs/. References in
CLAUDE.md and ROADMAP_V2.md point there. The docs-guide DSL validation
subtest is removed with the guide; dashboard example validation stays.

* security(fga): cap contextual tuples per check at the API boundary

check_permissions accepted unbounded contextual-tuple arrays from any
authenticated caller, relying on the embedded OpenFGA default limit as the
only guard. Enforce an explicit cap (100) in toContextualTuples with unit
coverage so the boundary no longer depends on engine configuration.

* fix(fga)!: recover store and model across restarts; non-fatal engine init

The engine created a fresh OpenFGA store on every boot whenever no StoreID
was passed — and no caller ever persisted one — so on SQL-backed deployments
a restart orphaned the model and every tuple, and all checks failed with
'no authorization model written yet' until an admin rebuilt everything.

New() now recovers the existing store by exact name via ListStores and
adopts the store's latest authorization model, so persistent deployments
survive restarts with zero operator action. Covered by a restart-continuity
test that boots a second engine on the same SQLite file and asserts the
original decisions still hold.

Engine-init failure no longer log.Fatal()s the instance: FGA is optional,
so init errors (e.g. missing DDL rights for OpenFGA migrations) now log
and leave the engine nil — permission APIs fail closed, core auth keeps
serving. Also inlines the no-op strconvItoa wrapper.

* feat(fga): list_permissions returns all subject permissions when filters omitted

relation and object_type are now optional on list_permissions. When either
is omitted, every matching (type, relation) pair of the active model is
enumerated — an empty input answers "what can this user access?" in one
call. Pairs come from the new TypeRelations engine SPI method and are
expanded via ListObjects with bounded concurrency (5) so a single request
cannot saturate the embedded engine.

The response now carries (object, relation) detail in permissions[] and an
explicit truncated flag when the 1000-entry cap is hit, replacing the
previous silent truncation. The subject trust gate is unchanged: callers
enumerate their own access unless super-admin.

* feat(dashboard): user permissions modal lists everything by default

The Users-table permissions modal now treats both filters as optional,
matching the new list_permissions API: an empty form lists every permission
the user holds. Results render as (object, permission) rows instead of bare
object ids, and a notice appears when the server truncated at 1000 entries.

* feat(dashboard): copyable user ID under the email in the Users table

FGA tuples and permission lookups need the user's UUID; admins previously
had to open the user detail view to get it. The ID now shows muted and
monospaced under the email with a one-click copy button (existing
clipboard + toast pattern); the click does not trigger the row's detail
view.

* feat(dashboard): permissions modal auto-loads the full list on open

The Users-table permissions modal now fetches everything the user can
access the moment it opens — no filter input or button click required.
The form is purely a narrowing filter (Apply filters / Refresh), skeleton
rows show while loading, and all state resets on close so the next open
starts fresh for any user.

* test(fga): fail-closed coverage for every admin op + explicit store override

TestFGADisabled now asserts that ALL admin FGA ops — including every write
path (_fga_write_model, _fga_write_tuples, _fga_delete_tuples, _fga_reset)
plus _fga_get_model, _fga_read_tuples, _fga_list_users, _fga_expand and the
public list_permissions — return the not-enabled error when no engine is
configured, even for a super admin. This proves no FGA record can be
created via the API on an unsupported database without --fga-store, and is
the exact error that switches the dashboard's Authorization tab into its
FgaNotEnabled state.

TestFGAExplicitStoreOverrideForUnsupportedDB proves the other direction at
the config→engine seam: a mongodb main DB with explicit --fga-store/
--fga-store-url resolves to an enabled FGA config, and an engine built from
it exactly as cmd/root.go wires it serves model writes, tuple writes, and
checks.

* test(dashboard): FgaNotEnabled rendering + not-enabled error detection

Adds the first component-level dashboard tests: FgaNotEnabled (what the
Authorization tab shows on databases without OpenFGA support and no
--fga-store) must explain the state and surface the exact flags that fix
it, and isFgaNotEnabledError — the single decision point that switches the
tab into that state — is covered for the backend message, case variants,
unrelated errors, and missing input. Component tests opt into jsdom per
file; pure DSL tests stay on the node environment.

New dev-only deps: jsdom, @testing-library/react, @testing-library/dom.

2.3.0-rc.1

Toggle 2.3.0-rc.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Remove unused badges from README

Removed CLOMonitor and Fuzzing Status badges from README.