Skip to main content

Tasks: Workflow RBAC on CAS (re-implementation of #1751)

Date: 2026-06-07 Stacked on: PR #1770 (CAS foundation) β€” branch feat/centralized-authz-service Supersedes: PR #1751 Companions: ../2026-06-06-cas-implementation/spec.md Β· architecture.md Β· research.md

Goal​

Re-implement #1751 (workflow RBAC: visibility, run access, team sharing, execution authz, editor UX) on top of CAS β€” and resolve the #1751 review feedback.

Status β€” 2026-06-07​

Implemented on local branch feat/workflow-rbac-on-cas (to be split β†’ #1770 CAS core + #1771 workflow).

  • Phase 1 (PAP) β€” PolicyAdmin + grant/revoke shipped in CAS core; grants exposed at /api/admin/authz/grants (admin-gated + meta-authz "caller manages the resource"), cas_grant audit. Path differs from the planned /api/authz/v1/grants.
  • Phase 2 (read/use) β€” data model from the #1751 merge; workflow-runs (read/delete/start) and workflow-configs read-path re-pointed onto CAS via filterAccessible + org-admin bypass. ⚠️ PUT/DELETE configs stay on the owner/visibility model β€” CAS write/delete grants aren't modeled yet.
  • Phase 3 (per-step gate) β€” authorize(user, step.agent_id, use) runs in the shared executeSteps() loop (covers start and resume); DENY fails the step. Tested.
  • Phase 4 (DA) β€” workflow_execution_authz.py and openfga_authz.py deleted. DA is now a thin PEP: auth/authz.py delegates every agent-use decision to CAS ({AUTHZ_SERVICE_URL}/api/authz/v1/decisions, OBO). No in-process OpenFGA, no bypass mirror, no DA-side Mongo audit β€” CAS is the single PDP. Fails closed if CAS is unreachable. Tested.
  • Phase 6 (editor UX) β€” landed via the #1751 merge.
  • Phase 7 β€” domains/workflow.ts stub dropped; Access Manager/Diagnostics dead code removed; missing tests added (Phase 3/4 + bearer fallback).

Remaining: Phase 5 (modal β†’ grant API, cosmetic); split branch β†’ PRs #1770/#1771 + DCO sign-off; reply to #1751 threads.

Addresses #1751 review comments​

CommentResolution here
@subbaksh on workflow_execution_authz.py β€” "Workflows are a UI-server concept; RBAC for invocation should be in the UI server, not DA."Per-step agent-use gate moves into the BFF executeSteps(); workflow_execution_authz.py is deleted. DA reads no workflow_configs.
@subbaksh on workflow-da-auth.ts β€” "Why send user/email in a header and have DA trust it?"Workflow authz no longer depends on X-User-Context. Header survives only for get_user_context() email; cleanup tracked in #1753.
code-quality bot β€” unused import in workflow-run-access.tsFile is replaced by CAS authorize / filterAccessible.

Architecture note​

The BFF executeSteps() orchestrates the workflow and invokes agents one step at a time (/api/v1/chat/stream/start per step). So the per-step CAS gate runs in-process in the BFF — no DA→CAS cross-process delegation, no decision tokens, no shared run registry. Direct agent chat still migrates DA's openfga_authz.py → CAS HTTP check.

Decisions (locked)​

  • Standing grants β€” sharing writes teamβ†’agent tuples; global workflows write a user:* can_use agent wildcard.
  • CAS owns grants via an intent-based PAP (PolicyAdmin); meta-authz: caller must manage the agent being granted.
  • Full #1751 parity (including editor UX).
  • Drop the domains/workflow.ts delegation stub.

Phases​

Phase 1 β€” CAS grant API (PAP)​

  • PolicyAdmin interface + OpenFgaPolicyAdmin (tuple write/delete transport)
  • grant() / revoke() in lib/authz; GrantIntent contract type
  • POST / DELETE /api/authz/v1/grants β€” JWKS verify + meta-authz (caller manages resource)
  • cas_grant audit event
  • unit + contract tests

Phase 2 β€” Workflow data model + BFF read/use​

  • visibility / shared_with_teams / owner_id on workflow config (via #1751 merge)
  • workflow-configs read-path β†’ CAS; runs read/delete/start β†’ CAS (PUT/DELETE configs stay on owner/visibility)
  • list filtering via filterAccessible

Phase 3 β€” BFF per-step execution gate (addresses @subbaksh #3)​

  • authorize({user, step.agent_id, use}) in executeSteps() before chat/stream/start
  • same gate covers resume (shared executeSteps() loop)
  • DENY β†’ fail step with a clean reason; audit

Phase 4 β€” DA cleanup (addresses @subbaksh #3 + #2)​

  • delete workflow_execution_authz.py + its test
  • openfga_authz.py removed entirely β†’ replaced by a thin auth/authz.py that delegates every agent-use decision to CAS (POST {AUTHZ_SERVICE_URL}/api/authz/v1/decisions, OBO bearer). No in-process OpenFGA check, no org-admin-bypass mirror, no DA-side Mongo audit β€” CAS is the single PDP and records cas_decision. Fails closed (503) when CAS is unreachable or AUTHZ_SERVICE_URL is unset.
  • X-User-Context reduced to email-only; link #1753

Phase 5 β€” Share / modal via CAS grant API​

  • WorkflowAgentAccessModal β†’ POST /api/authz/v1/grants (teamβ†’agent, globalβ†’user:*)
  • remove direct OpenFGA admin writes from the modal path

Phase 6 β€” Editor UX parity​

  • unsaved-changes badge + discard dialog
  • WorkflowCanvas / WorkflowSidebar / WorkflowStepSidebar / WorkflowToolbar

Phase 7 β€” Cleanup + verify​

  • drop domains/workflow.ts stub
  • fix unused import (bot comment) + remove Access Manager/Diagnostics dead code
  • Phase 3/4 + bearer-fallback tests added; targeted suites green (full-suite coverage pass pending)
  • reply to #1751 threads; mark #1751 superseded by this PR