Skip to content

Commit 3fd4777

Browse files
authored
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.
1 parent fcd34c0 commit 3fd4777

145 files changed

Lines changed: 17444 additions & 22648 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ Detailed rules load via skills (see below) — don't restate them here.
5858
| `principal-engineer` | opus | Full SDLC: Plan → Execute → Test → Review across Go, storage, GraphQL, HTTP. Use for any change touching >1 subsystem. |
5959
| `security-engineer` | opus | OAuth2/OIDC, JWT, MFA, vulnerability audit. Second-pass on auth-sensitive PRs. |
6060
| `doc-writer` | haiku | API docs, guides, migration docs. |
61+
| `authz-researcher` | opus | Deep, adversarially-verified research on authz standards (OpenFGA, RFC 8693/8707/9728, MCP, CIBA, AuthZEN). Run before designing/building any authz capability. |
62+
| `fga-engineer` | opus | Implements the OpenFGA migration (Wave 1) per specs/FGA_OPENFGA_MIGRATION_PLAN.md (authorizer-docs repo). |
63+
| `delegation-engineer` | opus | Implements the agentic delegation chain (Wave 2) per specs/AGENTIC_DELEGATION_DESIGN.md (authorizer-docs repo). Security-critical. |
6164

6265
## Project Skills (auto-load on matching files)
6366

@@ -70,6 +73,8 @@ Detailed rules load via skills (see below) — don't restate them here.
7073
| `authorizer-security` | auth-sensitive code or `security/` branches |
7174
| `authorizer-testing` | any `*_test.go` |
7275
| `authorizer-frontend` | `web/app/`, `web/dashboard/` |
76+
| `openfga-modeling` | FGA engine (`internal/authorization/`), authz models/tuples, `check_permissions`/`list_permissions`/`_fga_*` GraphQL |
77+
| `agentic-auth-standards` | token exchange, delegation, MCP, agent-identity (`internal/token/`, `internal/http_handlers/`) |
7378

7479
## Token Optimization Notes
7580

ROADMAP_V2.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818
| 13+ Database backends | Done | Unique differentiator |
1919
| Rate Limiting / Brute Force | Missing | No protection at all |
2020
| M2M / Client Credentials | Missing | No service accounts or API keys |
21-
| Fine-Grained Permissions | Missing | Roles only, no resource-level control |
21+
| Fine-Grained Permissions | In progress (pre-stable) | RBAC/ABAC Resource/Scope/Policy/Permission engine exists; migrating to OpenFGA ReBAC — see specs/FGA_OPENFGA_MIGRATION_PLAN.md (authorizer-docs repo) |
2222
| SAML | Missing | Zero support |
2323
| SCIM / Directory Sync | Missing | No provisioning |
24-
| Audit Logs | Missing | Webhooks only, no queryable audit trail |
24+
| Audit Logs | Partial | Structured AuditLog + audit provider exist (single actor); needs delegation-chain fields for agents — see specs/AGENTIC_DELEGATION_DESIGN.md (authorizer-docs repo) |
2525
| Bot Detection | Missing | No CAPTCHA, no fingerprinting |
2626
| Monitoring / Metrics | Missing | Health check only, no Prometheus |
2727
| MCP Auth | Missing | No OAuth 2.1 AS capabilities |
@@ -328,6 +328,40 @@ These are table-stakes features that every competitor has. Without them, Authori
328328

329329
---
330330

