Skip to content

Commit b9fadf8

Browse files
feat: pbac (calcom#21296)
* inital pbac setup * fix tests * docs intro * more docs * add comments in docs * revert pr_todo * update to use action + custom as defaults * use enums * move to change user role + multi check * add beforeEach import * docs: add PERMISSIONS.md documenting team/organization role usages Co-Authored-By: sean@cal.com <sean@cal.com> * docs: add permission string alternatives for helper functions Co-Authored-By: sean@cal.com <sean@cal.com> * feat: enhance permission registry and default role permissions Co-Authored-By: sean@cal.com <sean@cal.com> * add routers * add hooks + rsc. Also adjsuted test to use prismock * make hooks type safe + add some tests * add a permission context - trpc router to populate context * update docs * explore global and default roles member,admin,owner * fix tests * use kysley and abstract uril * tidy up * update trpc to use pbac route in endpoint + distinct on teamId * make transform util * remove role service dependancy from permission check class * checkTeamId fall back to orgId if team is parent * todos * remove manual migrations to launch softly * address feedback * move permission checks to a repo * move role service database calls to repo. improve mocks * update tests to have describe import * fix types * remove router that is not used to pass build --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 1740e34 commit b9fadf8

21 files changed

Lines changed: 2249 additions & 0 deletions

File tree

PERMISSIONS.md

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# Cal.com Permission Documentation
2+
3+
This document maps existing role-based permission checks to the new PBAC (Permission-Based Access Control) system's permission strings in the format `resource.action`.
4+
5+
## Permissions by Resource
6+
7+
### Team Permissions
8+
9+
| Permission String | Description | File Path | Line |
10+
|------------------|-------------|-----------|------|
11+
| team.create | Create teams | [packages/trpc/server/routers/viewer/teams/create.handler.ts](packages/trpc/server/routers/viewer/teams/create.handler.ts) | 55-57 |
12+
| team.update | Update team settings | [packages/trpc/server/routers/viewer/teams/update.handler.ts](packages/trpc/server/routers/viewer/teams/update.handler.ts) | 18 |
13+
| team.changeMemberRole | Change role of team members | [packages/trpc/server/routers/viewer/teams/changeMemberRole.handler.ts](packages/trpc/server/routers/viewer/teams/changeMemberRole.handler.ts) | 18-21 |
14+
| team.remove | Remove members from team | [packages/trpc/server/routers/viewer/teams/removeMember.handler.ts](packages/trpc/server/routers/viewer/teams/removeMember.handler.ts) | 35-36, 51-55 |
15+
| team.invite | Invite members to team | [packages/trpc/server/routers/viewer/teams/invite.handler.ts](packages/trpc/server/routers/viewer/teams/invite.handler.ts) | - |
16+
17+
### Event Type Permissions
18+
19+
| Permission String | Description | File Path | Line |
20+
|------------------|-------------|-----------|------|
21+
| eventType.create | Create event types | [packages/trpc/server/routers/viewer/eventTypes/create.handler.ts](packages/trpc/server/routers/viewer/eventTypes/create.handler.ts) | - |
22+
| eventType.update | Update event types | [packages/trpc/server/routers/viewer/eventTypes/update.handler.ts](packages/trpc/server/routers/viewer/eventTypes/update.handler.ts) | 162-164 |
23+
| eventType.delete | Delete event types | [packages/trpc/server/routers/viewer/eventTypes/delete.handler.ts](packages/trpc/server/routers/viewer/eventTypes/delete.handler.ts) | 13-31 |
24+
25+
### Booking Permissions
26+
27+
| Permission String | Description | File Path | Line |
28+
|------------------|-------------|-----------|------|
29+
| booking.read | Read booking details | [packages/trpc/server/routers/viewer/bookings/get.handler.ts](packages/trpc/server/routers/viewer/bookings/get.handler.ts) | 95-107, 132-134 |
30+
| booking.readTeamBookings | Read team bookings | [packages/trpc/server/routers/viewer/bookings/get.handler.ts](packages/trpc/server/routers/viewer/bookings/get.handler.ts) | 240-252 |
31+
| booking.readOrgBookings | Read organization bookings | [packages/trpc/server/routers/viewer/bookings/get.handler.ts](packages/trpc/server/routers/viewer/bookings/get.handler.ts) | 240-252 |
32+
33+
### Organization Permissions
34+
35+
| Permission String | Description | File Path | Line |
36+
|------------------|-------------|-----------|------|
37+
| organization.read | Read organization details | [packages/trpc/server/routers/viewer/organizations/get.handler.ts](packages/trpc/server/routers/viewer/organizations/get.handler.ts) | - |
38+
| organization.listMembers | List organization members | [packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts](packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts) | 68-76 |
39+
| organization.create | Create organization | [packages/trpc/server/routers/viewer/organizations/create.handler.ts](packages/trpc/server/routers/viewer/organizations/create.handler.ts) | - |
40+
41+
### API Key Permissions
42+
43+
| Permission String | Description | File Path | Line |
44+
|------------------|-------------|-----------|------|
45+
| apiKey.create | Create API keys | [packages/trpc/server/routers/viewer/apiKeys/create.handler.ts](packages/trpc/server/routers/viewer/apiKeys/create.handler.ts) | 25-26 |
46+
| apiKey.findKeyOfType | Find API keys by type | [packages/trpc/server/routers/viewer/apiKeys/findKeyOfType.handler.ts](packages/trpc/server/routers/viewer/apiKeys/findKeyOfType.handler.ts) | 18-19 |
47+
48+
## Helper Functions and Utilities
49+
50+
The following helper functions are commonly used for permission checks:
51+
52+
| Function | Description | File Path |
53+
|----------|-------------|-----------|
54+
| isTeamAdmin | Checks if user is admin or owner of a team | [packages/lib/server/queries/teams/index.ts](packages/lib/server/queries/teams/index.ts#L385-L405) |
55+
| isTeamOwner | Checks if user is owner of a team | [packages/lib/server/queries/teams/index.ts](packages/lib/server/queries/teams/index.ts#L407-L416) |
56+
| isTeamMember | Checks if user is a member of a team | [packages/lib/server/queries/teams/index.ts](packages/lib/server/queries/teams/index.ts#L418-L426) |
57+
| canEditEntity | Checks if user can edit an entity | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L13-L22) |
58+
| canAccessEntity | Checks if user can access an entity | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L24-L34) |
59+
| getEntityPermissionLevel | Gets permission level for an entity | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L36-L73) |
60+
| canCreateEntity | Checks if user can create an entity | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L102-L115) |
61+
| withRoleCanCreateEntity | Checks if role allows entity creation | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L119-L121) |
62+
63+
## Permission Level Enums
64+
65+
The codebase uses several enums to define permission levels:
66+
67+
### MembershipRole Enum
68+
69+
```typescript
70+
enum MembershipRole {
71+
OWNER = "OWNER",
72+
ADMIN = "ADMIN",
73+
MEMBER = "MEMBER"
74+
}
75+
```
76+
77+
### Entity Permission Level Enum
78+
79+
```typescript
80+
enum ENTITY_PERMISSION_LEVEL {
81+
NONE,
82+
USER_ONLY_WRITE,
83+
TEAM_READ_ONLY,
84+
TEAM_WRITE
85+
}
86+
```
87+
88+
## Common Permission Patterns
89+
90+
1. **Team Admin/Owner Check**: Many operations require the user to be a team admin or owner.
91+
```typescript
92+
if (!(await isTeamAdmin(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" });
93+
```
94+
95+
2. **Team Owner Check**: Some operations (like changing an owner's role) require the user to be a team owner.
96+
```typescript
97+
if (input.role === MembershipRole.OWNER && !(await isTeamOwner(ctx.user?.id, input.teamId)))
98+
throw new TRPCError({ code: "UNAUTHORIZED" });
99+
```
100+
101+
3. **Organization Admin Check**: Operations within an organization require the user to be an organization admin.
102+
```typescript
103+
if (user.profile?.organizationId && !user.organization.isOrgAdmin) {
104+
throw new TRPCError({ code: "FORBIDDEN", message: "org_admins_can_create_new_teams" });
105+
}
106+
```
107+
108+
4. **Entity Permission Check**: Entity operations use permission level checks.
109+
```typescript
110+
const permissionLevel = await getEntityPermissionLevel(entity, userId);
111+
return permissionLevel === ENTITY_PERMISSION_LEVEL.TEAM_WRITE ||
112+
permissionLevel === ENTITY_PERMISSION_LEVEL.USER_ONLY_WRITE;
113+
```
114+
115+
5. **Booking Access Control**: Complex permission checks for retrieving bookings based on user roles and team/organization membership.
116+
```typescript
117+
const membershipIdsWhereUserIsAdminOwner = (
118+
await prisma.membership.findMany({
119+
where: {
120+
userId: user.id,
121+
role: {
122+
in: ["ADMIN", "OWNER"],
123+
},
124+
},
125+
select: {
126+
id: true,
127+
},
128+
})
129+
).map((membership) => membership.id);
130+
```
131+
132+
## Migration to PBAC
133+
134+
When migrating to the new PBAC system, these existing role-based checks should be replaced with permission string checks using the `permissionMatches` function:
135+
136+
```typescript
137+
import { permissionMatches } from "@calcom/features/pbac/types/permission-registry";
138+
139+
// Instead of:
140+
if (!(await isTeamAdmin(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" });
141+
142+
// Use:
143+
if (!permissionMatches("team.update", userPermissions)) throw new TRPCError({ code: "UNAUTHORIZED" });
144+
```
145+
146+
## Permission String Alternatives for Helper Functions
147+
148+
The following table provides permission string alternatives for common helper functions:
149+
150+
| Helper Function | Permission String Alternative | Description |
151+
|-----------------|-------------------------------|-------------|
152+
| isTeamAdmin | `team.*` | Grants all team permissions |
153+
| isTeamAdmin | `team.update` | Update team settings |
154+
| isTeamAdmin | `team.invite` | Invite team members |
155+
| isTeamAdmin | `team.remove` | Remove team members |
156+
| isTeamOwner | `team.changeMemberRole` | Change role of team members |
157+
| isTeamOwner | `team.delete` | Delete team |
158+
| isTeamMember | `team.read` | Read-only access to team |
159+
160+
## Entity Permission Functions and Resources
161+
162+
The following functions are used to check permissions for entities with userId and teamId properties:
163+
164+
| Function | Description | Permission String Alternative |
165+
|----------|-------------|-------------------------------|
166+
| canEditEntity | Checks if user can edit an entity | `{resource}.update` |
167+
| canAccessEntity | Checks if user can access an entity | `{resource}.read` |
168+
| getEntityPermissionLevel | Gets permission level for an entity | N/A - Implementation detail |
169+
| canCreateEntity | Checks if user can create an entity | `{resource}.create` |
170+
171+
### Resources Used with Entity Permission Functions
172+
173+
| Resource | canEditEntity Usage | canAccessEntity Usage | Permission String |
174+
|----------|-------------------|---------------------|------------------|
175+
| routingForm | [packages/app-store/routing-forms/trpc/forms.handler.ts](packages/app-store/routing-forms/trpc/forms.handler.ts#L72) | [packages/app-store/routing-forms/trpc/getResponseWithFormFields.handler.ts](packages/app-store/routing-forms/trpc/getResponseWithFormFields.handler.ts#L78) | `routingForm.update`, `routingForm.read` |
176+
| routingForm | [packages/app-store/routing-forms/trpc/formMutation.handler.ts](packages/app-store/routing-forms/trpc/formMutation.handler.ts#L315) | | `routingForm.update` |
177+
| routingForm | [packages/app-store/routing-forms/api/responses/[formId].ts](packages/app-store/routing-forms/api/responses/[formId].ts#L96) | | `routingForm.update` |
178+
179+
These functions can be used with any entity that has userId and teamId properties. When migrating to PBAC, replace these checks with appropriate permission string checks:
180+
181+
```typescript
182+
// Instead of:
183+
if (!(await canEditEntity(form, user.id))) throw new TRPCError({ code: "UNAUTHORIZED" });
184+
185+
// Use:
186+
if (!permissionMatches("routingForm.update", userPermissions)) throw new TRPCError({ code: "UNAUTHORIZED" });
187+
```
188+
189+
This allows for more granular permission control and the creation of custom roles with specific permissions.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
2+
import { permissionsRouter } from "@calcom/trpc/server/routers/viewer/pbac/_router";
3+
4+
export default createNextApiHandler(permissionsRouter);

0 commit comments

Comments
 (0)