/** * PermissionService - RBAC resolution across org → team → repo hierarchy */ import type { TOrganizationRole, TTeamRole, TRepositoryRole, TRegistryProtocol, } from '../interfaces/auth.interfaces.ts'; import { User, Organization, OrganizationMember, Team, TeamMember, Repository, RepositoryPermission, } from '../models/index.ts'; export type TAction = 'read' | 'write' | 'delete' | 'admin'; export interface IPermissionContext { userId: string; organizationId?: string; repositoryId?: string; protocol?: TRegistryProtocol; } export interface IResolvedPermissions { canRead: boolean; canWrite: boolean; canDelete: boolean; canAdmin: boolean; effectiveRole: TRepositoryRole | null; organizationRole: TOrganizationRole | null; teamRoles: Array<{ teamId: string; role: TTeamRole }>; repositoryRole: TRepositoryRole | null; } export class PermissionService { /** * Resolve all permissions for a user in a specific context */ public async resolvePermissions(context: IPermissionContext): Promise { const result: IResolvedPermissions = { canRead: false, canWrite: false, canDelete: false, canAdmin: false, effectiveRole: null, organizationRole: null, teamRoles: [], repositoryRole: null, }; // Get user const user = await User.findById(context.userId); if (!user || !user.isActive) return result; // System admins have full access if (user.isSystemAdmin) { result.canRead = true; result.canWrite = true; result.canDelete = true; result.canAdmin = true; result.effectiveRole = 'admin'; return result; } if (!context.organizationId) return result; // Get organization membership const orgMember = await OrganizationMember.findMembership(context.organizationId, context.userId); if (orgMember) { result.organizationRole = orgMember.role; // Organization owners have full access to everything in the org if (orgMember.role === 'owner') { result.canRead = true; result.canWrite = true; result.canDelete = true; result.canAdmin = true; result.effectiveRole = 'admin'; return result; } // Organization admins have admin access to all repos if (orgMember.role === 'admin') { result.canRead = true; result.canWrite = true; result.canDelete = true; result.canAdmin = true; result.effectiveRole = 'admin'; return result; } } // If no repository specified, check org-level permissions if (!context.repositoryId) { if (orgMember) { result.canRead = true; // Members can read org info result.effectiveRole = 'reader'; } return result; } // Get repository const repository = await Repository.findById(context.repositoryId); if (!repository) return result; // Check if repository is public if (repository.isPublic) { result.canRead = true; } // Get team memberships that grant access to this repository if (orgMember) { const teams = await Team.getOrgTeams(context.organizationId); for (const team of teams) { const teamMember = await TeamMember.findMembership(team.id, context.userId); if (teamMember) { result.teamRoles.push({ teamId: team.id, role: teamMember.role }); // Check if team has access to this repository if (team.repositoryIds.includes(context.repositoryId)) { // Team maintainers get maintainer access to repos if (teamMember.role === 'maintainer') { this.applyRole(result, 'maintainer'); } else { // Team members get developer access this.applyRole(result, 'developer'); } } } } } // Get direct repository permission (highest priority) const repoPerm = await RepositoryPermission.findPermission(context.repositoryId, context.userId); if (repoPerm) { result.repositoryRole = repoPerm.role; this.applyRole(result, repoPerm.role); } return result; } /** * Check if user can perform a specific action */ public async checkPermission( context: IPermissionContext, action: TAction ): Promise { const permissions = await this.resolvePermissions(context); switch (action) { case 'read': return permissions.canRead; case 'write': return permissions.canWrite; case 'delete': return permissions.canDelete; case 'admin': return permissions.canAdmin; default: return false; } } /** * Check if user can access a package */ public async canAccessPackage( userId: string, organizationId: string, repositoryId: string, action: 'read' | 'write' | 'delete' ): Promise { return await this.checkPermission( { userId, organizationId, repositoryId }, action ); } /** * Check if user can manage organization */ public async canManageOrganization(userId: string, organizationId: string): Promise { const user = await User.findById(userId); if (!user || !user.isActive) return false; if (user.isSystemAdmin) return true; const orgMember = await OrganizationMember.findMembership(organizationId, userId); return orgMember?.role === 'owner' || orgMember?.role === 'admin'; } /** * Check if user can manage repository */ public async canManageRepository( userId: string, organizationId: string, repositoryId: string ): Promise { const permissions = await this.resolvePermissions({ userId, organizationId, repositoryId, }); return permissions.canAdmin; } /** * Get all repositories a user can access in an organization */ public async getAccessibleRepositories( userId: string, organizationId: string ): Promise { const user = await User.findById(userId); if (!user || !user.isActive) return []; // System admins and org owners/admins can access all repos if (user.isSystemAdmin) { return await Repository.getOrgRepositories(organizationId); } const orgMember = await OrganizationMember.findMembership(organizationId, userId); if (orgMember?.role === 'owner' || orgMember?.role === 'admin') { return await Repository.getOrgRepositories(organizationId); } const allRepos = await Repository.getOrgRepositories(organizationId); const accessibleRepos: Repository[] = []; for (const repo of allRepos) { // Public repos are always accessible if (repo.isPublic) { accessibleRepos.push(repo); continue; } // Check direct permission const directPerm = await RepositoryPermission.findPermission(repo.id, userId); if (directPerm) { accessibleRepos.push(repo); continue; } // Check team access const teams = await Team.getOrgTeams(organizationId); for (const team of teams) { if (team.repositoryIds.includes(repo.id)) { const teamMember = await TeamMember.findMembership(team.id, userId); if (teamMember) { accessibleRepos.push(repo); break; } } } } return accessibleRepos; } /** * Apply a role's permissions to the result */ private applyRole(result: IResolvedPermissions, role: TRepositoryRole): void { const roleHierarchy: Record = { reader: 1, developer: 2, maintainer: 3, admin: 4, }; const currentLevel = result.effectiveRole ? roleHierarchy[result.effectiveRole] : 0; const newLevel = roleHierarchy[role]; if (newLevel > currentLevel) { result.effectiveRole = role; } switch (role) { case 'admin': result.canRead = true; result.canWrite = true; result.canDelete = true; result.canAdmin = true; break; case 'maintainer': result.canRead = true; result.canWrite = true; result.canDelete = true; break; case 'developer': result.canRead = true; result.canWrite = true; break; case 'reader': result.canRead = true; break; } } }