Skip to content

fix: get bookings handler for pbac and fallback roles#28

Open
tomerqodo wants to merge 2 commits intocoderabbit_full_base_fix_get_bookings_handler_for_pbac_and_fallback_roles_pr1from
coderabbit_full_head_fix_get_bookings_handler_for_pbac_and_fallback_roles_pr1
Open

fix: get bookings handler for pbac and fallback roles#28
tomerqodo wants to merge 2 commits intocoderabbit_full_base_fix_get_bookings_handler_for_pbac_and_fallback_roles_pr1from
coderabbit_full_head_fix_get_bookings_handler_for_pbac_and_fallback_roles_pr1

Conversation

@tomerqodo
Copy link

@tomerqodo tomerqodo commented Jan 28, 2026

Benchmark PR from agentic-review-benchmarks#1

Summary by CodeRabbit

  • New Features

    • Permission checks now support organization-level scoping to limit results to specific organizations and their hierarchies.
    • Booking retrieval now uses a permission-based access control system, replacing previous membership-based checks.
  • Tests

    • Added integration tests for organization-scoped permission filtering and child team inclusion.
    • Added comprehensive test suite covering booking retrieval scenarios with permission-based access control.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

Walkthrough

The changes implement organization-scoped permission checking across the PBAC system and refactor the bookings handler to replace admin/owner membership-based access control with a permission-based system. This includes new optional orgId parameters in repository and service layers, organization hierarchy-aware query logic, comprehensive integration and unit tests, and restructured booking access filtering.

Changes

Cohort / File(s) Summary
Interface Updates
packages/features/pbac/domain/repositories/IPermissionRepository.ts
Added optional orgId?: number parameter to getTeamIdsWithPermission and getTeamIdsWithPermissions method signatures for organization scoping.
Repository Implementation
packages/features/pbac/infrastructure/repositories/PermissionRepository.ts
Implemented org-scoped permission lookups with new private helpers getTeamsWithPBACPermissions and getTeamsWithFallbackRoles. Introduces organization hierarchy filtering via Team joins and parentId constraints. Parallelized PBAC and fallback-role team retrieval with Promise.all and result deduplication.
Repository Tests
packages/features/pbac/infrastructure/repositories/__tests__/PermissionRepository.integration-test.ts
Added two integration test cases validating org-scoped filtering: one verifying multiple organizations return when orgId is omitted and scoped results when provided, and another confirming child teams are included with org scoping.
Service Layer
packages/features/pbac/services/permission-check.service.ts
Updated getTeamIdsWithPermission and getTeamIdsWithPermissions method signatures to accept optional orgId?: number and forward it to repository calls.
Booking Handler
packages/trpc/server/routers/viewer/bookings/get.handler.ts
Replaced admin/owner membership-based access control with permission-based system using PermissionCheckService. Refactored filtering logic to use teamIdsWithBookingPermission instead of membership-derived team sets. Introduced getUserIdsAndEmailsFromTeamIds helper to fetch accessible user data from permission-scoped teams.
Handler Tests
packages/trpc/server/routers/viewer/bookings/get.handler.test.ts
Added comprehensive test suite covering booking retrieval, PBAC permission flow validation, fallback role handling, multi-team access scenarios, and error paths (FORBIDDEN when lacking permissions, access granted for own userId).

Sequence Diagram(s)

sequenceDiagram
    participant Handler as Bookings Handler
    participant Service as PermissionCheckService
    participant Repo as PermissionRepository
    participant DB as Database

    Handler->>Service: getTeamIdsWithPermission(userId, permission, fallbackRoles, orgId?)
    Service->>Repo: getTeamIdsWithPermission(params with orgId)
    
    par Parallel Fetch
        Repo->>DB: Query PBAC permissions<br/>JOIN Team WHERE orgId/parentId
        Repo->>DB: Query fallback roles<br/>JOIN Team WHERE orgId/parentId
    end
    
    DB-->>Repo: PBAC team IDs
    DB-->>Repo: Fallback team IDs
    
    Repo->>Repo: Deduplicate & union team IDs
    Repo-->>Service: Combined team IDs (org-scoped)
    
    Service-->>Handler: Team IDs with booking.read permission
    
    Handler->>Handler: Filter bookings by accessible team IDs
    Handler-->>Client: Filtered booking results
