Skip to content

Commit a8f1ba2

Browse files
committed
refactor: standardize roles in packages/auth package
1 parent dc37a00 commit a8f1ba2

45 files changed

Lines changed: 497 additions & 533 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@aws-sdk/s3-request-presigner": "^3.859.0",
1414
"@browserbasehq/sdk": "^2.6.0",
1515
"@browserbasehq/stagehand": "^3.0.5",
16+
"@comp/auth": "workspace:*",
1617
"@comp/integration-platform": "workspace:*",
1718
"@mendable/firecrawl-js": "^4.9.3",
1819
"@nestjs/common": "^11.0.1",

apps/api/src/audit/audit-log.utils.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -193,20 +193,12 @@ export function buildDescription(
193193
switch (action) {
194194
case 'create':
195195
return `Created ${resource}`;
196+
case 'read':
197+
return `Viewed ${resource}`;
196198
case 'update':
197199
return `Updated ${resource}`;
198200
case 'delete':
199201
return `Deleted ${resource}`;
200-
case 'publish':
201-
return `Published ${resource}`;
202-
case 'approve':
203-
return `Approved ${resource}`;
204-
case 'assign':
205-
return `Assigned ${resource}`;
206-
case 'upload':
207-
return `Uploaded to ${resource}`;
208-
case 'export':
209-
return `Exported ${resource}`;
210202
default: {
211203
const capitalizedAction =
212204
action.charAt(0).toUpperCase() + action.slice(1);

apps/api/src/auth/api-key.service.ts

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
BadRequestException,
66
} from '@nestjs/common';
77
import { db } from '@trycompai/db';
8+
import { statement } from '@comp/auth';
89
import { createHash, randomBytes } from 'node:crypto';
910

1011
/** Result from validating an API key */
@@ -223,29 +224,8 @@ export class ApiKeyService {
223224
* Returns all valid `resource:action` scope pairs derived from the permission statement.
224225
*/
225226
getAvailableScopes(): string[] {
226-
// Import is dynamic-like but we use a hard-coded map matching the permission statement.
227-
// This is kept in sync with packages/auth/src/permissions.ts
228-
const resources: Record<string, readonly string[]> = {
229-
organization: ['read', 'update', 'delete'],
230-
member: ['create', 'read', 'update', 'delete'],
231-
invitation: ['create', 'read', 'cancel'],
232-
team: ['create', 'read', 'update', 'delete'],
233-
control: ['create', 'read', 'update', 'delete', 'assign', 'export'],
234-
evidence: ['create', 'read', 'update', 'delete', 'upload', 'export'],
235-
policy: ['create', 'read', 'update', 'delete', 'publish', 'approve'],
236-
risk: ['create', 'read', 'update', 'delete', 'assess', 'export'],
237-
vendor: ['create', 'read', 'update', 'delete', 'assess'],
238-
task: ['create', 'read', 'update', 'delete', 'assign', 'complete'],
239-
framework: ['create', 'read', 'update', 'delete'],
240-
audit: ['create', 'read', 'update', 'export'],
241-
finding: ['create', 'read', 'update', 'delete'],
242-
questionnaire: ['create', 'read', 'update', 'delete', 'respond'],
243-
integration: ['create', 'read', 'update', 'delete'],
244-
apiKey: ['create', 'read', 'delete'],
245-
};
246-
247227
const scopes: string[] = [];
248-
for (const [resource, actions] of Object.entries(resources)) {
228+
for (const [resource, actions] of Object.entries(statement)) {
249229
for (const action of actions) {
250230
scopes.push(`${resource}:${action}`);
251231
}

apps/api/src/auth/auth.server.ts

Lines changed: 1 addition & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -14,121 +14,7 @@ import {
1414
multiSession,
1515
organization,
1616
} from 'better-auth/plugins';
17-
import { createAccessControl } from 'better-auth/plugins/access';
18-
import {
19-
defaultStatements,
20-
adminAc,
21-
ownerAc,
22-
} from 'better-auth/plugins/organization/access';
23-
24-
// ============================================================================
25-
// Permissions (inlined from @comp/auth to avoid cross-package TS compilation)
26-
// ============================================================================
27-
28-
const statement = {
29-
...defaultStatements,
30-
organization: ['read', 'update', 'delete'],
31-
control: ['create', 'read', 'update', 'delete', 'assign', 'export'],
32-
evidence: ['create', 'read', 'update', 'delete', 'upload', 'export'],
33-
policy: ['create', 'read', 'update', 'delete', 'publish', 'approve'],
34-
risk: ['create', 'read', 'update', 'delete', 'assess', 'export'],
35-
vendor: ['create', 'read', 'update', 'delete', 'assess'],
36-
task: ['create', 'read', 'update', 'delete', 'assign', 'complete'],
37-
framework: ['create', 'read', 'update', 'delete'],
38-
audit: ['create', 'read', 'update', 'export'],
39-
finding: ['create', 'read', 'update', 'delete'],
40-
questionnaire: ['create', 'read', 'update', 'delete', 'respond'],
41-
integration: ['create', 'read', 'update', 'delete'],
42-
apiKey: ['create', 'read', 'delete'],
43-
app: ['read'],
44-
trust: ['read', 'update'],
45-
} as const;
46-
47-
const ac = createAccessControl(statement);
48-
49-
const owner = ac.newRole({
50-
...ownerAc.statements,
51-
organization: ['read', 'update', 'delete'],
52-
control: ['create', 'read', 'update', 'delete', 'assign', 'export'],
53-
evidence: ['create', 'read', 'update', 'delete', 'upload', 'export'],
54-
policy: ['create', 'read', 'update', 'delete', 'publish', 'approve'],
55-
risk: ['create', 'read', 'update', 'delete', 'assess', 'export'],
56-
vendor: ['create', 'read', 'update', 'delete', 'assess'],
57-
task: ['create', 'read', 'update', 'delete', 'assign', 'complete'],
58-
framework: ['create', 'read', 'update', 'delete'],
59-
audit: ['create', 'read', 'update', 'export'],
60-
finding: ['create', 'read', 'update', 'delete'],
61-
questionnaire: ['create', 'read', 'update', 'delete', 'respond'],
62-
integration: ['create', 'read', 'update', 'delete'],
63-
apiKey: ['create', 'read', 'delete'],
64-
app: ['read'],
65-
trust: ['read', 'update'],
66-
});
67-
68-
const admin = ac.newRole({
69-
...adminAc.statements,
70-
organization: ['read', 'update'],
71-
control: ['create', 'read', 'update', 'delete', 'assign', 'export'],
72-
evidence: ['create', 'read', 'update', 'delete', 'upload', 'export'],
73-
policy: ['create', 'read', 'update', 'delete', 'publish', 'approve'],
74-
risk: ['create', 'read', 'update', 'delete', 'assess', 'export'],
75-
vendor: ['create', 'read', 'update', 'delete', 'assess'],
76-
task: ['create', 'read', 'update', 'delete', 'assign', 'complete'],
77-
framework: ['create', 'read', 'update', 'delete'],
78-
audit: ['create', 'read', 'update', 'export'],
79-
finding: ['create', 'read', 'update', 'delete'],
80-
questionnaire: ['create', 'read', 'update', 'delete', 'respond'],
81-
integration: ['create', 'read', 'update', 'delete'],
82-
apiKey: ['create', 'read', 'delete'],
83-
app: ['read'],
84-
trust: ['read', 'update'],
85-
});
86-
87-
const auditor = ac.newRole({
88-
organization: ['read'],
89-
member: ['create'],
90-
invitation: ['create'],
91-
control: ['read', 'export'],
92-
evidence: ['read', 'export'],
93-
policy: ['read'],
94-
risk: ['read', 'export'],
95-
vendor: ['read'],
96-
task: ['read'],
97-
framework: ['read'],
98-
audit: ['read', 'export'],
99-
finding: ['create', 'read', 'update'],
100-
questionnaire: ['read'],
101-
integration: ['read'],
102-
app: ['read'],
103-
trust: ['read'],
104-
});
105-
106-
const employee = ac.newRole({
107-
task: ['read', 'complete'],
108-
evidence: ['read', 'upload'],
109-
policy: ['read'],
110-
questionnaire: ['read', 'respond'],
111-
trust: ['read', 'update'],
112-
});
113-
114-
const contractor = ac.newRole({
115-
task: ['read', 'complete'],
116-
evidence: ['read', 'upload'],
117-
policy: ['read'],
118-
trust: ['read', 'update'],
119-
});
120-
121-
const allRoles = {
122-
owner,
123-
admin,
124-
auditor,
125-
employee,
126-
contractor,
127-
} as const;
128-
129-
// ============================================================================
130-
// Auth Server Configuration
131-
// ============================================================================
17+
import { ac, allRoles } from '@comp/auth';
13218

13319
const MAGIC_LINK_EXPIRES_IN_SECONDS = 60 * 60; // 1 hour
13420

apps/api/src/auth/permission.guard.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Logger,
77
} from '@nestjs/common';
88
import { Reflector } from '@nestjs/core';
9+
import { RESTRICTED_ROLES, PRIVILEGED_ROLES } from '@comp/auth';
910
import { auth } from './auth.server';
1011
import { resolveServiceByName } from './service-token.config';
1112
import { AuthenticatedRequest } from './types';
@@ -23,16 +24,6 @@ export interface RequiredPermission {
2324
*/
2425
export const PERMISSIONS_KEY = 'required_permissions';
2526

26-
/**
27-
* Roles that require assignment-based filtering for resources
28-
*/
29-
const RESTRICTED_ROLES = ['employee', 'contractor'];
30-
31-
/**
32-
* Roles that have full access without assignment filtering
33-
*/
34-
const PRIVILEGED_ROLES = ['owner', 'admin', 'auditor'];
35-
3627
/**
3728
* PermissionGuard - Validates user permissions using better-auth's SDK
3829
*
@@ -196,14 +187,16 @@ export class PermissionGuard implements CanActivate {
196187
}
197188

198189
// If user has any privileged role, they're not restricted
190+
const privileged: readonly string[] = PRIVILEGED_ROLES;
191+
const restricted: readonly string[] = RESTRICTED_ROLES;
199192
const hasPrivilegedRole = roles.some((role) =>
200-
PRIVILEGED_ROLES.includes(role),
193+
privileged.includes(role),
201194
);
202195
if (hasPrivilegedRole) {
203196
return false;
204197
}
205198

206199
// Check if all roles are restricted
207-
return roles.every((role) => RESTRICTED_ROLES.includes(role));
200+
return roles.every((role) => restricted.includes(role));
208201
}
209202
}

apps/api/src/auth/require-permission.decorator.ts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { PERMISSIONS_KEY, RequiredPermission } from './permission.guard';
1919
* @example
2020
* // Use with guards
2121
* @UseGuards(HybridAuthGuard, PermissionGuard)
22-
* @RequirePermission('policy', 'publish')
22+
* @RequirePermission('policy', 'update')
2323
* @Post(':id/publish')
2424
* async publishPolicy(@Param('id') id: string) { ... }
2525
*/
@@ -41,7 +41,7 @@ export const RequirePermission = (
4141
* // Require permissions on multiple resources
4242
* @RequirePermissions([
4343
* { resource: 'control', actions: ['read'] },
44-
* { resource: 'evidence', actions: ['upload'] },
44+
* { resource: 'evidence', actions: ['create'] },
4545
* ])
4646
*/
4747
export const RequirePermissions = (permissions: RequiredPermission[]) =>
@@ -72,18 +72,6 @@ export type GRCResource =
7272
| 'trust';
7373

7474
/**
75-
* Action types available for GRC resources
75+
* Action types available for GRC resources — CRUD only
7676
*/
77-
export type GRCAction =
78-
| 'create'
79-
| 'read'
80-
| 'update'
81-
| 'delete'
82-
| 'assign'
83-
| 'export'
84-
| 'upload'
85-
| 'publish'
86-
| 'approve'
87-
| 'assess'
88-
| 'complete'
89-
| 'respond';
77+
export type GRCAction = 'create' | 'read' | 'update' | 'delete';

apps/api/src/auth/service-token.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const SERVICE_DEFINITIONS: Record<string, ServiceDefinition> = {
3636
'trust:read',
3737
'organization:read',
3838
'questionnaire:read',
39-
'questionnaire:respond',
39+
'questionnaire:update',
4040
],
4141
},
4242
};

apps/api/src/knowledge-base/knowledge-base.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export class KnowledgeBaseController {
5555
}
5656

5757
@Post('manual-answers')
58-
@RequirePermission('questionnaire', 'respond')
58+
@RequirePermission('questionnaire', 'update')
5959
@HttpCode(HttpStatus.OK)
6060
@ApiOperation({ summary: 'Save or update a manual answer' })
6161
@ApiConsumes('application/json')

apps/api/src/organization/organization.service.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ForbiddenException,
77
InternalServerErrorException,
88
} from '@nestjs/common';
9+
import { allRoles } from '@comp/auth';
910
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
1011
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
1112
import { db, Role } from '@trycompai/db';
@@ -316,13 +317,7 @@ export class OrganizationService {
316317
}
317318

318319
async getRoleNotificationSettings(organizationId: string) {
319-
const BUILT_IN_ROLES = [
320-
'owner',
321-
'admin',
322-
'auditor',
323-
'employee',
324-
'contractor',
325-
] as const;
320+
const BUILT_IN_ROLES = Object.keys(allRoles);
326321

327322
const BUILT_IN_DEFAULTS: Record<
328323
string,

apps/api/src/policies/policies.controller.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export class PoliciesController {
159159

160160
@Post('publish-all')
161161
@UseGuards(PermissionGuard)
162-
@RequirePermission('policy', 'publish')
162+
@RequirePermission('policy', 'update')
163163
@ApiOperation({ summary: 'Publish all draft/needs_review policies' })
164164
async publishAll(
165165
@OrganizationId() organizationId: string,
@@ -472,7 +472,7 @@ export class PoliciesController {
472472

473473
@Post(':id/versions/publish')
474474
@UseGuards(PermissionGuard)
475-
@RequirePermission('policy', 'publish')
475+
@RequirePermission('policy', 'update')
476476
@ApiOperation(VERSION_OPERATIONS.publishPolicyVersion)
477477
@ApiParam(VERSION_PARAMS.policyId)
478478
@ApiBody(VERSION_BODIES.publishVersion)
@@ -507,7 +507,7 @@ export class PoliciesController {
507507

508508
@Post(':id/versions/:versionId/activate')
509509
@UseGuards(PermissionGuard)
510-
@RequirePermission('policy', 'publish')
510+
@RequirePermission('policy', 'update')
511511
@ApiOperation(VERSION_OPERATIONS.setActivePolicyVersion)
512512
@ApiParam(VERSION_PARAMS.policyId)
513513
@ApiParam(VERSION_PARAMS.versionId)
@@ -541,7 +541,7 @@ export class PoliciesController {
541541

542542
@Post(':id/versions/:versionId/submit-for-approval')
543543
@UseGuards(PermissionGuard)
544-
@RequirePermission('policy', 'approve')
544+
@RequirePermission('policy', 'update')
545545
@ApiOperation(VERSION_OPERATIONS.submitVersionForApproval)
546546
@ApiParam(VERSION_PARAMS.policyId)
547547
@ApiParam(VERSION_PARAMS.versionId)
@@ -667,7 +667,7 @@ Keep responses helpful and focused on the policy editing task.`;
667667

668668
@Post(':id/deny-changes')
669669
@UseGuards(PermissionGuard)
670-
@RequirePermission('policy', 'approve')
670+
@RequirePermission('policy', 'update')
671671
@ApiOperation({ summary: 'Deny requested policy changes' })
672672
@ApiParam(POLICY_PARAMS.policyId)
673673
async denyPolicyChanges(
@@ -687,7 +687,7 @@ Keep responses helpful and focused on the policy editing task.`;
687687

688688
@Post(':id/accept-changes')
689689
@UseGuards(PermissionGuard)
690-
@RequirePermission('policy', 'approve')
690+
@RequirePermission('policy', 'update')
691691
@ApiOperation({ summary: 'Accept requested policy changes and publish' })
692692
@ApiParam(POLICY_PARAMS.policyId)
693693
async acceptPolicyChanges(

0 commit comments

Comments
 (0)