Skip to content

Conversation

@sean-brydon
Copy link
Member

@sean-brydon sean-brydon commented Aug 11, 2025

Visual Demo (For contributors especially)

Organization Members page demo: https://cap.so/s/qv2v71db6bn20x5
Teams member page demo: https://cap.so/s/ztcg5wxj3a8eys8

How should this be tested?

Seed your database with yarn seed-pbac
Create a role in organization with no permissions.
Assign a user this role.
Visit organization members page
Assign permissions
Work your way through assigning all members permissions - invite,remove,canListMembers,changeRole etc
Notice they all reflect in the UI and on the backend

Do the same for teams roles

Logout of pbac - login as teampro - test everything works as expected on a team without PBAC

Login to a plain org (or remove feature flag) and test everything works as expected for each role

Checklist

  • I haven't read the contributing guide
  • My code doesn't follow the style guidelines of this project
  • I haven't commented my code, particularly in hard-to-understand areas
  • I haven't checked if my changes generate no new warnings

@vercel
Copy link

vercel bot commented Aug 11, 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 Sep 2, 2025 0:49am
cal-eu Ignored Ignored Sep 2, 2025 0:49am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 11, 2025

Walkthrough

Introduces PBAC-first permission handling across frontend and backend. Adds MemberPermissions and SettingsPermissions types, a new getSpecificPermissions API for per-action checks (mixing CrudAction and CustomAction), scope-aware usePermissions, and new PBAC actions (ListMembers, Impersonate) in the permission registry. Wires permissions objects through settings layout, sidebar/tabs, members/team views, user/member tables, impersonation provider, and multiple server handlers (teams/orgs) with membership lookups and role-based fallbacks. Adds related Prisma migrations and locale keys for impersonation.

Possibly related PRs

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/pbac-team-members

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 @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai 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:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai 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 @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @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.

@github-actions
Copy link
Contributor

github-actions bot commented Aug 11, 2025

Hey there and thank you for opening this pull request! 👋🏼

We require pull request titles to follow the Conventional Commits specification and it looks like your proposed title needs to be adjusted.

Details:

No release type found in pull request title "PBAC on team members page + org members page to use specific permissi…". Add a prefix to indicate what kind of release this pull request corresponds to. For reference, see https://www.conventionalcommits.org/

Available types:
 - feat: A new feature
 - fix: A bug fix
 - docs: Documentation only changes
 - style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
 - refactor: A code change that neither fixes a bug nor adds a feature
 - perf: A code change that improves performance
 - test: Adding missing tests or correcting existing tests
 - build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
 - ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
 - chore: Other changes that don't modify src or test files
 - revert: Reverts a previous commit

@keithwillcode keithwillcode added consumer core area: core, team members only labels Aug 11, 2025
]);

// Get specific PBAC permissions for team member actions
const permissions = await getSpecificPermissions({
Copy link
Member Author

Choose a reason for hiding this comment

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

New function to do the same as getResourcePermissions but we do not limit to basic CRUD permissions


if (!team) return false;

if (isOrgAdminOrOwner && team?.parentId === orgId) {
Copy link
Member Author

Choose a reason for hiding this comment

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

isOrgAdminOrOwner just checks if youre admin of any org :O

@sean-brydon sean-brydon marked this pull request as ready for review August 12, 2025 09:11
@sean-brydon sean-brydon requested a review from a team August 12, 2025 09:11
@sean-brydon sean-brydon requested review from a team as code owners August 12, 2025 09:11
@dosubot dosubot bot added organizations area: organizations, orgs platform Anything related to our platform plan teams area: teams, round robin, collective, managed event-types labels Aug 12, 2025
@graphite-app graphite-app bot requested a review from a team August 12, 2025 09:14
@graphite-app
Copy link

graphite-app bot commented Aug 12, 2025

Graphite Automations

"Add platform team as reviewer" took an action on this PR • (08/12/25)

1 reviewer was added to this PR based on Keith Williams's automation.

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: 9

🔭 Outside diff range comments (2)
apps/web/modules/members/members-view.tsx (1)

24-26: Add missing localization key across translation files

The key "only_admin_can_see_members_of_org" isn’t defined in any of our locale JSON/TS files. Please add it to each supported language pack. For example:

• locales/en.json

{
  …,
  "only_admin_can_see_members_of_org": "Only admins can see members of this organization."
}

• locales/es.json

{
  …,
  "only_admin_can_see_members_of_org": "Sólo los administradores pueden ver los miembros de esta organización."
}

• (locales/fr.json, locales/de.json, etc.)

packages/features/ee/teams/components/MemberList.tsx (1)

639-639: Incorrect memorized columns dependencies.

The useMemo dependency array includes props.isOrgAdminOrOwner but this prop is no longer used within the memorized columns logic since it was replaced with PBAC permissions. The dependencies should include props.permissions instead.

-  }, [props.isOrgAdminOrOwner, dispatch, totalRowCount, session?.user.id]);
+  }, [props.permissions, dispatch, totalRowCount, session?.user.id]);
♻️ Duplicate comments (1)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[id]/members/page.tsx (1)

84-110: LGTM! Well-implemented PBAC permission retrieval

The implementation correctly utilizes the new getSpecificPermissions function with proper fallback roles. The permission structure is well-organized for team member management.

🧹 Nitpick comments (12)
packages/features/users/components/UserTable/types.ts (1)

56-61: MemberPermissions type looks good; consider clarifying scope and immutability

  • Nicely aligns with PBAC actions. Consider documenting whether these flags refer to Organization, Team, or context-derived scope to avoid misuse in consumers.
  • Optional: mark fields as readonly to discourage mutation in UI code.

Example:

-export interface MemberPermissions {
-  canListMembers: boolean;
-  canInvite: boolean;
-  canChangeMemberRole: boolean;
-  canRemove: boolean;
-}
+export interface MemberPermissions {
+  /** Scope is context-dependent (org or team); document at call sites. */
+  readonly canListMembers: boolean;
+  readonly canInvite: boolean;
+  readonly canChangeMemberRole: boolean;
+  readonly canRemove: boolean;
+}
packages/features/pbac/lib/resource-permissions.ts (2)

7-7: Minor: consolidate imports from permission-registry

Combine Resource, CrudAction, and CustomAction into a single import for consistency.

-import type { Resource } from "../domain/types/permission-registry";
-import { CrudAction, CustomAction } from "../domain/types/permission-registry";
+import type { Resource } from "../domain/types/permission-registry";
+import { CrudAction, CustomAction } from "../domain/types/permission-registry";

Or:

-import type { Resource } from "../domain/types/permission-registry";
-import { CrudAction, CustomAction } from "../domain/types/permission-registry";
+import { type Resource, CrudAction, CustomAction } from "../domain/types/permission-registry";

