feat(backend): Create Sprint 2 backend Stories and Tasks
Created detailed implementation plans for Sprint 2 backend work: Story 1: Audit Log Foundation (Phase 1) - Task 1: Design AuditLog database schema and create migration - Task 2: Create AuditLog entity and Repository - Task 3: Implement EF Core SaveChangesInterceptor - Task 4: Write unit tests for audit logging - Task 5: Integrate with ProjectManagement Module Story 2: Audit Log Core Features (Phase 2) - Task 1: Implement Changed Fields Detection (JSON Diff) - Task 2: Integrate User Context Tracking - Task 3: Add Multi-Tenant Isolation - Task 4: Implement Audit Query API - Task 5: Write Integration Tests Story 3: Sprint Management Module - Task 1: Create Sprint Aggregate Root and Domain Events - Task 2: Implement Sprint Repository and EF Core Configuration - Task 3: Create CQRS Commands and Queries - Task 4: Implement Burndown Chart Calculation - Task 5: Add SignalR Real-Time Notifications - Task 6: Write Integration Tests Total: 3 Stories, 16 Tasks, 24 Story Points (8+8+8) Estimated Duration: 10-12 days All tasks include: - Detailed technical implementation guidance - Code examples and file paths - Testing requirements (>= 90% coverage) - Performance benchmarks (< 5ms audit overhead) - Multi-tenant security validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
201
docs/plans/sprint_2_story_2_task_3.md
Normal file
201
docs/plans/sprint_2_story_2_task_3.md
Normal file
@@ -0,0 +1,201 @@
|
||||
---
|
||||
task_id: sprint_2_story_2_task_3
|
||||
story: sprint_2_story_2
|
||||
status: not_started
|
||||
estimated_hours: 3
|
||||
created_date: 2025-11-05
|
||||
assignee: Backend Team
|
||||
---
|
||||
|
||||
# Task 3: Add Multi-Tenant Isolation
|
||||
|
||||
**Story**: Story 2 - Audit Log Core Features (Phase 2)
|
||||
**Estimated**: 3 hours
|
||||
|
||||
## Description
|
||||
|
||||
Ensure audit logs are properly isolated by TenantId to prevent cross-tenant data access. Implement global query filters and verify with comprehensive tests.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Global query filter applied to AuditLog entity
|
||||
- [ ] TenantId automatically set on audit log creation
|
||||
- [ ] Cross-tenant queries blocked
|
||||
- [ ] Integration tests verify isolation
|
||||
- [ ] Security audit passed
|
||||
|
||||
## Implementation Details
|
||||
|
||||
**Already Implemented in Story 1!**
|
||||
|
||||
The `AuditLogInterceptor` already sets TenantId:
|
||||
```csharp
|
||||
var auditLog = new AuditLog
|
||||
{
|
||||
TenantId = _tenantContext.TenantId,
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
**This Task: Add Global Query Filter and Comprehensive Testing**
|
||||
|
||||
1. **Update EF Configuration**: `colaflow-api/src/ColaFlow.Infrastructure/Data/Configurations/AuditLogConfiguration.cs`
|
||||
```csharp
|
||||
public void Configure(EntityTypeBuilder<AuditLog> builder)
|
||||
{
|
||||
// ... existing configuration ...
|
||||
|
||||
// Multi-tenant global query filter
|
||||
// This will be dynamically replaced by TenantContext at runtime
|
||||
builder.HasQueryFilter(a => a.TenantId == Guid.Empty);
|
||||
}
|
||||
```
|
||||
|
||||
2. **Apply Global Filter in DbContext**: `colaflow-api/src/ColaFlow.Infrastructure/Data/ColaFlowDbContext.cs`
|
||||
```csharp
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// Apply tenant filter to AuditLog
|
||||
var tenantId = _tenantContext.TenantId;
|
||||
modelBuilder.Entity<AuditLog>().HasQueryFilter(a => a.TenantId == tenantId);
|
||||
}
|
||||
```
|
||||
|
||||
3. **Verify Repository Filtering**: `colaflow-api/src/ColaFlow.Infrastructure/Repositories/AuditLogRepository.cs`
|
||||
```csharp
|
||||
public async Task<List<AuditLog>> GetByEntityAsync(string entityType, Guid entityId)
|
||||
{
|
||||
var tenantId = _tenantContext.TenantId;
|
||||
|
||||
// Query automatically filtered by global query filter
|
||||
// Explicit TenantId check added for defense-in-depth
|
||||
return await _context.AuditLogs
|
||||
.Where(a => a.TenantId == tenantId && a.EntityType == entityType && a.EntityId == entityId)
|
||||
.OrderByDescending(a => a.Timestamp)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
```
|
||||
|
||||
**Defense-in-Depth Strategy**:
|
||||
1. **Layer 1**: TenantId automatically set by Interceptor
|
||||
2. **Layer 2**: Global Query Filter in EF Core
|
||||
3. **Layer 3**: Explicit TenantId checks in Repository
|
||||
4. **Layer 4**: Integration tests verify isolation
|
||||
|
||||
## Testing
|
||||
|
||||
**Integration Tests**: `colaflow-api/tests/ColaFlow.Application.IntegrationTests/AuditLog/MultiTenantIsolationTests.cs`
|
||||
|
||||
```csharp
|
||||
public class AuditLogMultiTenantIsolationTests : IntegrationTestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetByEntityAsync_ShouldOnlyReturnCurrentTenantAuditLogs()
|
||||
{
|
||||
// Arrange - Create audit logs for two tenants
|
||||
var tenant1Id = Guid.NewGuid();
|
||||
var tenant2Id = Guid.NewGuid();
|
||||
var entityId = Guid.NewGuid();
|
||||
|
||||
// Tenant 1 creates a project
|
||||
SetCurrentTenant(tenant1Id);
|
||||
var log1 = new AuditLog
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant1Id,
|
||||
EntityType = "Project",
|
||||
EntityId = entityId,
|
||||
Action = AuditAction.Create,
|
||||
Timestamp = DateTime.UtcNow
|
||||
};
|
||||
await Context.AuditLogs.AddAsync(log1);
|
||||
await Context.SaveChangesAsync();
|
||||
|
||||
// Tenant 2 tries to access (should fail)
|
||||
SetCurrentTenant(tenant2Id);
|
||||
|
||||
var repository = new AuditLogRepository(Context, TenantContext);
|
||||
|
||||
// Act
|
||||
var result = await repository.GetByEntityAsync("Project", entityId);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result); // Tenant 2 should NOT see Tenant 1's audit logs
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateProject_ShouldSetTenantIdAutomatically()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = Guid.NewGuid();
|
||||
SetCurrentTenant(tenantId);
|
||||
|
||||
// Act
|
||||
var projectId = await Mediator.Send(new CreateProjectCommand
|
||||
{
|
||||
Name = "Test Project",
|
||||
Key = "TEST"
|
||||
});
|
||||
|
||||
// Assert
|
||||
var auditLogs = await Context.AuditLogs
|
||||
.IgnoreQueryFilters() // Bypass filter to check actual data
|
||||
.Where(a => a.EntityId == projectId)
|
||||
.ToListAsync();
|
||||
|
||||
Assert.All(auditLogs, log => Assert.Equal(tenantId, log.TenantId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DirectDatabaseQuery_ShouldRespectGlobalQueryFilter()
|
||||
{
|
||||
// Arrange
|
||||
var tenant1Id = Guid.NewGuid();
|
||||
var tenant2Id = Guid.NewGuid();
|
||||
|
||||
// Create logs for both tenants (using IgnoreQueryFilters)
|
||||
await Context.AuditLogs.AddAsync(new AuditLog { Id = Guid.NewGuid(), TenantId = tenant1Id, /* ... */ });
|
||||
await Context.AuditLogs.AddAsync(new AuditLog { Id = Guid.NewGuid(), TenantId = tenant2Id, /* ... */ });
|
||||
await Context.SaveChangesAsync();
|
||||
|
||||
// Act - Set current tenant to tenant1
|
||||
SetCurrentTenant(tenant1Id);
|
||||
var logs = await Context.AuditLogs.ToListAsync();
|
||||
|
||||
// Assert - Should only see tenant1 logs
|
||||
Assert.All(logs, log => Assert.Equal(tenant1Id, log.TenantId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BypassFilter_ShouldWorkWithIgnoreQueryFilters()
|
||||
{
|
||||
// For admin/system operations that need to see all tenants
|
||||
var allLogs = await Context.AuditLogs
|
||||
.IgnoreQueryFilters()
|
||||
.ToListAsync();
|
||||
|
||||
Assert.True(allLogs.Count > 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Security Test Cases**:
|
||||
1. Cross-tenant read blocked
|
||||
2. TenantId automatically set on creation
|
||||
3. Global query filter applied
|
||||
4. IgnoreQueryFilters works for admin operations
|
||||
5. Repository explicit filtering works
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- Use `IgnoreQueryFilters()` for admin/system operations only
|
||||
- Always combine global filter with explicit TenantId checks (defense-in-depth)
|
||||
- Test with real database, not in-memory (to verify SQL generation)
|
||||
- Run security tests in CI/CD pipeline
|
||||
|
||||
---
|
||||
|
||||
**Created**: 2025-11-05 by Backend Agent
|
||||
Reference in New Issue
Block a user