* 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>
424 lines
10 KiB
Markdown
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)
|