331+
## Agentic Authorization Track (cross-phase sequencing)
332+
333+
> A re-sequencing lens over the items above, ordered by dependency for enterprise + agentic auth. Authorization for agents is a **pipeline**, not one feature; each wave builds on the last. ReBAC is necessary but not sufficient. Design detail: `../authorizer-docs/specs/FGA_OPENFGA_MIGRATION_PLAN.md`, `../authorizer-docs/specs/AGENTIC_DELEGATION_DESIGN.md`.
334+
335+
**The pipeline (evaluated per request):** Identity → Authentication → Token/Delegation → Authorization (scope → RBAC → ReBAC → ABAC → list_objects) → Human-in-the-loop → Governance.
336+
337+
### Wave 1 — Decision core *(now; replaces 2.1)*
338+
Object-level authorization + the RAG primitive.
339+
- [ ] **OpenFGA ReBAC engine** (replaces Resource/Scope/Policy/Permission) — `Check`, `list_objects`, `batch_check`, Conditions (ABAC). SQL-backed FGA store (embedded default / external optional).
340+
- [ ] **Keep & integrate** OAuth scopes (coarse gate), RBAC roles (optionally mirrored as tuples), custom-token-script hook (may call FGA).
341+
- [ ] **FGA-for-RAG** SDK helpers (`list_objects` pre-filter) in authorizer-go / authorizer-js.
342+
- *Unlocks:* document sharing, hierarchical/B2B authz, secure RAG retrieval.
343+
344+
### Wave 2 — Delegation core *(next; folds in 2.2, 4.2, 4.3, 5.5)*
345+
Who is asking, with whose borrowed authority, constrained to what.
346+
- [ ] **Agent identity** — agents as first-class service-account principals (4.2).
347+
- [ ] **RFC 8693 token exchange** + **`act` delegation claim** (5.5 / 4.2) — see design doc.
348+
- [ ] **Attenuation / least-privilege** — exchanged-token scope = `subject ∩ requested ∩ agent ceiling` (reuses `Principal.MaxScopes`) (4.3).
349+
- [ ] **Audit delegation chain**`on_behalf_of` + `act` chain on AuditLog (all DBs).
350+
- *Unlocks:* "agent acting for user, least-privilege, fully audited."
351+
352+
### Wave 3 — Async + credential custody
353+
Human approval and safe third-party access.
354+
- [ ] **CIBA + Rich Authorization Requests (RAR)** — out-of-band human approval for sensitive agent actions.
355+
- [ ] **Token Vault** — encrypted per-user third-party token custody; agent never sees raw credentials.
356+
- *Unlocks:* human-in-the-loop, agents calling Google/Slack/etc. on a user's behalf.
357+
358+
### Wave 4 — Enterprise hardening
359+
- [ ] **MCP authorization** (OAuth 2.1 + RFC 9728 + RFC 8707) (4.1) and **ID-JAG / Cross-App Access** for enterprise-managed MCP.
360+
- [ ] **JIT / time-bound grants** (TTL tuples), **per-agent guardrails** (spend/rate limits), **consent management**.
361+
- *Unlocks:* enterprise-managed agent deployments at scale.
362+
363+
---
364+
331365
## Phase 5: Advanced Security & Enterprise (Q2-Q3 2027)
332366

333367
### 5.1 Passkeys / WebAuthn

cmd/root.go

