Files
ColaFlow/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetEpicsByProjectId/GetEpicsByProjectIdQueryHandler.cs
Yaojia Wang 6046bad12e fix(backend): Add explicit TenantId validation to Epic/Story/Task Query/Command Handlers
CRITICAL SECURITY FIX: Implemented Defense in Depth security pattern by adding
explicit TenantId verification to all Epic/Story/Task Query and Command Handlers.

Security Impact:
- BEFORE: Relied solely on EF Core global query filters (single layer)
- AFTER: Explicit TenantId validation + EF Core filters (defense in depth)

This ensures that even if EF Core query filters are accidentally disabled or bypassed,
tenant isolation is still maintained at the application layer.

Changes:

Query Handlers (6 handlers):
- GetEpicByIdQueryHandler: Added ITenantContext injection + explicit TenantId check
- GetStoryByIdQueryHandler: Added ITenantContext injection + explicit TenantId check
- GetTaskByIdQueryHandler: Added ITenantContext injection + explicit TenantId check
- GetEpicsByProjectIdQueryHandler: Verify Project.TenantId before querying Epics
- GetStoriesByEpicIdQueryHandler: Verify Epic.TenantId before querying Stories
- GetTasksByStoryIdQueryHandler: Verify Story.TenantId before querying Tasks

Command Handlers (5 handlers):
- UpdateEpicCommandHandler: Verify Project.TenantId before updating
- UpdateStoryCommandHandler: Verify Project.TenantId before updating
- UpdateTaskCommandHandler: Verify Project.TenantId before updating
- DeleteStoryCommandHandler: Verify Project.TenantId before deleting
- DeleteTaskCommandHandler: Verify Project.TenantId before deleting

Unit Tests:
- Updated 5 unit test files to mock ITenantContext
- All 32 unit tests passing
- All 7 multi-tenant isolation integration tests passing

Defense Layers (Security in Depth):
Layer 1: EF Core global query filters (database level)
Layer 2: Application-layer explicit TenantId validation (handler level)
Layer 3: Integration tests verifying tenant isolation (test level)

Test Results:
- Unit Tests: 32/32 PASSING
- Integration Tests: 7/7 PASSING (multi-tenant isolation)

This fix addresses a critical security vulnerability where we relied on a single
layer of defense (EF Core query filters) for tenant data isolation. Now we have
multiple layers ensuring no cross-tenant data leaks can occur.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 20:30:24 +01:00

55 lines
2.4 KiB
C#

using MediatR;
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetEpicsByProjectId;
/// <summary>
/// Handler for GetEpicsByProjectIdQuery
/// </summary>
public sealed class GetEpicsByProjectIdQueryHandler(
IProjectRepository projectRepository,
ITenantContext tenantContext)
: IRequestHandler<GetEpicsByProjectIdQuery, List<EpicDto>>
{
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
private readonly ITenantContext _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
public async Task<List<EpicDto>> Handle(GetEpicsByProjectIdQuery request, CancellationToken cancellationToken)
{
// Get current tenant ID (Defense in Depth - Layer 2)
var currentTenantId = _tenantContext.GetCurrentTenantId();
// CRITICAL SECURITY: Verify Project belongs to current tenant before querying epics
var projectId = ProjectId.From(request.ProjectId);
var project = await _projectRepository.GetByIdAsync(projectId, cancellationToken);
if (project == null)
throw new NotFoundException("Project", request.ProjectId);
// Explicit TenantId validation (Defense in Depth)
if (project.TenantId.Value != currentTenantId)
throw new NotFoundException("Project", request.ProjectId);
// Now fetch epics (already filtered by EF Core, but we verified project ownership)
var epics = await _projectRepository.GetEpicsByProjectIdAsync(projectId, cancellationToken);
return epics.Select(epic => new EpicDto
{
Id = epic.Id.Value,
Name = epic.Name,
Description = epic.Description,
ProjectId = epic.ProjectId.Value,
Status = epic.Status.Name,
Priority = epic.Priority.Name,
CreatedBy = epic.CreatedBy.Value,
CreatedAt = epic.CreatedAt,
UpdatedAt = epic.UpdatedAt,
Stories = new List<StoryDto>() // Don't include stories in list view
}).ToList();
}
}