feat(AGX1-277): enforce task.read on message GET via Spark AuthZ#255
Open
harvhan wants to merge 2 commits into
Open
feat(AGX1-277): enforce task.read on message GET via Spark AuthZ#255harvhan wants to merge 2 commits into
harvhan wants to merge 2 commits into
Conversation
Guard GET /messages/{message_id} (previously unauthorized) by resolving
the message to its parent task_id and checking task.read via the
existing TaskChildResourceType pattern — message joins event and state.
Collapse task-child auth denials to 404 across path-id and query-id
surfaces so callers cannot probe cross-tenant existence by comparing
403 vs 404.
Correct the misleading TaskMessage | None on get_message — the
underlying service already raised ItemDoesNotExist; the None branch was
dead code.
Integration tests cover the auth matrix on /messages/{message_id} plus
an authorized list happy-path. POST/PUT routes and the agent-API-key
bypass are out of scope (handled in AGX1-275 and tracked separately).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the naming convention used for state_repository (DTaskStateRepository) and event_repository (DEventRepository) where the local parameter name drops the "task_" prefix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dm36
approved these changes
May 28, 2026
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.
Linear Issue
AGX1-277 — Parent epic: AGX1-264.
Summary
Guard
GET /messages/{message_id}(previously unauthorized) with a Spark AuthZ check on the parent task by resolving the message to itstask_idand requiringtask.read. Wires this through the existingTaskChildResourceTypepattern —messagejoinseventandstateso the existingDAuthorizedId(...)machinery handles it.Collapses task-child auth denials to 404 (
ItemDoesNotExist) across path-id and query-id surfaces so callers cannot probe cross-tenant existence by comparing 403 vs 404.Corrects the misleading
TaskMessage | Nonereturn type onget_message(use case + route) — the underlying service already raisedItemDoesNotExist; theNonebranch was dead code.Changes
src/api/schemas/authorization_types.py— addmessagetoTaskChildResourceType.src/utils/authorization_shortcuts.py— threadDTaskMessageRepositorythrough_get_parent_task_idand injectmessage_repositoryinto the_ensure_authorized_id/_ensure_authorized_querydeps. Wrap the task-child check intry/except AuthorizationError → ItemDoesNotExistto collapse denial to 404 on both surfaces.src/api/routes/messages.py— addDAuthorizedId(TaskChildResourceType.message, AuthorizedOperationType.read, ...)toget_message; drop the deadelse Nonebranch and tighten the return type.src/domain/use_cases/messages_use_case.py— fixget_messagereturn annotation fromTaskMessageEntity | NonetoTaskMessageEntity.tests/integration/api/messages/test_messages_authz_api.py(new) — integration tests covering the auth matrix.Behavior change beyond the ticket
The 404 collapse extends to
eventandstatechild routes as well, since theTaskChildResourceTypebranch is shared. The cross-tenant-leak argument is the same for those resources. Aligns with the direction AGX1-275 takes more broadly.Out of scope
POST/PUTmessage routes still surface 403 on deny. They useDAuthorizedBodyId(task, execute)and will be rewritten totask.update+ 404-collapse by AGX1-275.RPC message/sendsimilarly handled by AGX1-275.request.state.agent_identity) still bypasses FGAC viaAuthorizationService._bypass(). The ticket does not address this; it's a parent-epic-level gap and will be tracked separately under AGX1-264.Coordination
Will conflict with AGX1-275 (#249) on the
_ensure_authorized_id/_ensure_authorized_querybodies — that PR introduces a shared_check_task_or_collapse_to_404helper insrc/utils/task_authorization.pythat replaces the inlinetry/exceptwraps in this PR. Trivial 3-way merge regardless of merge order; the inline wraps in this PR get replaced by the canonical helper call when the two land together.Test Plan
tests/integration/api/messages/test_messages_authz_api.py— 4/4 passing (authorized GET → 200, denied GET → 404 via collapse, non-existent message → 404 with explicit assertion that no authz call fires, authorized list happy-path).tests/integration/api/messages/test_messages_api.py— 19/19 passing.tests/integration/api/events/— 3/3 passing (exercises the changed_get_parent_task_idsignature on theeventpath).tests/integration/api/states/— 7/7 passing (same, forstate).tests/integration/api/agents/test_agents_auth_api.py— 3/3 passing (exercises the changed_ensure_authorized_id/_ensure_authorized_queryfactories on a direct-resource path).Greptile Summary
Guards
GET /messages/{message_id}with atask.readSpark AuthZ check by resolving the message to its parenttask_idand collapsingAuthorizationErrorto 404, so callers cannot distinguish between "doesn't exist" and "exists but forbidden". The PR also corrects a dead-codeNonereturn branch on theget_messageuse case and adds full integration-test coverage for the auth matrix.messageis added toTaskChildResourceTypeand wired through_get_parent_task_id,DAuthorizedId, andDAuthorizedQuery, which now each acceptDTaskMessageRepositoryas an additional FastAPI dependency.AuthorizationErroris applied to both path-id and query-id surfaces and incidentally hardens theevent/statechild routes as well.Confidence Score: 4/5
Safe to merge. The auth enforcement and 404-collapse logic are correct and well-tested; the only concern is an extra MongoDB round-trip per authorized GET request.
The core auth change is correct: the message is resolved to its parent task before the authz check, and denial collapses to 404. Tests cover all four relevant paths. The one observation is that the message document is fetched twice on the happy path — once in the auth dependency to extract task_id, and once more in the route handler — but this is an efficiency concern rather than a correctness issue.
No files require special attention; authorization_shortcuts.py carries the double-fetch pattern worth revisiting in a follow-up.
Important Files Changed
Sequence Diagram
sequenceDiagram participant Client participant FastAPI as GET /messages/{id} participant AuthDep as DAuthorizedId dep participant MsgRepo as TaskMessageRepository participant AuthSvc as AuthorizationService participant UseCase as MessagesUseCase Client->>FastAPI: "GET /messages/{message_id}" FastAPI->>AuthDep: resolve dependency AuthDep->>MsgRepo: "get(id=message_id)" alt message not found MsgRepo-->>AuthDep: raises ItemDoesNotExist AuthDep-->>Client: 404 else message found MsgRepo-->>AuthDep: TaskMessageEntity (task_id extracted) AuthDep->>AuthSvc: check(task, task_id, read) alt AuthorizationError AuthSvc-->>AuthDep: raises AuthorizationError AuthDep-->>Client: 404 (collapsed) else authorized AuthSvc-->>AuthDep: OK AuthDep-->>FastAPI: message_id string FastAPI->>UseCase: get_message(message_id) UseCase->>MsgRepo: "get(id=message_id) [second fetch]" MsgRepo-->>UseCase: TaskMessageEntity UseCase-->>FastAPI: TaskMessageEntity FastAPI-->>Client: 200 TaskMessage end endComments Outside Diff (1)
agentex/src/utils/authorization_shortcuts.py, line 48-81 (link)GET /messages/{message_id}request_get_parent_task_idfetches the message entity from MongoDB to extracttask_id, but the route handler then callsmessage_use_case.get_messagea second time to retrieve and return the same document. For the common authorized path this means two round-trips to themessagescollection per request. The event and state child types have the same pattern, but messages are likely to be a much higher-traffic endpoint. A future improvement would be to thread the already-fetched entity through the dependency (or cache it per-request) so the handler can skip the re-fetch.Prompt To Fix With AI
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "refactor: rename task_message_repository..." | Re-trigger Greptile