Lines changed: 54 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import (
1515

1616
"github.com/authorizerdev/authorizer/internal/audit"
1717
"github.com/authorizerdev/authorizer/internal/authenticators"
18-
"github.com/authorizerdev/authorizer/internal/authorization"
18+
"github.com/authorizerdev/authorizer/internal/authorization/engine"
19+
fgaengine "github.com/authorizerdev/authorizer/internal/authorization/engine/openfga"
1920
"github.com/authorizerdev/authorizer/internal/config"
2021
"github.com/authorizerdev/authorizer/internal/constants"
2122
"github.com/authorizerdev/authorizer/internal/email"
2223
"github.com/authorizerdev/authorizer/internal/events"
23-
"github.com/authorizerdev/authorizer/internal/graph/model"
2424
"github.com/authorizerdev/authorizer/internal/http_handlers"
2525
"github.com/authorizerdev/authorizer/internal/memory_store"
2626
"github.com/authorizerdev/authorizer/internal/metrics"
@@ -237,10 +237,12 @@ func init() {
237237
// Back-channel logout (OIDC BCL 1.0)
238238
f.StringVar(&rootArgs.config.BackchannelLogoutURI, "backchannel-logout-uri", "", "URL to POST a signed logout_token to when users log out successfully. Leave empty (default) to disable back-channel logout notifications. See OIDC Back-Channel Logout 1.0.")
239239

240-
// Fine-grained authorization flags
241-
f.Int64Var(&rootArgs.config.AuthorizationCacheTTL, "authorization-cache-ttl", 300, "Cache TTL in seconds for permission checks (0 to disable)")
242-
f.BoolVar(&rootArgs.config.IncludePermissionsInToken, "include-permissions-in-token", false, "Include permissions in JWT access tokens")
243-
f.BoolVar(&rootArgs.config.AuthorizationLogAllChecks, "authorization-log-all-checks", false, "Audit log all permission checks, not just denials")
240+
// OpenFGA fine-grained authorization. By default FGA reuses the main database
241+
// when it is sqlite/postgres/mysql/mariadb (no extra config needed). These
242+
// flags override that — required only when the main DB is unsupported
243+
// (mongodb, dynamodb, …) or to use a dedicated FGA store.
244+
f.StringVar(&rootArgs.config.FGAStore, "fga-store", "", "Override the OpenFGA datastore: 'sqlite', 'postgres', 'mysql', or 'memory' (dev). Default: reuse the main database when it is SQL-compatible; required only for unsupported main DBs (mongodb, dynamodb, …)")
245+
f.StringVar(&rootArgs.config.FGAStoreURL, "fga-store-url", "", "Connection URI for an overridden --fga-store (file: URI for sqlite, DSN for postgres/mysql). Ignored when FGA reuses the main database")
244246

245247
// Deprecated flags
246248
f.MarkDeprecated("database_url", "use --database-url instead")
@@ -462,34 +464,51 @@ func runRoot(c *cobra.Command, args []string) {
462464
}
463465
defer rateLimitProvider.Close()
464466

465-
// Authorization provider
466-
authorizationProvider, err := authorization.New(
467-
&authorization.Config{
468-
CacheTTL: rootArgs.config.AuthorizationCacheTTL,
469-
},
470-
&authorization.Dependencies{
471-
Log: &log,
472-
StorageProvider: storageProvider,
473-
MemoryStoreProvider: memoryStoreProvider,
474-
},
475-
)
476-
if err != nil {
477-
log.Fatal().Err(err).Msg("failed to create authorization provider")
478-
}
479-
480-
// Check once at startup whether any permissions exist. If zero, emit a
481-
// loud warn so operators don't lock themselves out in prod. Bounded
482-
// context prevents a hung DB at boot from blocking startup indefinitely.
483-
probeCtx, probeCancel := context.WithTimeout(context.Background(), 5*time.Second)
484-
_, pr, lerr := storageProvider.ListPermissions(probeCtx, &model.Pagination{Limit: 1, Page: 1})
485-
probeCancel()
486-
switch {
487-
case lerr != nil:
488-
log.Warn().Err(lerr).Msg("authz: failed to probe permission count at startup; authorization is enforcing")
489-
case pr != nil && pr.Total == 0:
490-
log.Warn().Msg("authz: 0 permissions configured — all authorization checks will DENY. Seed permissions via the dashboard or admin GraphQL mutations.")
491-
default:
492-
log.Info().Msg("authz: enforcing; unmatched CheckPermission calls will be DENIED.")
467+
// OpenFGA fine-grained authorization engine (embedded, in-process).
468+
//
469+
// Authorizer embeds OpenFGA — it IS the engine. The engine is constructed
470+
// only when an FGA store is configured (--fga-store); otherwise it stays nil
471+
// and the fga_* resolvers fail closed ("fine-grained authorization is not
472+
// enabled"). Routed into GraphQL and session/validate below.
473+
//
474+
// By default FGA reuses the main database (sqlite/postgres/mysql/mariadb);
475+
// --fga-store is only needed when the main DB is unsupported (mongodb,
476+
// dynamodb, etc.) or to point at a dedicated store. FGAStoreConfig() resolves
477+
// this. OpenFGA migrations run on boot for SQL stores (idempotent); memory
478+
// needs none. NOTE: multi-replica deployments should prefer running
479+
// migrations once via an init job — concurrent on-boot migrations rely on
480+
// the migration tool's own locking and add cold-start latency.
481+
//
482+
// Engine-init failure is deliberately NON-fatal: FGA is an optional
483+
// subsystem, so a failure here (e.g. the DB user lacks DDL rights for the
484+
// OpenFGA tables) logs loudly and leaves authzEngine nil — fga_* and the
485+
// permission APIs fail closed while core authentication keeps serving.
486+
var authzEngine engine.AuthorizationEngine
487+
if fgaStore, fgaStoreURL, fgaEnabled := rootArgs.config.FGAStoreConfig(); fgaEnabled {
488+
runMigrations := !strings.EqualFold(fgaStore, fgaengine.StoreMemory)
489+
fgaEngine, ferr := fgaengine.New(
490+
&fgaengine.Config{
491+
Store: fgaStore,
492+
StoreURL: fgaStoreURL,
493+
StoreName: rootArgs.config.OrganizationName,
494+
RunMigrations: runMigrations,
495+
},
496+
&fgaengine.Dependencies{Log: &log},
497+
)
498+
if ferr != nil {
499+
log.Error().Err(ferr).
500+
Str("fga_store", fgaStore).
501+
Msg("failed to initialize OpenFGA authorization engine; fine-grained authorization is DISABLED (fail-closed) — core auth continues")
502+
} else {
503+
if closer, ok := fgaEngine.(interface{ Close() }); ok {
504+
defer closer.Close()
505+
}
506+
authzEngine = fgaEngine
507+
log.Info().
508+
Str("fga_store", fgaStore).
509+
Bool("reused_main_db", strings.TrimSpace(rootArgs.config.FGAStore) == "").
510+
Msg("OpenFGA authorization engine initialized (embedded); routed into GraphQL + session/validate")
511+
}
493512
}
494513

495514
// SMS provider
@@ -542,7 +561,7 @@ func runRoot(c *cobra.Command, args []string) {
542561
TokenProvider: tokenProvider,
543562
OAuthProvider: oauthProvider,
544563
RateLimitProvider: rateLimitProvider,
545-
AuthorizationProvider: authorizationProvider,
564+
AuthzEngine: authzEngine,
546565
})
547566
if err != nil {
548567
log.Fatal().Err(err).Msg("failed to create http provider")

0 commit comments

Comments
 (0)