using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; using Marco.Pms.Services.Helpers; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.EntityFrameworkCore; namespace Marco.Pms.Services.Service { public class PermissionServices { private readonly ApplicationDbContext _context; private readonly RolesHelper _rolesHelper; private readonly CacheUpdateHelper _cache; private readonly ILoggingService _logger; private readonly Guid tenantId; public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache, ILoggingService logger, UserHelper userHelper) { _context = context; _rolesHelper = rolesHelper; _cache = cache; _logger = logger; tenantId = userHelper.GetTenantId(); } /// /// Checks whether an employee has a specific feature permission, optionally within a project context. /// /// The target feature permission ID to check. /// The ID of the employee. /// Optional project ID for project-scoped permissions. /// True if the user has the permission, otherwise false. public async Task HasPermission(Guid featurePermissionId, Guid employeeId, Guid? projectId = null) { // 1. Try fetching permissions from cache (fast-path lookup). var featurePermissionIds = await _cache.GetPermissions(employeeId, tenantId); // If not found in cache, fallback to database (slower). if (featurePermissionIds == null) { var featurePermissions = await _rolesHelper.GetFeaturePermissionByEmployeeId(employeeId, tenantId); featurePermissionIds = featurePermissions.Select(fp => fp.Id).ToList(); } // 2. Handle project-level permission overrides if a project is specified. if (projectId.HasValue) { // Fetch permissions explicitly assigned to this employee in the project. var projectLevelPermissionIds = await _context.ProjectLevelPermissionMappings .AsNoTracking() .Where(pl => pl.ProjectId == projectId.Value && pl.EmployeeId == employeeId) .Select(pl => pl.PermissionId) .ToListAsync(); if (projectLevelPermissionIds?.Any() ?? false) { // Define modules where project-level overrides apply. var projectLevelModuleIds = new HashSet { Guid.Parse("53176ebf-c75d-42e5-839f-4508ffac3def"), Guid.Parse("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), Guid.Parse("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), Guid.Parse("a8cf4331-8f04-4961-8360-a3f7c3cc7462") }; // Get all feature permissions under those modules where the user didn't have explicit project-level grants. var allOverriddenPermissions = await _context.FeaturePermissions .AsNoTracking() .Where(fp => projectLevelModuleIds.Contains(fp.FeatureId) && !projectLevelPermissionIds.Contains(fp.Id)) .Select(fp => fp.Id) .ToListAsync(); // Apply overrides: // - Remove global permissions overridden by project-level rules. // - Add explicit project-level permissions. featurePermissionIds = featurePermissionIds .Except(allOverriddenPermissions) // Remove overridden .Concat(projectLevelPermissionIds) // Add project-level .Distinct() // Ensure no duplicates .ToList(); } } // 3. Final check: does the employee have the requested permission? return featurePermissionIds.Contains(featurePermissionId); } public async Task HasPermissionAny(List featurePermissionIds, Guid employeeId) { var allFeaturePermissionIds = await _cache.GetPermissions(employeeId, tenantId); if (allFeaturePermissionIds == null) { List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeId(employeeId, tenantId); allFeaturePermissionIds = featurePermission.Select(fp => fp.Id).ToList(); } var hasPermission = featurePermissionIds.Any(f => allFeaturePermissionIds.Contains(f)); return hasPermission; } public async Task HasProjectPermission(Employee LoggedInEmployee, Guid projectId) { var employeeId = LoggedInEmployee.Id; var projectIds = await _cache.GetProjects(employeeId, tenantId); if (projectIds == null) { var hasPermission = await HasPermission(PermissionsMaster.ManageProject, employeeId); if (hasPermission) { var projects = await _context.Projects.AsNoTracking().Where(c => c.TenantId == LoggedInEmployee.TenantId).ToListAsync(); projectIds = projects.Select(p => p.Id).ToList(); } else { var allocation = await _context.ProjectAllocations.AsNoTracking().Where(c => c.EmployeeId == employeeId && c.IsActive).ToListAsync(); if (!allocation.Any()) { return false; } projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); } await _cache.AddProjects(LoggedInEmployee.Id, projectIds, tenantId); } return projectIds.Contains(projectId); } /// /// Determines if an employee has permission to access a specific service project. /// Permission is granted if the user is directly allocated to the project OR /// assigned to any active job ticket within the project. /// /// The ID of the user requesting access. /// The ID of the project to access. /// True if access is allowed, otherwise False. public async Task HasServiceProjectPermission(Guid loggedInEmployeeId, Guid projectId) { Guid ReviewDoneStatus = Guid.Parse("ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7"); Guid ClosedStatus = Guid.Parse("3ddeefb5-ae3c-4e10-a922-35e0a452bb69"); // 1. Input Validation if (loggedInEmployeeId == Guid.Empty || projectId == Guid.Empty) { _logger.LogWarning("Permission check failed: Invalid input parameters. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployeeId, projectId); return false; } try { _logger.LogInfo("Starting permission check for Employee: {EmployeeId} on Project: {ProjectId}", loggedInEmployeeId, projectId); // 2. Check Level 1: Is the user a generic Team Member of the project? // This is usually the most common case, so checking this first saves complex query execution. bool isTeamMember = await _context.ServiceProjectAllocations .AsNoTracking() // Optimization: Read-only query does not need tracking .AnyAsync(spa => spa.ProjectId == projectId && spa.EmployeeId == loggedInEmployeeId && spa.IsActive && spa.TenantId == tenantId); if (isTeamMember) { _logger.LogInfo("Access Granted: User {EmployeeId} is a team member of Project {ProjectId}.", loggedInEmployeeId, projectId); return true; } // 3. Check Level 2: Is the user assigned to any ACTIVE specific Job Ticket? // Optimization: Combined the check for JobTicket and Mapping into a single Join query. // This prevents pulling a list of JobIds into memory (fixing memory bloat) and reduces DB roundtrips. bool hasActiveJobAssignment = await _context.JobTickets .AsNoTracking() .Where(jt => jt.ProjectId == projectId && jt.StatusId != ReviewDoneStatus && jt.StatusId != ClosedStatus && jt.IsActive) .Join(_context.JobEmployeeMappings, ticket => ticket.Id, mapping => mapping.JobTicketId, (ticket, mapping) => mapping) .AnyAsync(mapping => mapping.AssigneeId == loggedInEmployeeId && mapping.TenantId == tenantId); if (hasActiveJobAssignment) { _logger.LogInfo("Access Granted: User {EmployeeId} is assigned active tickets in Project {ProjectId}.", loggedInEmployeeId, projectId); return true; } // 4. Default Deny _logger.LogWarning("Access Denied: User {EmployeeId} has no permissions for Project {ProjectId}.", loggedInEmployeeId, projectId); return false; } catch (Exception ex) { // 5. Robust Error Handling // Log the full stack trace for debugging, but return false to maintain security (fail-closed). _logger.LogError(ex, "An error occurred while checking permissions for Employee: {EmployeeId} on Project: {ProjectId}", loggedInEmployeeId, projectId); return false; } } } }