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>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Commands.DeleteStory;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
@@ -12,13 +13,19 @@ public class DeleteStoryCommandHandlerTests
|
||||
{
|
||||
private readonly Mock<IProjectRepository> _projectRepositoryMock;
|
||||
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
|
||||
private readonly Mock<ITenantContext> _tenantContextMock;
|
||||
private readonly DeleteStoryCommandHandler _handler;
|
||||
private readonly Guid _tenantId;
|
||||
|
||||
public DeleteStoryCommandHandlerTests()
|
||||
{
|
||||
_projectRepositoryMock = new Mock<IProjectRepository>();
|
||||
_unitOfWorkMock = new Mock<IUnitOfWork>();
|
||||
_handler = new DeleteStoryCommandHandler(_projectRepositoryMock.Object, _unitOfWorkMock.Object);
|
||||
_tenantContextMock = new Mock<ITenantContext>();
|
||||
_tenantId = Guid.NewGuid();
|
||||
|
||||
_tenantContextMock.Setup(x => x.GetCurrentTenantId()).Returns(_tenantId);
|
||||
_handler = new DeleteStoryCommandHandler(_projectRepositoryMock.Object, _unitOfWorkMock.Object, _tenantContextMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -26,7 +33,7 @@ public class DeleteStoryCommandHandlerTests
|
||||
{
|
||||
// Arrange
|
||||
var userId = UserId.Create();
|
||||
var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TST", userId);
|
||||
var project = Project.Create(TenantId.Create(_tenantId), "Test Project", "Description", "TST", userId);
|
||||
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
||||
var story = epic.CreateStory("Story to Delete", "Description", TaskPriority.Medium, userId);
|
||||
var storyId = story.Id;
|
||||
@@ -70,7 +77,7 @@ public class DeleteStoryCommandHandlerTests
|
||||
{
|
||||
// Arrange
|
||||
var userId = UserId.Create();
|
||||
var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TST", userId);
|
||||
var project = Project.Create(TenantId.Create(_tenantId), "Test Project", "Description", "TST", userId);
|
||||
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
||||
var story = epic.CreateStory("Story with Tasks", "Description", TaskPriority.Medium, userId);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Commands.DeleteTask;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
@@ -12,13 +13,19 @@ public class DeleteTaskCommandHandlerTests
|
||||
{
|
||||
private readonly Mock<IProjectRepository> _projectRepositoryMock;
|
||||
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
|
||||
private readonly Mock<ITenantContext> _tenantContextMock;
|
||||
private readonly DeleteTaskCommandHandler _handler;
|
||||
private readonly Guid _tenantId;
|
||||
|
||||
public DeleteTaskCommandHandlerTests()
|
||||
{
|
||||
_projectRepositoryMock = new Mock<IProjectRepository>();
|
||||
_unitOfWorkMock = new Mock<IUnitOfWork>();
|
||||
_handler = new DeleteTaskCommandHandler(_projectRepositoryMock.Object, _unitOfWorkMock.Object);
|
||||
_tenantContextMock = new Mock<ITenantContext>();
|
||||
_tenantId = Guid.NewGuid();
|
||||
|
||||
_tenantContextMock.Setup(x => x.GetCurrentTenantId()).Returns(_tenantId);
|
||||
_handler = new DeleteTaskCommandHandler(_projectRepositoryMock.Object, _unitOfWorkMock.Object, _tenantContextMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -26,7 +33,7 @@ public class DeleteTaskCommandHandlerTests
|
||||
{
|
||||
// Arrange
|
||||
var userId = UserId.Create();
|
||||
var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TST", userId);
|
||||
var project = Project.Create(TenantId.Create(_tenantId), "Test Project", "Description", "TST", userId);
|
||||
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
||||
var story = epic.CreateStory("Test Story", "Story Description", TaskPriority.Medium, userId);
|
||||
var task = story.CreateTask("Task to Delete", "Description", TaskPriority.Medium, userId);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateStory;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
@@ -12,13 +13,19 @@ public class UpdateStoryCommandHandlerTests
|
||||
{
|
||||
private readonly Mock<IProjectRepository> _projectRepositoryMock;
|
||||
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
|
||||
private readonly Mock<ITenantContext> _tenantContextMock;
|
||||
private readonly UpdateStoryCommandHandler _handler;
|
||||
private readonly Guid _tenantId;
|
||||
|
||||
public UpdateStoryCommandHandlerTests()
|
||||
{
|
||||
_projectRepositoryMock = new Mock<IProjectRepository>();
|
||||
_unitOfWorkMock = new Mock<IUnitOfWork>();
|
||||
_handler = new UpdateStoryCommandHandler(_projectRepositoryMock.Object, _unitOfWorkMock.Object);
|
||||
_tenantContextMock = new Mock<ITenantContext>();
|
||||
_tenantId = Guid.NewGuid();
|
||||
|
||||
_tenantContextMock.Setup(x => x.GetCurrentTenantId()).Returns(_tenantId);
|
||||
_handler = new UpdateStoryCommandHandler(_projectRepositoryMock.Object, _unitOfWorkMock.Object, _tenantContextMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -26,7 +33,7 @@ public class UpdateStoryCommandHandlerTests
|
||||
{
|
||||
// Arrange
|
||||
var userId = UserId.Create();
|
||||
var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TST", userId);
|
||||
var project = Project.Create(TenantId.Create(_tenantId), "Test Project", "Description", "TST", userId);
|
||||
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
||||
var story = epic.CreateStory("Original Title", "Original Description", TaskPriority.Low, userId);
|
||||
var storyId = story.Id;
|
||||
@@ -89,7 +96,7 @@ public class UpdateStoryCommandHandlerTests
|
||||
{
|
||||
// Arrange
|
||||
var userId = UserId.Create();
|
||||
var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TST", userId);
|
||||
var project = Project.Create(TenantId.Create(_tenantId), "Test Project", "Description", "TST", userId);
|
||||
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
||||
var story = epic.CreateStory("Original", "Original", TaskPriority.Low, userId);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetStoryById;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
@@ -11,12 +12,18 @@ namespace ColaFlow.Application.Tests.Queries.GetStoryById;
|
||||
public class GetStoryByIdQueryHandlerTests
|
||||
{
|
||||
private readonly Mock<IProjectRepository> _projectRepositoryMock;
|
||||
private readonly Mock<ITenantContext> _tenantContextMock;
|
||||
private readonly GetStoryByIdQueryHandler _handler;
|
||||
private readonly Guid _tenantId;
|
||||
|
||||
public GetStoryByIdQueryHandlerTests()
|
||||
{
|
||||
_projectRepositoryMock = new Mock<IProjectRepository>();
|
||||
_handler = new GetStoryByIdQueryHandler(_projectRepositoryMock.Object);
|
||||
_tenantContextMock = new Mock<ITenantContext>();
|
||||
_tenantId = Guid.NewGuid();
|
||||
|
||||
_tenantContextMock.Setup(x => x.GetCurrentTenantId()).Returns(_tenantId);
|
||||
_handler = new GetStoryByIdQueryHandler(_projectRepositoryMock.Object, _tenantContextMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -24,7 +31,7 @@ public class GetStoryByIdQueryHandlerTests
|
||||
{
|
||||
// Arrange
|
||||
var userId = UserId.Create();
|
||||
var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TST", userId);
|
||||
var project = Project.Create(TenantId.Create(_tenantId), "Test Project", "Description", "TST", userId);
|
||||
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
||||
var story = epic.CreateStory("Test Story", "Story Description", TaskPriority.High, userId);
|
||||
var task1 = story.CreateTask("Task 1", "Description 1", TaskPriority.Medium, userId);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetTaskById;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
@@ -11,12 +12,20 @@ namespace ColaFlow.Application.Tests.Queries.GetTaskById;
|
||||
public class GetTaskByIdQueryHandlerTests
|
||||
{
|
||||
private readonly Mock<IProjectRepository> _projectRepositoryMock;
|
||||
private readonly Mock<ITenantContext> _tenantContextMock;
|
||||
private readonly GetTaskByIdQueryHandler _handler;
|
||||
private readonly Guid _tenantId;
|
||||
|
||||
public GetTaskByIdQueryHandlerTests()
|
||||
{
|
||||
_projectRepositoryMock = new Mock<IProjectRepository>();
|
||||
_handler = new GetTaskByIdQueryHandler(_projectRepositoryMock.Object);
|
||||
_tenantContextMock = new Mock<ITenantContext>();
|
||||
_tenantId = Guid.NewGuid();
|
||||
|
||||
// Setup default tenant context
|
||||
_tenantContextMock.Setup(x => x.GetCurrentTenantId()).Returns(_tenantId);
|
||||
|
||||
_handler = new GetTaskByIdQueryHandler(_projectRepositoryMock.Object, _tenantContextMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -24,7 +33,7 @@ public class GetTaskByIdQueryHandlerTests
|
||||
{
|
||||
// Arrange
|
||||
var userId = UserId.Create();
|
||||
var project = Project.Create(TenantId.Create(Guid.NewGuid()), "Test Project", "Description", "TST", userId);
|
||||
var project = Project.Create(TenantId.Create(_tenantId), "Test Project", "Description", "TST", userId);
|
||||
var epic = project.CreateEpic("Test Epic", "Epic Description", userId);
|
||||
var story = epic.CreateStory("Test Story", "Story Description", TaskPriority.Medium, userId);
|
||||
var task = story.CreateTask("Test Task", "Task Description", TaskPriority.High, userId);
|
||||
|
||||
Reference in New Issue
Block a user