All 8 tasks checked off. Change archived to openspec/changes/archive/
per OpenSpec protocol. Implementation committed in 5943ff1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
55 lines
3.7 KiB
Markdown
55 lines
3.7 KiB
Markdown
# Proposal: Enforce Tenant Isolation on All Agent Endpoints
|
|
|
|
## Title
|
|
Enforce tenant (organization) isolation on all agent CRUD endpoints — P0 Security Fix
|
|
|
|
## Problem Statement
|
|
|
|
Field trial Test C.7 — Org Isolation Failure — has identified a critical security defect.
|
|
All five agent endpoints (`POST /agents`, `GET /agents`, `GET /agents/{agentId}`,
|
|
`PATCH /agents/{agentId}`, `DELETE /agents/{agentId}`) perform **no tenant isolation**.
|
|
|
|
Any authenticated agent from Organization A can:
|
|
- Read the full agent list of Organization B (`GET /agents`)
|
|
- Read any individual agent record across any organization (`GET /agents/{agentId}`)
|
|
- Modify any agent's metadata across any organization (`PATCH /agents/{agentId}`)
|
|
- Decommission any agent across any organization (`DELETE /agents/{agentId}`)
|
|
- Register agents under any organization by supplying an arbitrary `organizationId` in the request body (`POST /agents`)
|
|
|
|
The JWT issued by the system already contains an `organization_id` claim (present in `ITokenPayload`). The enforcement layer between this claim and the data access layer is entirely absent.
|
|
|
|
This is a **P0 security incident** — it breaks multi-tenancy at its most fundamental level and must be resolved before any field trial continues.
|
|
|
|
## Proposed Solution
|
|
|
|
Enforce organization scoping at the service layer, driven by the `organization_id` claim extracted from the verified JWT on every request. No request body value or query parameter may override the caller's organization context.
|
|
|
|
Three enforcement rules are applied:
|
|
|
|
**Rule 1 — List scoping (`GET /agents`):** Results are always filtered to the caller's `organization_id`. The `owner` query parameter may further sub-filter within the caller's org, but can never widen the scope beyond it.
|
|
|
|
**Rule 2 — Ownership guard (`GET /agents/{agentId}`, `PATCH /agents/{agentId}`, `DELETE /agents/{agentId}`):** After retrieving the target agent record, the service compares the agent's stored `organization_id` against the caller's `organization_id`. If they do not match, the operation is rejected with `403 Forbidden` and error code `AUTHORIZATION_ERROR`.
|
|
|
|
**Rule 3 — Register scoping (`POST /agents`):** The `organizationId` field in the request body is ignored. The agent is always registered under the caller's `organization_id` from the JWT, regardless of what the body contains.
|
|
|
|
## Scope of Changes
|
|
|
|
- `src/types/index.ts` — add `organizationId` field to `IAgentListFilters`
|
|
- `src/repositories/AgentRepository.ts` — filter `findAll()` by `organizationId`
|
|
- `src/services/AgentService.ts` — pass `organizationId` into `getAgentById()`, `updateAgent()`, `decommissionAgent()`; throw `AuthorizationError` on mismatch
|
|
- `src/controllers/AgentController.ts` — extract `req.user.organization_id` and apply to all five endpoint handlers
|
|
- `docs/openapi/agent-registry.yaml` — document enforcement rules and 403 responses on all five endpoints
|
|
- `src/tests/` — add Test C.7 regression suite and ownership guard tests
|
|
|
|
## Acceptance Criteria
|
|
|
|
- [ ] `GET /agents` never returns agents from a different organization than the caller's
|
|
- [ ] `GET /agents/{agentId}` returns `403 AUTHORIZATION_ERROR` if the target agent belongs to a different organization
|
|
- [ ] `PATCH /agents/{agentId}` returns `403 AUTHORIZATION_ERROR` if the target agent belongs to a different organization
|
|
- [ ] `DELETE /agents/{agentId}` returns `403 AUTHORIZATION_ERROR` if the target agent belongs to a different organization
|
|
- [ ] `POST /agents` ignores any `organizationId` in the request body; agent is always registered under the caller's org
|
|
- [ ] OpenAPI spec documents these rules and all 403 responses on all five endpoints
|
|
- [ ] Test C.7 regression suite passes
|
|
- [ ] All ownership guard paths have test coverage
|
|
- [ ] Overall test coverage remains above 80%
|