feat(AGX1-275): per-RPC task permission rewire and 404/403 wrap#249
Draft
asherfink wants to merge 1 commit into
Draft
feat(AGX1-275): per-RPC task permission rewire and 404/403 wrap#249asherfink wants to merge 1 commit into
asherfink wants to merge 1 commit into
Conversation
4 tasks
4 tasks
dm36
added a commit
that referenced
this pull request
May 27, 2026
…se and two-factor mutations Mirrors AGX1-275 (PR #249) for agent_api_keys. Wires Spark AuthZ checks into every api_key route, collapses denials to 404 (so name/id probes can't distinguish "present in another tenant" from "absent"), and relies on SpiceDB's transitive expansion of api_key.{update,delete} (= editor & parent_agent->update & tenant_gate) for two-factor mutations rather than issuing two explicit checks at the route layer. - src/utils/agent_api_key_authorization.py (new): _check_api_key_or_collapse_to_404 — catches AuthorizationError, raises ItemDoesNotExist. Same shape as Asher's task helper. - src/utils/authorization_shortcuts.py: DAuthorizedId routes AgentexResourceType.api_key through the wrap. (DAuthorizedName isn't used for api_keys; the name lookup is (agent_id, name, api_key_type), not a single globally-unique path param — the route handlers call the collapse helper inline instead.) - src/api/routes/agent_api_keys.py: * POST: explicit agent.update on parent (no api_key resource yet). * GET list: DAuthorizedResourceIds + filter; None passes through. * GET /name/{name}: inline collapse helper. * GET /{id}: DAuthorizedId(api_key, read). * DELETE /{id}: DAuthorizedId(api_key, delete). Two-factor via SpiceDB schema (api_key.delete expands to parent_agent.update); no second route-layer check. * DELETE /name/{api_key_name}: inline collapse helper. - tests/unit/api/test_agent_api_keys_authz.py (new): 12 tests, all pass. Stacked on dhruv/agx1-272-agent-api-keys-dual-write (PR A). Does NOT touch dual-write logic. Does NOT modify agentex-auth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related work
This stack lands per-task FGAC for AGX1-264. Merge order: 2b → 2 → 3 (scale-agentex assumes agentex-auth understands
cancelbefore sending it).cancelopParent epic: AGX1-264. Follow-ups bundled in AGX1-291.
Summary
AuthorizedOperationType:MESSAGE_SEND/EVENT_SEND→update,TASK_CANCEL→cancel,TASK_CREATEstayscreate. Previously every method usedupdate.404(viaItemDoesNotExist) across all surfaces — path id, query id, body id, and name routes — so callers can no longer distinguish "task present in another tenant" from "task absent" by comparing 403 vs 404.src/utils/task_authorization.py, reused from both the FastAPI dep factories and the RPC authorize hook.What changed
src/utils/task_authorization.py(new):_check_task_or_collapse_to_404(authorization, task_id, operation)— the shared wrap. Renamed from_check_task_or_distinguish_404(the previous name implied a 403/404 split the helper does not actually perform).src/utils/authorization_shortcuts.py:DAuthorizedId/DAuthorizedQuery/DAuthorizedBodyIdroute task checks through the wrap; their inner deps no longer take atask_repository(the parameter was unused).DAuthorizedNamenow applies the wrap whenresource_type == AgentexResourceType.task— previously the name surface leaked 403 vs 404 becausetasks.nameis globally unique, so a probe checked the entire system rather than a tenant.src/api/routes/agents.py_authorize_rpc_request: each task-resource branch now routes through the wrap.MESSAGE_SENDwithtask_namewas restructured to atry/elseshape so a denied-update on an existing task surfaces as 404 (it must NOT fall through to the create-fallbackexcept— that would silently promote a denied-update into a create check).TASK_CREATEand the wildcardtask("*")checks intentionally untouched.Why the structural change in
MESSAGE_SENDThe original block wrapped both
get_task(name=...)and the auth check in onetry. Applying the new wrap inside it as-is would let a denied-update raiseItemDoesNotExist, get caught by the outerexcept ItemDoesNotExist, and silently fall through to the create check — a privilege escalation. Splitting intotry: get_task / except: create-check / else: wrapped update-checkkeeps the create-on-absent semantics while ensuring denied-update propagates outward (→ 404).Tests
tests/unit/api/test_tasks_authz.py— 17/17 pass.TestPerRpcOperationRoutingtests (incl.MESSAGE_SENDcreate-fallback preserved through the restructure).TestCheckTaskOrCollapseTo404tests (allow + denied-collapses-to-404).TestDAuthorizedBodyIdTaskWraptests.TestDAuthorizedNameTaskWraptests (denied-task → 404, allow returns name, agent path unaffected)."cancel") mirrors agentex-auth's.Out of scope / follow-ups (tracked in AGX1-291)
/agents/name/{agent_name}has the same leak shape — agent FGAC is outside AGX1-264.Test plan
uv run pytest tests/unit/api/test_tasks_authz.py— 17 passed.