Skip to main content
Version: main 🚧

Research: RBAC / PDP Inventory and CAS Migration Map

Date: 2026-06-06 Purpose: Inventory every authorization surface in the codebase, document its current approach, and map how it migrates to CAS.


1. Surface inventory​

1.1 BFF β€” ui/src/​

FileFunctionPDPResource / ActionAnti-patterns
lib/rbac/openfga-agent-authz.tsrequireAgentUsePermissionOpenFGA direct + team-unionagent:id / can_useReturns NextResponse|null (mixed concerns); leaks agent#use + pdp_denied in error body
lib/rbac/pdp-shared.tsevaluateAgentAccessOpenFGA direct + parallel team-unionagent:id / can_useSecond independent agent-use algorithm; different return shape from above
lib/rbac/resource-authz.tsrequireResourcePermission requireAgentPermission requireSkillPermission filterResourcesByPermissionOpenFGA via checkOpenFgaTupleAll resource types / all actionsAction→relation map lives here (not in adapter); openFgaRelationForResourceAction is FGA-internal logic in a shared module
lib/rbac/openfga-team-membership.tslistUserTeamSlugsOpenFGA list-objectsteam#memberPer-subject 60s LRU cache β€” not shared with decision cache
lib/rbac/conversation-implicit-authz.tsrequireConversationResourcePermission filterConversationsByImplicitOrExplicitPermissionOpenFGA + implicit owner bypassconversation / writeImplicit owner logic embedded in authz module
lib/rbac/keycloak-authz.tscheckPermission checkPermissions getEffectivePermissionsKeycloak UMA (legacy)AnyDeprecated; kept for legacy compatibility
app/api/user/check_agent_access/route.tsPOST handlerBFF in-process (calls evaluateAgentAccess)agent:id / can_useAd-hoc PDP endpoint; not versioned; used by Slack/Webex bots

1.2 Dynamic Agents β€” ai_platform_engineering/dynamic_agents/​

FileFunctionPDPResource / ActionAnti-patterns
auth/openfga_authz.pyrequire_agent_use_permissionOpenFGA directagent:id / can_useStore ID re-resolved per call via HTTP; own Mongo audit writes; JWT base64-decoded without re-verification; leaks agent#use + pdp_denied in HTTP error detail
auth/keycloak_authz.pyrequire_da_permissionShared util Keycloak β†’ OpenFGAdynamic_agent:id#scopeThin wrapper; calls shared utils path
auth/access.pycan_view_agent can_use_agent can_access_conversationIn-memory (no PDP)agent / visibilityVisibility enum check only; no tuple enforcement; deprecated for fine-grained
auth/jwt_middleware.pyJwtAuthMiddleware.dispatchKeycloak JWKS validationBearer tokenLenient mode allows X-User-Context fallback when DA_REQUIRE_BEARER=false β€” #1730 vector

1.3 RAG Server β€” ai_platform_engineering/knowledge_bases/rag/​

FileFunctionPDPResource / ActionAnti-patterns
server/rbac.py:556authorize_searchOpenFGA organization#can_searchorg / searchRBAC_TEAM_SCOPE_ENABLED flag scattered; client-creds bypass inline
server/rbac.py:678authorize_mcp_tool_manageOpenFGA mcp_tool#can_managemcp_tool / manageCoarse ADMIN bypass inline
server/rbac.py:716authorize_mcp_tool_createOpenFGA team#can_usemcp_tool / createMongo team-membership lookup in hot path
server/rbac.py:918check_datasource_accessOpenFGA data_source#can_read/can_ingest/can_managedata_source / read, ingest, manageRedundant org-admin bypass
server/rbac.py:874get_accessible_datasource_idsOpenFGA list-objectsdata_source / readN sequential checks before list-objects added
server/rbac.py:831derive_team_for_requestMongoDB lookup (channel_team_mappings β†’ teams)β€” (team resolution)Mongo query per request; degrades silently on Mongo failure
server/rbac.py:1101inject_kb_filterDelegates to get_accessible_datasource_idsdata_source / readMutates query in-place; early-exit on empty set
server/rbac.py:973authorize_datasource_createOpenFGA team#can_use + tuple writedata_source / createMongo team-slug resolution; own tuple write after create
server/auth.pyOIDCProvider.validate_token AuthManager.validate_tokenKeycloak JWKSBearer JWTDual-provider chain (UI + ingestor); 1h JWKS cache

1.4 Slack Bot β€” ai_platform_engineering/integrations/slack_bot/​

FileFunctionPDPResource / ActionAnti-patterns
utils/rbac_middleware.pyrequire_permissionShared util β†’ Keycloak β†’ OpenFGAresource#scope (org-level)Tenant extracted from OBO JWT org claim; decorator pattern leaks detail into Slack ephemeral messages
utils/dm_authz_client.pyDmAuthzClient.check_agent_accessBFF /api/user/check_agent_access (HTTP)agent:id / can_useCalls the ad-hoc unversioned BFF PDP endpoint; fail-closed on transport error

1.5 Shared Utils β€” ai_platform_engineering/utils/auth/​

FileFunctionPDPResource / ActionAnti-patterns
keycloak_authz.py:250require_rbac_permissionOpenFGA organization#<relation>org-level resource+scope β†’ computed relationStore ID re-resolved per-call (no env var set β†’ HTTP lookup every time); token-hash cache (60s)
keycloak_authz.py:321require_rbac_permission_depSame, FastAPI dependency wrapperβ€”Reads from current_bearer_token ContextVar

2. Duplication map​

The same decision logic is implemented independently across surfaces:

DecisionImplementationsLines
user can_use agentopenfga-agent-authz.ts Β· pdp-shared.ts Β· openfga_authz.py~300 lines Γ— 3
OpenFGA store-id resolutionopenfga.ts (BFF) Β· openfga_authz.py (DA) Β· rbac.py (RAG) Β· keycloak_authz.py (utils)4Γ—
OpenFGA HTTP client setupSame 4 files4Γ—
Mongo audit writeopenfga_authz.py (DA) Β· rbac.py (RAG)2Γ— separate schemas
Action→relation mapresource-authz.ts (openFgaRelationForResourceAction) · keycloak_authz.py (_organization_relation_for)2×
JWT payload decode (unsigned)openfga_authz.py Β· keycloak_authz.py Β· rbac_middleware.py3Γ—

3. Anti-patterns by severity​

