🔐 Proposal: Central Authorization Service (CAS) (PRs #1770 + #1772) #1778
sriaradhyula
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Note: This change does not affect any of the current RBAC functionality. This is a future simplification we can move toward for other features that are already implemented — they will work as-is without using CAS.
Based on the auth-review feedback in #1751 and the remediation epic #1742, I have been thinking about a common way (as suggested previously by @subbaksh) to solve this instead of patching each component one-off. The recurring question across those reviews was that the same OpenFGA decision logic and org-admin bypass is re-implemented in multiple places — the BFF/UI server, Dynamic Agents, RAG, and others.
So I have implemented a Centralized Authorization Service (CAS) inside the UI Server/BFF to simplify and centralize every authz decision behind one PDP-agnostic API.
I would like folks to review and give feedback before we converge the other components onto it.
PR #1770 — CAS core, HTTP API, admin UI
#1770
check/batchCheck) —POST /api/authz/v1/decisionswith subject bindingcas_decision,cas_grantevents)PR #1772 — Workflow RBAC on CAS (re-implements #1751)
#1772
executeSteps), so Dynamic Agents no longer readsworkflow_configsfrom Mongo — directly addresses @subbaksh's comment in feat(workflows): RBAC visibility, run access, team slugs, and unsaved UX #1751.authz.pythat just asks CAS over HTTP (AUTHZ_SERVICE_URL). Directly clears Epic: RBAC/Auth review remediation (PR #1444 follow-ups) #1742 items refactor(dynamic-agents): remove duplicate OpenFGA can_use check on chat path #1731 (duplicate OpenFGA hop) and refactor(dynamic-agents): decouple audit + OTEL export from authz decision #1733 (audit coupled to authz).Why this is simpler
Architecture
Component view
CAS lives inside the BFF. Out-of-process callers reach it over HTTP. Solid = shipped; dashed = planned.
flowchart TB subgraph BFF["BFF — caipe-ui (Next.js)"] R["Route handlers\nworkflow-runs ✓ migrated\nworkflow-configs ✓ migrated"] V1["HTTP API\n/api/authz/v1\ndecisions · batch · explain"] AD["Admin API\n/api/admin/authz\ngrants · stats · explain"] subgraph CAS["lib/authz — CAS core"] P["authorize · authorizeMany\nfilterAccessible · grant · revoke"] CMP["compose — product policy layer"] ENG["PolicyEngine + PolicyAdmin"] OFA["OpenFGA adapter\nwarm store-id · pooled client\ndecision cache · circuit breaker"] AU["audit"] P --> CMP --> ENG --> OFA P --> AU end R -->|in-process| P V1 --> P AD --> P end DA["Dynamic Agents ✓ migrated"] -->|HTTP + user JWT| V1 RB["RAG · Slack · Webex — planned"] -. HTTP .-> V1 OFA --> STORE[("OpenFGA store")] AU --> M[("audit_events — Mongo")] M --> UIADMIN["Admin UI\nPermissions Tool · Audit tab"] AD --> UIADMIN BR["openfga-authz-bridge\ndata-plane ext_authz (separate)"] -. Check .-> STOREDecision flow (PDP)
A DENY is a successful evaluation (
200+ body). HTTP error codes are reserved for meta-failures.sequenceDiagram participant C as Caller (route / DA / bot) participant API as /api/authz/v1/decisions participant Core as CAS core participant FGA as OpenFGA participant Aud as audit_events C->>API: POST {subject, resource, action} + Bearer API->>API: verify JWT (JWKS) API->>API: subject-binding (caller.sub == subject) API->>Core: authorize() Core->>Core: decision cache lookup alt cache miss Core->>FGA: Check(user, relation, object) FGA-->>Core: allowed? end Core->>Aud: cas_decision event Core-->>API: {decision, reason, retriable} API-->>C: 200 ALLOW/DENY · 401/403 auth · 503 unavailableGrant flow (PAP)
Intent-based and meta-authz'd — callers send
{resource, grantee, capability}, never raw tuples. CAS verifies the caller may manage the resource before writing.sequenceDiagram participant M as Workflow share / agent-access modal participant G as /api/admin/authz/grants participant Core as CAS core participant FGA as OpenFGA participant Aud as audit_events M->>G: POST {resource, grantee, capability} + Bearer G->>Core: meta-authz — caller may manage resource? Core->>FGA: Check(can_manage) FGA-->>Core: allow G->>Core: grant(intent) Core->>FGA: write tuple (team→agent, or user:* for global) G->>Aud: cas_grant event G-->>M: 200 {granted}Migration posture
flowchart LR subgraph done["Done"] d1["workflow-runs / workflow-configs → CAS"] d2["DA → CAS HTTP\n(openfga_authz.py removed)"] end subgraph next["Planned"] x1["RAG → CAS"] x2["Slack · Webex bots → CAS"] x3["bridge: align vocabulary / audit"] end done --> nextEach surface migrates independently — nothing is forced. The service runs today with two real consumers (BFF routes + DA) and absorbs the rest incrementally.
Beta Was this translation helpful? Give feedback.
All reactions