Files
calendar/packages/features/pbac/utils/permissionTraversal.ts
T

125 lines
4.5 KiB
TypeScript

import { CrudAction, type CustomAction, PERMISSION_REGISTRY } from "../domain/types/permission-registry";
/**
* Helper function to split permission string into resource and action
* Handles dotted resource names like "organization.attributes.read"
* @param permission Permission string (e.g., "organization.attributes.read")
* @returns Object with resource and action parts
*/
const splitPermission = (permission: string): { resource: string; action: string } => {
const lastDotIndex = permission.lastIndexOf(".");
if (lastDotIndex === -1) {
throw new Error(`Invalid permission format: ${permission}`);
}
const resource = permission.substring(0, lastDotIndex);
const action = permission.substring(lastDotIndex + 1);
return { resource, action };
};
/**
* Generic permission graph traversal function using BFS
* @param startPermission The permission to start traversal from
* @param direction Whether to find dependencies or dependents
* @returns Array of related permissions (excluding the start permission itself)
*/
export const traversePermissions = (
startPermission: string,
direction: "dependencies" | "dependents"
): string[] => {
const visited = new Set<string>();
const result = new Set<string>();
const queue: string[] = [startPermission];
while (queue.length > 0) {
const currentPermission = queue.shift();
if (!currentPermission) {
break;
}
if (visited.has(currentPermission)) {
continue;
}
visited.add(currentPermission);
if (direction === "dependencies") {
// Find what the current permission depends on
const { resource, action } = splitPermission(currentPermission);
const resourceConfig = PERMISSION_REGISTRY[resource as keyof typeof PERMISSION_REGISTRY];
if (resourceConfig && resourceConfig[action as CrudAction | CustomAction]) {
const permissionDetails = resourceConfig[action as CrudAction | CustomAction];
if (permissionDetails?.dependsOn) {
permissionDetails.dependsOn.forEach((dependency: string) => {
if (!visited.has(dependency)) {
result.add(dependency);
queue.push(dependency);
}
});
}
}
// Backward compatibility: add read dependency for CRUD operations
if (action === CrudAction.Create || action === CrudAction.Update || action === CrudAction.Delete) {
const readPermission = `${resource}.${CrudAction.Read}`;
if (!visited.has(readPermission)) {
result.add(readPermission);
queue.push(readPermission);
}
}
} else {
// Find what depends on the current permission
Object.entries(PERMISSION_REGISTRY).forEach(([resource, config]) => {
Object.entries(config).forEach(([action, details]) => {
if (action.startsWith("_")) return; // Skip internal keys
const permissionDetails = details as any;
const candidatePermission = `${resource}.${action}`;
// Check explicit dependencies
if (permissionDetails?.dependsOn?.includes(currentPermission)) {
if (!visited.has(candidatePermission)) {
result.add(candidatePermission);
queue.push(candidatePermission);
}
}
// Backward compatibility: check CRUD dependencies on read
const { resource: currentResource, action: currentAction } = splitPermission(currentPermission);
if (
currentAction === CrudAction.Read &&
resource === currentResource &&
(action === CrudAction.Create || action === CrudAction.Update || action === CrudAction.Delete)
) {
if (!visited.has(candidatePermission)) {
result.add(candidatePermission);
queue.push(candidatePermission);
}
}
});
});
}
}
return Array.from(result);
};
/**
* Get all permissions that the given permission transitively depends on
* @param permission The permission to find dependencies for
* @returns Array of dependency permissions
*/
export const getTransitiveDependencies = (permission: string): string[] => {
return traversePermissions(permission, "dependencies");
};
/**
* Get all permissions that transitively depend on the given permission
* @param permission The permission to find dependents for
* @returns Array of dependent permissions
*/
export const getTransitiveDependents = (permission: string): string[] => {
return traversePermissions(permission, "dependents");
};