SeverityAnti-patternLocation
P1 β€” SecurityX-User-Context trusted when DA_REQUIRE_BEARER=false β€” forgeable identity [#1730]jwt_middleware.py lenient mode
P1 β€” Securityagent#use + pdp_denied leaked in HTTP error body β†’ reveals PDP internals to calleropenfga_authz.py:433, openfga-agent-authz.ts
P2 β€” ReliabilityOpenFGA store-id re-resolved via HTTP on every authz call (4 separate implementations)DA, RAG, utils, BFF
P2 β€” ReliabilityNew httpx.AsyncClient per request in RAG rbac.pyrbac.py:_openfga_check_object
P2 — ReliabilityMongo channel→team lookup in hot path; silent degradation on failurerbac.py:derive_team_for_request
P3 β€” CorrectnessTwo independent user can_use agent algorithms in BFF with different return shapesopenfga-agent-authz.ts vs pdp-shared.ts
P3 β€” CorrectnessRBAC_TEAM_SCOPE_ENABLED conditional scattered across 3 functions in rbac.pylines 570, 925, 996
P3 — MaintainabilityAction→relation map duplicated; each site could drift independentlyresource-authz.ts vs keycloak_authz.py
P3 β€” MaintainabilityMongo audit schema differs between DA and RAG; no shared shapeopenfga_authz.py vs rbac.py

4. CAS migration map​

Each surface migrates in two steps: (1) CAS ships standalone, (2) surface flips from its current PDP to calling CAS. Step 1 is the spec in spec.md. Step 2 is per-surface work.

4.1 BFF agent-use (two algorithms β†’ one)​

Current: requireAgentUsePermission (openfga-agent-authz.ts) + evaluateAgentAccess (pdp-shared.ts) β€” two independent implementations, different return shapes.

Migration: Both become thin wrappers over authorizeOrThrow / authorize from lib/authz/index.ts. The direct+team-union algorithm moves into lib/authz/index.ts as the canonical implementation. The bot-facing /api/user/check_agent_access route calls authorize() and returns its Decision.

What changes: Delete openfga-agent-authz.ts; pdp-shared.ts becomes a re-export shim then deleted. Zero behavior change.

4.2 BFF resource authz​

Current: resource-authz.ts — requireResourcePermission, filterResourcesByPermission, action→relation map.

Migration: requireResourcePermission → authorizeOrThrow. filterResourcesByPermission → filterAccessible. The action→relation map moves into engines/openfga.ts; resource-authz.ts stops knowing about FGA strings.

What changes: resource-authz.ts becomes a thin call-through; action→relation map is internal to the adapter. Exported function signatures unchanged for existing callers.

4.3 BFF bot-facing PDP endpoint​

Current: POST /api/user/check_agent_access β€” ad-hoc, unversioned, calls evaluateAgentAccess.

Migration:

  • Option A (preferred): Slack/Webex bots switch from /api/user/check_agent_access to POST /api/authz/v1/decisions with resource.type=agent, action=use. The new endpoint is versioned and carries ReasonCode.
  • Option B (interim): /api/user/check_agent_access internally calls authorize() from the core (one-line change), keeping the old URL alive until bots migrate.

What changes (option A): DmAuthzClient updated to call the v1 endpoint. Old route deprecated.

4.4 Dynamic Agents β€” require_agent_use_permission​

Current: Own OpenFGA HTTP client; store-id re-resolved per call; own Mongo audit writes; own OTEL spans.

Migration: Replace the entire function body with POST /api/authz/v1/decisions (Transport B). DA sends {subject, resource:{type:agent,id}, action:use} with its Bearer token. CAS evaluates and audits; DA enforces the Decision.

What's deleted: openfga_authz.py β€” the entire file. DA drops httpx OpenFGA calls, Mongo audit client, OTEL authz span export. Only the HTTP call to CAS remains.

What stays: jwt_middleware.py (DA still validates its own inbound Bearer JWT). keycloak_authz.py coarse wrapper stays until org-level Keycloak gates also migrate.

Prerequisite: DA_REQUIRE_BEARER=true must be enforced first to close [#1730] before DA can trust token.sub as the subject it sends to CAS.

4.5 RAG β€” search, datasource, MCP tool gates​

Current: rbac.py (1158 lines) — roles + OpenFGA + Mongo channel→team lookup, all inline.

Migration path:

RAG FunctionCAS Call
authorize_searchPOST /api/authz/v1/decisions {resource:{type:knowledge_base,id:org},action:discover}
authorize_datasource_createPOST /api/authz/v1/decisions {resource:{type:data_source,id:owner_team},action:ingest}
check_datasource_accessPOST /api/authz/v1/decisions {resource:{type:data_source,id},action:read|ingest|manage}
get_accessible_datasource_idsPOST /api/authz/v1/decisions:batch {resource_type:data_source,action:read,ids:[…]}
authorize_mcp_tool_managePOST /api/authz/v1/decisions {resource:{type:mcp_tool,id},action:manage}
inject_kb_filtercalls filterAccessible equivalent via :batch

Channel→team lookup: derive_team_for_request moves into CAS domains/slack-channel.ts + domains/webex-space.ts. RAG stops reading channel_team_mappings from Mongo; sends X-Channel-Id as context to CAS, which resolves the team internally.

Coarse roles: READONLY/INGESTONLY/ADMIN demoted to a local pre-filter for client-credentials/automation tokens only. Human-path decisions go to CAS.

What's deleted from rbac.py: OpenFGA client, store-id resolution, Mongo channel-team lookup, audit writes. What remains: role enum, require_authenticated_user, coarse pre-filter, thin CAS HTTP calls.

4.6 Slack bot middleware​

Current: rbac_middleware.py β†’ shared util require_rbac_permission β†’ OpenFGA organization#<relation>.

Migration: Replace check_permission(RbacCheckRequest) call with POST /api/authz/v1/decisions. The decorator keeps the same interface; only the underlying HTTP call changes.

DmAuthzClient: Migrates to v1 endpoint per Β§4.3 option A.

4.7 Shared utils keycloak_authz.py​

Current: require_rbac_permission β€” org-level OpenFGA gate with token-hash cache; re-resolves store-id.

Migration: require_rbac_permission β†’ POST /api/authz/v1/decisions for resource+action pairs that map to CAS ResourceType. The token-hash cache is subsumed by CAS's own decision cache (15s TTL). The function becomes a thin HTTP wrapper.

Store-id re-resolution: Eliminated β€” CAS caches this at boot.


5. Migration priority​

PrioritySurfaceDriver
1DA require_agent_use_permissionCloses [#1730] P1 (after DA_REQUIRE_BEARER=true); eliminates riskiest OpenFGA reimplementation
2BFF agent-use consolidation (two algos β†’ one)Correctness; prerequisite for DA migration to trust BFF decision
3Bot-facing endpoint /api/user/check_agent_access β†’ v1Removes unversioned ad-hoc PDP surface
4RAG datasource + search gatesRemoves Mongo-in-hot-path; largest volume of OpenFGA reimplementation
5BFF resource-authz.ts wrappersLow risk; behavior-neutral refactor
6Slack bot middlewareDepends on shared utils migration
7Shared utils keycloak_authz.pyLast; many callers; broadest blast radius

6. What CAS does NOT replace​

ComponentStays as-isReason
auth/jwt_middleware.py (DA)DA still validates its own inbound JWTCAS validates the CAS-caller token; DA's inbound user JWT is a separate responsibility
server/auth.py (RAG OIDCProvider)RAG still validates its own inbound JWTSame reason
Tuple write paths (write_datasource_ownership, etc.)CAS is evaluate-onlyOwnership reconciliation stays in existing modules
openfga-authz-bridgeData-plane PEP; not BFF-residentAligning its vocabulary/adapter/audit to match the CAS contract is a separate step
Keycloak UMA keycloak-authz.tsLegacy compatibility shimDelete only when all callers are confirmed migrated
access.py visibility checks (DA)In-memory coarse filterNot a PDP call; used for list-endpoint visibility only