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>
55 lines
2.4 KiB
C#
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();
|
|
}
|
|
}
|