105-142: De-dupe actions and strengthen action map indexing

  • If actions contains duplicates, we perform duplicate work. De-duplicate via Set.
  • Indexing roleActions[action] can be typed more safely by asserting to a compatible map.
 export const getSpecificPermissions = async ({
@@
-}: SpecificPermissionsOptions): Promise<Record<string, boolean>> => {
+}: SpecificPermissionsOptions): Promise<Partial<Record<ActionType, boolean>>> => {
@@
-  if (!pbacEnabled) {
-    const permissions: Record<string, boolean> = {};
-    for (const action of actions) {
+  if (!pbacEnabled) {
+    const permissions: Partial<Record<ActionType, boolean>> = {};
+    for (const action of new Set(actions)) {
       permissions[action] = checkRoleAccess(userRole, fallbackRoles[action]);
     }
     return permissions;
   }
@@
-  const roleActions = PermissionMapper.toActionMap(resourcePermissions, resource);
+  const roleActions = PermissionMapper.toActionMap(resourcePermissions, resource) as Partial<
+    Record<ActionType, boolean>
+  >;
@@
-  const permissions: Record<string, boolean> = {};
-  for (const action of actions) {
+  const permissions: Partial<Record<ActionType, boolean>> = {};
+  for (const action of new Set(actions)) {
     permissions[action] = roleActions[action] ?? false;
   }
packages/trpc/server/routers/viewer/organizations/bulkDeleteUsers.handler.ts (2)

37-39: Unauthorized message is fine; consider using a consistent error shape

Current message is explicit and helpful. Ensure consistency with other handlers’ UNAUTHORIZED messaging.


41-54: Fallback roles and PBAC check look correct; consider typing and reuse

  • Mapping Remove to ADMIN/OWNER covers non-PBAC environments.
  • Optionally, extract a shared helper for “get current org membership role (accepted only)” to avoid duplication across handlers.
packages/features/users/components/UserTable/UserListTable.tsx (1)

180-183: Consider renaming tablePermissions to avoid confusion with the permissions prop

The variable name tablePermissions could be confusing since it's actually the resolved permissions (PBAC or fallback), not specifically table-related permissions. Consider renaming it to resolvedPermissions or effectivePermissions for clarity.

-    const tablePermissions = {
+    const effectivePermissions = {
       canEdit: permissions?.canChangeMemberRole ?? adminOrOwner,
       canRemove: permissions?.canRemove ?? adminOrOwner,
       canResendInvitation: permissions?.canInvite ?? adminOrOwner,

Then update all references from tablePermissions to effectivePermissions throughout the component.

packages/trpc/server/routers/viewer/teams/removeMember.handler.ts (1)

31-62: Good PBAC implementation but consider extracting permission checks to a helper

The PBAC permission checking logic is correctly implemented but could benefit from being extracted to a reusable helper function since similar patterns appear in other handlers.

Consider creating a shared helper function like checkUserPermissionsForTeams that can be reused across different handlers to reduce code duplication and improve maintainability.

packages/trpc/server/routers/viewer/teams/listMembers.handler.ts (1)

184-185: Document why Invite permission is used as a proxy for listing members

While the comment mentions using Invite as a proxy, it would be helpful to explain why CustomAction.ListMembers isn't available for teams and why Invite was chosen as the proxy permission.

-    actions: [CustomAction.Invite], // Using Invite as proxy for member access since teams don't have ListMembers
+    actions: [CustomAction.Invite], // Using Invite as proxy for member access since teams don't have ListMembers action in the permission registry. Invite is used because users who can invite typically need to see existing members.
packages/trpc/server/routers/viewer/organizations/listMembers.handler.test.ts (1)

39-51: Consider adding test cases for permission-denied scenarios.

While the mock setup correctly returns listMembers: true, the test suite would benefit from additional test cases that verify behavior when PBAC permissions are denied (e.g., listMembers: false). This would ensure proper handling of unauthorized access attempts.

Would you like me to generate additional test cases that cover:

  1. When listMembers permission is false
  2. When the user has no membership in the organization
  3. Different permission combinations for comprehensive coverage?
packages/features/ee/teams/components/MemberList.tsx (1)

434-435: Track the impersonation permission TODO.

The comment indicates that impersonation will get its own custom permission in a follow-up PR. This is a good approach for incremental migration.

Would you like me to create a GitHub issue to track the implementation of the custom impersonation permission for organizations and teams?

apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx (1)

46-50: Consider standardizing authentication redirects across pages

The authentication check redirects to /settings/profile when session/organization details are missing, while the teams page redirects to /auth/login. Consider implementing a consistent authentication strategy.

Apply this diff to align with the team page's authentication pattern:

 const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
 
 if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
-  return redirect("/settings/profile");
+  return redirect("/auth/login");
 }
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[id]/members/page.tsx (1)

106-108: Consider broader access for listing team members

The ListMembers fallback currently restricts to ADMIN and OWNER only. Team members typically expect to see their teammates for collaboration purposes.

Consider allowing MEMBER role to list team members, similar to the organization page:

 [CustomAction.ListMembers]: {
-  roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
+  roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
 },
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 01a0d43 and 868e104.

📒 Files selected for processing (18)
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx (9 hunks)
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx (1 hunks)
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[id]/members/page.tsx (4 hunks)
  • apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx (3 hunks)
  • apps/web/modules/members/members-view.tsx (1 hunks)
  • apps/web/modules/teams/team-members-view.tsx (3 hunks)
  • packages/features/ee/teams/components/MemberList.tsx (4 hunks)
  • packages/features/pbac/domain/types/permission-registry.ts (1 hunks)
  • packages/features/pbac/lib/resource-permissions.ts (2 hunks)
  • packages/features/users/components/UserTable/UserListTable.tsx (9 hunks)
  • packages/features/users/components/UserTable/types.ts (1 hunks)
  • packages/prisma/migrations/20250811123707_add_team_list_members_permission_to_admin_role/migration.sql (1 hunks)
  • packages/trpc/server/routers/viewer/organizations/bulkDeleteUsers.handler.ts (2 hunks)
  • packages/trpc/server/routers/viewer/organizations/getUser.handler.ts (2 hunks)
  • packages/trpc/server/routers/viewer/organizations/listMembers.handler.test.ts (2 hunks)
  • packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts (2 hunks)
  • packages/trpc/server/routers/viewer/teams/listMembers.handler.ts (3 hunks)
  • packages/trpc/server/routers/viewer/teams/removeMember.handler.ts (2 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/trpc/server/routers/viewer/organizations/bulkDeleteUsers.handler.ts
  • packages/features/users/components/UserTable/types.ts
  • packages/features/pbac/domain/types/permission-registry.ts
  • packages/trpc/server/routers/viewer/organizations/getUser.handler.ts
  • packages/trpc/server/routers/viewer/teams/listMembers.handler.ts
  • packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts
  • packages/features/pbac/lib/resource-permissions.ts
  • packages/trpc/server/routers/viewer/organizations/listMembers.handler.test.ts
  • packages/trpc/server/routers/viewer/teams/removeMember.handler.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/trpc/server/routers/viewer/organizations/bulkDeleteUsers.handler.ts
  • packages/features/users/components/UserTable/types.ts
  • packages/features/pbac/domain/types/permission-registry.ts
  • apps/web/modules/members/members-view.tsx
  • packages/trpc/server/routers/viewer/organizations/getUser.handler.ts
  • packages/trpc/server/routers/viewer/teams/listMembers.handler.ts
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[id]/members/page.tsx
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx
  • packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts
  • packages/features/pbac/lib/resource-permissions.ts
  • apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx
  • packages/trpc/server/routers/viewer/organizations/listMembers.handler.test.ts
  • packages/features/users/components/UserTable/UserListTable.tsx
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx
  • packages/features/ee/teams/components/MemberList.tsx
  • apps/web/modules/teams/team-members-view.tsx
  • packages/trpc/server/routers/viewer/teams/removeMember.handler.ts
**/*.tsx

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

Always use t() for text localization in frontend code; direct text embedding should trigger a warning

Files:

  • apps/web/modules/members/members-view.tsx
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[id]/members/page.tsx
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx
  • apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx
  • packages/features/users/components/UserTable/UserListTable.tsx
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx
  • packages/features/ee/teams/components/MemberList.tsx
  • apps/web/modules/teams/team-members-view.tsx
🧬 Code Graph Analysis (13)
packages/trpc/server/routers/viewer/organizations/bulkDeleteUsers.handler.ts (1)
packages/features/pbac/lib/resource-permissions.ts (1)
  • getSpecificPermissions (105-142)
apps/web/modules/members/members-view.tsx (2)
packages/features/users/components/UserTable/UserListTable.tsx (2)
  • UserListTableProps (110-126)
  • UserListTable (128-134)
packages/features/users/components/UserTable/types.ts (1)
  • MemberPermissions (56-61)
packages/trpc/server/routers/viewer/organizations/getUser.handler.ts (1)
packages/features/pbac/lib/resource-permissions.ts (1)
  • getSpecificPermissions (105-142)
packages/trpc/server/routers/viewer/teams/listMembers.handler.ts (3)
apps/api/v2/src/modules/organizations/memberships/services/organizations-membership.service.ts (1)
  • isOrgAdminOrOwner (30-39)
packages/features/pbac/lib/resource-permissions.ts (1)
  • getSpecificPermissions (105-142)
packages/platform/libraries/index.ts (1)
  • MembershipRole (98-98)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[id]/members/page.tsx (4)
packages/features/auth/lib/getServerSession.ts (1)
  • getServerSession (32-136)
packages/features/pbac/lib/resource-permissions.ts (1)
  • getSpecificPermissions (105-142)
packages/platform/libraries/index.ts (1)
  • MembershipRole (98-98)
apps/web/modules/teams/team-members-view.tsx (1)
  • TeamMembersView (30-70)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx (1)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx (1)
  • SettingsLayoutAppDirClient (802-856)
packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts (1)
packages/features/pbac/lib/resource-permissions.ts (1)
  • getSpecificPermissions (105-142)
packages/features/pbac/lib/resource-permissions.ts (5)
packages/platform/libraries/index.ts (1)
  • MembershipRole (98-98)
packages/features/flags/features.repository.ts (1)
  • FeaturesRepository (18-254)
packages/platform/libraries/repositories.ts (1)
  • FeaturesRepository (9-9)
packages/features/pbac/services/permission-check.service.ts (1)
  • PermissionCheckService (20-370)
packages/features/pbac/domain/mappers/PermissionMapper.ts (1)
  • PermissionMapper (7-120)
apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx (6)
packages/features/auth/lib/getServerSession.ts (1)
  • getServerSession (32-136)
apps/web/app/_trpc/context.ts (1)
  • createRouterCaller (18-22)
packages/trpc/server/routers/viewer/organizations/_router.tsx (1)
  • viewerOrganizationsRouter (33-166)
packages/features/pbac/lib/resource-permissions.ts (1)
  • getSpecificPermissions (105-142)
packages/platform/libraries/index.ts (1)
  • MembershipRole (98-98)
apps/web/modules/members/members-view.tsx (1)
  • MembersView (10-30)
packages/features/users/components/UserTable/UserListTable.tsx (3)
packages/features/users/components/UserTable/types.ts (1)
  • MemberPermissions (56-61)
scripts/prepare-local-for-delegation-credentials-testing.js (1)
  • org (42-44)
packages/features/users/components/UserTable/BulkActions/MassAssignAttributes.tsx (1)
  • MassAssignAttributesBulkAction (335-341)
packages/features/ee/teams/components/MemberList.tsx (1)
packages/features/users/components/UserTable/types.ts (1)
  • MemberPermissions (56-61)
apps/web/modules/teams/team-members-view.tsx (1)
packages/features/users/components/UserTable/types.ts (1)
  • MemberPermissions (56-61)
packages/trpc/server/routers/viewer/teams/removeMember.handler.ts (3)
packages/features/pbac/lib/resource-permissions.ts (1)
  • getSpecificPermissions (105-142)
packages/platform/libraries/index.ts (2)
  • MembershipRole (98-98)
  • TRPCError (56-56)
packages/lib/server/queries/teams/index.ts (1)
  • isTeamOwner (409-418)
🔇 Additional comments (18)
packages/trpc/server/routers/viewer/organizations/bulkDeleteUsers.handler.ts (1)

2-4: PBAC imports and usage LGTM

Switching to Resource/CustomAction and getSpecificPermissions aligns with the new PBAC model.

packages/features/users/components/UserTable/UserListTable.tsx (2)

456-466: Good implementation of fine-grained permissions for user actions

The permissions logic correctly handles the nuanced requirements for each action (edit, remove, impersonate, leave, resend invitation) by combining PBAC permissions with contextual checks like isSelf and user.accepted.


630-644: LGTM! Proper PBAC gating for bulk actions

The bulk action visibility is correctly gated using the PBAC permissions with appropriate fallback to the adminOrOwner check when permissions are not provided.

packages/trpc/server/routers/viewer/organizations/getUser.handler.ts (1)

39-53: LGTM! Well-structured PBAC permission check with appropriate fallback roles

The implementation correctly:

  1. Fetches PBAC permissions for viewing/editing organization members
  2. Provides sensible fallback roles for non-PBAC scenarios
  3. Uses the appropriate resource type (Resource.Organization)
packages/trpc/server/routers/viewer/teams/listMembers.handler.ts (1)

151-195: LGTM! Clean PBAC implementation with appropriate access control logic

The implementation correctly handles both organization-level and team-level access with proper PBAC checks and sensible fallbacks for private vs public teams.

apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx (2)

184-186: Good architectural decision to introduce SettingsPermissions type

Creating a dedicated SettingsPermissions interface provides good extensibility for future permission additions and maintains clean separation of concerns.


234-234: LGTM! Proper PBAC gating for roles and permissions menu

The condition correctly checks both PBAC enablement and the user's permission to view roles before showing the menu item, preventing unauthorized access.

apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx (1)

70-74: LGTM! Clean permissions object structure for PBAC integration.

The change from a boolean canViewRoles prop to a nested permissions object follows good architectural patterns for extensibility. This allows for future permission additions without modifying the component's prop signature.

packages/trpc/server/routers/viewer/organizations/listMembers.handler.test.ts (1)

12-13: LGTM! Comprehensive test setup for PBAC-enabled flows.

The test correctly mocks the required Prisma methods (findFirst for membership lookup and findMany for attributes) and installs the necessary spies to support the new PBAC authentication flow.

Also applies to: 21-22, 27-29

packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts (3)

75-88: Good security improvement with membership-based access control.

The implementation correctly validates membership before checking permissions, preventing unauthorized access attempts. The error messages are appropriately generic to avoid information leakage.


90-104: Well-structured PBAC permission check with appropriate fallback logic.

The permission check correctly:

  1. Uses the new PBAC system when available
  2. Falls back to role-based access for backward compatibility
  3. Handles both private and public organization scenarios

The separation of concerns between membership validation and permission checking follows security best practices.


106-114: Confirm Frontend Handling of Permission-Denied Response
I ran searches for canUserGetMembers and trpc.viewer.organizations.listMembers in our .ts/.tsx files but didn’t find any matches. Please manually verify that every component calling this endpoint handles the { canUserGetMembers: false, rows: [], meta: { totalRowCount: 0 } } case correctly. For example:

  • Ensure any useQuery(trpc.viewer.organizations.listMembers…) hooks check canUserGetMembers before rendering member data.
  • Show an appropriate empty state or disable list actions when permission is denied.
  • Add or update frontend tests to cover this scenario.
apps/web/modules/members/members-view.tsx (1)

14-18: Clean implementation of PBAC permissions with backward compatibility.

The implementation elegantly handles the transition to PBAC by:

  1. Checking PBAC permissions first when available
  2. Falling back to the existing role-based logic for backward compatibility
  3. Using the nullish coalescing operator appropriately

The comment clearly documents the dual-path logic.

packages/features/ee/teams/components/MemberList.tsx (2)

430-433: PBAC permissions correctly integrated for edit mode.

The implementation properly uses PBAC permissions (canChangeMemberRole and canRemove) to determine edit capabilities, replacing the previous role-based checks. The self-edit prevention (!isSelf) is correctly maintained.


715-724: Clean permission-based gating for the invite button.

The Add Member button is now correctly gated by the canInvite permission instead of role checks, maintaining consistency with the PBAC approach throughout the component.

apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx (1)

58-84: LGTM! Well-structured PBAC permission retrieval

The implementation correctly uses the new getSpecificPermissions function with appropriate fallback roles for backward compatibility. The permission structure aligns well with the standard member management actions.

apps/web/modules/teams/team-members-view.tsx (1)

35-37: LGTM! Clean implementation of permission fallback logic

The implementation properly uses PBAC permissions when available and gracefully falls back to role-based checks. The comment clearly documents the behavior.

apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[id]/members/page.tsx (1)

62-66: LGTM! Consistent authentication check implementation

The authentication check properly validates the session and redirects to the login page when authentication is missing.

Comment on lines +55 to +57
if (!permissions[CustomAction.Remove]) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Guard rails: prevent self-deletion and protect owners

Independent of PBAC, consider:

  • Disallow deleting the acting user’s own account in bulk operations.
  • Prevent deleting the last OWNER in an org.

Would you like me to propose a guarded implementation that filters input.userIds and validates owner counts before proceeding?


🏁 Script executed:

#!/bin/bash
# Locate the handler file and print the first 200 lines to inspect existing guards
fd bulkDeleteUsers.handler.ts --exec sed -n '1,200p' {} \;

Length of output: 4103


Add guards to prevent self-deletion and preserve at least one OWNER

After the PBAC check, ensure that the acting user cannot delete themselves and that you never remove the last OWNER from an organization.

Locations to update:

  • packages/trpc/server/routers/viewer/organizations/bulkDeleteUsers.handler.ts, immediately after the if (!permissions[CustomAction.Remove]) block.

Suggested diff:

   if (!permissions[CustomAction.Remove]) {
     throw new TRPCError({ code: "UNAUTHORIZED" });
   }

+  // Prevent self-deletion
+  if (input.userIds.includes(currentUser.id)) {
+    throw new TRPCError({
+      code: "BAD_REQUEST",
+      message: "Cannot bulk‐delete the current user",
+    });
+  }
+
+  // Prevent deleting the last OWNER in the organization
+  const totalOwners = await prisma.membership.count({
+    where: { teamId: currentUserOrgId, role: MembershipRole.OWNER },
+  });
+  const ownersToRemove = await prisma.membership.count({
+    where: {
+      teamId: currentUserOrgId,
+      role: MembershipRole.OWNER,
+      userId: { in: input.userIds },
+    },
+  });
+  if (totalOwners - ownersToRemove < 1) {
+    throw new TRPCError({
+      code: "BAD_REQUEST",
+      message: "Cannot delete the last OWNER of the organization",
+    });
+  }

These checks will filter out the current user’s ID and guard against removing the final OWNER before running the deletion transaction.

🤖 Prompt for AI Agents
In packages/trpc/server/routers/viewer/organizations/bulkDeleteUsers.handler.ts
around lines 55 to 57, after the existing PBAC guard (if
(!permissions[CustomAction.Remove]) ...), add two safeguards: first, filter the
incoming list of user IDs to remove to exclude ctx.session.user.id so the acting
user cannot delete themselves; second, before performing the delete transaction,
query the org membership roles for the target users and the current count of
OWNERs and ensure the operation will not remove the last OWNER (i.e., compute
remainingOwnerCount = totalOwnerCount - ownersInDeleteList and throw a
TRPCError({ code: "BAD_REQUEST" }) if remainingOwnerCount < 1). Apply these
checks prior to starting the deletion transaction and return a clear error when
the request would self-delete or remove the final OWNER.

Comment on lines +103 to +115
if (isRemovingSelf && hasRemovePermission) {
// Additional check: ensure they're not an owner trying to remove themselves
const isOwnerOfAnyTeam = await Promise.all(
teamIds.map(async (teamId) => await isTeamOwner(ctx.user.id, teamId))
).then((results) => results.some((result) => result));

if (isOwnerOfAnyTeam) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You can not remove yourself from a team you own.",
});
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential logical issue: Self-removal check may be incorrect

The condition if (isRemovingSelf && hasRemovePermission) on line 103 within the non-PBAC block seems incorrect. If a user is removing themselves and has removal permission, why would you then check if they're an owner? The logic should prevent owners from removing themselves regardless of having permission.

     // Check if user is trying to remove themselves from a team they own (prevent this)
-    if (isRemovingSelf && hasRemovePermission) {
+    if (isRemovingSelf) {
       // Additional check: ensure they're not an owner trying to remove themselves
       const isOwnerOfAnyTeam = await Promise.all(
         teamIds.map(async (teamId) => await isTeamOwner(ctx.user.id, teamId))
       ).then((results) => results.some((result) => result));

       if (isOwnerOfAnyTeam) {
         throw new TRPCError({
           code: "FORBIDDEN",
           message: "You can not remove yourself from a team you own.",
         });
       }
     }
🤖 Prompt for AI Agents
In packages/trpc/server/routers/viewer/teams/removeMember.handler.ts around
lines 103 to 115, the self-removal guard is wrongly gated by
hasRemovePermission; change the conditional so owners are prevented from
removing themselves regardless of permission (i.e., check if isRemovingSelf,
then check isTeamOwner for each teamId and throw FORBIDDEN if any true). Keep
the existing Promise.all owner checks and error details, but remove
hasRemovePermission from the outer if condition so owner-self-removal is always
blocked.

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: 5

🔭 Outside diff range comments (2)
packages/features/ee/teams/components/MemberList.tsx (1)

638-639: Update useMemo dependencies to include permissions.

Columns depend on props.permissions (e.g., actions column behavior), but the dependency array omits it. This risks stale UI when permissions change.

-  }, [props.isOrgAdminOrOwner, dispatch, totalRowCount, session?.user.id]);
+  }, [props.isOrgAdminOrOwner, props.permissions, dispatch, totalRowCount, session?.user.id]);
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx (1)

232-239: Missing useMemo dependencies: include isPbacEnabled and permissions.

The tabs list is memoized without isPbacEnabled and permissions?.canViewRoles in the dependency array. Sidebar won’t update if PBAC toggles or permissions change.

Apply this diff to the dependency array:

-  }, [isAdmin, orgBranding, isOrgAdminOrOwner, user, isDelegationCredentialEnabled]);
+  }, [
+    isAdmin,
+    orgBranding,
+    isOrgAdminOrOwner,
+    user,
+    isDelegationCredentialEnabled,
+    isPbacEnabled,
+    permissions?.canViewRoles,
+  ]);
♻️ Duplicate comments (2)
packages/trpc/server/routers/viewer/teams/listMembers.handler.ts (1)

151-157: Security concern: isOrgAdminOrOwner check is overly permissive.

The current implementation of isOrgAdminOrOwner (Line 151) only checks ctx.user.organization?.isOrgAdmin, which doesn't verify if the user is an admin of this specific organization. This could allow an admin from any organization to access members of teams in other organizations.

Consider verifying the user's admin status for the specific organization:

-  const isOrgAdminOrOwner = ctx.user.organization?.isOrgAdmin;
-  const isTargetingOrg = teamId === ctx.user.organizationId;
-
-  // If targeting org and user is org admin, allow access
-  if (isTargetingOrg && isOrgAdminOrOwner) {
-    return true;
-  }
+  const isTargetingOrg = teamId === ctx.user.organizationId;
+
+  // If targeting org and user is org admin of THIS org, allow access
+  if (isTargetingOrg && ctx.user.organization?.isOrgAdmin) {
+    // Additional check: verify this is the correct org
+    const orgMembership = await prisma.membership.findFirst({
+      where: {
+        teamId: ctx.user.organizationId,
+        userId: ctx.user.id,
+        role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER] },
+        accepted: true,
+      },
+    });
+    if (orgMembership) return true;
+  }
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[id]/members/page.tsx (1)

85-110: Well-structured PBAC permission configuration.

The implementation correctly:

  • Uses the new getSpecificPermissions function as documented
  • Provides appropriate fallback roles for non-PBAC scenarios
  • Maps all necessary team member actions (Invite, ChangeMemberRole, Remove, ListMembers)
🧹 Nitpick comments (12)
packages/trpc/server/routers/viewer/teams/removeMember.handler.ts (2)

34-42: Add null check for user membership.

The code fetches the user's membership but doesn't handle the case where the user might not be a member of the team. While Line 44 returns false for this case, it would be clearer to handle this explicitly.

Consider adding an explicit check and error message:

      const membership = await prisma.membership.findFirst({
        where: {
          userId: ctx.user.id,
          teamId: teamId,
        },
        select: {
          role: true,
        },
      });

-      if (!membership) return false;
+      if (!membership) {
+        // User is not a member of this team
+        return false;
+      }

47-58: Consider caching PBAC permissions for multiple teams.

When removing members from multiple teams, the code makes separate PBAC permission checks for each team. Consider batching these checks or implementing a caching strategy if this becomes a performance bottleneck.

For future optimization, consider implementing a batch permission check method that can retrieve permissions for multiple teams in a single query, especially if users frequently remove members from multiple teams simultaneously.

packages/trpc/server/routers/viewer/organizations/listMembers.handler.test.ts (2)

12-12: Mock should return a more complete membership object.

The findFirst mock only returns { role: "ADMIN" }, but the actual code might expect additional fields like teamId, userId, etc.

Consider returning a more complete mock object:

-    findFirst: vi.fn().mockResolvedValue({ role: "ADMIN" }),
+    findFirst: vi.fn().mockResolvedValue({ 
+      role: "ADMIN",
+      userId: 1,
+      teamId: ORGANIZATION_ID,
+      accepted: true
+    }),

39-44: Consider making the PBAC mock more flexible.

The PBAC permissions mock always returns { listMembers: true }. For more comprehensive testing, consider making it configurable to test both permitted and denied scenarios.

Make the mock more flexible for different test scenarios:

// Mock PBAC permissions
+const mockGetSpecificPermissions = vi.fn().mockResolvedValue({
+  listMembers: true,
+});
+
vi.mock("@calcom/features/pbac/lib/resource-permissions", () => ({
-  getSpecificPermissions: vi.fn().mockResolvedValue({
-    listMembers: true,
-  }),
+  getSpecificPermissions: mockGetSpecificPermissions,
}));

This allows individual tests to override the behavior:

mockGetSpecificPermissions.mockResolvedValueOnce({ listMembers: false });
apps/web/modules/members/members-view.tsx (2)

7-7: Consider using a more specific import path

Instead of importing from the general types file, consider importing MemberPermissions from a more specific location if it exists, to improve code organization and reduce coupling.


14-18: Clarify the complex permission logic with better variable naming

The permission check logic is complex with multiple conditions. Consider extracting the fallback logic into a separate variable for better readability.

-  // Use PBAC permissions if available, otherwise fall back to role-based check
-  const isOrgAdminOrOwner = props.org && checkAdminOrOwner(props.org.user.role);
-  const canLoggedInUserSeeMembers =
-    permissions?.canListMembers ??
-    ((props.org?.isPrivate && isOrgAdminOrOwner) || isOrgAdminOrOwner || !props.org?.isPrivate);
+  // Use PBAC permissions if available, otherwise fall back to role-based check
+  const isOrgAdminOrOwner = props.org && checkAdminOrOwner(props.org.user.role);
+  const roleBasedCanSeeMembers = props.org?.isPrivate 
+    ? isOrgAdminOrOwner 
+    : true;
+  const canLoggedInUserSeeMembers = permissions?.canListMembers ?? roleBasedCanSeeMembers;
packages/trpc/server/routers/viewer/organizations/getUser.handler.ts (1)

34-36: Consider using a constant for the error message

The error message "User is not a member of this organization." is hardcoded. Consider extracting it to a constant or using localization for consistency across the codebase.

packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts (1)

106-114: Consider extracting the empty response to a constant

The empty response object with canUserGetMembers: false could be extracted to a constant for reusability and consistency.

+const UNAUTHORIZED_RESPONSE = {
+  canUserGetMembers: false,
+  rows: [],
+  meta: {
+    totalRowCount: 0,
+  },
+};

 if (!permissions[CustomAction.ListMembers]) {
-  return {
-    canUserGetMembers: false,
-    rows: [],
-    meta: {
-      totalRowCount: 0,
-    },
-  };
+  return UNAUTHORIZED_RESPONSE;
 }
packages/features/users/components/UserTable/UserListTable.tsx (1)

456-456: Consider renaming permissionsRaw for clarity

The variable name permissionsRaw doesn't clearly indicate its purpose. Consider renaming it to better reflect that it contains the table-level permissions.

-  const permissionsRaw = tablePermissions;
+  const tablePermissionsForRow = tablePermissions;
apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx (2)

46-51: Avoid unnecessary awaits on headers() and cookies().

headers() and cookies() are synchronous in the Next.js app router. Dropping await avoids confusion and slightly reduces overhead.

-  const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
+  const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });

58-85: Keep session.user.profile.organizationId as teamId and confirm ListMembers fallback semantics

Our search shows that most pages (general, dsync, sso, privacy, attributes, members, etc.) consistently use session.user.profile.organizationId for the organization/team ID. Switching to session.user.org.id here would be inconsistent with the rest of the codebase.

However, the updated fallback for CustomAction.ListMembers now grants members the ability to list org members when PBAC is disabled, which differs from the previous privacy-based gating in MembersView. Please confirm that this broader access for MEMBER roles in private orgs is intentional.

• Location to review:
apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx (lines 61–69)
• Fallback mapping in question:

fallbackRoles: {
  [CustomAction.ListMembers]: {
    roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
  },
  // …
},

No change needed for teamId. Please verify the intended behavior for the ListMembers fallback.

packages/features/ee/teams/components/MemberList.tsx (1)

165-166: Prop surface extension looks good, but consider deduplicating Props.

You now declare interface Props twice in this file (Lines 66-70 and 150-166). TypeScript merges them, but it’s a code smell. Prefer consolidating into a single interface for maintainability.

Example consolidated interface (apply outside the shown hunk):

interface Props {
  team: NonNullable<RouterOutputs["viewer"]["teams"]["get"]>;
  isOrgAdminOrOwner: boolean | undefined;
  setShowMemberInvitationModal: Dispatch<SetStateAction<boolean>>;
  facetedTeamValues?: {
    roles: { id: string; name: string }[];
    teams: RouterOutputs["viewer"]["teams"]["get"][];
    attributes: { id: string; name: string; options: { value: string }[] }[];
  };
  permissions: MemberPermissions;
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 01a0d43 and 868e104.

📒 Files selected for processing (18)
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx (9 hunks)
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx (1 hunks)
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[id]/members/page.tsx (4 hunks)
  • apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx (3 hunks)
  • apps/web/modules/members/members-view.tsx (1 hunks)
  • apps/web/modules/teams/team-members-view.tsx (3 hunks)
  • packages/features/ee/teams/components/MemberList.tsx (4 hunks)
  • packages/features/pbac/domain/types/permission-registry.ts (1 hunks)
  • packages/features/pbac/lib/resource-permissions.ts (2 hunks)
  • packages/features/users/components/UserTable/UserListTable.tsx (9 hunks)
  • packages/features/users/components/UserTable/types.ts (1 hunks)
  • packages/prisma/migrations/20250811123707_add_team_list_members_permission_to_admin_role/migration.sql (1 hunks)
  • packages/trpc/server/routers/viewer/organizations/bulkDeleteUsers.handler.ts (2 hunks)
  • packages/trpc/server/routers/viewer/organizations/getUser.handler.ts (2 hunks)
  • packages/trpc/server/routers/viewer/organizations/listMembers.handler.test.ts (2 hunks)
  • packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts (2 hunks)
  • packages/trpc/server/routers/viewer/teams/listMembers.handler.ts (3 hunks)
  • packages/trpc/server/routers/viewer/teams/removeMember.handler.ts (2 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/trpc/server/routers/viewer/organizations/bulkDeleteUsers.handler.ts
  • packages/features/users/components/UserTable/types.ts
  • packages/trpc/server/routers/viewer/organizations/getUser.handler.ts
  • packages/features/pbac/domain/types/permission-registry.ts
  • packages/trpc/server/routers/viewer/organizations/listMembers.handler.test.ts
  • packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts
  • packages/trpc/server/routers/viewer/teams/listMembers.handler.ts
  • packages/features/pbac/lib/resource-permissions.ts
  • packages/trpc/server/routers/viewer/teams/removeMember.handler.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/trpc/server/routers/viewer/organizations/bulkDeleteUsers.handler.ts
  • packages/features/users/components/UserTable/types.ts
  • packages/trpc/server/routers/viewer/organizations/getUser.handler.ts
  • apps/web/modules/members/members-view.tsx
  • packages/features/pbac/domain/types/permission-registry.ts
  • packages/trpc/server/routers/viewer/organizations/listMembers.handler.test.ts
  • packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts
  • packages/trpc/server/routers/viewer/teams/listMembers.handler.ts
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx
  • apps/web/modules/teams/team-members-view.tsx
  • apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx
  • packages/features/users/components/UserTable/UserListTable.tsx
  • packages/features/pbac/lib/resource-permissions.ts
  • packages/trpc/server/routers/viewer/teams/removeMember.handler.ts
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[id]/members/page.tsx
  • packages/features/ee/teams/components/MemberList.tsx
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx
**/*.tsx

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

Always use t() for text localization in frontend code; direct text embedding should trigger a warning

Files:

  • apps/web/modules/members/members-view.tsx
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx
  • apps/web/modules/teams/team-members-view.tsx
  • apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx
  • packages/features/users/components/UserTable/UserListTable.tsx
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[id]/members/page.tsx
  • packages/features/ee/teams/components/MemberList.tsx
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx
🧬 Code Graph Analysis (15)
packages/trpc/server/routers/viewer/organizations/bulkDeleteUsers.handler.ts (1)
packages/features/pbac/lib/resource-permissions.ts (1)
  • getSpecificPermissions (105-142)
packages/trpc/server/routers/viewer/organizations/getUser.handler.ts (1)
packages/features/pbac/lib/resource-permissions.ts (1)
  • getSpecificPermissions (105-142)
apps/web/modules/members/members-view.tsx (2)
packages/features/users/components/UserTable/UserListTable.tsx (2)
  • UserListTableProps (110-126)
  • UserListTable (128-134)
packages/features/users/components/UserTable/types.ts (1)
  • MemberPermissions (56-61)
packages/trpc/server/routers/viewer/organizations/listMembers.handler.test.ts (1)
scripts/prepare-local-for-delegation-credentials-testing.js (1)
  • user (15-17)
packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts (1)
packages/features/pbac/lib/resource-permissions.ts (1)
  • getSpecificPermissions (105-142)
packages/trpc/server/routers/viewer/teams/listMembers.handler.ts (3)
apps/api/v2/src/modules/organizations/memberships/services/organizations-membership.service.ts (1)
  • isOrgAdminOrOwner (30-39)
packages/features/pbac/lib/resource-permissions.ts (1)
  • getSpecificPermissions (105-142)
packages/platform/libraries/index.ts (1)
  • MembershipRole (98-98)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx (1)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx (1)
  • SettingsLayoutAppDirClient (802-856)
apps/web/modules/teams/team-members-view.tsx (1)
packages/features/users/components/UserTable/types.ts (1)
  • MemberPermissions (56-61)
apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx (5)
apps/web/app/_trpc/context.ts (1)
  • createRouterCaller (18-22)
packages/trpc/server/routers/viewer/organizations/_router.tsx (1)
  • viewerOrganizationsRouter (33-166)
packages/features/pbac/lib/resource-permissions.ts (1)
  • getSpecificPermissions (105-142)
packages/platform/libraries/index.ts (1)
  • MembershipRole (98-98)
apps/web/modules/members/members-view.tsx (1)
  • MembersView (10-30)
packages/features/users/components/UserTable/UserListTable.tsx (2)
packages/features/users/components/UserTable/types.ts (1)
  • MemberPermissions (56-61)
packages/features/users/components/UserTable/BulkActions/MassAssignAttributes.tsx (1)
  • MassAssignAttributesBulkAction (335-341)
packages/features/pbac/lib/resource-permissions.ts (5)
packages/platform/libraries/index.ts (1)
  • MembershipRole (98-98)
packages/features/flags/features.repository.ts (1)
  • FeaturesRepository (18-254)
packages/platform/libraries/repositories.ts (1)
  • FeaturesRepository (9-9)
packages/features/pbac/services/permission-check.service.ts (1)
  • PermissionCheckService (20-370)
packages/features/pbac/domain/mappers/PermissionMapper.ts (1)
  • PermissionMapper (7-120)
packages/trpc/server/routers/viewer/teams/removeMember.handler.ts (2)
packages/features/pbac/lib/resource-permissions.ts (1)
  • getSpecificPermissions (105-142)
packages/lib/server/queries/teams/index.ts (1)
  • isTeamOwner (409-418)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[id]/members/page.tsx (3)
packages/features/auth/lib/getServerSession.ts (1)
  • getServerSession (32-136)
packages/features/pbac/lib/resource-permissions.ts (1)
  • getSpecificPermissions (105-142)
apps/web/modules/teams/team-members-view.tsx (1)
  • TeamMembersView (30-70)
packages/features/ee/teams/components/MemberList.tsx (1)
packages/features/users/components/UserTable/types.ts (1)
  • MemberPermissions (56-61)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx (1)
packages/features/shell/Shell.tsx (1)
  • Shell (108-121)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (27)
packages/features/users/components/UserTable/types.ts (1)

55-61: LGTM! Well-structured permission interface.

The MemberPermissions interface provides a clean abstraction for PBAC-driven permission checks across member management UIs. The boolean flags align well with the specific actions needed for team and organization member management.

packages/prisma/migrations/20250811123707_add_team_list_members_permission_to_admin_role/migration.sql (1)

1-9: Migration depends on existing admin_role – please verify

The migration correctly adds the team.listMembers permission to the admin role with conflict-safe inserts, but I didn’t find any prior migration that creates a role with ID 'admin_role'. Before deploying, please confirm that a record with id = 'admin_role' exists (for example, in your seed script or an earlier migration).

• File:
packages/prisma/migrations/20250811123707_add_team_list_members_permission_to_admin_role/migration.sql (lines 1–9)

packages/trpc/server/routers/viewer/organizations/listMembers.handler.test.ts (1)

47-51: UserRepository mock implementation looks good.

The mock correctly returns the user object without modification, which is appropriate for unit testing where we want to isolate the handler logic from the enrichment logic.

apps/web/modules/members/members-view.tsx (1)

10-22: Good implementation of PBAC permissions with backward compatibility

The implementation correctly prioritizes PBAC permissions when available and falls back to role-based checks, ensuring backward compatibility for organizations without PBAC enabled.

packages/trpc/server/routers/viewer/organizations/getUser.handler.ts (1)

39-53: Well-structured PBAC permission check

The implementation correctly uses getSpecificPermissions with appropriate fallback roles, properly mapping actions to membership roles. The separation between ListMembers (MEMBER+) and ChangeMemberRole (ADMIN+) permissions is appropriate.

packages/trpc/server/routers/viewer/organizations/bulkDeleteUsers.handler.ts (2)

26-39: Good consistent authorization pattern

The membership check and error handling pattern is consistent with other handlers in the PR, which improves maintainability.


41-53: Appropriate PBAC permission configuration for delete operations

The permission check correctly restricts the Remove action to ADMIN and OWNER roles only, which is appropriate for a destructive operation like bulk deletion.

packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts (2)

75-88: Consistent membership validation pattern

Good consistency with the membership validation pattern used across other handlers in this PR.


99-102: Verify fallback role logic for private organizations
I didn’t find any tests or documentation defining expected member‐visibility rules for private vs. public organizations. Please confirm that private orgs should indeed only expose ADMIN and OWNER roles when listing members.

• File: packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts
• Lines 99–102:

roles: ctx.user.organization.isPrivate
  ? [MembershipRole.ADMIN, MembershipRole.OWNER]
  : [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
packages/features/users/components/UserTable/UserListTable.tsx (4)

179-185: Good implementation of permission-based table controls

The tablePermissions object correctly maps PBAC permissions to table-specific actions with appropriate fallbacks to maintain backward compatibility.


630-630: Verify permission checks are applied consistently

The permission checks using nullish coalescing (??) are applied consistently across all bulk actions. This ensures PBAC permissions take precedence when available.

Also applies to: 639-644, 647-652


675-691: Correct permission check for the Add button

The Add button correctly uses the canInvite permission, maintaining consistency with the permission model.


481-481: Good dependency management in useMemo

The addition of permissions to the dependency array ensures the columns are properly recalculated when permissions change.

packages/features/pbac/lib/resource-permissions.ts (1)

88-142: LGTM! The new getSpecificPermissions function provides flexible PBAC support.

The implementation correctly:

  • Reuses the existing FeaturesRepository and PermissionCheckService infrastructure
  • Provides proper fallback to role-based access when PBAC is disabled
  • Maintains consistency with the existing getResourcePermissions pattern
  • Uses the same permission mapping logic via PermissionMapper.toActionMap
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx (1)

70-74: LGTM! Clean migration to permissions object pattern.

The change from canViewRoles to permissions={{ canViewRoles }} properly encapsulates permissions in an object, providing better extensibility for future permission additions.

apps/web/modules/teams/team-members-view.tsx (1)

30-37: LGTM! Clean implementation of PBAC permissions with proper fallback.

The implementation correctly:

  • Uses optional chaining to safely access permissions?.canListMembers
  • Falls back to the existing role-based logic when permissions are not available
  • Maintains backward compatibility while enabling PBAC
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[id]/members/page.tsx (2)

62-66: LGTM! Proper session validation and redirect.

The session check correctly validates user authentication before proceeding with permission checks.


113-118: LGTM! Clean mapping of PBAC permissions to UI requirements.

The permissions are properly mapped to a consistent memberPermissions object that aligns with the MemberPermissions interface used throughout the UI components.

apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx (2)

87-93: LGTM: Clear mapping from PBAC results to UI permissions.

The projection into MemberPermissions is straightforward and consistent with the UI contract.


107-113: LGTM: Wiring permissions into MembersView.

Passing the computed memberPermissions enables consistent UI gating downstream.

packages/features/ee/teams/components/MemberList.tsx (2)

32-32: LGTM: MemberPermissions imported at source.

Type import keeps bundle clean on the client.


715-724: LGTM: Add-member CTA gated by canInvite.

This aligns UI affordances with PBAC.

apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx (5)

184-187: LGTM: Centralized SettingsPermissions type.

A single surface for layout permissions reduces prop proliferation.


191-196: Hook signature accepts permissions — good.

Threading permissions into useTabs enables PBAC-aware sidebar composition.


521-525: LGTM: Passing permissions into useTabs.

Keeps tab gating concentrated in one place.


840-841: LGTM: Propagating permissions into SidebarContainer.

Consistent with the new API surface.


888-889: LGTM: Final propagation to SettingsSidebarContainer.

Completes the flow.

Comment on lines 430 to 436
// Use PBAC permissions for edit mode, with role-based fallback
const canChangeRole = props.permissions?.canChangeMemberRole ?? false;
const canRemove = props.permissions?.canRemove ?? false;
const editMode = (canChangeRole || canRemove) && !isSelf;
// For now impersonation just lives on the canChangeRole OR canRemove permission (since without pbac these check admin or owner)
// In my next PR on this stack i will implement a custom impersonation permission that lives on orgs + teams.
const impersonationMode =
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Gate edit actions per permission; avoid showing “Remove” and “Resend invitation” when not allowed.

Currently, editMode becomes true if either canChangeRole or canRemove is true, and the menu always shows “remove” and “resend invitation” options. Users who cannot remove still see “remove,” and users who cannot invite can see “resend invitation”. Gate each action individually.

Refactor local flags:

- // Use PBAC permissions for edit mode, with role-based fallback
- const canChangeRole = props.permissions?.canChangeMemberRole ?? false;
- const canRemove = props.permissions?.canRemove ?? false;
- const editMode = (canChangeRole || canRemove) && !isSelf;
+ // Use PBAC permissions for granular gating
+ const canChangeRole = props.permissions?.canChangeMemberRole ?? false;
+ const canRemove = props.permissions?.canRemove ?? false;
+ const canInvite = props.permissions?.canInvite ?? false;
+ const editMode = (canChangeRole || canRemove || canInvite) && !isSelf;

Apply outside this hunk (non-diff example) to gate items:

// Desktop menu
{impersonationMode && ...}

{resendInvitation && canInvite && (
  /* resend invitation item */
)}

{canRemove && (
  <DropdownMenuItem>
    <DropdownItem
      type="button"
      onClick={() => dispatch({ type: "SET_DELETE_ID", payload: { user, showModal: true } })}
      color="destructive"
      StartIcon="user-x">
      {t("remove")}
    </DropdownItem>
  </DropdownMenuItem>
)}

And similarly for the mobile menu’s “remove” item.

Also, consider gating the bulk remove bar (DeleteBulkTeamMembers) behind props.permissions.canRemove to keep UX consistent.

🤖 Prompt for AI Agents
In packages/features/ee/teams/components/MemberList.tsx around lines 430-436,
editMode is currently derived from canChangeRole || canRemove and the menu
always renders “Remove” and “Resend invitation”; update this to gate each action
individually: read explicit flags from props.permissions (e.g., const
canChangeRole = props.permissions?.canChangeMemberRole ?? false; const canRemove
= props.permissions?.canRemove ?? false; const canInvite =
props.permissions?.canInvite ?? false), keep editMode only for whether any edit
UI should be shown, but replace unconditional menu items so each action is
wrapped with its own conditional (resend invitation only when canInvite, remove
only when canRemove, impersonation UI only when its specific flag), apply the
same conditionals in the mobile menu, and gate the bulk remove bar
(DeleteBulkTeamMembers) behind props.permissions.canRemove so users who lack
remove permission never see remove/resend options.

});

// User needs either ListMembers (to view) or ChangeMemberRole (to edit) permission
if (!permissions[CustomAction.ListMembers] || !permissions[CustomAction.ChangeMemberRole]) {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix incorrect comment - permission logic uses AND not OR

The comment states "either...or" but the condition uses AND (&&), requiring both permissions.

-  // User needs either ListMembers (to view) or ChangeMemberRole (to edit) permission
+  // User needs both ListMembers (to view) and ChangeMemberRole (to edit) permissions
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!permissions[CustomAction.ListMembers] || !permissions[CustomAction.ChangeMemberRole]) {
// User needs both ListMembers (to view) and ChangeMemberRole (to edit) permissions
if (!permissions[CustomAction.ListMembers] || !permissions[CustomAction.ChangeMemberRole]) {
🤖 Prompt for AI Agents
In packages/trpc/server/routers/viewer/organizations/getUser.handler.ts around
line 56, the inline comment describing the permission check is wrong: the code
uses a logical AND requiring both CustomAction.ListMembers and
CustomAction.ChangeMemberRole, but the comment says "either...or"; update the
comment to state that both permissions are required (or if the intended behavior
is to allow either permission, change the condition to use || instead of &&) so
the code and comment match.

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: 1

♻️ Duplicate comments (2)
apps/web/playwright/fixtures/users.ts (1)

982-994: Use correct NextAuth payload keys and form-encoded body (carryover from earlier review)

Use callbackUrl (not callbackURL), booleans for redirect/json, and send x-www-form-urlencoded via form to align with NextAuth’s credentials callback.

-  // Make the login request
-  const loginData = {
-    email: user.email ?? `${user.username}@example.com`,
-    password: user.password ?? user.username,
-    callbackURL: WEBAPP_URL,
-    redirect: "false",
-    json: "true",
-    csrfToken,
-  };
-
-  const response = await page.context().request.post("/api/auth/callback/credentials", {
-    data: loginData,
-  });
+  // Make the login request (NextAuth expects form-encoded body and specific keys)
+  const form = {
+    email: user.email ?? `${user.username}@example.com`,
+    password: user.password ?? user.username,
+    callbackUrl: "/",
+    redirect: false,
+    json: true,
+    csrfToken,
+  };
+
+  const response = await page.context().request.post("/api/auth/callback/credentials", {
+    form,
+  });
apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx (1)

59-64: Revisit fallback visibility for member listing (matches prior thread).

To keep least-privilege when PBAC is off, consider restricting ListMembers to ADMIN/OWNER regardless of org privacy. This mirrors the team page and avoids exposing member data to regular members.

Apply if desired:

- const fallbackRolesThatCanSeeMembers: MembershipRole[] = [MembershipRole.ADMIN, MembershipRole.OWNER];
-
- if (!org?.isPrivate) {
-   fallbackRolesThatCanSeeMembers.push(MembershipRole.MEMBER);
- }
+ const fallbackRolesThatCanSeeMembers: MembershipRole[] = [MembershipRole.ADMIN, MembershipRole.OWNER];
🧹 Nitpick comments (3)
apps/web/playwright/fixtures/users.ts (1)

1007-1008: Harden session wait to reduce flakes

Match the exact session request and ensure it succeeded before proceeding.

-  // Wait for the session API call to complete to ensure session is fully established
-  await page.waitForResponse("/api/auth/session");
+  // Wait for the successful session response to ensure session is fully established
+  await page.waitForResponse((res) => res.url().endsWith("/api/auth/session") && res.ok());
apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx (2)

78-95: DRY up repeated ADMIN/OWNER role arrays in fallbacks.

Reduces duplication and drift.

Apply:

   const permissions = await getSpecificPermissions({
@@
-    fallbackRoles: {
+    fallbackRoles: {
+      // Avoid repetition: ADMIN/OWNER used across multiple actions
+      // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+      // (Keep it inline or lift to a top-level const if preferred)
+      const ADMIN_OWNER_ROLES: MembershipRole[] = [MembershipRole.ADMIN, MembershipRole.OWNER];
       [CustomAction.ListMembers]: {
         roles: fallbackRolesThatCanSeeMembers,
       },
       [CustomAction.Invite]: {
-        roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
+        roles: ADMIN_OWNER_ROLES,
       },
       [CustomAction.ChangeMemberRole]: {
-        roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
+        roles: ADMIN_OWNER_ROLES,
       },
       [CustomAction.Remove]: {
-        roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
+        roles: ADMIN_OWNER_ROLES,
       },
       [CustomAction.Impersonate]: {
-        roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
+        roles: ADMIN_OWNER_ROLES,
       },
     },
   });

If placing a const inside the object literal isn’t allowed by your lint rules, hoist this line just above the getSpecificPermissions call:

const ADMIN_OWNER_ROLES: MembershipRole[] = [MembershipRole.ADMIN, MembershipRole.OWNER];

119-125: Prop wiring OK; consider avoiding redundant “teams” passing.

facetedTeamValues already includes teams; if MembersView doesn’t need raw teams separately, drop the extra prop to reduce prop surface.

Confirm by checking MembersView’s props usage in apps/web/modules/members/members-view.tsx and UserListTable.

📜 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 fdaa107 and c16462d.

📒 Files selected for processing (2)
  • apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx (3 hunks)
  • apps/web/playwright/fixtures/users.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.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/playwright/fixtures/users.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/playwright/fixtures/users.ts
  • apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx
**/*.{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/playwright/fixtures/users.ts
  • apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx
**/*.tsx

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

Always use t() for text localization in frontend code; direct text embedding should trigger a warning

Files:

  • apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx
🧠 Learnings (1)
📚 Learning: 2025-08-08T09:12:08.280Z
Learnt from: hariombalhara
PR: calcom/cal.com#22968
File: packages/features/auth/lib/next-auth-options.ts:327-327
Timestamp: 2025-08-08T09:12:08.280Z
Learning: In packages/features/auth/lib/next-auth-options.ts, do not log credentials in authorize() handlers (e.g., the "saml-idp" CredentialsProvider). Remove accidental console.log statements and avoid including credential contents in logs; prefer either no logging or structured logs without sensitive data.

Applied to files:

  • apps/web/playwright/fixtures/users.ts
🧬 Code graph analysis (1)
apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx (4)
packages/features/auth/lib/getServerSession.ts (1)
  • getServerSession (32-136)
apps/web/lib/buildLegacyCtx.ts (1)
  • buildLegacyRequest (47-49)
packages/features/pbac/lib/resource-permissions.ts (1)
  • getSpecificPermissions (105-142)
apps/web/modules/members/members-view.tsx (1)
  • MembersView (10-30)
⏰ 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). (5)
  • GitHub Check: Tests / Unit
  • GitHub Check: Production builds / Build Docs
  • GitHub Check: Production builds / Build Web App
  • GitHub Check: Production builds / Build Atoms
  • GitHub Check: Linters / lint
🔇 Additional comments (7)
apps/web/app/(use-page-wrapper)/settings/organizations/(org-user-only)/members/page.tsx (7)

4-5: LGTM on App Router primitives.

Correct use of headers/cookies with server component.


7-9: PBAC imports look correct.

Types and helpers wired appropriately.


12-13: Prisma imports OK.

Enums and client usage align with downstream code.


16-17: Legacy request bridge OK.

buildLegacyRequest is the right adapter for getServerSession.


49-53: Auth guard + redirect is sound.

Early exit prevents rendering without an org-scoped session.


97-105: Permission mapping looks correct.

Keys align with MemberPermissions and Action enums.


71-77: Server-side PBAC enforcement present
ImpersonationProvider invokes getSpecificPermissions with CustomAction.Impersonate to enforce permissions on the server (packages/features/ee/impersonation/lib/ImpersonationProvider.ts). No further changes needed.

Comment on lines +61 to +63
if (!org?.isPrivate) {
fallbackRolesThatCanSeeMembers.push(MembershipRole.MEMBER);
}
Copy link
Member Author

Choose a reason for hiding this comment

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

If the org is not private - members can see it too

(Fall back roles if PBAC is disabled)

This logic is also replicated in the handler -> We just return []

eunjae-lee
eunjae-lee previously approved these changes Sep 2, 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: 0

♻️ Duplicate comments (5)
packages/features/users/components/UserTable/UserListTable.tsx (5)

651-656: Delete bulk users should require list permission too.
Aligns with PBAC consistency.

-            {(permissions?.canRemove ?? adminOrOwner) && (
+            {(permissions?.canListMembers ?? adminOrOwner) &&
+             (permissions?.canRemove ?? adminOrOwner) && (
               <DeleteBulkUsers
                 users={table.getSelectedRowModel().flatRows.map((row) => row.original)}
                 onRemove={() => table.toggleAllPageRowsSelected(false)}
               />
             )}

679-695: “Add” button should be gated by list permission + invite.
Prevents invites when the user can’t view the list.

-            {(permissions?.canInvite ?? adminOrOwner) && (
+            {(permissions?.canListMembers ?? adminOrOwner) &&
+             (permissions?.canInvite ?? adminOrOwner) && (
               <DataTableToolbar.CTA
                 type="button"
                 color="primary"
                 StartIcon="plus"
                 onClick={() =>
                   dispatch({
                     type: "INVITE_MEMBER",
                     payload: {
                       showModal: true,
                     },
                   })
                 }
                 data-testid="new-organization-member-button">
                 {t("add")}
               </DataTableToolbar.CTA>
             )}

179-185: Gate all actions by canListMembers to avoid inconsistent UI states.
Without gating, users lacking list permission can still see edit/remove/impersonate affordances if role fallback is true. Normalize via canList first.

Apply:

-    // Use PBAC permissions if available, otherwise fall back to role-based check
-    const tablePermissions = {
-      canEdit: permissions?.canChangeMemberRole ?? adminOrOwner,
-      canRemove: permissions?.canRemove ?? adminOrOwner,
-      canResendInvitation: permissions?.canInvite ?? adminOrOwner,
-      canImpersonate: permissions?.canImpersonate ?? adminOrOwner,
-    };
+    // Normalize: gate by canListMembers, then apply PBAC with role fallback
+    const canList = permissions?.canListMembers ?? adminOrOwner;
+    const tablePermissions = {
+      canEdit: canList && (permissions?.canChangeMemberRole ?? adminOrOwner),
+      canRemove: canList && (permissions?.canRemove ?? adminOrOwner),
+      canResendInvitation: canList && (permissions?.canInvite ?? adminOrOwner),
+      canImpersonate: canList && (permissions?.canImpersonate ?? adminOrOwner),
+    };

If upstream already folds role fallbacks into permissions, we can drop the redundant ?? adminOrOwner. Verify with:

#!/bin/bash
# Find MemberPermissions producers and any fallbacks
rg -nC3 'MemberPermissions|canListMembers|canChangeMemberRole|canInvite|canRemove|canImpersonate|adminOrOwner' --type=ts --type=tsx

634-634: Bulk action needs list gating.
Users without list permission shouldn’t see team assignment.

-                {permissions?.canChangeMemberRole && <TeamListBulkAction table={table} />}
+                {(permissions?.canListMembers ?? adminOrOwner) &&
+                 (permissions?.canChangeMemberRole ?? adminOrOwner) && (
+                  <TeamListBulkAction table={table} />
+                )}

643-648: Mass-assign and Event Types should also require list permission.
Prevent action exposure when listing is denied.

-                {(permissions?.canChangeMemberRole ?? adminOrOwner) && (
+                {(permissions?.canListMembers ?? adminOrOwner) &&
+                 (permissions?.canChangeMemberRole ?? adminOrOwner) && (
                   <MassAssignAttributesBulkAction table={table} filters={columnFilters} />
                 )}
-                {(permissions?.canChangeMemberRole ?? adminOrOwner) && (
+                {(permissions?.canListMembers ?? adminOrOwner) &&
+                 (permissions?.canChangeMemberRole ?? adminOrOwner) && (
                   <EventTypesList table={table} orgTeams={teams} />
                 )}
🧹 Nitpick comments (3)
packages/features/users/components/UserTable/UserListTable.tsx (3)

485-485: Stabilize memo deps (add t, drop dispatch).
t is used within the memo; dispatch from useReducer is stable and need not be a dep.

-  }, [session?.user.id, adminOrOwner, dispatch, domain, attributes, org?.canAdminImpersonate, permissions]);
+  }, [session?.user.id, adminOrOwner, domain, attributes, org?.canAdminImpersonate, permissions, t]);

354-358: Fix stray data-testid2 attribute.
Looks like a typo; keep a single data-testid to avoid selector ambiguity.

-                <Badge
-                  data-testid2={`member-${username}-pending`}
+                <Badge
                   variant="red"
                   className="text-xs"
                   data-testid={`email-${email.replace("@", "")}-pending`}

267-279: Localize aria-labels to meet i18n guidelines.
Direct strings violate the repo rule to always use t() in TSX.

-            aria-label="Select all"
+            aria-label={t("select_all")}
...
-            aria-label="Select row"
+            aria-label={t("select_row")}

Also consider localizing “No username” (Line 304) and error toasts in handleDownload (Lines 550, 555, 582, 588, 594).

📜 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 dcce84b and 8e3ef83.

📒 Files selected for processing (1)
  • packages/features/users/components/UserTable/UserListTable.tsx (9 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.tsx

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

Always use t() for text localization in frontend code; direct text embedding should trigger a warning

Files:

  • packages/features/users/components/UserTable/UserListTable.tsx
**/*.{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/users/components/UserTable/UserListTable.tsx
**/*.{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/users/components/UserTable/UserListTable.tsx
🧬 Code graph analysis (1)
packages/features/users/components/UserTable/UserListTable.tsx (1)
packages/features/users/components/UserTable/types.ts (1)
  • MemberPermissions (56-62)
⏰ 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 (4)
packages/features/users/components/UserTable/UserListTable.tsx (4)

53-53: Type import looks good.
Importing MemberPermissions locally keeps coupling minimal.


125-126: Prop addition is fine and backwards compatible.
Optional permissions with sane fallbacks preserves pre-PBAC behavior.


136-142: Props destructuring OK.
No functional concerns.


456-468: Row-level permissions derivation looks correct.
Self-guards and org.canAdminImpersonate are correctly applied. This will inherit list gating from tablePermissions after the fix above.

const memberPermissions = {
canListMembers: permissions[CustomAction.ListMembers],
canInvite: permissions[CustomAction.Invite],
canChangeMemberRole: permissions[CustomAction.ChangeMemberRole],
Copy link
Member

@CarinaWolli CarinaWolli Sep 2, 2025

Choose a reason for hiding this comment

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

Change Member Role: Change role of organization members

ChangeMemberRole also includes updating other member details like username or the "about" section, is this intended? If yes we should rename the setting

dependsOn: ["team.read"],
dependsOn: ["team.read", "team.listMembers"],
},
[CustomAction.Impersonate]: {
Copy link
Member

Choose a reason for hiding this comment

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

I can have impersonation turned on but nothing else. Do we want that?
Screenshot 2025-09-02 at 4 25 20 PM

Copy link
Member

Choose a reason for hiding this comment

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

Can't do that for the team settings it automatically turns on "List Members" which seems like the correct behaviour

Copy link
Member Author

Choose a reason for hiding this comment

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

Nice spot Will fix in a follow up if this gets merged before tomorrow (unlikely)

@keithwillcode keithwillcode merged commit 75465ac into main Sep 2, 2025
45 checks passed
@keithwillcode keithwillcode deleted the feat/pbac-team-members branch September 2, 2025 15:52
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 ❗️ migrations contains migration files organizations area: organizations, orgs platform Anything related to our platform plan ready-for-e2e teams area: teams, round robin, collective, managed event-types

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants