- Added SettingsComponent for user profile management, including display name and password change functionality. - Introduced TokensComponent for managing API tokens, including creation and revocation. - Created LayoutComponent for consistent application layout with navigation and user information. - Established main application structure in index.html and main.ts. - Integrated Tailwind CSS for styling and responsive design. - Configured TypeScript settings for strict type checking and module resolution.
308 lines
8.3 KiB
TypeScript
308 lines
8.3 KiB
TypeScript
/**
|
|
* 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<IResolvedPermissions> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
return await this.checkPermission(
|
|
{ userId, organizationId, repositoryId },
|
|
action
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if user can manage organization
|
|
*/
|
|
public async canManageOrganization(userId: string, organizationId: string): Promise<boolean> {
|
|
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<boolean> {
|
|
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<Repository[]> {
|
|
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<TRepositoryRole, number> = {
|
|
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;
|
|
}
|
|
}
|
|
}
|