Files
calendar/packages/features/pbac/README.md
T
sean-brydonGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>Eunjae Leecubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
9dca13305f feat: pbac core ui (#21471)
* migration plus feature flag

* show navigation route + inital roles migration

* add a check permission use case to take in the feature flags for a team

* bulk update script

* inital frontend work for displaying roles

* move to a more "anemic domain models" approach

* update test to match new DDD strutcture

* fix tests

* update transaction call back types to include trx

* align fe types after transaction to DDD

* move away from usecases to a more domain tailored approach

* get permissions per resource and map them to domain permission string

* update permision logic

* correctly get the logic for *.* permissions on owner

* wip sheet logic for ssr

* role list

* use nuqs for sheet parsing on handle change

* fox

* improve hook usage

* enable PBAC router

* delete modal etc

* i18n and inital rough layout of roles + permisions creating

* add color to migrations

* add colors and new method to tests

* move hooks out of infra into client with provider

* move hooks out of infra into client with provider

* memo features and ensure render once

* remove comment

* seed color

* use role colours

* match i18n

* add custom color picker to edit/create form

* fix advanced mode toggle

* more work on adv permission group

* update migrations

* abstract lots of core form logic to a custom hook

* improve UX for selecting all and toggling all

* improve code quality and use domain mappers in role repositoryu

* call server action to revalidate cache

* call invalidate cache on delete

* fix re-render + improves update logic wip

* fix txn for assinging role to member

* wip on assigning users custom roles

* fix repo

* update logic for checking if users can update roles

* remove member from permission check

* check users permission and assign roles

* move to factory approach

* move default rolesIDs to constant

* add facuted values to table

* display custom role in table

* fix type error

* fix role filter

* check pbac feature flag to see what column to filter on

* push repo mocks and other mocks to fix unit tests

* fix and add test for empty permissions when creating a role

* pass updates to repo so we actually update roles

* fix types

* fix types

* restore lock changes

* fix role service test for new updates section

* fix updated at types

* update mocks to use feature repository mock

* remove roletype from db in model

* prevent multiple queries

* fix typeof in role model

* fix and migrate i18n to one registery

* fix and update i18n to be in registery

* fix type error + fall back in service instead of repo for BL

* more type errors

* update members faceted values to bennys refactor

* fix types

* remove the _resource from type conditionally

* fix managment factory types to expose PBAC enbaled obol

* narrow down types

* wip fix for types

* more fix types

* cast role

* fix tests

* attempt of fixing _resoucre key access type

* attempt of fixing _resoucre key access type

* seperate migraations to batches

* add invalidate time to team features

* restore router to main

* push main lock

* Update packages/features/pbac/domain/types/permission-registry.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Update packages/features/pbac/domain/mappers/PermissionMapper.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Update packages/prisma/migrations/20250527091330_add_color_to_pbac_role/migration.sql

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Update packages/prisma/migrations/20250617070118_update_memberships_one_time/migration.sql

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Update apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/AdvancedPermissionGroup.tsx

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* fix mapped type

* restore migration

* skip _resource

* use partical

* fix type errors in tests and hooks

* Simplified the role field in the editSchema to use z.nativeEnum(MembershipRole)

* fix type errors for editsheet

* fix type errors for editsheet

* Apply suggestion from comment 2151515295

* remove footer since we dont have docs yet

* add i18n

* lock all toggle chevron

* use prisma

* tidy up old manage permission

* fix i18n

* remove can manage from role permission check

* auto select read

* address benny feedback

* fix type

* fix type

* update function name due to merge

* fix types

* update tests to match new membership method from merge

* address cubic feedback

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-07-07 10:20:24 +01:00

424 lines
10 KiB
Markdown

# Permission-Based Access Control (PBAC) System
## Overview
The PBAC system provides fine-grained access control for Cal.com using a combination of CRUD-based permissions and custom actions, while maintaining backward compatibility with the existing role system.
## Implementation
### Permission Store
The system uses a centralized Zustand store with optimized data structures for fast permission lookups:
```typescript
interface TeamPermissions {
roleId: string;
permissions: Set<PermissionString>; // O(1) lookup
}
interface PermissionStore {
teamPermissions: Map<number, TeamPermissions>; // O(1) lookup
setTeamPermissions: (permissions: Record<number, { roleId: string; permissions: PermissionString[] }>) => void;
hasPermission: (teamId: number, permission: PermissionString) => boolean;
hasPermissions: (teamId: number, permissions: PermissionString[]) => boolean;
}
```
### Context Provider
The system uses a React context provider that fetches all permissions once at the root level:
```tsx
<PermissionProvider>
<App />
</PermissionProvider>
```
The provider automatically:
- Fetches permissions for all teams the user has access to
- Caches the results (5-minute stale time)
- Updates the central store
- Provides loading states
### Permission Hooks
Two main hooks are provided for checking permissions:
```typescript
// Check single permission
const { hasPermission, isLoading } = usePermission(teamId, "team.update");
// Check multiple permissions
const { hasPermissions, isLoading } = usePermissions(teamId, ["team.update", "team.invite"]);
```
## Permission Format
Permissions follow two formats:
1. CRUD Permissions: `${resource}.${action}`
2. Custom Actions: `custom:${resource}.${action}`
```typescript
// CRUD Permission Examples
"eventType.create"
"booking.read"
// Custom Action Examples
"custom:team.invite"
"custom:booking.readRecordings"
```
## Role Types
1. **Default Roles** (MembershipRole)
- OWNER: Full access (`*.*`)
- ADMIN: Extensive management permissions
- MEMBER: Basic read permissions
2. **Custom Roles**
- Team-specific roles with granular permissions
- Can be assigned alongside default roles
## Permission Structure
### CRUD Actions
```typescript
export enum CrudAction {
Create = 'create',
Read = 'read',
Update = 'update',
Delete = 'delete',
}
```
### Custom Actions
```typescript
export enum CustomAction {
Invite = 'invite', // Invite members to team/org
Remove = 'remove', // Remove members from team/org
Override = 'override', // Override availability
ReadRecordings = 'readRecordings', // Access booking recordings
ManageBilling = 'manageBilling', // Manage org billing
}
```
### Resources
```typescript
export enum Resource {
EventType = 'eventType',
Booking = 'booking',
Team = 'team',
Organization = 'organization',
Insights = 'insights',
Availability = 'availability',
Workflow = 'workflow',
RoutingForm = 'routingForm',
}
```
## Usage Guide
### 1. Setup Provider
```tsx
// app/layout.tsx or similar
import { PermissionProvider } from "@calcom/features/pbac/context/PermissionProvider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<PermissionProvider>
{children}
</PermissionProvider>
);
}
```
### 2. Use in Components
```tsx
import { usePermission, usePermissions } from "@calcom/features/pbac/hooks/usePermission";
function TeamSettings({ teamId }: { teamId: number }) {
// Single permission check
const { hasPermission, isLoading } = usePermission(teamId, "team.update");
// Multiple permissions check
const { hasPermissions } = usePermissions(teamId, [
"team.update",
"team.invite"
]);
if (isLoading) return <div>Loading...</div>;
return (
<div>
{hasPermission && <button>Update Team</button>}
{hasPermissions && <button>Update and Invite</button>}
</div>
);
}
```
### 3. Performance Considerations
- Permission checks are O(1) using Map and Set data structures
- Permissions are fetched once and cached for 5 minutes
- No redundant API calls for permission checks
- Automatic updates when permissions change via store
## Common Permission Combinations
### Event Manager Role
```typescript
const permissions = [
// CRUD Permissions
"eventType.create",
"eventType.read",
"eventType.update",
"eventType.delete",
// Custom Actions
"custom:booking.readRecordings",
"custom:availability.override"
];
```
### Analytics Role
```typescript
const permissions = [
// CRUD Permissions
"insights.read",
"booking.read",
"eventType.read",
// Custom Actions
"custom:organization.manageBilling"
];
```
### Team Admin Role
```typescript
const permissions = [
// CRUD Permissions
"team.create",
"team.read",
"team.update",
// Custom Actions
"custom:team.invite",
"custom:team.remove",
"custom:availability.override"
];
```
## Database Schema
```prisma
model Role {
id String @id @default(cuid())
name String
teamId Int?
permissions RolePermission[]
}
model RolePermission {
id String @id @default(cuid())
roleId String
resource String
action String
isCustom Boolean @default(false) // Indicates if this is a custom action
role Role @relation(fields: [roleId], references: [id])
}
```
## Example Use Cases
### 1. Event Type Management
```typescript
// Full event type management
[
"eventType.create",
"eventType.read",
"eventType.update",
"eventType.delete"
]
// Read-only access
["eventType.read"]
```
### 2. Team Management with Custom Actions
```typescript
[
"team.read",
"team.update",
"custom:team.invite",
"custom:team.remove"
]
```
### 3. Booking Management with Recordings
```typescript
[
"booking.read",
"booking.update",
"custom:booking.readRecordings"
]
```
## Usage
### Server-Side (React Server Components)
```typescript
import { cookies, headers } from "next/headers";
import { checkUserPermissionInTeam } from "@calcom/features/pbac/lib/server/checkPermissions";
// In a Server Component
export default async function TeamSettings({ params }: { params: { teamId: string } }) {
const session = buildLegacyRequest(await headers(), await cookies())
if(!session?.user?.id){
return null
}
const hasPermission = await checkUserPermissionInTeam({
userId: session.user.id,
teamId: parseInt(params.teamId),
permission: "team.update",
});
if (!hasPermission) {
return <div>Not authorized</div>;
}
return <div>Team Settings</div>;
}
// Check multiple permissions
const hasPermissions = await checkMultiplePermissionsInTeam({
userId: session.user.id,
teamId: teamId,
permissions: ["team.update", "team.invite"],
});
```
### Client-Side (React Components)
```typescript
import { usePermission, usePermissions } from "@calcom/features/pbac/hooks/usePermission";
// In a React Component
function TeamSettingsButton({ teamId }: { teamId: number }) {
// Single permission check
const { hasPermission, isLoading } = usePermission(teamId, "team.update");
if (isLoading) return <div>Loading...</div>;
if (!hasPermission) return null;
return <button>Update Team Settings</button>;
}
// Multiple permissions check
function TeamAdminPanel({ teamId }: { teamId: number }) {
const { hasPermissions, isLoading } = usePermissions(teamId, [
"team.update",
"team.invite"
]);
if (isLoading) return <div>Loading...</div>;
if (!hasPermissions) return <div>Insufficient permissions</div>;
return <div>Admin Panel</div>;
}
```
### Available Permissions
Permissions follow the format `resource.action` where:
- `resource` is the entity being accessed (e.g., team, eventType, booking)
- `action` is the operation being performed (e.g., create, read, update, delete)
Common permissions include:
- `team.create` - Create teams
- `team.update` - Update team settings
- `team.invite` - Invite team members
- `team.remove` - Remove team members
- `eventType.create` - Create event types
- `eventType.update` - Update event types
- `booking.read` - Read booking details
For a complete list of permissions, see [PERMISSIONS.md](./PERMISSIONS.md).
### Caching
The client-side hooks automatically cache permission results for 5 minutes to reduce API calls. The cache can be invalidated by calling the TRPC mutation to update permissions.
## Default Roles and Permissions
The system comes with three pre-configured default roles:
### 1. Owner Role (owner_role)
- Has full access to all resources via wildcard permission (`*.*`)
- Automatically assigned to team/organization creators
- Cannot be modified or deleted
- Permissions:
```typescript
"*.*" // Grants access to all actions on all resources
```
### 2. Admin Role (admin_role)
- Has extensive management permissions
- Can manage team settings and members
- Permissions:
```typescript
// Booking permissions
"booking.*" // All booking operations
"booking.readTeamBookings"
"booking.readOrgBookings"
// Event Type permissions
"eventType.*" // All event type operations
// Team management
"team.invite"
"team.remove"
"team.changeMemberRole"
// Organization permissions
"organization.listMembers"
"organization.read"
"organization.update"
// Other resource permissions
"apiKey.*" // All API key operations
"routingForm.*" // All routing form operations
"workflow.*" // All workflow operations
"insights.read" // Read access to insights
```
### 3. Member Role (member_role)
- Basic read access to resources
- Default role for new team members
- Permissions:
```typescript
"booking.read"
"eventType.read"
"team.read"
"organization.read"
"routingForm.read"
```
### Using Default Roles
You can reference these roles programmatically using the constants provided in `@calcom/features/pbac/lib/constants`:
```typescript
import { DEFAULT_ROLES } from "@calcom/features/pbac/lib/constants";
// Reference roles
const ownerRoleId = DEFAULT_ROLES.OWNER; // 'owner_role'
const adminRoleId = DEFAULT_ROLES.ADMIN; // 'admin_role'
const memberRoleId = DEFAULT_ROLES.MEMBER; // 'member_role'
```
### Role Assignment
1. Default roles are automatically assigned during:
- Team creation (creator gets OWNER role)
- Member invitation (gets MEMBER role by default)
- Role changes through team management UI
2. Roles can be changed by team owners and admins through:
- Team member management interface
- Organization member management interface
- API endpoints (with proper permissions)