Files
sentryagent-idp/openspec/changes/archive/2026-04-09-tenant-isolation-enforcement/proposal.md
SentryAgent.ai Developer 4cb168bbba docs(openspec): mark tenant-isolation-enforcement complete and archive
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>
2026-04-09 05:29:54 +00:00

3.7 KiB

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%