feat(AGX1-263): agent_api_keys route migration to FGAC (404 collapse + two-factor)#252
Draft
dm36 wants to merge 1 commit into
Draft
Conversation
5 tasks
…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>
b94f0cb to
d15bc88
Compare
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
PR B of the agent_api_keys FGAC stack. Stacks on top of #TBD AGX1-272 dual-write PR A. Mirrors the AGX1-275 task pattern set by Asher in #249 (route-layer rewire + 404 collapse) — same helper shape, same dep-factory hook, same test layout.
Parent ticket: AGX1-263.
Summary
agent_api_keysroutes.404(ItemDoesNotExist) on the id and name surfaces so callers can't distinguish "present in another tenant" from "absent".GET /agent_api_keysto the set of api_keys the caller can read.agent.updateexplicitly onPOST /agent_api_keys(no api_key resource exists yet — only enforcement surface at create time).delete: relies on SpiceDB's transitive expansion (api_key.delete = editor & parent_agent->update & tenant_gate), not a second route-layercheck— same approach as feat(AGX1-275): per-RPC task permission rewire and 404/403 wrap #249.What changed
src/utils/agent_api_key_authorization.py(new):_check_api_key_or_collapse_to_404(authorization, api_key_id, operation)— catchesAuthorizationError, raisesItemDoesNotExist. Direct parallel of_check_task_or_collapse_to_404.src/utils/authorization_shortcuts.py:DAuthorizedIdnow routesAgentexResourceType.api_keythrough the collapse wrap (gated branch alongside the existing task-child and direct-resource branches).src/api/routes/agent_api_keys.py: per-route wiring. The name routes call the collapse helper inline because the lookup key is(agent_id, name, api_key_type)— not a single globally-unique name path param — soDAuthorizedNamedoesn't fit.Why structural choices
Why no
DAuthorizedNameextension. Asher extendedDAuthorizedNamefor tasks becausetasks.nameis a single globally-unique path component.agent_api_keysnames are scoped — the unique key is(agent_id, name, api_key_type)and the path is/agent_api_keys/name/{name}?agent_id=.... Stuffing this intoDAuthorizedName'sname -> repository.get(name=...)shape would have required changing its signature in a way that doesn't generalize. Inline collapse helper at the route handler is cleaner and matches the cross-resource composite lookup.Why no explicit two-factor at delete. Per the SpiceDB schema,
api_key.delete = internal_effective_editor & parent_agent->update & internal_tenant_gate. Issuing a secondauthorization_service.check(agent.update, agent_id)at the route layer would only duplicate what spark-authz already enforces atomically. Mirrors #249's approach. Note (see Known unknowns): this depends on the api_key→agentparent_agentrelation being populated bygrant(). PR A's dual-write code path needs to wire that relation; otherwise this PR fails closed on every delete because spark-authz cannot evaluateparent_agent->update.Why explicit parent check at create. Create is the one place where no api_key resource exists yet, so SpiceDB cannot gate transitively. The route MUST issue
check(agent.update, parent_agent_id)directly. This is the only explicit two-factor check in the file.Tests
tests/unit/api/test_agent_api_keys_authz.py— 12/12 pass.TestCheckApiKeyOrCollapseTo404(allow + denied-collapse + delete-op forwarding).TestDAuthorizedIdApiKeyWrap(denial → 404, allow returns id, delete op propagation).TestNameRouteCollapse(both name handlers; verifies delete is NOT invoked when check fails).TestListFiltering(authorized-subset filter + None-passthrough).TestCreateParentAgentCheck(explicit parent-agent.update + create-not-invoked on denial).Test plan
uv run pytest agentex/tests/unit/api/test_agent_api_keys_authz.py— 12 passed.Out of scope
spark_mapping.pyupdates (cross-repo work).