Loading
sequenceDiagram
    participant Client
    participant Handler as get.handler
    participant PermissionService as PermissionCheckService
    participant Repo as PermissionRepository
    
    Client->>Handler: getHandler(request with userId filter)
    
    Handler->>Handler: Check if filtering by own userId
    
    alt User filtering by own ID
        Handler-->>Client: Return user's own bookings
    else User filtering by other userIds
        Handler->>PermissionService: getTeamIdsWithPermission(userId, booking.read)
        PermissionService->>Repo: Query org-scoped teams
        Repo-->>PermissionService: Team IDs where user has booking.read
        PermissionService-->>Handler: Team IDs
        
        Handler->>Handler: Check if user accessible via permission-scoped teams
        
        alt User lacks required permission
            Handler-->>Client: FORBIDDEN error
        else User has permission
            Handler->>Handler: Filter bookings by allowed team scope
            Handler-->>Client: Filtered bookings
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

The changes involve substantial refactoring across multiple layers (repository, service, handler) with new org-scoped query logic, complex SQL filtering with joins and hierarchical constraints, a comprehensive test suite with numerous scenarios, and significant control flow changes replacing membership-based access with permission-based access. The heterogeneous nature of modifications (interface, implementation, logic, and test changes) requires careful verification of correctness across layers.

Poem

🐰 With orgIds and permissions intertwined,
We hoist the access checks realigned,
From admin gates to scopes so free,
The booking realm now organized, you'll see! ✨
Organization hierarchies bloom,
As permission flowers chase the gloom! 🌱

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: updating the get bookings handler to support PBAC (Permission-Based Access Control) and fallback roles throughout the repository layer and service layer.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

Copy link

@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

Caution

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

⚠️ Outside diff range comments (1)
packages/features/pbac/infrastructure/repositories/PermissionRepository.ts (1)

211-223: Critical: orgId parameter is not forwarded to getTeamIdsWithPermissions.

The getTeamIdsWithPermission method accepts orgId but does not pass it to getTeamIdsWithPermissions, causing org-scoped filtering to be ignored when using this method.

🐛 Fix: Forward orgId to getTeamIdsWithPermissions
   async getTeamIdsWithPermission({
     userId,
     permission,
     fallbackRoles,
     orgId,
   }: {
     userId: number;
     permission: PermissionString;
     fallbackRoles: MembershipRole[];
     orgId?: number;
   }): Promise<number[]> {
-    return this.getTeamIdsWithPermissions({ userId, permissions: [permission], fallbackRoles });
+    return this.getTeamIdsWithPermissions({ userId, permissions: [permission], fallbackRoles, orgId });
   }
🤖 Fix all issues with AI agents
In `@packages/features/pbac/services/permission-check.service.ts`:
- Line 6: Remove the unused TRPCError import from the top-level imports in
permission-check.service.ts: delete the `TRPCError` specifier from the import
statement so only used symbols remain, and run the linter/TS build to confirm
there are no remaining references to `TRPCError`.
🧹 Nitpick comments (2)
packages/features/pbac/infrastructure/repositories/__tests__/PermissionRepository.integration-test.ts (1)

1050-1053: Cleanup may fail due to foreign key constraint order.

The cleanup deletes teams before memberships could potentially reference them. While the current order (membership.deleteMany before team.deleteMany) is correct, the delete statement includes all team IDs in a single operation, which could fail if parent teams are deleted before child teams due to the parentId foreign key.

🔧 Suggested fix to ensure child teams are deleted first
       // Cleanup
       await prisma.membership.deleteMany({ where: { userId: testUserId } });
-      await prisma.team.deleteMany({ where: { id: { in: [org1.id, org2.id, team1.id, team2.id] } } });
+      // Delete child teams first, then parent orgs to respect foreign key constraints
+      await prisma.team.deleteMany({ where: { id: { in: [team1.id, team2.id] } } });
+      await prisma.team.deleteMany({ where: { id: { in: [org1.id, org2.id] } } });
packages/trpc/server/routers/viewer/bookings/get.handler.ts (1)

114-122: Consider injecting PermissionCheckService rather than instantiating per request.

Creating a new PermissionCheckService instance on every call to getBookings may have performance implications if the service constructor performs expensive operations. Consider passing it as a parameter or using dependency injection for better testability and potential caching.

♻️ Optional refactor to inject the service
 export async function getBookings({
   user,
   prisma,
   kysely,
   bookingListingByStatus,
   sort,
   filters,
   take,
   skip,
+  permissionCheckService = new PermissionCheckService(),
 }: {
   user: { id: number; email: string; orgId?: number | null };
   filters: TGetInputSchema["filters"];
   prisma: PrismaClient;
   kysely: Kysely<DB>;
   bookingListingByStatus: InputByStatus[];
   sort?: { ... };
   take: number;
   skip: number;
+  permissionCheckService?: PermissionCheckService;
 }) {
-  const permissionCheckService = new PermissionCheckService();
   const fallbackRoles: MembershipRole[] = [MembershipRole.ADMIN, MembershipRole.OWNER];
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb600a8 and 544a43b.

📒 Files selected for processing (6)
  • packages/features/pbac/domain/repositories/IPermissionRepository.ts
  • packages/features/pbac/infrastructure/repositories/PermissionRepository.ts
  • packages/features/pbac/infrastructure/repositories/__tests__/PermissionRepository.integration-test.ts
  • packages/features/pbac/services/permission-check.service.ts
  • packages/trpc/server/routers/viewer/bookings/get.handler.test.ts
  • packages/trpc/server/routers/viewer/bookings/get.handler.ts
🧰 Additional context used
🧬 Code graph analysis (6)
packages/features/pbac/infrastructure/repositories/__tests__/PermissionRepository.integration-test.ts (3)
scripts/prepare-local-for-delegation-credentials-testing.js (2)
  • prisma (7-7)
  • org (45-47)
packages/platform/libraries/index.ts (1)
  • MembershipRole (34-34)
apps/web/test/lib/getSchedule/expects.ts (1)
  • expect (152-152)
packages/features/pbac/infrastructure/repositories/PermissionRepository.ts (2)
packages/features/pbac/domain/types/permission-registry.ts (1)
  • PermissionString (72-72)
packages/platform/libraries/index.ts (1)
  • MembershipRole (34-34)
packages/trpc/server/routers/viewer/bookings/get.handler.test.ts (2)
packages/trpc/server/routers/viewer/bookings/get.handler.ts (2)
  • getHandler (37-82)
  • getBookings (90-769)
packages/features/pbac/services/permission-check.service.ts (1)
  • PermissionCheckService (20-377)
packages/features/pbac/domain/repositories/IPermissionRepository.ts (2)
packages/features/pbac/domain/types/permission-registry.ts (1)
  • PermissionString (72-72)
packages/platform/libraries/index.ts (1)
  • MembershipRole (34-34)
packages/features/pbac/services/permission-check.service.ts (2)
packages/platform/libraries/index.ts (1)
  • MembershipRole (34-34)
packages/features/pbac/domain/types/permission-registry.ts (1)
  • PermissionString (72-72)
packages/trpc/server/routers/viewer/bookings/get.handler.ts (1)
packages/platform/libraries/index.ts (1)
  • MembershipRole (34-34)
🔇 Additional comments (15)
packages/features/pbac/domain/repositories/IPermissionRepository.ts (1)

64-84: LGTM!

The interface changes are well-designed. The optional orgId parameter is properly typed and the JSDoc comments clearly explain the scoping behavior. The changes align with the implementation in PermissionRepository.ts.

packages/features/pbac/infrastructure/repositories/__tests__/PermissionRepository.integration-test.ts (2)

972-1053: Good test coverage for org-scoped filtering.

The test properly validates:

  1. Without orgId, both organizations are returned
  2. With orgId, only the specified org and its child teams are returned
  3. Teams from other organizations are excluded

1055-1098: LGTM!

The test correctly validates that child teams are included when orgId is provided, which is essential for the organization hierarchy permission model.

packages/features/pbac/services/permission-check.service.ts (2)

314-342: LGTM!

The orgId parameter is properly added to the method signature and correctly forwarded to the repository call. The JSDoc comment clearly documents the parameter's purpose.


348-376: LGTM!

Consistent implementation with getTeamIdsWithPermission. The orgId parameter is properly forwarded and documented.

packages/trpc/server/routers/viewer/bookings/get.handler.ts (2)

144-159: LGTM!

The permission validation logic correctly:

  1. Checks if filtered userIds are within accessible teams
  2. Allows users to always access their own bookings (isCurrentUser check)
  3. Throws a clear FORBIDDEN error with descriptive message when access is denied

946-978: LGTM!

The getUserIdsAndEmailsFromTeamIds helper is well-implemented:

  • Early return for empty teamIds prevents unnecessary database queries
  • Proper deduplication using Set
  • Clear return type [number[], string[]]
packages/features/pbac/infrastructure/repositories/PermissionRepository.ts (3)

248-258: LGTM!

The parallel execution with Promise.all and subsequent deduplication is well-implemented. This approach efficiently combines PBAC permissions and fallback roles while avoiding duplicates.


264-326: LGTM!

The PBAC permissions query correctly:

  • Uses a CTE for required permissions
  • Handles both direct team memberships and child teams via org membership
  • Applies org scoping with (orgId IS NULL OR t.id = orgId OR t.parentId = orgId)
  • Uses proper wildcard matching for permissions

332-359: LGTM!

The fallback roles query correctly:

  • Excludes teams where PBAC is enabled (f.teamId IS NULL)
  • Handles both direct memberships and child teams
  • Applies proper org scoping
packages/trpc/server/routers/viewer/bookings/get.handler.test.ts (5)

10-28: LGTM!

The mock setup is comprehensive and properly isolates the unit under test by mocking external dependencies including the logger, kysely client, and permission service.


196-247: LGTM!

The test correctly validates that a FORBIDDEN error is thrown when a user tries to filter by userIds that they don't have permission to access. The test setup properly mocks the scenario where user 4 is not in the accessible teams.


249-278: LGTM!

Good test coverage for the edge case where users should always be able to access their own bookings regardless of PBAC permissions.


309-335: LGTM!

The test correctly validates that PBAC permissions and fallback ADMIN/OWNER roles are combined, allowing access to bookings from multiple teams.


189-194: Remove this comment - no issue exists.

The test assertion is correct. In Prisma, enum values without explicit assignments default to string literals. MembershipRole.ADMIN and MembershipRole.OWNER are equivalent to the strings "ADMIN" and "OWNER", so the test assertion fallbackRoles: ["ADMIN", "OWNER"] accurately reflects what the implementation passes to the mocked function.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "TRPCError" packages/features/pbac/services/permission-check.service.ts

Repository: qodo-benchmark/cal.com-combined-coderabbit

Length of output: 129


Remove unused import TRPCError.

The TRPCError import at line 6 is not used anywhere in this file.

🤖 Prompt for AI Agents
In `@packages/features/pbac/services/permission-check.service.ts` at line 6,
Remove the unused TRPCError import from the top-level imports in
permission-check.service.ts: delete the `TRPCError` specifier from the import
statement so only used symbols remain, and run the linter/TS build to confirm
there are no remaining references to `TRPCError`.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant