Skip to content

Commit d410863

Browse files
committed
expose membership permissions via GraphQL API
1 parent 10cd6cc commit d410863

7 files changed

Lines changed: 413 additions & 6 deletions

File tree

packages/services/api/src/modules/auth/lib/authz.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,14 @@ const permissionsByLevel = {
388388
],
389389
} as const;
390390

391+
export const allPermissions = [
392+
...permissionsByLevel.organization.map(v => v.value),
393+
...permissionsByLevel.project.map(v => v.value),
394+
...permissionsByLevel.target.map(v => v.value),
395+
...permissionsByLevel.service.map(v => v.value),
396+
...permissionsByLevel.appDeployment.map(v => v.value),
397+
] as const;
398+
391399
export const PermissionsPerResourceLevelAssignmentModel = z.object({
392400
organization: z.set(z.union(permissionsByLevel.organization)),
393401
project: z.set(z.union(permissionsByLevel.project)),
@@ -400,7 +408,7 @@ export type PermissionsPerResourceLevelAssignment = z.TypeOf<
400408
typeof PermissionsPerResourceLevelAssignmentModel
401409
>;
402410

403-
type ResourceLevels = keyof PermissionsPerResourceLevelAssignment;
411+
export type ResourceLevel = keyof PermissionsPerResourceLevelAssignment;
404412

405413
export const PermissionsModel = z.union([
406414
...permissionsByLevel.organization,
@@ -410,11 +418,11 @@ export const PermissionsModel = z.union([
410418
...permissionsByLevel.appDeployment,
411419
]);
412420

413-
type Permissions = z.TypeOf<typeof PermissionsModel>;
421+
export type Permission = z.TypeOf<typeof PermissionsModel>;
414422

415423
const permissionResourceLevelLookupMap = new Map<
416424
z.TypeOf<typeof PermissionsModel>,
417-
ResourceLevels
425+
ResourceLevel
418426
>();
419427

420428
for (const [key, permissions] of objectEntries(permissionsByLevel)) {
@@ -424,7 +432,7 @@ for (const [key, permissions] of objectEntries(permissionsByLevel)) {
424432
}
425433

426434
/** Get the permission group for a specific permissions */
427-
function getPermissionGroup(permission: Permissions): ResourceLevels {
435+
export function getPermissionGroup(permission: Permission): ResourceLevel {
428436
const group = permissionResourceLevelLookupMap.get(permission);
429437

430438
if (group === undefined) {
@@ -438,7 +446,7 @@ function getPermissionGroup(permission: Permissions): ResourceLevels {
438446
* Transforms a flat permission array into an object that groups the permissions per resource level.
439447
*/
440448
export function permissionsToPermissionsPerResourceLevelAssignment(
441-
permissions: Array<Permissions>,
449+
permissions: Array<Permission>,
442450
): PermissionsPerResourceLevelAssignment {
443451
const assignment: PermissionsPerResourceLevelAssignment = {
444452
organization: new Set(),
@@ -450,7 +458,7 @@ export function permissionsToPermissionsPerResourceLevelAssignment(
450458

451459
for (const permission of permissions) {
452460
const group = getPermissionGroup(permission);
453-
(assignment[group] as Set<Permissions>).add(permission);
461+
(assignment[group] as Set<Permission>).add(permission);
454462
}
455463

456464
return assignment;
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
import { allPermissions, Permission } from './authz';
2+
3+
export type PermissionRecord = {
4+
id: Permission;
5+
title: string;
6+
description: string;
7+
dependsOn?: Permission;
8+
readOnly?: true;
9+
};
10+
11+
export type PermissionGroup = {
12+
id: string;
13+
title: string;
14+
permissions: Array<PermissionRecord>;
15+
};
16+
17+
export const allPermissionGroups: Array<PermissionGroup> = [
18+
{
19+
id: 'organization',
20+
title: 'Organization',
21+
permissions: [
22+
{
23+
id: 'organization:describe',
24+
title: 'View organization',
25+
description: 'Member can see the organization. Permission can not be modified.',
26+
readOnly: true,
27+
},
28+
{
29+
id: 'support:manageTickets',
30+
title: 'Access support tickets',
31+
description: 'Member can access, create and reply to support tickets.',
32+
},
33+
{
34+
id: 'organization:modifySlug',
35+
title: 'Update organization slug',
36+
description: 'Member can modify the organization slug.',
37+
},
38+
{
39+
id: 'auditLog:export',
40+
title: 'Export audit log',
41+
description: 'Member can access and export the audit log.',
42+
},
43+
{
44+
id: 'organization:delete',
45+
title: 'Delete organization',
46+
description: 'Member can delete the Organization.',
47+
},
48+
],
49+
},
50+
{
51+
id: 'members',
52+
title: 'Members',
53+
permissions: [
54+
{
55+
id: 'member:describe',
56+
title: 'View members',
57+
description: 'Member can access the organization member overview.',
58+
},
59+
{
60+
id: 'member:assignRole',
61+
title: 'Assign member role',
62+
description: 'Member can assign roles to users.',
63+
dependsOn: 'member:describe',
64+
},
65+
{
66+
id: 'member:modifyRole',
67+
title: 'Modify member role',
68+
description: 'Member can modify, create and delete roles.',
69+
dependsOn: 'member:describe',
70+
},
71+
{
72+
id: 'member:removeMember',
73+
title: 'Remove member',
74+
description: 'Member can remove users from the organization.',
75+
dependsOn: 'member:describe',
76+
},
77+
{
78+
id: 'member:manageInvites',
79+
title: 'Manage invites',
80+
description: 'Member can invite users via email and modify or delete pending invites.',
81+
dependsOn: 'member:describe',
82+
},
83+
],
84+
},
85+
{
86+
id: 'billing',
87+
title: 'Billing',
88+
permissions: [
89+
{
90+
id: 'billing:describe',
91+
title: 'View billing',
92+
description: 'Member can view the billing information.',
93+
},
94+
{
95+
id: 'billing:update',
96+
title: 'Update billing',
97+
description: 'Member can change the organization plan.',
98+
dependsOn: 'billing:describe',
99+
},
100+
],
101+
},
102+
{
103+
id: 'oidc',
104+
title: 'OpenID Connect',
105+
permissions: [
106+
{
107+
id: 'oidc:modify',
108+
title: 'Manage OpenID Connect integration',
109+
description: 'Member can connect, modify, and remove an OIDC provider to the connection.',
110+
},
111+
],
112+
},
113+
{
114+
id: 'github',
115+
title: 'GitHub Integration',
116+
permissions: [
117+
{
118+
id: 'gitHubIntegration:modify',
119+
title: 'Manage GitHub integration',
120+
description:
121+
'Member can connect, modify, and remove access for the GitHub integration and repository access.',
122+
},
123+
],
124+
},
125+
{
126+
id: 'slack',
127+
title: 'Slack Integration',
128+
permissions: [
129+
{
130+
id: 'slackIntegration:modify',
131+
title: 'Manage Slack integration',
132+
description:
133+
'Member can connect, modify, and remove access for the Slack integration and repository access.',
134+
},
135+
],
136+
},
137+
{
138+
id: 'project',
139+
title: 'Project',
140+
permissions: [
141+
{
142+
id: 'project:create',
143+
title: 'Create project',
144+
description: 'Member can create new projects.',
145+
},
146+
{
147+
id: 'project:describe',
148+
title: 'View project',
149+
description: 'Member can access the specified projects.',
150+
},
151+
{
152+
id: 'project:delete',
153+
title: 'Delete project',
154+
description: 'Member can access the specified projects.',
155+
dependsOn: 'project:describe',
156+
},
157+
{
158+
id: 'project:modifySettings',
159+
title: 'Modify Settings',
160+
description: 'Member can access the specified projects.',
161+
dependsOn: 'project:describe',
162+
},
163+
],
164+
},
165+
{
166+
id: 'schema-linting',
167+
title: 'Schema Linting',
168+
permissions: [
169+
{
170+
id: 'schemaLinting:modifyOrganizationRules',
171+
title: 'Manage organization level schema linting',
172+
description: 'Member can view and modify the organization schema linting rules.',
173+
},
174+
{
175+
id: 'schemaLinting:modifyProjectRules',
176+
title: 'Manage project level schema linting',
177+
description: 'Member can view and modify the projects schema linting rules.',
178+
dependsOn: 'project:describe',
179+
},
180+
],
181+
},
182+
{
183+
id: 'target',
184+
title: 'Target',
185+
permissions: [
186+
{
187+
id: 'target:create',
188+
title: 'Create target',
189+
description: 'Member can create new projects.',
190+
dependsOn: 'project:describe',
191+
},
192+
{
193+
id: 'target:delete',
194+
title: 'Delete target',
195+
description: 'Member can access the specified projects.',
196+
dependsOn: 'project:describe',
197+
},
198+
{
199+
id: 'target:modifySettings',
200+
title: 'Modify settings',
201+
description: 'Member can access the specified projects.',
202+
dependsOn: 'project:describe',
203+
},
204+
{
205+
id: 'alert:modify',
206+
title: 'Modify alerts',
207+
description: 'Can create alerts for schema versions.',
208+
dependsOn: 'project:describe',
209+
},
210+
{
211+
id: 'schemaVersion:approve',
212+
title: 'Approve schema version (legacy)',
213+
description: 'Can approve schema versions on projects using the legacy registry model.',
214+
},
215+
{
216+
id: 'targetAccessToken:modify',
217+
title: 'Manage registry access tokens',
218+
description: 'Allow managing access tokens for CLI and Usage Reporting.',
219+
dependsOn: 'project:describe',
220+
},
221+
{
222+
id: 'cdnAccessToken:modify',
223+
title: 'Manage CDN access tokens',
224+
description: 'Allow managing access tokens for the CDN.',
225+
dependsOn: 'project:describe',
226+
},
227+
],
228+
},
229+
{
230+
id: 'laboratory',
231+
title: 'Laboratory',
232+
permissions: [
233+
{
234+
id: 'laboratory:describe',
235+
title: 'View laboratory',
236+
description: 'Member can access the laboratory, view and execute GraphQL documents.',
237+
dependsOn: 'project:describe',
238+
},
239+
{
240+
id: 'laboratory:modify',
241+
title: 'Modify laboratory',
242+
description:
243+
'Member can create, delete and update collections and documents in the laboratory.',
244+
dependsOn: 'laboratory:describe',
245+
},
246+
{
247+
id: 'laboratory:modifyPreflightScript',
248+
title: 'Modify the laboratory preflight script',
249+
description: 'Member can update the laboratory preflight script.',
250+
dependsOn: 'laboratory:describe',
251+
},
252+
],
253+
},
254+
{
255+
id: 'app-deployments',
256+
title: 'App Deployments',
257+
permissions: [
258+
{
259+
id: 'appDeployment:describe',
260+
title: 'View app deployments',
261+
description: 'Member can view app deployments.',
262+
dependsOn: 'project:describe',
263+
},
264+
],
265+
},
266+
{
267+
id: 'schema-checks',
268+
title: 'Schema Checks',
269+
permissions: [
270+
{
271+
id: 'schemaCheck:approve',
272+
title: 'Approve schema check',
273+
description: 'Member can approve failed schema checks.',
274+
dependsOn: 'project:describe',
275+
},
276+
],
277+
},
278+
] as const;
279+
280+
function assertAllRulesAreAssigned(excluded: Array<Permission>) {
281+
const p = new Set(allPermissions);
282+
for (const item of excluded) {
283+
p.delete(item);
284+
}
285+
286+
for (const group of allPermissionGroups) {
287+
for (const per of group.permissions) {
288+
p.delete(per.id);
289+
}
290+
}
291+
292+
if (p.size) {
293+
throw new Error('The following permissions are not assigned: \n' + Array.from(p).join(`\n`));
294+
}
295+
}
296+
297+
/**
298+
* This seems like the easiest way to make sure that all the permissions we have are
299+
* assignable and exposed via our API.
300+
*/
301+
assertAllRulesAreAssigned([
302+
/** These are CLI only actions for now. */
303+
'schema:loadFromRegistry',
304+
'schema:compose',
305+
'schemaCheck:create',
306+
'schemaVersion:publish',
307+
'schemaVersion:deleteService',
308+
'appDeployment:create',
309+
'appDeployment:publish',
310+
'appDeployment:retire',
311+
]);

packages/services/api/src/modules/auth/module.graphql.mappers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Member, User } from '../../shared/entities';
2+
import { PermissionGroup, PermissionRecord } from './lib/organization-member-permissions';
23
import type { OrganizationAccessScope } from './providers/organization-access';
34
import type { ProjectAccessScope } from './providers/project-access';
45
import type { TargetAccessScope } from './providers/target-access';
@@ -10,3 +11,5 @@ export type UserConnectionMapper = readonly User[];
1011
export type MemberConnectionMapper = readonly Member[];
1112
export type MemberMapper = Member;
1213
export type UserMapper = User;
14+
export type PermissionGroupMapper = PermissionGroup;
15+
export type PermissionMapper = PermissionRecord;

0 commit comments

Comments
 (0)