--- story_id: story_5_7 sprint_id: sprint_5 phase: Phase 2 - Resources status: not_started priority: P0 story_points: 5 assignee: backend estimated_days: 2 created_date: 2025-11-06 dependencies: [story_5_5, story_5_2] --- # Story 5.7: Multi-Tenant Isolation Verification **Phase**: Phase 2 - Resources (Week 3-4) **Priority**: P0 CRITICAL **Estimated Effort**: 5 Story Points (2 days) ## User Story **As a** Security Engineer **I want** 100% multi-tenant data isolation for all MCP operations **So that** AI agents cannot access data from other tenants ## Business Value Multi-tenant security is CRITICAL for M2. A single data leak could: - Violate customer trust - Cause legal liability (GDPR, SOC2) - Damage brand reputation - Block enterprise adoption **This Story ensures zero cross-tenant data access.** ## Acceptance Criteria ### AC1: TenantContext Service - [ ] Extract `TenantId` from API Key - [ ] Set `CurrentTenantId` in request context - [ ] Accessible to all MCP operations ### AC2: Global Query Filters - [ ] EF Core Global Query Filters applied to all entities - [ ] All queries automatically filtered by `TenantId` - [ ] Cannot be disabled by accident ### AC3: Repository Level Isolation - [ ] All repositories enforce `TenantId` filter - [ ] No queries bypass tenant check - [ ] Aggregate roots include `TenantId` property ### AC4: Integration Tests - [ ] Create 2 test tenants (Tenant A, Tenant B) - [ ] Create test data in each tenant - [ ] Test 1: Tenant A API Key cannot read Tenant B projects - [ ] Test 2: Tenant A API Key cannot read Tenant B issues - [ ] Test 3: Tenant A API Key cannot read Tenant B users - [ ] Test 4: Direct ID access fails for other tenant's data - [ ] Test 5: Search queries never return cross-tenant results - [ ] All tests must pass (100% isolation) ### AC5: Security Audit - [ ] Code review by security team - [ ] Static analysis (no raw SQL without TenantId) - [ ] Verify all API endpoints enforce tenant filter ## Technical Design ### TenantContext Service ```csharp public interface ITenantContext { Guid CurrentTenantId { get; } } public class McpTenantContext : ITenantContext { private readonly IHttpContextAccessor _httpContextAccessor; public Guid CurrentTenantId { get { var tenantId = _httpContextAccessor.HttpContext?.Items["TenantId"]; if (tenantId == null) throw new McpUnauthorizedException("Tenant context not set"); return (Guid)tenantId; } } } ``` ### Global Query Filters ```csharp public class ColaFlowDbContext : DbContext { private readonly ITenantContext _tenantContext; protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Apply tenant filter to all tenant-scoped entities modelBuilder.Entity() .HasQueryFilter(p => p.TenantId == _tenantContext.CurrentTenantId); modelBuilder.Entity() .HasQueryFilter(e => e.TenantId == _tenantContext.CurrentTenantId); modelBuilder.Entity() .HasQueryFilter(s => s.TenantId == _tenantContext.CurrentTenantId); modelBuilder.Entity() .HasQueryFilter(t => t.TenantId == _tenantContext.CurrentTenantId); modelBuilder.Entity() .HasQueryFilter(s => s.TenantId == _tenantContext.CurrentTenantId); modelBuilder.Entity() .HasQueryFilter(u => u.TenantId == _tenantContext.CurrentTenantId); } } ``` ### Integration Test Example ```csharp [Fact] public async Task TenantA_CannotReadTenantB_Projects() { // Arrange var tenantA = await CreateTestTenant("Tenant A"); var tenantB = await CreateTestTenant("Tenant B"); var apiKeyA = await CreateApiKey(tenantA.Id, "API Key A"); var projectB = await CreateProject(tenantB.Id, "Project B"); // Act - Tenant A tries to access Tenant B's project var result = await CallMcpResource( apiKeyA, $"colaflow://projects.get/{projectB.Id}"); // Assert Assert.Equal(404, result.StatusCode); // NOT 403 (don't leak existence) Assert.Null(result.Data); } [Fact] public async Task IssuesSearch_NeverReturnsCrossTenant_Results() { // Arrange var tenantA = await CreateTestTenant("Tenant A"); var tenantB = await CreateTestTenant("Tenant B"); await CreateIssue(tenantA.Id, "Issue A"); await CreateIssue(tenantB.Id, "Issue B"); var apiKeyA = await CreateApiKey(tenantA.Id, "API Key A"); // Act - Tenant A searches all issues var result = await CallMcpResource( apiKeyA, "colaflow://issues.search"); // Assert var issues = result.Data["issues"]; Assert.Single(issues); // Only Tenant A's issue Assert.Equal("Issue A", issues[0]["title"]); } ``` ## Tasks ### Task 1: TenantContext Service (2 hours) - [ ] Create `ITenantContext` interface - [ ] Implement `McpTenantContext` (extract from HttpContext) - [ ] Register in DI container - [ ] Update API Key middleware to set TenantId **Files to Create**: - `ColaFlow.Modules.Mcp/Services/McpTenantContext.cs` ### Task 2: Global Query Filters (3 hours) - [ ] Add `TenantId` to all aggregate roots (if missing) - [ ] Configure Global Query Filters in DbContext - [ ] Test filters apply automatically - [ ] Verify cannot be disabled **Files to Modify**: - `ColaFlow.Infrastructure/Data/ColaFlowDbContext.cs` ### Task 3: Repository Validation (2 hours) - [ ] Audit all repository methods - [ ] Ensure all queries include TenantId - [ ] Add TenantId to method signatures if needed ### Task 4: Integration Tests - Cross-Tenant Access (6 hours) - [ ] Create multi-tenant test infrastructure - [ ] Test 1: Projects cross-tenant access - [ ] Test 2: Issues cross-tenant access - [ ] Test 3: Users cross-tenant access - [ ] Test 4: Direct ID access (404, not 403) - [ ] Test 5: Search never returns cross-tenant results **Files to Create**: - `ColaFlow.Modules.Mcp.Tests/Integration/MultiTenantIsolationTests.cs` ### Task 5: Security Audit (3 hours) - [ ] Code review by security team - [ ] Static analysis (grep for raw SQL) - [ ] Verify all API endpoints use TenantContext - [ ] Document security architecture **Files to Create**: - `docs/security/multi-tenant-isolation-audit.md` ## Testing Strategy ### Integration Tests (Critical) - 2 test tenants with separate data - 10+ test scenarios covering all Resources - 100% isolation verified - Tests must fail if isolation broken ### Security Audit Checklist - [ ] All entities have TenantId property - [ ] Global Query Filters configured - [ ] No raw SQL without TenantId - [ ] All API endpoints use TenantContext - [ ] Return 404 (not 403) for cross-tenant access - [ ] Audit logs include TenantId ## Dependencies **Prerequisites**: - Story 5.2 (API Key Management) - API Key linked to Tenant - Story 5.5 (Core Resources) - Resources to test **Critical Path**: BLOCKS M2 production deployment ## Risks & Mitigation | Risk | Impact | Probability | Mitigation | |------|--------|-------------|------------| | Tenant leak vulnerability | CRITICAL | Low | 100% test coverage, code review | | Global filter bypass | HIGH | Low | EF Core best practices, testing | | Performance impact | Medium | Low | Indexed TenantId columns | ## Definition of Done - [ ] TenantContext service working - [ ] Global Query Filters applied - [ ] ALL integration tests passing (100%) - [ ] Security audit complete (no findings) - [ ] Code reviewed by security team - [ ] Documentation updated ## Notes ### Why This Story Matters - **CRITICAL SECURITY**: Single most important security Story in M2 - **Compliance**: Required for GDPR, SOC2, enterprise customers - **Trust**: Multi-tenant leaks destroy customer trust - **Legal**: Data breaches have severe legal consequences ### Security Best Practices 1. **Defense in Depth**: Multiple layers (API Key, TenantContext, Global Filters) 2. **Fail Closed**: No data if TenantId missing (throw exception) 3. **404 not 403**: Don't leak existence of other tenant's data 4. **Audit Everything**: Log all tenant context access 5. **Test Religiously**: 100% integration test coverage ### Reference Materials - Multi-Tenant Security: https://learn.microsoft.com/en-us/ef/core/querying/filters - Sprint 5 Plan: `docs/plans/sprint_5.md`