Skip to content

Comments

refactor: replace isTeamAdminOrOwner with PBAC team.listMembers permission#24006

Merged
eunjae-lee merged 17 commits intomainfrom
devin/pbac-refactor-bookings-1758631859
Sep 26, 2025
Merged

refactor: replace isTeamAdminOrOwner with PBAC team.listMembers permission#24006
eunjae-lee merged 17 commits intomainfrom
devin/pbac-refactor-bookings-1758631859

Conversation

@eunjae-lee
Copy link
Contributor

What does this PR do?

This PR refactors the bookings listing view to replace the role-based user?.isTeamAdminOrOwner check with PBAC (Permission-Based Access Control) using the "team.listMembers" permission. The change moves permission checking from client-side role checks to server-side permission validation.

Key Changes:

  • Adds server-side permission check in apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx
  • Passes canListMembers boolean prop down to BookingsContent component
  • Replaces user?.isTeamAdminOrOwner ?? false with canListMembers for the member column filter
  • Implements organization vs team context handling as specified
  • Fixes React Hook dependency arrays for ESLint compliance

Permission Logic:

  • If user belongs to an organization: checks "team.listMembers" permission at organization level
  • If user doesn't belong to an organization: checks permission across all teams using getTeamIdsWithPermission
  • Uses [MembershipRole.ADMIN, MembershipRole.OWNER] as fallback roles for backward compatibility

Visual Demo (For contributors especially)

N/A - This is a backend permission refactor without visual UI changes.

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. N/A - Internal permission refactor, no public API changes.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

Test with different user roles:

  1. Organization Admin/Owner: Should be able to filter by member column
  2. Organization Member: Should NOT be able to filter by member column
  3. Team Admin/Owner: Should be able to filter by member column
  4. Team Member: Should NOT be able to filter by member column
  5. User with no team membership: Should NOT be able to filter by member column

Test Steps:

  1. Navigate to /bookings/upcoming (or any booking status page)
  2. Check if the "Member" column filter is enabled/disabled based on user permissions
  3. Verify the permission logic works correctly for both organization and team contexts

⚠️ Critical Review Areas:

  • Permission logic correctness: The organization vs team permission checking logic is complex - please verify it matches the intended behavior
  • Database query efficiency: Added prisma query to fetch user memberships - consider performance impact
  • Fallback role alignment: Verify [MembershipRole.ADMIN, MembershipRole.OWNER] matches original isTeamAdminOrOwner behavior exactly

Checklist

  • My code follows the style guidelines of this project
  • I have checked if my changes generate no new warnings (fixed ESLint dependency issues)
  • I have read the contributing guide

Link to Devin run: https://app.devin.ai/sessions/6512603a66cf4744a0a71b31f1a75e73
Requested by: @eunjae-lee

…ssion

- Add canListMembers prop to BookingsProps interface
- Implement server-side permission check using PermissionCheckService
- Handle organization vs team context as specified
- Use ADMIN/OWNER fallback roles for backward compatibility
- Replace user?.isTeamAdminOrOwner check in bookings column filter
- Fix React Hook dependency arrays for ESLint compliance

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 23, 2025

Walkthrough

  • page.tsx computes canReadOthersBookings by calling PermissionCheckService.getTeamIdsWithPermission(userId, "booking.read") and passes permissions={{ canReadOthersBookings }} to BookingsList.
  • BookingsList props now include permissions: { canReadOthersBookings: boolean }. Rendering and columns use this flag to enable/disable the userId filter; dependencies updated to include permissions.canReadOthersBookings and user?.timeZone. Tabs generation now depends on searchParams object.
  • Added Playwright e2e tests verifying the user filter visibility for member vs admin on /bookings/upcoming.
  • PBAC_REFACTORING_GUIDE.md expanded to PBAC-first patterns and examples.
  • Prisma migration removes booking.read permission from the member role.

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed This title clearly summarizes the primary change of refactoring the role-based isTeamAdminOrOwner check to use the team.listMembers PBAC permission in a concise, specific manner that aligns with the pull request changes.
Description Check ✅ Passed The description accurately outlines the goals and specific code changes of the pull request, including server-side permission checks, prop updates, context handling, and testing guidance, making it clearly related to the changeset.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch devin/pbac-refactor-bookings-1758631859

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • 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 47985e8 and f78b83d.

📒 Files selected for processing (2)
  • apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx (2 hunks)
  • packages/features/pbac/PBAC_REFACTORING_GUIDE.md (2 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:

  • apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.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:

  • apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/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/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx
🧠 Learnings (4)
📓 Common learnings
Learnt from: eunjae-lee
PR: calcom/cal.com#24006
File: apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx:37-48
Timestamp: 2025-09-24T11:53:40.185Z
Learning: In bookings listing views, when checking permissions for the member filter UI, use "booking.read" permission rather than "team.listMembers" because the filter's purpose is to read bookings of other team members, not just to list the members themselves. The permission check should align with the actual capability being granted.
📚 Learning: 2025-09-24T11:53:40.185Z
Learnt from: eunjae-lee
PR: calcom/cal.com#24006
File: apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx:37-48
Timestamp: 2025-09-24T11:53:40.185Z
Learning: In bookings listing views, when checking permissions for the member filter UI, use "booking.read" permission rather than "team.listMembers" because the filter's purpose is to read bookings of other team members, not just to list the members themselves. The permission check should align with the actual capability being granted.

Applied to files:

  • packages/features/pbac/PBAC_REFACTORING_GUIDE.md
  • apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx
📚 Learning: 2025-08-27T12:15:43.830Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22995
File: packages/trpc/server/routers/viewer/aiVoiceAgent/testCall.handler.ts:41-44
Timestamp: 2025-08-27T12:15:43.830Z
Learning: In calcom/cal.com, the AgentService.getAgent() method in packages/features/calAIPhone/providers/retellAI/services/AgentService.ts does NOT include authorization checks - it only validates the agentId parameter and directly calls the repository without verifying user/team access. This contrasts with other methods like getAgentWithDetails() which properly use findByIdWithUserAccessAndDetails() for authorization. When reviewing updateToolsFromAgentId() calls, always verify both agent ownership and eventType ownership are checked.

Applied to files:

  • packages/features/pbac/PBAC_REFACTORING_GUIDE.md
📚 Learning: 2025-07-15T12:59:34.389Z
Learnt from: eunjae-lee
PR: calcom/cal.com#22106
File: packages/features/insights/components/FailedBookingsByField.tsx:65-71
Timestamp: 2025-07-15T12:59:34.389Z
Learning: In the FailedBookingsByField component (packages/features/insights/components/FailedBookingsByField.tsx), although routingFormId is typed as optional in useInsightsParameters, the system automatically enforces a routing form filter, so routingFormId is always present in practice. This means the data always contains only one entry, making the single-entry destructuring approach safe.

Applied to files:

  • apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx
🧬 Code graph analysis (1)
apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx (3)
packages/features/auth/lib/next-auth-options.ts (1)
  • session (746-771)
packages/features/pbac/services/permission-check.service.ts (1)
  • PermissionCheckService (19-306)
packages/platform/libraries/index.ts (1)
  • MembershipRole (34-34)
⏰ 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: Production builds / Build API v2
  • GitHub Check: Production builds / Build Atoms
  • GitHub Check: Production builds / Build Web App
  • GitHub Check: Production builds / Build API v1
  • GitHub Check: Tests / Unit
🔇 Additional comments (2)
apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx (1)

43-52: Server-side “booking.read” gate is spot on

The PBAC lookup mirrors the real server validation, and the fallback roles cover non-PBAC teams—looks good.

packages/features/pbac/PBAC_REFACTORING_GUIDE.md (1)

240-248: Update bookings example to use booking.read + canReadOthersBookings

The Bookings page now wires the member filter through booking.read and canReadOthersBookings. Leaving the guide on team.listMembers/canListMembers will send future refactors back to the old (incorrect) permission. Please align the documentation with the actual implementation.

 // In page component (server-side)
 const permissions = {
-  canListMembers: teamIdsWithPermission.length > 0,
+  canReadOthersBookings: teamIdsWithPermission.length > 0,
 };
 
-// In UI component - permission-specific variable name
-const canListMembers = permissions.canListMembers;
-const canSeeMembers = canListMembers; // Use permission-specific name
+// In UI component - permission-specific variable name
+const canReadOthersBookings = permissions.canReadOthersBookings;
+const canSeeMembers = canReadOthersBookings; // Use permission-specific name

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@keithwillcode keithwillcode added consumer core area: core, team members only labels Sep 23, 2025
devin-ai-integration bot and others added 4 commits September 23, 2025 13:09
…er logic

- Wrap canListMembers in permissions object for future extensibility
- Simplify server-side logic to only use getTeamIdsWithPermission
- Remove unused imports (prisma, MembershipRole)
- Address user feedback on PR #24006

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>
- Clarify that teamIdsWithPermission.length > 0 check is for UI purposes
- Actual accurate filtering happens server-side for filter values

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>
- Verify that users with the MEMBER role cannot see the member filter
- Test creates team with ADMIN and MEMBER users
- Confirms UI correctly reflects PBAC permissions
- Address user feedback on PR #24006

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>
- Replace invalid teamId property with hasTeam and teammates pattern
- Fix TypeScript error in booking-filters.e2e.ts
- Resolve CI type check failure

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>
@github-actions github-actions bot added the ❗️ migrations contains migration files label Sep 23, 2025
@vercel
Copy link

vercel bot commented Sep 23, 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 26, 2025 8:14am
cal-eu Ignored Ignored Sep 26, 2025 8:14am

@eunjae-lee eunjae-lee force-pushed the devin/pbac-refactor-bookings-1758631859 branch from 3f7a8a1 to 64694e4 Compare September 24, 2025 08:36
@pull-request-size pull-request-size bot added size/L and removed size/M labels Sep 24, 2025
@eunjae-lee eunjae-lee marked this pull request as ready for review September 24, 2025 08:59
@eunjae-lee eunjae-lee requested a review from a team as a code owner September 24, 2025 08:59
@graphite-app graphite-app bot requested a review from a team September 24, 2025 08:59
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: 4

🧹 Nitpick comments (11)
packages/prisma/migrations/20250924082337_remove_booking_read_permission_from_member/migration.sql (1)

1-1: Comment is clear; consider naming the exact permission.

Nit: Reference the exact permission tuple to aid future grep/debug (e.g., booking/read).

apps/web/playwright/booking-filters.e2e.ts (3)

11-16: Use unique teammate names to avoid collisions across parallel runs

Hardcoding "team mate 1" can collide under parallel execution or across retries. Generate a unique suffix to ensure reliable lookup.

-const teamMateName = "team mate 1";
+const teamMateName = `team mate ${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;

18-24: Avoid global users.get() + name search; prefer direct handles from the fixture

Searching all users by name risks matching the wrong user under parallelism. If the fixture returns created entities, capture and use that reference directly.

If supported by your fixture, prefer:

const { teammates } = await users.create(undefined, {
  hasTeam: true,
  isOrg: true,
  teammates: [{ name: teamMateName }],
});
const memberUser = teammates[0];

If not supported today, consider extending the fixture to return created teammates.


27-31: Make the network wait more robust

waitForResponse may resolve on the first matching request; SPA navigations can trigger multiple TRPC calls. Consider waiting for navigation plus the response together or for network idle.

-const bookingsGetResponse = page.waitForResponse((response) =>
-  /\/api\/trpc\/bookings\/get.*/.test(response.url())
-);
-await page.goto(`/bookings/upcoming`, { waitUntil: "domcontentloaded" });
-await bookingsGetResponse;
+await Promise.all([
+  page.waitForResponse((response) => /\/api\/trpc\/bookings\/get.*/.test(response.url())),
+  page.goto(`/bookings/upcoming`, { waitUntil: "networkidle" }),
+]);
apps/web/modules/bookings/views/bookings-listing-view.tsx (2)

358-359: Reduce dayjs work in hot paths

Both “upcoming” filtering and “today” grouping recompute formatted dates per booking and per render. Precompute the “today” key and reuse parsed values to cut allocations.

Example refactor (outside the selected lines, for illustration):

const tz = user?.timeZone;
const todayKey = dayjs().tz(tz).format("YYYY-MM-DD");

const isSameLocalDay = (d: string | Date) =>
  dayjs(d).tz(tz).format("YYYY-MM-DD") === todayKey;

// In filter for upcoming:
return dayjs(booking.startTime).tz(tz).format("YYYY-MM-DD") !== todayKey;

This avoids multiple dayjs() calls and repeated format() invocations per row.


377-377: Apply the same precompute optimization to bookingsToday

Reuse todayKey/isSameLocalDay to reduce per-row allocations in this map/filter.

apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx (1)

52-56: Optional: Rename to reflect semantics

Consider renaming canReadOthersBookings to canListMembers across the prop and usage to better match the permission being checked.

If you opt to do this, update:

  • BookingsProps in bookings-listing-view.tsx
  • Column gating at enableColumnFilter
  • The prop here in page.tsx
  • Any tests referencing the behavior
packages/features/pbac/PBAC_REFACTORING_GUIDE.md (4)

111-119: Make the “booking.read” example consistent.

The snippet introduces “booking.read” but uses generic “resource.*” identifiers, which may confuse readers.

Use booking-specific names:

-if (resource.teamId) {
-  // Team resource - check permissions
-  const hasPermission = await permissionService.hasPermission(userId, "resource.read", resource.teamId);
-  if (!hasPermission) throw new ForbiddenError();
-} else {
-  // Personal resource - check ownership only
-  if (resource.userId !== currentUserId) throw new ForbiddenError();
-}
+if (booking.teamId) {
+  // Team resource - check permissions
+  const hasPermission = await permissionService.hasPermission(currentUserId, "booking.read", booking.teamId);
+  if (!hasPermission) throw new ForbiddenError();
+} else {
+  // Personal resource - check ownership only
+  if (booking.userId !== currentUserId) throw new ForbiddenError();
+}

73-78: Clarify “No Fallback Logic Needed” to distinguish no-permission vs. errors.

As written, it can be read as endorsing error suppression. Recommend making error behavior explicit to avoid masking outages.

- - `getTeamIdsWithPermission()` handles errors internally
- - Returns empty array `[]` when user has no permissions (this is legitimate)
- - Don't assume empty array means PBAC failure
+ - `getTeamIdsWithPermission()` returns an empty array `[]` when the user legitimately lacks the permission.
+ - Do not treat exceptions the same as “no permission.” Surface/track errors (telemetry/logs) or let them propagate, rather than converting them to `[]`.
+ - An empty array means “no permission,” not “PBAC failure.”

52-61: Add org-vs-team scope example (matches PR behavior).

PR logic checks “team.listMembers” at org scope when the user belongs to an organization; otherwise it aggregates team-scoped permissions. Add this snippet to guide implementers.

 // Server-side permission check in page/layout
 const permissionCheckService = new PermissionCheckService();
-const teamIdsWithPermission = await permissionCheckService.getTeamIdsWithPermission(
-  session.user.id,
-  "team.listMembers"
-);
+const orgId = session.user.organizationId;
+const teamIdsWithPermission = orgId
+  // Org context: check org-scoped permission
+  ? (await permissionCheckService.hasPermission(session.user.id, "team.listMembers", orgId)) ? ["ORG_SCOPE"] : []
+  // No org: collect team IDs where the user has permission
+  : await permissionCheckService.getTeamIdsWithPermission(session.user.id, "team.listMembers");
 
 const permissions = {
-  canListMembers: teamIdsWithPermission.length > 0,
+  // In org scope, a boolean is enough; in team scope, >0 team IDs implies permission.
+  canListMembers: Array.isArray(teamIdsWithPermission)
+    ? teamIdsWithPermission.length > 0
+    : Boolean(teamIdsWithPermission),
 };

Please confirm hasPermission(userId, permission, scopeId) supports org scope; if not, we should adjust the example to the correct API.


86-96: Reinforce naming guidance with a type-safe pattern.

Consider showing a typed permissions bag to prevent typos and ease discovery.

-// Pass to component
-<MyComponent permissions={permissions} />;
+// Example: typed permissions object helps avoid stringly-typed mistakes
+type BookingsPermissions = {
+  canListMembers: boolean;
+  // add more as needed
+};
+const permissions: BookingsPermissions = { canListMembers };
+<MyComponent permissions={permissions} />;
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • 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 788fbdb and dfbfe98.

📒 Files selected for processing (5)
  • apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx (2 hunks)
  • apps/web/modules/bookings/views/bookings-listing-view.tsx (7 hunks)
  • apps/web/playwright/booking-filters.e2e.ts (1 hunks)
  • packages/features/pbac/PBAC_REFACTORING_GUIDE.md (2 hunks)
  • packages/prisma/migrations/20250924082337_remove_booking_read_permission_from_member/migration.sql (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.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)/(main-nav)/bookings/[status]/page.tsx
  • apps/web/modules/bookings/views/bookings-listing-view.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:

  • apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx
  • apps/web/playwright/booking-filters.e2e.ts
  • apps/web/modules/bookings/views/bookings-listing-view.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/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx
  • apps/web/playwright/booking-filters.e2e.ts
  • apps/web/modules/bookings/views/bookings-listing-view.tsx
**/*.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/booking-filters.e2e.ts
🧠 Learnings (1)
📚 Learning: 2025-08-27T13:32:46.887Z
Learnt from: supalarry
PR: calcom/cal.com#23364
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/internal-to-api.spec.ts:295-296
Timestamp: 2025-08-27T13:32:46.887Z
Learning: In calcom/cal.com, when transforming booking fields from internal to API format, tests in organizations-event-types.e2e-spec.ts already expect name field label and placeholder to be empty strings ("") rather than undefined. PR changes that set these to explicit empty strings are typically fixing implementation to match existing test expectations rather than breaking changes.

Applied to files:

  • apps/web/playwright/booking-filters.e2e.ts
🧬 Code graph analysis (1)
apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx (2)
packages/features/auth/lib/next-auth-options.ts (1)
  • session (746-771)
packages/features/pbac/services/permission-check.service.ts (1)
  • PermissionCheckService (19-290)
🪛 markdownlint-cli2 (0.18.1)
packages/features/pbac/PBAC_REFACTORING_GUIDE.md

171-171: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ 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 (8)
packages/prisma/migrations/20250924082337_remove_booking_read_permission_from_member/migration.sql (1)

2-2: Avoid hard-coding roleId; delete by joining on the canonical Role (slug/name).

'member_role' risks being env-specific. If IDs differ across environments, the delete becomes a no-op, leaving the permission in place. Prefer joining via Role’s stable identifier (e.g., slug/name).

Proposed safer SQL (adjust column/table names to your schema):

-delete from "RolePermission" where "roleId" = 'member_role' and resource = 'booking' and action = 'read';
+delete from "RolePermission" rp
+using "Role" r
+where rp."roleId" = r.id
+  and r.slug = 'member'           -- or r.name = 'MEMBER'
+  and rp.resource = 'booking'
+  and rp.action = 'read';

Verify schema and constants with:

apps/web/playwright/booking-filters.e2e.ts (2)

33-34: LGTM: Member should not see the filter

Asserting the member user filter is hidden aligns with the permission‑gated UI.


49-50: LGTM: Admin should see the filter

Visibility assertion for admins is correct and matches the intended PBAC behavior.

apps/web/modules/bookings/views/bookings-listing-view.tsx (4)

117-117: Prop threading into BookingsContent is correct

Accepting permissions at this level keeps the table logic cleanly decoupled from server checks.


155-161: Using searchParams object as a dep is fine

Relying on searchParams identity to recompute tabs is acceptable in Next.js app router. The tab query preservation logic is correct.


218-223: Correctly gates the member filter by permission

enableColumnFilter: permissions.canReadOthersBookings replaces role checks and aligns with PBAC. Good.


320-321: Good: included permission flag in memo deps

This prevents stale column config when permissions or i18n change.

apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx (1)

52-56: LGTM: Plumbs permission to BookingsList

Passing the computed boolean to the table component follows the new contract.

// While this may not be entirely accurate, it's acceptable
// because we perform a thorough validation on the server side for the actual filter values.
// This variable is primarily for UI purposes.
canReadOthersBookings = teamIdsWithPermission.length > 0;
Copy link
Member

Choose a reason for hiding this comment

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

We need to keep in mind fallback roles here - getTeamIdsWithpermissions doesnt account for that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah !

Copy link
Contributor Author

Choose a reason for hiding this comment

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

for the future reference: getTeamIdsWithpermissions now supports fallbackRoles since #24042

@@ -0,0 +1,2 @@
-- Do not allow members to read others' bookings in the team
delete from "RolePermission" where "roleId" = 'member_role' and resource = 'booking' and action = 'read';
Copy link
Member

Choose a reason for hiding this comment

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

LGTM = not sure why this was in the members role

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

🧹 Nitpick comments (7)
packages/features/pbac/PBAC_REFACTORING_GUIDE.md (7)

73-79: Document transitional fallback explicitly (guide currently contradicts PR behavior).

This section says no fallback is needed, but the PR uses ADMIN/OWNER fallback for compatibility. Add a brief “Transitional fallback” note to avoid confusion.

Apply:

 ### 2. No Fallback Logic Needed
-
- - `getTeamIdsWithPermission()` handles errors internally
- - Returns empty array `[]` when user has no permissions (this is legitimate)
- - Don't assume empty array means PBAC failure
+ 
+ - `getTeamIdsWithPermission()` handles errors internally
+ - Returns empty array `[]` when user has no permissions (this is legitimate)
+ - Don't assume empty array means PBAC failure
+ - Transitional fallback: During migration, you may optionally gate with legacy role checks (e.g., ADMIN/OWNER) as a temporary fallback. Keep this isolated server-side and plan removal.

110-118: Use concrete permission in example or clarify placeholder.

Using "resource.read" is abstract. Either switch to a concrete example (e.g., "booking.read") or add a brief note that the permission string must match the resource/action.

Apply (option A):

-  const hasPermission = await permissionService.hasPermission(userId, "resource.read", resource.teamId);
+  const hasPermission = await permissionService.hasPermission(userId, "booking.read", resource.teamId);

Or (option B) add a comment:

-  const hasPermission = await permissionService.hasPermission(userId, "resource.read", resource.teamId);
+  // Use the permission aligned to the resource/action (e.g., "booking.read", "team.update")
+  const hasPermission = await permissionService.hasPermission(userId, "resource.read", resource.teamId);

142-148: Clarify permission choice in handler example (don’t hardcode listMembers for non-member flows).

Add guidance to pick the correct permission per endpoint; avoid implying "team.listMembers" is the default for all team queries.

Apply:

    const permissionCheckService = new PermissionCheckService();
    const teamsToQuery = await permissionCheckService.getTeamIdsWithPermission(
      ctx.user.id,
-     "team.listMembers"
+     "team.listMembers" // Choose the permission aligned to the endpoint (e.g., "booking.read" for bookings APIs)
    );

180-185: Align props interface with bookings permission (rename to canReadOthersBookings).

This prevents accidental reuse of the wrong permission flag.

Apply:

    interface BookingsProps {
      permissions: {
-       canListMembers: boolean;
+       canReadOthersBookings: boolean;
      };
    }

190-194: Update usage example to the renamed permission.

Apply:

    // Replace: const isTeamAdminOrOwner = user?.isTeamAdminOrOwner ?? false
-   // With: const canListMembers = permissions.canListMembers
+   // With: const canReadOthersBookings = permissions.canReadOthersBookings

210-216: Also adjust the permission retrieval in this section to use "booking.read".

Make the preceding retrieval consistent with the changed flag.

Apply:

-// Moved permission check from client (`user?.isTeamAdminOrOwner`) to server
-// Added `permissions` prop with `canListMembers` boolean
-// Used `teamIdsWithPermission.length > 0` pattern for UI control
+// Moved permission check from client (`user?.isTeamAdminOrOwner`) to server
+// Added `permissions` prop with `canReadOthersBookings` boolean
+// Used `teamIdsWithPermission.length > 0` pattern with "booking.read" for UI control

98-106: Add org-vs-team context note.

PR logic handles org-level aggregation for permissions. Add a short note here to reflect that pattern so consumers don’t miss org-context checks.

Apply:

 ### 6. Team vs Personal Resources
+
+Note: If the user belongs to an organization, evaluate permissions at the org scope when applicable (e.g., aggregate teamIds with permission via org context before enforcing).
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • 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 dfbfe98 and 47985e8.

📒 Files selected for processing (1)
  • packages/features/pbac/PBAC_REFACTORING_GUIDE.md (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: eunjae-lee
PR: calcom/cal.com#24006
File: apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx:37-48
Timestamp: 2025-09-24T11:53:40.162Z
Learning: In bookings listing views, when checking permissions for the member filter UI, use "booking.read" permission rather than "team.listMembers" because the filter's purpose is to read bookings of other team members, not just to list the members themselves. The permission check should align with the actual capability being granted.
📚 Learning: 2025-09-24T11:53:40.162Z
Learnt from: eunjae-lee
PR: calcom/cal.com#24006
File: apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx:37-48
Timestamp: 2025-09-24T11:53:40.162Z
Learning: In bookings listing views, when checking permissions for the member filter UI, use "booking.read" permission rather than "team.listMembers" because the filter's purpose is to read bookings of other team members, not just to list the members themselves. The permission check should align with the actual capability being granted.

Applied to files:

  • packages/features/pbac/PBAC_REFACTORING_GUIDE.md
⏰ 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)
packages/features/pbac/PBAC_REFACTORING_GUIDE.md (2)

171-176: Re-run markdownlint for fenced code blocks; ensure all fences have a language and use triple backticks.

A previous review flagged MD040 and 4‑backtick fences around here. It looks corrected, but please re-verify to avoid CI noise.

Run:

#!/bin/bash
set -euo pipefail
file='packages/features/pbac/PBAC_REFACTORING_GUIDE.md'
echo "Unlabeled fences (should be empty):"
rg -nP '^\s*```(?!\w)' "$file" || true
echo
echo "Four-backtick fences (should be empty):"
rg -nP '````' "$file" || true
echo
echo "Fence summary around 160-190:"
nl -ba -w1 -s': ' "$file" | sed -n '160,190p'

121-129: Good list of common permissions.

We leveraged the retrieved learning that bookings member filter should use "booking.read". Please confirm this aligns with the final PR implementation in apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx.

@github-actions
Copy link
Contributor

github-actions bot commented Sep 24, 2025

E2E results are ready!

@eunjae-lee eunjae-lee marked this pull request as ready for review September 25, 2025 12:47
@dosubot dosubot bot added bookings area: bookings, availability, timezones, double booking teams area: teams, round robin, collective, managed event-types labels Sep 25, 2025
@eunjae-lee eunjae-lee enabled auto-merge (squash) September 26, 2025 08:14
Copy link
Contributor

@keithwillcode keithwillcode left a comment

Choose a reason for hiding this comment

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

Migration looks good

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

Labels

bookings area: bookings, availability, timezones, double booking consumer core area: core, team members only ❗️ migrations contains migration files ready-for-e2e 💻 refactor size/L teams area: teams, round robin, collective, managed event-types

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants