feat(AGX1-277): enforce parent-task authz on GET /messages/{message_id}#255
Open
harvhan wants to merge 2 commits into
Open
feat(AGX1-277): enforce parent-task authz on GET /messages/{message_id}#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
3 tasks
dm36
added a commit
that referenced
this pull request
May 29, 2026
…Id/Query CI flagged my denied-list test: it expected 404 but got 403, because the direct-resource branch of _ensure_authorized_id / _ensure_authorized_query didn't have the AuthorizationError -> ItemDoesNotExist collapse that the child-resource branches got. Inconsistent — the events GET endpoint returns 404 on denial (via AgentChildResourceType branch) while the events LIST endpoint returns 403 (via direct-resource branch on agent_id). Same security argument as for child resources: a 403/404 split on a direct resource leaks existence of that resource across tenants. Aligns with Harvey's #255 direction. Risk audit: only one existing test asserts 403, and it's about a missing api-key header (authentication, not authorization). All existing 4xx assertions on direct-resource routes are already 404 (no-row-exists), not 403. So no test regressions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
asherfink
approved these changes
May 29, 2026
dm36
added a commit
that referenced
this pull request
May 29, 2026
Per Asher's review: the previous commit (5deda18) wrapped every direct-resource DAuthorizedId/DAuthorizedQuery check in AuthorizationError -> ItemDoesNotExist, which flipped 403 to 404 for every direct-resource route service-wide. That diverges from #249 (tasks) and #255 (task-children) which keep direct-resource denials as 403. Restore the else branch to a bare authorization.check() in both helpers. Only child-resource branches (TaskChildResourceType, AgentChildResourceType) collapse to 404, which is the intended scope: the parent-resolution path already loads the child to find its parent, so 404 is the natural shape. Update the events list test to assert 403 for an unauthorized agent_id (consistent with the direct-resource convention). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
dm36
added a commit
that referenced
this pull request
May 29, 2026
…ceType Per Asher's review: the AgentChildResourceType branch in DAuthorizedId/ DAuthorizedQuery added an extra parent-resolution hop and a new resource- type family compared to #249 (tasks) and #255 (task-children), both of which only special-case task/task-children. Mirror #249's shape exactly: - Add agent_authorization.check_agent_or_collapse_to_404, parallel to task_authorization.check_task_or_collapse_to_404 from #249. Same docstring rationale (no tenant column on AgentORM, denial reason not discriminable, so collapse to 404 to prevent cross-tenant existence leak; TODO references AGX1-290). - Delete AgentChildResourceType enum. - Delete _get_parent_agent_id and the elif branch in both shortcuts. - Drop DEventRepository injection from the shortcuts (was only used by the deleted branch). - Rewrite get_event to do event_use_case.get(...) -> agent check inline, using the helper. Identical wire behavior (single /v1/authz/check on the parent agent, denial -> 404) but no new abstraction. Net: shortcuts file is now structurally identical to main; events.py follows the same pattern Harvey/Asher established for direct/child task routes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
dm36
added a commit
that referenced
this pull request
May 29, 2026
Mirror Harvey's #255 inline pattern exactly in the task-child branch of DAuthorizedId/DAuthorizedQuery — same imports, same comment, same try/except shape. Direct-resource else branch stays as plain check (403), matching both #249 and #255. Net diff vs main is consistent with #255's shape: collapse for task-children, plain check for direct resources. Only delta-from-#255 is this PR's own scope (removing event from TaskChildResourceType and DEventRepository injection, since events delegate to the parent agent instead). 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.
Linear Issue
AGX1-277 — Parent epic: AGX1-264.
Summary
Guard
GET /messages/{message_id}(previously unauthorized) by resolving the message to itstask_idand requiringtask.readon the parent task. Wires this through the existingTaskChildResourceTypepattern —messagejoinseventandstateso the standardDAuthorizedId(...)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 authz viaAuthorizationService._bypass(). Tracked separately in AGX1-305.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).