Files
calendar/packages/features/pbac/utils/permissionTraversal.ts
sean-brydonGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>Keith WilliamsDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
76ced2d3b8 feat: event type pbac (#22618)
* fix members page crash with pbac feature flag

* invite member to org backend

* update org permissions

* feat: org profile update settings

* org general page

* remove redudant me call

* privacy page

* add attributes to pbac

* dync + sso on organization permissions

* add tests for resource-permission util

* pass permissions to attributes\

* restore invite members

* update org update and attribute backends

* fix type errors

* fix orgId

* fix types attempt two

* fix types attempt two

* fix type error

* show/hide team event types based on eventType.read permission

* fix dupe string in i18n

* fix tests

* fix team-dsync

* update session to use profile

* use profile metadata to get orgRolew

* fix dsync

* fix typing

* Event type create pbac check

* fix the readonly permission check. WIP

* refactor getUserEventTypes

* fix test

* add generate util for update

* update router to use new generator function for pbac procedures

* update transform utils

* fix members being able to update on UI

* fix allSettled

* fix fallback permissions

* fix permission logic

* fix compare memebrship type error

* fix unit test error

* fix manage test error

* fix nested permissions

* filter based on canView permission in getUserEventGroups

* Update packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix tests

* fix tests

* feat: Adding workflow permission checks inside of eventTypes (#23038)

* add permission checks for workflows

* wip getall active workflows

* check eventType.update permission on activiating workflow on event type

* pass permissions to context and use context in client

* remove local parsePermissionString and use registery

* Use new scoped function

* default scope to orgs

* use object values for workflow permissions

* fix type errors

* Fix permission traversal + fix all state caluclation

* fix eventType group to use permissions.canRead

* fix: update failing PBAC unit tests and resolve ESLint warnings

- Fix getUserEventGroups test: MEMBER role should not have update permissions in fallback scenario
- Fix EventTypeGroupFilter test: add missing canRead property to mock permissions map
- Replace 'as any' type assertions with proper 'as unknown as' type assertions to resolve ESLint warnings

Both tests now pass and align with PBAC permission model where MEMBER role has read-only access to event types.

Co-Authored-By: [email protected] <[email protected]>

* add pbac to _heavy router

* Merge main

* restore versions

* Parallelizable members query

* fix nits

* fix create

* Restore lock file

* Fix import from merge artifact

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Keith Williams <[email protected]>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-17 10:25:30 +01:00

136 lines
4.9 KiB
TypeScript

import {
CrudAction,
type CustomAction,
PERMISSION_REGISTRY,
Scope,
getPermissionsForScope,
} 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
* @param scope The scope to use for permission registry (defaults to Organization)
* @returns Array of related permissions (excluding the start permission itself)
*/
export const traversePermissions = (
startPermission: string,
direction: "dependencies" | "dependents",
scope: Scope = Scope.Organization
): string[] => {
const visited = new Set<string>();
const result = new Set<string>();
const queue: string[] = [startPermission];
const registry = getPermissionsForScope(scope);
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 = registry[resource as keyof typeof 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(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
* @param scope The scope to use for permission registry (defaults to Organization)
* @returns Array of dependency permissions
*/
export const getTransitiveDependencies = (permission: string, scope: Scope = Scope.Organization): string[] => {
return traversePermissions(permission, "dependencies", scope);
};
/**
* Get all permissions that transitively depend on the given permission
* @param permission The permission to find dependents for
* @param scope The scope to use for permission registry (defaults to Organization)
* @returns Array of dependent permissions
*/
export const getTransitiveDependents = (permission: string, scope: Scope = Scope.Organization): string[] => {
return traversePermissions(permission, "dependents", scope);
};