import { ForbiddenException, Injectable } from '@nestjs/common'; import { PrismaService } from '../../prisma/prisma.service'; export type ProjectGovernanceRole = 'owner' | 'admin' | 'member' | 'viewer'; export interface ProjectRoleAssertion { actorId?: string; projectId?: string | null; allowedRoles: ProjectGovernanceRole[]; permissions?: string[]; } export interface GlobalPermissionAssertion { actorId?: string; permissions?: string[]; requiredPermissions: string[]; } export interface ProjectRoleDecision { actorId: string; projectId: string; role: ProjectGovernanceRole; via: 'system' | 'project_member'; } export interface GlobalPermissionDecision { actorId: string; via: 'system' | 'permission'; } const ROLE_RANK: Record = { owner: 4, admin: 3, member: 2, viewer: 1, }; @Injectable() export class RbacService { constructor(private readonly prisma: PrismaService) {} async assertGlobalPermission(input: GlobalPermissionAssertion): Promise { const actorId = input.actorId?.trim(); if (!actorId) { throw new ForbiddenException('Missing actor scope'); } if (input.permissions?.includes('*')) { return { actorId, via: 'system' }; } if (hasAnyPermission(input.permissions ?? [], input.requiredPermissions)) { return { actorId, via: 'permission' }; } throw new ForbiddenException('Insufficient global permission'); } async assertProjectRole(input: ProjectRoleAssertion): Promise { const actorId = input.actorId?.trim(); const projectId = input.projectId?.trim(); if (!actorId || !projectId) { throw new ForbiddenException('Missing actor or project scope'); } if (input.permissions?.includes('*')) { return { actorId, projectId, role: 'owner', via: 'system' }; } const membership = await this.prisma.projectMember.findUnique({ where: { projectId_userId: { projectId, userId: actorId } }, }); const role = normalizeProjectRole(membership?.role); if (!role || !hasRequiredRole(role, input.allowedRoles)) { throw new ForbiddenException('Insufficient project role'); } return { actorId, projectId, role, via: 'project_member' }; } } export function normalizeProjectRole(role: string | null | undefined): ProjectGovernanceRole | null { if (role === 'owner' || role === 'admin' || role === 'member' || role === 'viewer') return role; return null; } export function hasRequiredRole(role: ProjectGovernanceRole, allowedRoles: ProjectGovernanceRole[]): boolean { if (allowedRoles.length === 0) return false; const minimumRank = Math.min(...allowedRoles.map((allowedRole) => ROLE_RANK[allowedRole])); return ROLE_RANK[role] >= minimumRank; } export function hasAnyPermission(permissions: string[], requiredPermissions: string[]): boolean { if (requiredPermissions.length === 0) return false; const granted = new Set(permissions); return requiredPermissions.some((permission) => granted.has(permission)); }