fix(signalr): Add project-level permission validation to ProjectHub
SECURITY FIX: Prevent intra-tenant unauthorized project access
Problem:
Users within the same tenant could join ANY project room via SignalR
without permission checks, causing potential data leakage. The TODO
at line 18 in ProjectHub.cs left this critical validation unimplemented.
Solution:
- Created IProjectPermissionService interface for permission checking
- Implemented ProjectPermissionService with owner-based validation
- Added permission validation to ProjectHub.JoinProject() and LeaveProject()
- Returns clear HubException if user lacks permission
- Multi-tenant isolation enforced via PMDbContext query filters
Implementation Details:
1. IProjectPermissionService.IsUserProjectMemberAsync() checks if user
is the project owner (currently based on Project.OwnerId)
2. Service registered as Scoped in DI container via ModuleExtensions
3. ProjectHub throws HubException with clear error message for unauthorized access
4. TODO comments added for future ProjectMember table implementation
Files Changed:
- Added: IProjectPermissionService.cs (Application layer interface)
- Added: ProjectPermissionService.cs (Infrastructure layer implementation)
- Modified: ProjectHub.cs (permission checks in JoinProject/LeaveProject)
- Modified: ModuleExtensions.cs (service registration)
Testing:
- All existing tests pass (437 tests, 0 failures)
- Build succeeds with no errors
- Multi-tenant isolation preserved via DbContext filters
Future Enhancement:
When ProjectMember table is implemented, extend permission check to:
return project.OwnerId == userId ||
await _dbContext.ProjectMembers.AnyAsync(pm =>
pm.ProjectId == projectId && pm.UserId == userId)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,10 @@ public static class ModuleExtensions
|
||||
services.AddScoped<IProjectRepository, ProjectRepository>();
|
||||
services.AddScoped<IUnitOfWork, ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence.UnitOfWork>();
|
||||
|
||||
// Register services
|
||||
services.AddScoped<ColaFlow.Modules.ProjectManagement.Application.Services.IProjectPermissionService,
|
||||
ColaFlow.Modules.ProjectManagement.Infrastructure.Services.ProjectPermissionService>();
|
||||
|
||||
// Register MediatR handlers from Application assembly (v13.x syntax)
|
||||
services.AddMediatR(cfg =>
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Services;
|
||||
|
||||
namespace ColaFlow.API.Hubs;
|
||||
|
||||
@@ -7,6 +8,13 @@ namespace ColaFlow.API.Hubs;
|
||||
/// </summary>
|
||||
public class ProjectHub : BaseHub
|
||||
{
|
||||
private readonly IProjectPermissionService _permissionService;
|
||||
|
||||
public ProjectHub(IProjectPermissionService permissionService)
|
||||
{
|
||||
_permissionService = permissionService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Join project room (to receive project-level updates)
|
||||
/// </summary>
|
||||
@@ -15,7 +23,14 @@ public class ProjectHub : BaseHub
|
||||
var tenantId = GetCurrentTenantId();
|
||||
var userId = GetCurrentUserId();
|
||||
|
||||
// TODO: Validate user has permission to access this project
|
||||
// Validate user has permission to access this project
|
||||
var hasPermission = await _permissionService.IsUserProjectMemberAsync(
|
||||
userId, projectId, Context.ConnectionAborted);
|
||||
|
||||
if (!hasPermission)
|
||||
{
|
||||
throw new HubException("You do not have permission to access this project");
|
||||
}
|
||||
|
||||
var groupName = GetProjectGroupName(projectId);
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
|
||||
@@ -37,6 +52,16 @@ public class ProjectHub : BaseHub
|
||||
public async Task LeaveProject(Guid projectId)
|
||||
{
|
||||
var userId = GetCurrentUserId();
|
||||
|
||||
// Validate user has permission to access this project (for consistency)
|
||||
var hasPermission = await _permissionService.IsUserProjectMemberAsync(
|
||||
userId, projectId, Context.ConnectionAborted);
|
||||
|
||||
if (!hasPermission)
|
||||
{
|
||||
throw new HubException("You do not have permission to access this project");
|
||||
}
|
||||
|
||||
var groupName = GetProjectGroupName(projectId);
|
||||
|
||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace ColaFlow.Modules.ProjectManagement.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for checking project-level permissions
|
||||
/// </summary>
|
||||
public interface IProjectPermissionService
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if a user has permission to access a project
|
||||
/// Currently checks if user is the project owner
|
||||
/// TODO: Extend to check ProjectMember table when implemented
|
||||
/// </summary>
|
||||
/// <param name="userId">User ID to check</param>
|
||||
/// <param name="projectId">Project ID to check access for</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>True if user has access, false otherwise</returns>
|
||||
Task<bool> IsUserProjectMemberAsync(Guid userId, Guid projectId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Services;
|
||||
using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of project permission checking service
|
||||
/// </summary>
|
||||
public sealed class ProjectPermissionService : IProjectPermissionService
|
||||
{
|
||||
private readonly PMDbContext _dbContext;
|
||||
|
||||
public ProjectPermissionService(PMDbContext dbContext)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a user has permission to access a project
|
||||
/// Currently checks if user is the project owner
|
||||
/// Multi-tenant isolation is enforced by the DbContext query filter
|
||||
/// TODO: Extend to check ProjectMember table when implemented
|
||||
/// </summary>
|
||||
public async Task<bool> IsUserProjectMemberAsync(Guid userId, Guid projectId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Query will automatically apply tenant filter from PMDbContext
|
||||
var project = await _dbContext.Projects
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(p => p.Id.Value == projectId, cancellationToken);
|
||||
|
||||
if (project == null)
|
||||
{
|
||||
// Project doesn't exist or user's tenant doesn't have access
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if user is the project owner
|
||||
// TODO: When ProjectMember table is implemented, also check:
|
||||
// await _dbContext.ProjectMembers.AnyAsync(pm => pm.ProjectId == projectId && pm.UserId == userId)
|
||||
return project.OwnerId.Value == userId;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user