481359ddb2
Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
3.1 KiB
3.1 KiB
id, title, status, wave, depends_on, plan, spec
| id | title | status | wave | depends_on | plan | spec | |
|---|---|---|---|---|---|---|---|
| 03-reach-and-scope-resolution | Reach predicate and scope resolution | todo | 1 |
|
plan.md | ../../specs/workspaces/requirements/org-units.md |
Task 03: Reach Predicate and Scope Resolution
The load-bearing task. Review as a security change.
Acceptance
workspaceVisibleToimplements the spec's reach table exactly: owner, then explicit membership row, thenvisibility = 'org'for a non-guestorg member, else unreachable. Aguestnever reaches an org-visible workspace without a row.authz.ScopesFor(user, workspace)implements the spec's resolution order and is the only place permissions are derived. An explicit membership row outranks the org default in both directions: it admits a guest and it narrows a member toviewer.- Every guarded workspace action checks a scope through the resolver. A caller
with
workspace.readbut not the action's scope gets 403; a caller withoutworkspace.readgets 404. - Resolution fails closed: a transient error loading the workspace or membership row yields no scopes. It never falls back to the org default role. A fault-injection test covers this, because it is the branch that silently widens access under load.
- Scopes are resolved once per request and carried. A board render issues a constant number of membership queries regardless of row count, proven by a query-count assertion, not a timing assertion.
- No effective-scope result is cached beyond the request.
- Every existing per-user isolation test passes unmodified.
- A reach-and-scope matrix covers every row of the spec's Permissions table across the full surface: workspace, task, session, transcript, diff, preview, terminal, environment, repository, attachment.
- The in-session agent MCP identity remains the task owner, not the last prompting member.
Verification
go test ./internal/task/... ./internal/authz/... ./internal/mcp/... ./internal/gateway/websocket/...go test ./internal/task/service/... -run 'TestReachMatrix|TestScopeResolution|TestScopeQueryCount|TestScopeFailsClosed'- Mutation check: invert the membership branch, and separately make the fail-closed branch return the org default. Both must fail the matrix; a suite that still passes is not testing the resolver.
Files Likely Touched
apps/backend/internal/task/service/service_access.goapps/backend/internal/authz/resolve.goapps/backend/internal/task/service/per-action scope checksapps/backend/internal/mcp/scope/scope.go(assert unchanged behavior)
Inputs
- Spec: Permissions (the reach table), Failure modes (403 vs 404, fail closed);
roles-and-scopes.mdResolution. - Patterns: the
*NotFoundno-leak convention and the "new user-facing service entry points must apply scoping" rule inapps/backend/AGENTS.md.
Output Contract
Report the matrix surface count against the authorize-helper count, the query-count assertion, both mutations and their failures, RED/GREEN commands, and set this task plus its plan checkbox to done.