Skip to content

refactor(auth): narrow workspace-scoped role deps to WorkspaceRole#2851

Open
daryllimyt wants to merge 1 commit into
mainfrom
refactor/auth-workspace-role
Open

refactor(auth): narrow workspace-scoped role deps to WorkspaceRole#2851
daryllimyt wants to merge 1 commit into
mainfrom
refactor/auth-workspace-role

Conversation

@daryllimyt

@daryllimyt daryllimyt commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

What

Introduces WorkspaceRole, a Role subtype with a non-optional workspace_id, and makes all RoleACL(require_workspace="yes") dependency aliases produce it. This deletes ~35 repeated route-level guards of the form:

if role.workspace_id is None:
    raise HTTPException(status_code=400, detail="Workspace ID is required")

Why

_validate_role already raises 403 before any route body runs when require_workspace="yes" and the workspace is missing, so these guards were unreachable at runtime — they existed only to narrow Role.workspace_id: WorkspaceID | None for the type checker. Encoding the guarantee in the dependency's return type does the narrowing once instead of per-route.

Do we lose the HTTP exceptions?

No. FastAPI resolves the RoleACL(require_workspace="yes") dependency before the route body runs, and _validate_role raises there if the workspace is missing:

if require_workspace == "yes" and role.workspace_id is None:
    logger.warning("User does not have access to this workspace", role=role)
    raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")

The executor-token path has the same 403 check. A request without workspace context still gets an HTTP 403 — it just comes from the dependency, exactly as it always did.

The deleted guards sat after that dependency in the same handlers, so they could only run when workspace_id was already guaranteed non-None. Their responses (e.g. the secrets route's 400 "Workspace context is required", the agent routes' 403 "Workspace access required") were never reachable, so there is no client-facing behavior change: the real response was always the dependency's 403 Forbidden.

How

  • tracecat/auth/types.py: new WorkspaceRole(Role) overriding workspace_id: WorkspaceID (non-optional, frozen).
  • tracecat/auth/credentials.py: _validate_role re-validates into WorkspaceRole when require_workspace == "yes" (after scope computation, so ctx_role also carries the narrowed instance).
  • tracecat/auth/dependencies.py: all workspace-scoped aliases (WorkspaceUserRole, WorkspaceActorRole, WorkspaceActorRouteRole, WorkspaceUserRouteRole, WorkspaceServiceAccountRole, WorkspaceUserPathRole, ExecutorWorkspaceRole) plus the inline WorkspaceUserInPath / WorkspaceUserOnlyInPath aliases now annotate WorkspaceRole.
  • Routers: redundant guards removed across integrations, workflow/{management,actions,executions,store}, agent/channels, agent/session, secrets, and the EE tracecat_ee/agent routers (approvals + agent). In workflow/executions/router.py, the two private search helpers now take WorkspaceRole directly.
  • oauth_callback (a require_workspace="no" route) now binds the workspace from the validated OAuth state row to a local instead of re-narrowing the role.

What is intentionally unchanged

  • BaseWorkspaceService.__init__'s runtime guard: services are also constructed from Temporal activities, the executor, and system_role(), where no RoleACL guarantee exists.
  • All workspace_id is None checks in engine/runtime code (dsl/, executor/, agent/), which receive roles deserialized from payloads/headers — there, no RoleACL guarantee exists, so the runtime checks still matter.

Since WorkspaceRole subclasses Role, every service signature taking role: Role is unaffected.

Testing

  • uv run basedpyright --warnings: 0 errors.
  • uv run ruff check / ruff format: clean.
  • Targeted auth/router unit tests (test_role_acl, test_auth_scope_resolution, test_role_headers, test_workspace_scoped_routes, test_api_actor_roles, plus the touched routers' tests): 88 passed.
  • Routes covered by the follow-up sweep (test_api_secrets, test_agent_session_router, test_approvals_delete): 46 passed.
  • Full tests/unit sweep: identical failure set (222) on this branch and on clean main in my environment — zero regressions introduced.

Summary by cubic

Introduce WorkspaceRole (a Role with a non-optional workspace_id) and update all workspace-scoped auth dependencies to return it. This removes ~30 unreachable guards and tightens typing with no runtime behavior changes.

  • Refactors
    • Added WorkspaceRole(Role) in tracecat/auth/types.py.
    • _validate_role now returns WorkspaceRole when require_workspace="yes" after computing scopes.
    • Annotated all workspace-scoped dependency aliases to WorkspaceRole (e.g., WorkspaceUserRole, WorkspaceActorRole, WorkspaceActorRouteRole, WorkspaceUserRouteRole, WorkspaceServiceAccountRole, WorkspaceUserPathRole, ExecutorWorkspaceRole, WorkspaceUserInPath, WorkspaceUserOnlyInPath).
    • Removed redundant workspace_id is None checks across integrations, workflow/*, and agent/channels; oauth_callback now uses a local workspace_id.
    • Kept service-layer/runtime guards for untrusted contexts unchanged; WorkspaceRole subclasses Role, so existing service signatures remain compatible.

Written for commit 07f2c62. Summary will update on new commits.

Review in cubic

@daryllimyt daryllimyt added refactor Refactoring api Improvements or additions to the backend API rbac Enterprise RBAC features and bugs labels Jun 12, 2026
@zeropath-ai

zeropath-ai Bot commented Jun 12, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to 5bf701b.

Security Overview
Detected Code Changes

| Change Type | Relevant files

... (code changes summary truncated to fit VCS comment limits.)

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 17 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Re-trigger cubic

RoleACL(require_workspace="yes") already rejects requests without a
workspace before route handlers run, but Role.workspace_id was typed
WorkspaceID | None, forcing every workspace-scoped route to repeat an
unreachable 'if role.workspace_id is None: raise' guard for the type
checker.

Introduce WorkspaceRole (Role with non-optional workspace_id), produce
it in _validate_role when a workspace is required, and annotate the
workspace-scoped dependency aliases with it. Delete the now-redundant
route-level guards. Service-layer checks at genuine trust boundaries
(BaseWorkspaceService, Temporal/executor payloads) are unchanged.
@daryllimyt daryllimyt force-pushed the refactor/auth-workspace-role branch from 07f2c62 to 5bf701b Compare June 12, 2026 05:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Improvements or additions to the backend API rbac Enterprise RBAC features and bugs refactor Refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant