Skip to content

Conversation

@sean-brydon
Copy link
Member

What does this PR do?

Fixes the issue @CarinaWolli mentioned here where its possible to have a confusing logic of permissions. So we are introducing a dependsOn registery item.

#23004 (comment)

Video Demo (if applicable):

CleanShot 2025-08-29 at 09 38 45

How should this be tested?

Run yarn seed-pbac
Create a new role.
Toggle ChangeMember Role
Notice the permissions in dependsOn get toggled

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 29, 2025

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

📥 Commits

Reviewing files that changed from the base of the PR and between 27c52a3 and 5a40048.

📒 Files selected for processing (2)
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts (2 hunks)
  • packages/features/pbac/utils/permissionTraversal.ts (1 hunks)
 _______________________________________________
< Show me the code! And I'll show you the bugs. >
 -----------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).

Walkthrough

  • Adds dependency-aware permission toggling in apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts: enabling a permission auto-enables its PERMISSION_REGISTRY dependsOn; disabling removes it and cascades removal to permissions that depend on it (ignoring internal keys starting with "_"). Existing auto-enable of Read for Create/Update/Delete is retained.
  • Extends packages/features/pbac/domain/types/permission-registry.ts with PermissionDetails.dependsOn?: PermissionString[] and populates dependencies across resources (e.g., role, eventType, team, organization, booking, workflow, attributes, routingForm).
  • No exported/public signature changes in the hook; PermissionDetails gains a new optional field.

Possibly related PRs

Tip

You can customize the tone of the review comments and chat replies.

Set the tone_instructions setting in your project's settings in CodeRabbit to customize the tone of the review comments and chat replies. For example, you can set the tone to Act like a strict teacher, Act like a pirate and more.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests

❌ Error creating Unit Test PR.

  • Create PR with unit tests
  • Commit unit tests in branch feat/pbac-depends-on-registery
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@graphite-app graphite-app bot requested a review from a team August 29, 2025 08:45
@keithwillcode keithwillcode added consumer core area: core, team members only labels Aug 29, 2025
@dosubot dosubot bot added the ✨ feature New feature or request label Aug 29, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts (2)

126-127: Fix permission parsing for dotted resources (e.g., "organization.attributes.read").

Splitting on the first dot breaks hierarchical resource ids. Use lastIndexOf(".") to split once at the final dot.

Apply this diff:

-    const [resource, action] = permission.split(".");
+    const lastDot = permission.lastIndexOf(".");
+    if (lastDot === -1) return newPermissions; // defensive
+    const resource = permission.slice(0, lastDot) as keyof typeof PERMISSION_REGISTRY;
+    const action = permission.slice(lastDot + 1) as CrudAction | CustomAction;

And import CustomAction:

-import { CrudAction } from "@calcom/features/pbac/domain/types/permission-registry";
+import { CrudAction, CustomAction } from "@calcom/features/pbac/domain/types/permission-registry";

1-3: Add minimal helpers and memoized reverse index for O(k) toggles.

Computing closure each toggle via full scans is O(N*M). Add small helpers with a cached reverse graph.

Add the following near the top of the module (outside the function):

type Registry = typeof PERMISSION_REGISTRY;
type Permission = `${string}.${string}`;

let reverseIndexCache: Map<Permission, Set<Permission>> | null = null;

function buildReverseIndex(registry: Registry): Map<Permission, Set<Permission>> {
  const map = new Map<Permission, Set<Permission>>();
  Object.entries(registry).forEach(([res, config]) => {
    Object.entries(config).forEach(([act, details]) => {
      if (act.startsWith("_")) return;
      const perm = `${res}.${act}` as Permission;
      const deps = (details as { dependsOn?: Permission[] })?.dependsOn ?? [];
      deps.forEach((d) => {
        if (!map.has(d)) map.set(d, new Set());
        map.get(d)!.add(perm);
      });
      // ensure key exists
      if (!map.has(perm)) map.set(perm, new Set());
    });
  });
  return map;
}

function getReverseIndex(registry: Registry) {
  if (!reverseIndexCache) reverseIndexCache = buildReverseIndex(registry);
  return reverseIndexCache;
}

export function getTransitiveDependencies(perm: Permission, registry: Registry): Set<Permission> {
  // forward crawl
  const [res, act] = [perm.slice(0, perm.lastIndexOf(".")), perm.slice(perm.lastIndexOf(".")+1)];
  const details = (registry as any)[res]?.[act] as { dependsOn?: Permission[] } | undefined;
  const visited = new Set<Permission>();
  const stack = [...(details?.dependsOn ?? [])];
  while (stack.length) {
    const d = stack.pop()!;
    if (visited.has(d)) continue;
    visited.add(d);
    const [dr, da] = [d.slice(0, d.lastIndexOf(".")), d.slice(d.lastIndexOf(".")+1)];
    const dd = (registry as any)[dr]?.[da] as { dependsOn?: Permission[] } | undefined;
    if (dd?.dependsOn) stack.push(...dd.dependsOn);
  }
  return visited;
}

export function getTransitiveDependents(perm: Permission, registry: Registry): Set<Permission> {
  const rev = getReverseIndex(registry);
  const visited = new Set<Permission>();
  const stack = [...(rev.get(perm) ?? [])];
  while (stack.length) {
    const d = stack.pop()!;
    if (visited.has(d)) continue;
    visited.add(d);
    (rev.get(d) ?? []).forEach((n) => stack.push(n));
  }
  return visited;
}

Note: invalidate reverseIndexCache if PERMISSION_REGISTRY is hot-reloaded.

Also applies to: 117-121

🧹 Nitpick comments (9)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts (5)

145-151: Keep or drop the read auto-enable? Prefer relying on registry for consistency.

If registry encodes Create/Update/Delete → Read (it does), this block is redundant and risks drift. Either remove it or guard behind a feature flag for backward compatibility.

Apply this diff to remove redundancy:

-      // If enabling create, update, or delete, automatically enable read permission (backward compatibility)
-      if (action === CrudAction.Create || action === CrudAction.Update || action === CrudAction.Delete) {
-        const readPermission = `${resource}.${CrudAction.Read}`;
-        if (!newPermissions.includes(readPermission)) {
-          newPermissions.push(readPermission);
-        }
-      }

14-15: Type tightening and safety nits.

  • Prefer PermissionString over string for APIs (toggleSinglePermission, hasAllPermissions) to reduce invalid inputs.
  • Use keyof typeof resourceConfig for action indexing.

Apply this diff:

-  hasAllPermissions: (permissions: string[]) => boolean;
+  hasAllPermissions: (permissions: PermissionString[]) => boolean;
@@
-  toggleSinglePermission: (permission: string, enabled: boolean, currentPermissions: string[]) => string[];
+  toggleSinglePermission: (permission: PermissionString, enabled: boolean, currentPermissions: PermissionString[]) => PermissionString[];

And within toggleSinglePermission, replace:

-      if (resourceConfig && resourceConfig[action as CrudAction]) {
-        const permissionDetails = resourceConfig[action as CrudAction];
+      if (resourceConfig && (action in resourceConfig)) {
+        const permissionDetails = (resourceConfig as Record<string, any>)[action] as { dependsOn?: PermissionString[] } | undefined;

Also applies to: 66-115, 179-185


72-81: Global “select all” correctness check.

When selecting All (“”), you add both "." and all individual perms. If you rely on "." elsewhere for checks, consider returning only ".*" here to keep state minimal; recompute individuals when needed.

Apply this diff if desired:

-        const allPossiblePerms = getAllPossiblePermissions();
-        newPermissions = ["*.*", ...allPossiblePerms];
+        newPermissions = ["*.*"];

179-185: Avoid re-adding duplicate ".".

Edge case: if hasAllPermissions returns true while "." already exists (future refactors), guard before push.

Apply this diff:

-    if (hasAllPermissions(newPermissions)) {
-      newPermissions.push("*.*");
-    }
+    if (hasAllPermissions(newPermissions) && !newPermissions.includes("*.*")) {
+      newPermissions.push("*.*");
+    }

117-151: Add tests for the exact scenarios called out in the PR.

  • Enabling organization.changeMemberRole should auto-enable organization.listMembers, organization.read, and role.read.
  • Disabling organization.read should remove listMembers and changeMemberRole.

I can add unit tests for toggleSinglePermission covering transitive enable/disable and dotted resources. Want me to open a follow-up PR with tests?

Also applies to: 153-177

packages/features/pbac/domain/types/permission-registry.ts (4)

44-45: Interface extension LGTM; optional docs on transitivity.

dependsOn field addition is good. Document that dependencies are acyclic permission strings and intended to be transitively closed by consumers.

Apply this diff:

-  dependsOn?: PermissionString[]; // Dependencies that must be enabled when this permission is enabled
+  dependsOn?: PermissionString[]; // Direct prerequisites (acyclic). Consumers should resolve transitively.

121-147: Data looks consistent; ensure it remains cycle-free and referentially valid.

Great coverage of CRUD→read and custom actions. Given UI now relies on this, add a dev-only invariant to:

  • assert each dependsOn points to an existing permission key
  • detect cycles

I can add a small validation that runs in dev/test. Example helper:

export function validatePermissionRegistry(reg: PermissionRegistry) {
  const perms = new Set<string>();
  const edges = new Map<string, string[]>();
  Object.entries(reg).forEach(([res, cfg]) => {
    Object.entries(cfg).forEach(([act, d]) => {
      if (act.startsWith("_")) return;
      const id = `${res}.${act}`;
      perms.add(id);
      const deps = (d as PermissionDetails).dependsOn ?? [];
      edges.set(id, deps);
    });
  });
  // referential integrity
  for (const [id, deps] of edges) {
    for (const dep of deps) {
      if (!perms.has(dep)) throw new Error(`PBAC: "${id}" depends on missing permission "${dep}"`);
    }
  }
  // cycle detection
  const seen = new Set<string>(), stack = new Set<string>();
  const dfs = (n: string) => {
    if (stack.has(n)) throw new Error(`PBAC: dependency cycle detected at "${n}"`);
    if (seen.has(n)) return;
    seen.add(n); stack.add(n);
    (edges.get(n) ?? []).forEach(dfs);
    stack.delete(n);
  };
  perms.forEach(dfs);
}

Call once in non-production.

Also applies to: 153-180, 185-234, 253-301, 306-342, 358-385, 390-417, 422-449


286-293: Copy text nit: “organization” vs “team”.

Organization.ChangeMemberRole description says “team members”. Prefer “organization members”.

Apply this diff:

-      description: "Change role of team members",
+      description: "Change role of organization members",

254-260: Optional: include transitive deps explicitly if UI doesn’t compute closure.

If you decide not to implement transitive resolution in the hook, add organization.read to invite/remove/changeMemberRole (they already depend on listMembers which depends on read). Otherwise keep current minimal form.

Would you prefer registry to be transitively complete or keep it minimal and let the hook resolve transitively?

Also applies to: 262-276, 278-285, 286-293, 294-301

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b332d98 and 27c52a3.

📒 Files selected for processing (2)
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts (1 hunks)
  • packages/features/pbac/domain/types/permission-registry.ts (14 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts
  • packages/features/pbac/domain/types/permission-registry.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts
  • packages/features/pbac/domain/types/permission-registry.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts
  • packages/features/pbac/domain/types/permission-registry.ts
🧬 Code graph analysis (1)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts (1)
packages/features/pbac/domain/types/permission-registry.ts (1)
  • PERMISSION_REGISTRY (104-450)

@vercel
Copy link

vercel bot commented Aug 29, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
cal Ignored Ignored Aug 29, 2025 9:12am
cal-eu Ignored Ignored Aug 29, 2025 9:12am

CarinaWolli
CarinaWolli previously approved these changes Aug 29, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 29, 2025

Note

Unit test generation is an Early Access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts (2)

86-90: Don’t use startsWith for resource filtering; it drops sibling resources (critical).

organization.* filtering will also remove organization.attributes.* because of the shared prefix.

Apply this diff:

-      newPermissions = newPermissions.filter((p) => !p.startsWith(`${resource}.`));
+      newPermissions = newPermissions.filter((p) => {
+        const i = p.lastIndexOf(".");
+        if (i === -1) return true;
+        const pResource = p.slice(0, i);
+        return pResource !== resource;
+      });

104-109: Add transitive dependencies when setting a resource to “all”.

Some actions depend on other resources (e.g., organization.changeMemberRole → role.read). Current code doesn’t add cross-resource deps.

Apply this diff:

-          allResourcePerms = Object.keys(resourceConfig)
+          allResourcePerms = Object.keys(resourceConfig)
             .filter((action) => !action.startsWith("_"))
             .map((action) => `${resource}.${action}`);
           newPermissions.push(...allResourcePerms);
+          // Add transitive dependencies for all added actions
+          const depSet = new Set<string>();
+          allResourcePerms.forEach((p) => {
+            getTransitiveDependencies(p).forEach((d) => depSet.add(d));
+          });
+          depSet.forEach((d) => {
+            if (!newPermissions.includes(d)) newPermissions.push(d);
+          });
🧹 Nitpick comments (5)
packages/features/pbac/utils/permissionTraversal.ts (2)

9-12: Tighten types: use PermissionString and remove any.

Strengthen the API and avoid unsafe casts.

Apply this diff:

-import { CrudAction, type CustomAction, PERMISSION_REGISTRY } from "../domain/types/permission-registry";
+import { CrudAction, type CustomAction, PERMISSION_REGISTRY, type PermissionString } from "../domain/types/permission-registry";
@@
-export const traversePermissions = (
-  startPermission: string,
+export const traversePermissions = (
+  startPermission: PermissionString,
@@
-export const getTransitiveDependencies = (permission: string): string[] => {
+export const getTransitiveDependencies = (permission: PermissionString): string[] => {
   return traversePermissions(permission, "dependencies");
 };
@@
-export const getTransitiveDependents = (permission: string): string[] => {
+export const getTransitiveDependents = (permission: PermissionString): string[] => {
   return traversePermissions(permission, "dependents");
 };

Also consider replacing as any on Line 56 with a precise type derived from the registry to prevent accidental shape drift.

Also applies to: 92-95, 101-103


52-80: Optional: build a reverse index for dependents to avoid O(N) scans per BFS step.

Precompute dependsOn -> [dependents] once (module scope) and reuse in traversal.

I can sketch a small index builder if helpful.

apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts (3)

129-135: Remove unused destructuring and avoid duplicates when enabling.

resource/action are unused, and pushing the same permission repeatedly causes churn.

Apply this diff:

-    // Parse the permission to get resource and action
-    const [resource, action] = permission.split(".");
-
     if (enabled) {
       // Add the requested permission
-      newPermissions.push(permission);
+      if (!newPermissions.includes(permission)) {
+        newPermissions.push(permission);
+      }

36-43: Optional: memoize hasAllPermissions for large registries.

This runs often in the UI; a simple memo keyed by the permission set hash can help.


70-83: Cache all permissions for “all resources” toggling
getAllPossiblePermissions is invoked on every toggle when resource === "*". Move its result into a module-level constant (e.g. ALL_PERMISSIONS) to avoid rebuilding the array each time, and verify it contains the correct count of unique entries.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 27c52a3 and 5a40048.

📒 Files selected for processing (2)
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts (2 hunks)
  • packages/features/pbac/utils/permissionTraversal.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/features/pbac/utils/permissionTraversal.ts
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/features/pbac/utils/permissionTraversal.ts
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/features/pbac/utils/permissionTraversal.ts
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts
🧬 Code graph analysis (2)
packages/features/pbac/utils/permissionTraversal.ts (1)
packages/features/pbac/domain/types/permission-registry.ts (1)
  • PERMISSION_REGISTRY (104-450)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts (1)
packages/features/pbac/utils/permissionTraversal.ts (2)
  • getTransitiveDependencies (92-94)
  • getTransitiveDependents (101-103)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Install dependencies / Yarn install & cache
🔇 Additional comments (2)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts (2)

136-143: Good switch to transitive closure.

Enabling via getTransitiveDependencies and removing via getTransitiveDependents addresses multi-hop chains and keeps state consistent.

Also applies to: 144-152


1-7: Imports look correct; prefer named utilities and keep traversal local to features.

No default exports and feature-local utils are aligned with guidelines.

eunjae-lee
eunjae-lee previously approved these changes Aug 29, 2025
Copy link
Contributor

@eunjae-lee eunjae-lee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome !

@sean-brydon sean-brydon enabled auto-merge (squash) August 29, 2025 09:17
@sean-brydon sean-brydon merged commit 18423e5 into main Aug 29, 2025
37 of 38 checks passed
@sean-brydon sean-brydon deleted the feat/pbac-depends-on-registery branch August 29, 2025 09:33
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 29, 2025

Note

Unit test generation is an Early Access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@github-actions
Copy link
Contributor

E2E results are ready!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

consumer core area: core, team members only ✨ feature New feature or request ready-for-e2e

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants