Skip to content

Commit 788fbdb

Browse files
refactor: implement PBAC for team member listing in listSimpleMembers handler (calcom#24005)
* refactor: implement PBAC for team member listing - Replace membership-based team filtering with getTeamIdsWithPermission - Use team.listMembers permission for access control - Maintain fallback to original logic when PBAC fails - Add comprehensive PBAC refactoring guide for future use Fixes team fetching logic to use Permission-Based Access Control while preserving existing functionality and privacy checks. Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * docs: move PBAC refactoring guide to packages/features/pbac/ Move the PBAC refactoring guide to the appropriate location within the PBAC feature package for better organization and discoverability. Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * refactor: remove unnecessary try-catch wrapper The getTeamIdsWithPermission method already handles all errors internally and returns an empty array instead of throwing exceptions, making the try-catch wrapper redundant. Simplified to use direct fallback logic based on empty array return. Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * refactor: simplify PBAC implementation - Remove flawed fallback logic that assumed empty array meant PBAC failure - Use direct string 'team.listMembers' instead of PermissionMapper - Remove unused imports (PermissionMapper, Resource, CustomAction) - Empty array from getTeamIdsWithPermission is legitimate (no permissions) Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * docs: simplify PBAC refactoring guide - Reduce from 260 to 87 lines by removing bloated content - Focus on core pattern: direct permission strings, no fallback logic - Align with actual PR implementation - Remove verbose theoretical sections and complex patterns Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent b223937 commit 788fbdb

2 files changed

Lines changed: 89 additions & 19 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# PBAC Refactoring Guide
2+
3+
Quick guide for refactoring Cal.com handlers to use Permission-Based Access Control (PBAC) instead of membership-based queries.
4+
5+
## Core Pattern: Team Filtering with PBAC
6+
7+
**Before (Membership-based)**:
8+
```typescript
9+
const teamsToQuery = (
10+
await prisma.membership.findMany({
11+
where: {
12+
userId: ctx.user.id,
13+
accepted: true,
14+
NOT: [
15+
{
16+
role: MembershipRole.MEMBER,
17+
team: { isPrivate: true },
18+
},
19+
],
20+
},
21+
select: { teamId: true },
22+
})
23+
).map((membership) => membership.teamId);
24+
```
25+
26+
**After (PBAC-based)**:
27+
```typescript
28+
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
29+
30+
const permissionCheckService = new PermissionCheckService();
31+
const teamsToQuery = await permissionCheckService.getTeamIdsWithPermission(
32+
ctx.user.id,
33+
"team.listMembers"
34+
);
35+
```
36+
37+
## Key Points
38+
39+
### 1. Use Direct Permission Strings
40+
- Use `"team.listMembers"` directly instead of `PermissionMapper.toPermissionString()`
41+
- Simpler and more readable
42+
43+
### 2. No Fallback Logic Needed
44+
- `getTeamIdsWithPermission()` handles errors internally
45+
- Returns empty array `[]` when user has no permissions (this is legitimate)
46+
- Don't assume empty array means PBAC failure
47+
48+
### 3. Common Team Permissions
49+
- `"team.listMembers"` - List team members
50+
- `"team.read"` - View team details
51+
- `"team.update"` - Edit team settings
52+
- `"team.invite"` - Invite members
53+
- `"team.remove"` - Remove members
54+
55+
## Step-by-Step Refactoring
56+
57+
1. **Add import**:
58+
```typescript
59+
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
60+
```
61+
62+
2. **Replace membership query**:
63+
```typescript
64+
const permissionCheckService = new PermissionCheckService();
65+
const teamsToQuery = await permissionCheckService.getTeamIdsWithPermission(
66+
ctx.user.id,
67+
"team.listMembers"
68+
);
69+
```
70+
71+
3. **Keep existing privacy checks** - PBAC doesn't replace organization-level privacy logic
72+
73+
## Example: listSimpleMembers.handler.ts
74+
75+
**What changed**:
76+
- Replaced 17 lines of membership query with 2 lines of PBAC
77+
- Removed unnecessary imports and fallback logic
78+
- Used direct permission string `"team.listMembers"`
79+
80+
**Result**: Cleaner, more maintainable code that respects fine-grained permissions.
81+
82+
## Verification
83+
84+
- Run `yarn type-check:ci --force`
85+
- Ensure existing privacy checks remain intact
86+
- Test with users who have different permission levels

packages/trpc/server/routers/viewer/teams/listSimpleMembers.handler.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
* Simplified version of legacyListMembers.handler.ts that returns basic member info.
33
* Used for filtering people on /bookings.
44
*/
5+
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
56
import type { PrismaClient } from "@calcom/prisma";
6-
import { MembershipRole } from "@calcom/prisma/enums";
77
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
88

99
type ListSimpleMembersOptions = {
@@ -22,24 +22,8 @@ export const listSimpleMembers = async ({ ctx }: ListSimpleMembersOptions) => {
2222
return [];
2323
}
2424

25-
// query all teams the user is a member of and the team is not private
26-
const teamsToQuery = (
27-
await prisma.membership.findMany({
28-
where: {
29-
userId: ctx.user.id,
30-
accepted: true,
31-
NOT: [
32-
{
33-
role: MembershipRole.MEMBER,
34-
team: {
35-
isPrivate: true,
36-
},
37-
},
38-
],
39-
},
40-
select: { teamId: true },
41-
})
42-
).map((membership) => membership.teamId);
25+
const permissionCheckService = new PermissionCheckService();
26+
const teamsToQuery = await permissionCheckService.getTeamIdsWithPermission(ctx.user.id, "team.listMembers");
4327

4428
if (!teamsToQuery.length) {
4529
return [];

0 commit comments

Comments
 (0)