import { AbilityBuilder, PureAbility, AbilityClass } from '@casl/ability'; import { Injectable } from '@nestjs/common'; import { User } from '../models/user.model'; import { RoleObjectPermission } from '../models/role-object-permission.model'; import { RoleFieldPermission } from '../models/role-field-permission.model'; import { RecordShare } from '../models/record-share.model'; // Define action types export type Action = 'create' | 'read' | 'update' | 'delete' | 'view_all' | 'modify_all'; // Define subject types - can be string (object API name) or actual object with fields export type Subject = string | { objectApiName: string; ownerId?: string; id?: string; [key: string]: any }; // Define field actions export type FieldAction = 'read' | 'edit'; export type AppAbility = PureAbility<[Action, Subject], { field?: string }>; @Injectable() export class AbilityFactory { /** * Build CASL ability for a user based on their roles and permissions * This aggregates permissions from all roles the user has */ async defineAbilityFor( user: User & { roles?: Array<{ objectPermissions?: RoleObjectPermission[]; fieldPermissions?: RoleFieldPermission[] }> }, recordShares?: RecordShare[], ): Promise { const { can, cannot, build } = new AbilityBuilder(PureAbility as AbilityClass); if (!user.roles || user.roles.length === 0) { // No roles = no permissions return build(); } // Aggregate object permissions from all roles const objectPermissionsMap = new Map(); // Aggregate field permissions from all roles const fieldPermissionsMap = new Map(); // Process all roles for (const role of user.roles) { // Aggregate object permissions if (role.objectPermissions) { for (const perm of role.objectPermissions) { const existing = objectPermissionsMap.get(perm.objectDefinitionId) || { canCreate: false, canRead: false, canEdit: false, canDelete: false, canViewAll: false, canModifyAll: false, }; // Union of permissions (if any role grants it, user has it) objectPermissionsMap.set(perm.objectDefinitionId, { canCreate: existing.canCreate || perm.canCreate, canRead: existing.canRead || perm.canRead, canEdit: existing.canEdit || perm.canEdit, canDelete: existing.canDelete || perm.canDelete, canViewAll: existing.canViewAll || perm.canViewAll, canModifyAll: existing.canModifyAll || perm.canModifyAll, }); } } // Aggregate field permissions if (role.fieldPermissions) { for (const perm of role.fieldPermissions) { const existing = fieldPermissionsMap.get(perm.fieldDefinitionId) || { canRead: false, canEdit: false, }; fieldPermissionsMap.set(perm.fieldDefinitionId, { canRead: existing.canRead || perm.canRead, canEdit: existing.canEdit || perm.canEdit, }); } } } // Convert aggregated permissions to CASL rules for (const [objectId, perms] of objectPermissionsMap) { // Create permission if (perms.canCreate) { can('create', objectId); } // Read permission if (perms.canRead) { can('read', objectId); } // View all permission (can see all records regardless of ownership) if (perms.canViewAll) { can('view_all', objectId); } // Edit permission if (perms.canEdit) { can('update', objectId); } // Modify all permission (can edit all records regardless of ownership) if (perms.canModifyAll) { can('modify_all', objectId); } // Delete permission if (perms.canDelete) { can('delete', objectId); } } // Add record sharing permissions if (recordShares) { for (const share of recordShares) { // Only add if share is active (not expired, not revoked) const now = new Date(); const isExpired = share.expiresAt && share.expiresAt < now; const isRevoked = share.revokedAt !== null; if (!isExpired && !isRevoked) { // Note: Record-level sharing will be checked in authorization service // CASL abilities are primarily for object-level permissions // Individual record access is validated in applyScopeToQuery } } } return build(); } /** * Check if user can access a specific field * Returns true if user has permission or if no restriction exists */ canAccessField( fieldDefinitionId: string, action: FieldAction, user: User & { roles?: Array<{ fieldPermissions?: RoleFieldPermission[] }> }, ): boolean { if (!user.roles || user.roles.length === 0) { return false; } // Collect all field permissions from all roles const allFieldPermissions: RoleFieldPermission[] = []; for (const role of user.roles) { if (role.fieldPermissions) { allFieldPermissions.push(...role.fieldPermissions); } } // If there are NO field permissions configured at all, allow by default if (allFieldPermissions.length === 0) { return true; } // If field permissions exist, check for explicit grants (union of all roles) for (const role of user.roles) { if (role.fieldPermissions) { const fieldPerm = role.fieldPermissions.find(fp => fp.fieldDefinitionId === fieldDefinitionId); if (fieldPerm) { if (action === 'read' && fieldPerm.canRead) return true; if (action === 'edit' && fieldPerm.canEdit) return true; } } } // Field permissions exist but this field is not explicitly granted → deny return false; } /** * Filter fields based on user permissions * Returns array of field IDs the user can access with the specified action */ filterFields( fieldDefinitionIds: string[], action: FieldAction, user: User & { roles?: Array<{ fieldPermissions?: RoleFieldPermission[] }> }, ): string[] { return fieldDefinitionIds.filter(fieldId => this.canAccessField(fieldId, action, user)); } }