@platecms/delta-types 1.16.1 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platecms/delta-types",
3
- "version": "1.16.1",
3
+ "version": "1.18.0",
4
4
  "description": "This package contains various utility types.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -17,8 +17,8 @@
17
17
  "src/**/*"
18
18
  ],
19
19
  "peerDependencies": {
20
- "@platecms/delta-cast": "1.16.1",
21
- "@platecms/delta-plate-resource-notation": "1.16.1",
20
+ "@platecms/delta-cast": "1.18.0",
21
+ "@platecms/delta-plate-resource-notation": "1.18.0",
22
22
  "class-transformer": "0.5.1",
23
23
  "reflect-metadata": "0.2.2",
24
24
  "tslib": "2.8.1"
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ export * from "./lib/content-value-type.type";
4
4
  export * from "./lib/primitive.type";
5
5
  export * from "./lib/stage.enum";
6
6
  export * from "./lib/header-keys.enum";
7
- export * from "./lib/action-type.enum";
7
+ export * from "./lib/permissions.type";
8
8
  export * from "./lib/processed-asset-options.interface";
9
9
  export * from "./lib/language-status.enum";
10
10
  export type * from "./lib/language-option.interface";
@@ -0,0 +1,524 @@
1
+ import {
2
+ PRESET_PERMISSION_SETS,
3
+ PermissionAction,
4
+ PermissionResource,
5
+ Permissions,
6
+ compactPermissions,
7
+ getMissingPermissions,
8
+ hasPermissions,
9
+ isValidPermissions,
10
+ mergePermissions,
11
+ } from "./permissions.type";
12
+
13
+ describe("isValidPermissions", () => {
14
+ const unknownResource = "PlateMaintainer";
15
+
16
+ it.each([
17
+ { description: "null", value: null },
18
+ { description: "undefined", value: undefined },
19
+ { description: "boolean", value: true },
20
+ { description: "number", value: 1 },
21
+ { description: "string", value: "x" },
22
+ { description: "array", value: [] },
23
+ { description: "empty object", value: {} },
24
+ {
25
+ description: "only explicit false grants",
26
+ value: {
27
+ [PermissionResource.CHANNEL]: {
28
+ [PermissionAction.READ]: false,
29
+ },
30
+ },
31
+ },
32
+ {
33
+ description: "resource with empty actions object",
34
+ value: {
35
+ [PermissionResource.CHANNEL]: {},
36
+ },
37
+ },
38
+ {
39
+ description: "invalid resource key",
40
+ value: { [unknownResource]: { read: true } },
41
+ },
42
+ {
43
+ description: "resource value is null",
44
+ value: { [PermissionResource.CHANNEL]: null },
45
+ },
46
+ {
47
+ description: "resource value is a string",
48
+ value: { [PermissionResource.CHANNEL]: "read" },
49
+ },
50
+ {
51
+ description: "resource value is a number",
52
+ value: { [PermissionResource.CHANNEL]: 1 },
53
+ },
54
+ {
55
+ description: "resource value is a boolean",
56
+ value: { [PermissionResource.CHANNEL]: true },
57
+ },
58
+ {
59
+ description: "resource value is an array",
60
+ value: { [PermissionResource.CHANNEL]: ["read"] },
61
+ },
62
+ {
63
+ description: "action not allowed on resource",
64
+ value: {
65
+ [PermissionResource.ORGANIZATION]: {
66
+ [PermissionAction.CREATE]: true,
67
+ },
68
+ },
69
+ },
70
+ {
71
+ description: "unknown action key",
72
+ value: {
73
+ [PermissionResource.CHANNEL]: {
74
+ notAnAction: true,
75
+ },
76
+ },
77
+ },
78
+ {
79
+ description: "unknown resource key alongside a valid grant",
80
+ value: {
81
+ [PermissionResource.CHANNEL]: {
82
+ [PermissionAction.READ]: true,
83
+ },
84
+ [unknownResource]: {
85
+ [PermissionAction.READ]: true,
86
+ },
87
+ },
88
+ },
89
+ {
90
+ description: "unknown action key alongside a valid grant",
91
+ value: {
92
+ [PermissionResource.CHANNEL]: {
93
+ [PermissionAction.READ]: true,
94
+ notAnAction: true,
95
+ },
96
+ },
97
+ },
98
+ {
99
+ description: "grant value is a string",
100
+ value: {
101
+ [PermissionResource.CHANNEL]: {
102
+ [PermissionAction.READ]: "yes",
103
+ },
104
+ },
105
+ },
106
+ {
107
+ description: "grant value is a number",
108
+ value: {
109
+ [PermissionResource.CHANNEL]: {
110
+ [PermissionAction.READ]: 1,
111
+ },
112
+ },
113
+ },
114
+ {
115
+ description: "grant value is null",
116
+ value: {
117
+ [PermissionResource.CHANNEL]: {
118
+ [PermissionAction.READ]: null,
119
+ },
120
+ },
121
+ },
122
+ {
123
+ description: "grant value is undefined",
124
+ value: {
125
+ [PermissionResource.CHANNEL]: {
126
+ [PermissionAction.READ]: undefined,
127
+ },
128
+ },
129
+ },
130
+ {
131
+ description: "grant value is an object",
132
+ value: {
133
+ [PermissionResource.CHANNEL]: {
134
+ [PermissionAction.READ]: {},
135
+ },
136
+ },
137
+ },
138
+ ])("returns false for $description", ({ value }) => {
139
+ // Act & Assert
140
+ expect(isValidPermissions(value)).toBe(false);
141
+ });
142
+
143
+ it.each([
144
+ {
145
+ description: "mixed true and false grants across resources",
146
+ value: {
147
+ [PermissionResource.CONTENT_ITEM]: {
148
+ [PermissionAction.READ]: true,
149
+ [PermissionAction.UPDATE]: false,
150
+ },
151
+ [PermissionResource.CHANNEL]: {
152
+ [PermissionAction.READ]: false,
153
+ },
154
+ },
155
+ },
156
+ {
157
+ description: "true grant after earlier all-false resource",
158
+ value: {
159
+ [PermissionResource.CHANNEL]: {
160
+ [PermissionAction.READ]: false,
161
+ },
162
+ [PermissionResource.ROLE]: {
163
+ [PermissionAction.CREATE]: true,
164
+ },
165
+ },
166
+ },
167
+ {
168
+ description: "organization read and update",
169
+ value: {
170
+ [PermissionResource.ORGANIZATION]: {
171
+ [PermissionAction.READ]: true,
172
+ [PermissionAction.UPDATE]: true,
173
+ },
174
+ },
175
+ },
176
+ {
177
+ description: "true grant with null sibling actions",
178
+ value: {
179
+ [PermissionResource.CONTENT_ITEM]: {
180
+ [PermissionAction.READ]: true,
181
+ [PermissionAction.UPDATE]: null,
182
+ [PermissionAction.CREATE]: undefined,
183
+ },
184
+ },
185
+ },
186
+ ])("returns true for $description", ({ value }) => {
187
+ // Act & Assert
188
+ expect(isValidPermissions(value)).toBe(true);
189
+ });
190
+ });
191
+
192
+ describe("mergePermissions", () => {
193
+ it.each([
194
+ {
195
+ description: "no inputs",
196
+ inputs: [] as Parameters<typeof mergePermissions>,
197
+ expected: {},
198
+ },
199
+ {
200
+ description: "empty permission objects",
201
+ inputs: [{}, {}],
202
+ expected: {},
203
+ },
204
+ {
205
+ description: "null and undefined resource actions are skipped",
206
+ inputs: [
207
+ {
208
+ [PermissionResource.CHANNEL]: null,
209
+ [PermissionResource.ROLE]: undefined,
210
+ },
211
+ ],
212
+ expected: {},
213
+ },
214
+ {
215
+ description: "false grants are ignored",
216
+ inputs: [
217
+ {
218
+ [PermissionResource.CHANNEL]: {
219
+ [PermissionAction.READ]: false,
220
+ [PermissionAction.CREATE]: false,
221
+ },
222
+ },
223
+ ],
224
+ expected: {
225
+ [PermissionResource.CHANNEL]: {},
226
+ },
227
+ },
228
+ {
229
+ description: "empty actions object is kept as empty resource map",
230
+ inputs: [
231
+ {
232
+ [PermissionResource.CHANNEL]: {},
233
+ },
234
+ ],
235
+ expected: {
236
+ [PermissionResource.CHANNEL]: {},
237
+ },
238
+ },
239
+ {
240
+ description: "true grants are kept",
241
+ inputs: [
242
+ {
243
+ [PermissionResource.CHANNEL]: {
244
+ [PermissionAction.READ]: true,
245
+ },
246
+ [PermissionResource.ROLE]: {
247
+ [PermissionAction.CREATE]: true,
248
+ },
249
+ },
250
+ ],
251
+ expected: {
252
+ [PermissionResource.CHANNEL]: {
253
+ [PermissionAction.READ]: true,
254
+ },
255
+ [PermissionResource.ROLE]: {
256
+ [PermissionAction.CREATE]: true,
257
+ },
258
+ },
259
+ },
260
+ {
261
+ description: "OR-merges true grants across inputs",
262
+ inputs: [
263
+ {
264
+ [PermissionResource.CHANNEL]: {
265
+ [PermissionAction.READ]: true,
266
+ },
267
+ },
268
+ {
269
+ [PermissionResource.CHANNEL]: {
270
+ [PermissionAction.UPDATE]: true,
271
+ },
272
+ [PermissionResource.ROLE]: {
273
+ [PermissionAction.READ]: true,
274
+ },
275
+ },
276
+ ],
277
+ expected: {
278
+ [PermissionResource.CHANNEL]: {
279
+ [PermissionAction.READ]: true,
280
+ [PermissionAction.UPDATE]: true,
281
+ },
282
+ [PermissionResource.ROLE]: {
283
+ [PermissionAction.READ]: true,
284
+ },
285
+ },
286
+ },
287
+ {
288
+ description: "later false does not clear an earlier true",
289
+ inputs: [
290
+ {
291
+ [PermissionResource.CHANNEL]: {
292
+ [PermissionAction.READ]: true,
293
+ },
294
+ },
295
+ {
296
+ [PermissionResource.CHANNEL]: {
297
+ [PermissionAction.READ]: false,
298
+ },
299
+ },
300
+ ],
301
+ expected: {
302
+ [PermissionResource.CHANNEL]: {
303
+ [PermissionAction.READ]: true,
304
+ },
305
+ },
306
+ },
307
+ {
308
+ description: "earlier false then later true results in true",
309
+ inputs: [
310
+ {
311
+ [PermissionResource.CHANNEL]: {
312
+ [PermissionAction.READ]: false,
313
+ },
314
+ },
315
+ {
316
+ [PermissionResource.CHANNEL]: {
317
+ [PermissionAction.READ]: true,
318
+ },
319
+ },
320
+ ],
321
+ expected: {
322
+ [PermissionResource.CHANNEL]: {
323
+ [PermissionAction.READ]: true,
324
+ },
325
+ },
326
+ },
327
+ ])("returns expected result for $description", ({ inputs, expected }) => {
328
+ // Act
329
+ const result = mergePermissions(...(inputs as Permissions[]));
330
+
331
+ // Assert
332
+ expect(result).toEqual(expected);
333
+ });
334
+ });
335
+
336
+ describe("getMissingPermissions", () => {
337
+ it("returns only requested true grants that are unavailable", () => {
338
+ // Arrange
339
+ const availablePermissions: Permissions = {
340
+ [PermissionResource.CONTENT_ITEM]: {
341
+ [PermissionAction.READ]: true,
342
+ },
343
+ };
344
+ const permissions: Permissions = {
345
+ [PermissionResource.CONTENT_ITEM]: {
346
+ [PermissionAction.READ]: true,
347
+ [PermissionAction.UPDATE]: true,
348
+ [PermissionAction.REMOVE]: false,
349
+ },
350
+ [PermissionResource.TAG]: {
351
+ [PermissionAction.READ]: true,
352
+ },
353
+ };
354
+
355
+ // Act
356
+ const result = getMissingPermissions(availablePermissions, permissions);
357
+
358
+ // Assert
359
+ expect(result).toStrictEqual({
360
+ [PermissionResource.CONTENT_ITEM]: {
361
+ [PermissionAction.UPDATE]: true,
362
+ },
363
+ [PermissionResource.TAG]: {
364
+ [PermissionAction.READ]: true,
365
+ },
366
+ });
367
+ });
368
+ });
369
+
370
+ describe("hasPermissions", () => {
371
+ it("returns true when all requested grants are available", () => {
372
+ // Act & Assert
373
+ expect(
374
+ hasPermissions(PRESET_PERMISSION_SETS.contentEditor, {
375
+ [PermissionResource.CONTENT_ITEM]: {
376
+ [PermissionAction.READ]: true,
377
+ },
378
+ }),
379
+ ).toBe(true);
380
+ });
381
+
382
+ it("returns false when a requested grant is unavailable", () => {
383
+ // Act & Assert
384
+ expect(
385
+ hasPermissions(PRESET_PERMISSION_SETS.contentViewer, {
386
+ [PermissionResource.CONTENT_ITEM]: {
387
+ [PermissionAction.UPDATE]: true,
388
+ },
389
+ }),
390
+ ).toBe(false);
391
+ });
392
+ });
393
+
394
+ describe("compactPermissions", () => {
395
+ it.each([
396
+ {
397
+ description: "empty object",
398
+ value: {},
399
+ expected: {},
400
+ },
401
+ {
402
+ description: "already compact true grants",
403
+ value: {
404
+ [PermissionResource.CHANNEL]: {
405
+ [PermissionAction.READ]: true,
406
+ },
407
+ },
408
+ expected: {
409
+ [PermissionResource.CHANNEL]: {
410
+ [PermissionAction.READ]: true,
411
+ },
412
+ },
413
+ },
414
+ {
415
+ description: "false grants are omitted",
416
+ value: {
417
+ [PermissionResource.CHANNEL]: {
418
+ [PermissionAction.READ]: true,
419
+ [PermissionAction.UPDATE]: false,
420
+ },
421
+ },
422
+ expected: {
423
+ [PermissionResource.CHANNEL]: {
424
+ [PermissionAction.READ]: true,
425
+ },
426
+ },
427
+ },
428
+ {
429
+ description: "null and undefined grants are omitted",
430
+ value: {
431
+ [PermissionResource.CONTENT_ITEM]: {
432
+ [PermissionAction.READ]: true,
433
+ [PermissionAction.UPDATE]: null,
434
+ [PermissionAction.CREATE]: undefined,
435
+ },
436
+ },
437
+ expected: {
438
+ [PermissionResource.CONTENT_ITEM]: {
439
+ [PermissionAction.READ]: true,
440
+ },
441
+ },
442
+ },
443
+ {
444
+ description: "resource with only false grants is omitted",
445
+ value: {
446
+ [PermissionResource.CHANNEL]: {
447
+ [PermissionAction.READ]: false,
448
+ [PermissionAction.CREATE]: false,
449
+ },
450
+ },
451
+ expected: {},
452
+ },
453
+ {
454
+ description: "empty actions object is omitted",
455
+ value: {
456
+ [PermissionResource.CHANNEL]: {},
457
+ },
458
+ expected: {},
459
+ },
460
+ {
461
+ description: "null and undefined resource actions are omitted",
462
+ value: {
463
+ [PermissionResource.CHANNEL]: null,
464
+ [PermissionResource.ROLE]: undefined,
465
+ },
466
+ expected: {},
467
+ },
468
+ {
469
+ description: "empty resources are omitted next to compact grants",
470
+ value: {
471
+ [PermissionResource.CHANNEL]: {
472
+ [PermissionAction.READ]: false,
473
+ },
474
+ [PermissionResource.ROLE]: {
475
+ [PermissionAction.CREATE]: true,
476
+ },
477
+ [PermissionResource.ORGANIZATION]: {},
478
+ },
479
+ expected: {
480
+ [PermissionResource.ROLE]: {
481
+ [PermissionAction.CREATE]: true,
482
+ },
483
+ },
484
+ },
485
+ ])("returns expected result for $description", ({ value, expected }) => {
486
+ // Act
487
+ const result = compactPermissions(value as Permissions);
488
+
489
+ // Assert
490
+ expect(result).toEqual(expected);
491
+ });
492
+
493
+ it("does not mutate the input object", () => {
494
+ // Arrange
495
+ const value = {
496
+ [PermissionResource.CHANNEL]: {
497
+ [PermissionAction.READ]: true,
498
+ [PermissionAction.UPDATE]: false,
499
+ },
500
+ [PermissionResource.ROLE]: {
501
+ [PermissionAction.CREATE]: false,
502
+ },
503
+ };
504
+
505
+ // Act
506
+ const result = compactPermissions(value);
507
+
508
+ // Assert
509
+ expect(result).toEqual({
510
+ [PermissionResource.CHANNEL]: {
511
+ [PermissionAction.READ]: true,
512
+ },
513
+ });
514
+ expect(value).toEqual({
515
+ [PermissionResource.CHANNEL]: {
516
+ [PermissionAction.READ]: true,
517
+ [PermissionAction.UPDATE]: false,
518
+ },
519
+ [PermissionResource.ROLE]: {
520
+ [PermissionAction.CREATE]: false,
521
+ },
522
+ });
523
+ });
524
+ });
@@ -0,0 +1,698 @@
1
+ /**
2
+ * Resources that can appear in a permission grant.
3
+ * String values match GraphQL / OPA resource type names (e.g. `"ContentItem"`).
4
+ */
5
+ export enum PermissionResource {
6
+ CONTENT_TYPE = "ContentType",
7
+ CONTENT_ITEM = "ContentItem",
8
+ CONTENT_VALUE = "ContentValue",
9
+ ASSET = "Asset",
10
+ TAG = "Tag",
11
+ CONTENT_EXPERIENCE = "ContentExperience",
12
+ EXPERIENCE_COMPONENT = "ExperienceComponent",
13
+ BUILDING_BLOCK = "BuildingBlock",
14
+ BUILDING_BLOCK_FIELD = "BuildingBlockField",
15
+ BUILDING_BLOCK_FIELD_FULFILLMENT = "BuildingBlockFieldFulfillment",
16
+ PATH_PART = "PathPart",
17
+ CONTENT_FIELD = "ContentField",
18
+ CHANNEL = "Channel",
19
+ THEME = "Theme",
20
+ TEMPLATE = "Template",
21
+ BLUEPRINT = "Blueprint",
22
+ GRID = "Grid",
23
+ GRID_DEFINITION = "GridDefinition",
24
+ GRID_PLACEMENT = "GridPlacement",
25
+ ORGANIZATION = "Organization",
26
+ ROLE = "Role",
27
+ ROLE_ASSIGNMENT = "RoleAssignment",
28
+ INVITATION = "Invitation",
29
+ API_TOKEN = "ApiToken",
30
+ LANGUAGE = "Language",
31
+ ISSUE = "Issue",
32
+ PUBLICATION = "Publication",
33
+ CONTENT_VALIDATION_RULE = "ContentValidationRule",
34
+ SCHEDULED_PUBLICATION = "ScheduledPublication",
35
+ }
36
+
37
+ /**
38
+ * Resources that OPA authorizes but a Role cannot grant.
39
+ * @remarks Deliberately kept out of {@link PermissionResource}: these are application-level and decided by the OPA
40
+ * policy, so they must never appear in a Role's {@link Permissions}.
41
+ */
42
+ export enum AuthorizationOnlyResource {
43
+ PLATE_MAINTAINER = "PlateMaintainer",
44
+ }
45
+
46
+ /**
47
+ * Every resource that can be sent to OPA as `input.resource.type`.
48
+ */
49
+ export type AuthorizationResource = AuthorizationOnlyResource | PermissionResource;
50
+
51
+ /**
52
+ * Actions that can be granted on a {@link PermissionResource}.
53
+ * String values are what we send to OPA as `input.action` (e.g. `"read"`).
54
+ * @remarks Not every action applies to every resource — see {@link RESOURCE_ACTIONS}.
55
+ */
56
+ export enum PermissionAction {
57
+ READ = "read",
58
+ CREATE = "create",
59
+ UPDATE = "update",
60
+ REMOVE = "remove",
61
+ }
62
+
63
+ const CRUD_ACTIONS = [
64
+ PermissionAction.READ,
65
+ PermissionAction.CREATE,
66
+ PermissionAction.UPDATE,
67
+ PermissionAction.REMOVE,
68
+ ] as const;
69
+
70
+ /**
71
+ * Catalog of which actions exist per resource.
72
+ * Keep this the single source of truth when adding resources or actions.
73
+ * @remarks This is applicability, not grants. Grants are {@link Permissions} objects.
74
+ * @example
75
+ * ```ts
76
+ * RESOURCE_ACTIONS[PermissionResource.CONTENT_ITEM] // ["read", "create", "update", "remove"]
77
+ * RESOURCE_ACTIONS[PermissionResource.ORGANIZATION] // ["read", "update"]
78
+ * ```
79
+ */
80
+ export const RESOURCE_ACTIONS: Record<PermissionResource, readonly PermissionAction[]> = {
81
+ [PermissionResource.CONTENT_TYPE]: CRUD_ACTIONS,
82
+ [PermissionResource.CONTENT_ITEM]: CRUD_ACTIONS,
83
+ [PermissionResource.CONTENT_VALUE]: CRUD_ACTIONS,
84
+ [PermissionResource.ASSET]: CRUD_ACTIONS,
85
+ [PermissionResource.TAG]: CRUD_ACTIONS,
86
+ [PermissionResource.CONTENT_EXPERIENCE]: CRUD_ACTIONS,
87
+ [PermissionResource.EXPERIENCE_COMPONENT]: CRUD_ACTIONS,
88
+ [PermissionResource.BUILDING_BLOCK]: CRUD_ACTIONS,
89
+ [PermissionResource.BUILDING_BLOCK_FIELD]: CRUD_ACTIONS,
90
+ [PermissionResource.BUILDING_BLOCK_FIELD_FULFILLMENT]: CRUD_ACTIONS,
91
+ [PermissionResource.PATH_PART]: CRUD_ACTIONS,
92
+ [PermissionResource.CONTENT_FIELD]: CRUD_ACTIONS,
93
+ [PermissionResource.CHANNEL]: CRUD_ACTIONS,
94
+ [PermissionResource.THEME]: CRUD_ACTIONS,
95
+ [PermissionResource.TEMPLATE]: CRUD_ACTIONS,
96
+ [PermissionResource.BLUEPRINT]: CRUD_ACTIONS,
97
+ [PermissionResource.GRID]: CRUD_ACTIONS,
98
+ [PermissionResource.GRID_DEFINITION]: CRUD_ACTIONS,
99
+ [PermissionResource.GRID_PLACEMENT]: CRUD_ACTIONS,
100
+ [PermissionResource.ORGANIZATION]: [PermissionAction.READ, PermissionAction.UPDATE],
101
+ [PermissionResource.ROLE]: CRUD_ACTIONS,
102
+ [PermissionResource.ROLE_ASSIGNMENT]: [PermissionAction.READ, PermissionAction.CREATE, PermissionAction.REMOVE],
103
+ [PermissionResource.INVITATION]: [PermissionAction.READ, PermissionAction.CREATE, PermissionAction.REMOVE],
104
+ [PermissionResource.API_TOKEN]: CRUD_ACTIONS,
105
+ [PermissionResource.LANGUAGE]: CRUD_ACTIONS,
106
+ [PermissionResource.ISSUE]: CRUD_ACTIONS,
107
+ [PermissionResource.PUBLICATION]: [PermissionAction.READ, PermissionAction.CREATE, PermissionAction.REMOVE],
108
+ [PermissionResource.CONTENT_VALIDATION_RULE]: CRUD_ACTIONS,
109
+ [PermissionResource.SCHEDULED_PUBLICATION]: CRUD_ACTIONS,
110
+ } as const satisfies Record<PermissionResource, readonly PermissionAction[]>;
111
+
112
+ /**
113
+ * Actions allowed for a specific resource according to {@link RESOURCE_ACTIONS}.
114
+ * @remarks An {@link AuthorizationOnlyResource} has no entry there, so every action applies — which ones are
115
+ * actually permitted is up to the OPA policy.
116
+ */
117
+ export type ActionForResource<TResource extends AuthorizationResource> = TResource extends PermissionResource
118
+ ? (typeof RESOURCE_ACTIONS)[TResource][number]
119
+ : PermissionAction;
120
+
121
+ /**
122
+ * Nested grant map keyed by {@link PermissionResource}, then action.
123
+ * @remarks Additive only — missing keys or `false` means not granted. There is no explicit deny.
124
+ * @example
125
+ * ```ts
126
+ * const permissions: Permissions = {
127
+ * [PermissionResource.CONTENT_ITEM]: {
128
+ * [PermissionAction.READ]: true,
129
+ * [PermissionAction.UPDATE]: true,
130
+ * },
131
+ * };
132
+ * ```
133
+ */
134
+ export type Permissions = {
135
+ [TResource in PermissionResource]?: Partial<Record<ActionForResource<TResource>, boolean>>;
136
+ };
137
+
138
+ /**
139
+ * A single resource + action check (e.g. for `@AuthorizationOptions` / OPA input).
140
+ */
141
+ export interface AuthorizationCheck<TResource extends PermissionResource = PermissionResource> {
142
+ resource: TResource;
143
+ action: ActionForResource<TResource>;
144
+ }
145
+
146
+ export const PERMISSION_RESOURCES = new Set<string>(Object.values(PermissionResource));
147
+
148
+ export const AUTHORIZATION_RESOURCES = new Set<string>([
149
+ ...PERMISSION_RESOURCES,
150
+ ...Object.values(AuthorizationOnlyResource),
151
+ ]);
152
+
153
+ export const PRESET_PERMISSION_SETS: Record<string, Permissions> = {
154
+ admin: {
155
+ [PermissionResource.CONTENT_TYPE]: {
156
+ [PermissionAction.READ]: true,
157
+ [PermissionAction.CREATE]: true,
158
+ [PermissionAction.UPDATE]: true,
159
+ [PermissionAction.REMOVE]: true,
160
+ },
161
+ [PermissionResource.CONTENT_ITEM]: {
162
+ [PermissionAction.READ]: true,
163
+ [PermissionAction.CREATE]: true,
164
+ [PermissionAction.UPDATE]: true,
165
+ [PermissionAction.REMOVE]: true,
166
+ },
167
+ [PermissionResource.CONTENT_VALUE]: {
168
+ [PermissionAction.READ]: true,
169
+ [PermissionAction.CREATE]: true,
170
+ [PermissionAction.UPDATE]: true,
171
+ [PermissionAction.REMOVE]: true,
172
+ },
173
+ [PermissionResource.ASSET]: {
174
+ [PermissionAction.READ]: true,
175
+ [PermissionAction.CREATE]: true,
176
+ [PermissionAction.UPDATE]: true,
177
+ [PermissionAction.REMOVE]: true,
178
+ },
179
+ [PermissionResource.TAG]: {
180
+ [PermissionAction.READ]: true,
181
+ [PermissionAction.CREATE]: true,
182
+ [PermissionAction.UPDATE]: true,
183
+ [PermissionAction.REMOVE]: true,
184
+ },
185
+ [PermissionResource.CONTENT_EXPERIENCE]: {
186
+ [PermissionAction.READ]: true,
187
+ [PermissionAction.CREATE]: true,
188
+ [PermissionAction.UPDATE]: true,
189
+ [PermissionAction.REMOVE]: true,
190
+ },
191
+ [PermissionResource.EXPERIENCE_COMPONENT]: {
192
+ [PermissionAction.READ]: true,
193
+ [PermissionAction.CREATE]: true,
194
+ [PermissionAction.UPDATE]: true,
195
+ [PermissionAction.REMOVE]: true,
196
+ },
197
+ [PermissionResource.BUILDING_BLOCK]: {
198
+ [PermissionAction.READ]: true,
199
+ [PermissionAction.CREATE]: true,
200
+ [PermissionAction.UPDATE]: true,
201
+ [PermissionAction.REMOVE]: true,
202
+ },
203
+ [PermissionResource.BUILDING_BLOCK_FIELD]: {
204
+ [PermissionAction.READ]: true,
205
+ [PermissionAction.CREATE]: true,
206
+ [PermissionAction.UPDATE]: true,
207
+ [PermissionAction.REMOVE]: true,
208
+ },
209
+ [PermissionResource.BUILDING_BLOCK_FIELD_FULFILLMENT]: {
210
+ [PermissionAction.READ]: true,
211
+ [PermissionAction.CREATE]: true,
212
+ [PermissionAction.UPDATE]: true,
213
+ [PermissionAction.REMOVE]: true,
214
+ },
215
+ [PermissionResource.PATH_PART]: {
216
+ [PermissionAction.READ]: true,
217
+ [PermissionAction.CREATE]: true,
218
+ [PermissionAction.UPDATE]: true,
219
+ [PermissionAction.REMOVE]: true,
220
+ },
221
+ [PermissionResource.CONTENT_FIELD]: {
222
+ [PermissionAction.READ]: true,
223
+ [PermissionAction.CREATE]: true,
224
+ [PermissionAction.UPDATE]: true,
225
+ [PermissionAction.REMOVE]: true,
226
+ },
227
+ [PermissionResource.CHANNEL]: {
228
+ [PermissionAction.READ]: true,
229
+ [PermissionAction.CREATE]: true,
230
+ [PermissionAction.UPDATE]: true,
231
+ [PermissionAction.REMOVE]: true,
232
+ },
233
+ [PermissionResource.THEME]: {
234
+ [PermissionAction.READ]: true,
235
+ [PermissionAction.CREATE]: true,
236
+ [PermissionAction.UPDATE]: true,
237
+ [PermissionAction.REMOVE]: true,
238
+ },
239
+ [PermissionResource.TEMPLATE]: {
240
+ [PermissionAction.READ]: true,
241
+ [PermissionAction.CREATE]: true,
242
+ [PermissionAction.UPDATE]: true,
243
+ [PermissionAction.REMOVE]: true,
244
+ },
245
+ [PermissionResource.BLUEPRINT]: {
246
+ [PermissionAction.READ]: true,
247
+ [PermissionAction.CREATE]: true,
248
+ [PermissionAction.UPDATE]: true,
249
+ [PermissionAction.REMOVE]: true,
250
+ },
251
+ [PermissionResource.GRID]: {
252
+ [PermissionAction.READ]: true,
253
+ [PermissionAction.CREATE]: true,
254
+ [PermissionAction.UPDATE]: true,
255
+ [PermissionAction.REMOVE]: true,
256
+ },
257
+ [PermissionResource.GRID_DEFINITION]: {
258
+ [PermissionAction.READ]: true,
259
+ [PermissionAction.CREATE]: true,
260
+ [PermissionAction.UPDATE]: true,
261
+ [PermissionAction.REMOVE]: true,
262
+ },
263
+ [PermissionResource.GRID_PLACEMENT]: {
264
+ [PermissionAction.READ]: true,
265
+ [PermissionAction.CREATE]: true,
266
+ [PermissionAction.UPDATE]: true,
267
+ [PermissionAction.REMOVE]: true,
268
+ },
269
+ [PermissionResource.ORGANIZATION]: {
270
+ [PermissionAction.READ]: true,
271
+ [PermissionAction.UPDATE]: true,
272
+ },
273
+ [PermissionResource.ROLE]: {
274
+ [PermissionAction.READ]: true,
275
+ [PermissionAction.CREATE]: true,
276
+ [PermissionAction.UPDATE]: true,
277
+ [PermissionAction.REMOVE]: true,
278
+ },
279
+ [PermissionResource.ROLE_ASSIGNMENT]: {
280
+ [PermissionAction.READ]: true,
281
+ [PermissionAction.CREATE]: true,
282
+ [PermissionAction.REMOVE]: true,
283
+ },
284
+ [PermissionResource.INVITATION]: {
285
+ [PermissionAction.READ]: true,
286
+ [PermissionAction.CREATE]: true,
287
+ [PermissionAction.REMOVE]: true,
288
+ },
289
+ [PermissionResource.API_TOKEN]: {
290
+ [PermissionAction.READ]: true,
291
+ [PermissionAction.CREATE]: true,
292
+ [PermissionAction.UPDATE]: true,
293
+ [PermissionAction.REMOVE]: true,
294
+ },
295
+ [PermissionResource.LANGUAGE]: {
296
+ [PermissionAction.READ]: true,
297
+ [PermissionAction.CREATE]: true,
298
+ [PermissionAction.UPDATE]: true,
299
+ [PermissionAction.REMOVE]: true,
300
+ },
301
+ [PermissionResource.ISSUE]: {
302
+ [PermissionAction.READ]: true,
303
+ [PermissionAction.CREATE]: true,
304
+ [PermissionAction.UPDATE]: true,
305
+ [PermissionAction.REMOVE]: true,
306
+ },
307
+ [PermissionResource.PUBLICATION]: {
308
+ [PermissionAction.READ]: true,
309
+ [PermissionAction.CREATE]: true,
310
+ [PermissionAction.REMOVE]: true,
311
+ },
312
+ [PermissionResource.SCHEDULED_PUBLICATION]: {
313
+ [PermissionAction.READ]: true,
314
+ [PermissionAction.CREATE]: true,
315
+ [PermissionAction.UPDATE]: true,
316
+ [PermissionAction.REMOVE]: true,
317
+ },
318
+ [PermissionResource.CONTENT_VALIDATION_RULE]: {
319
+ [PermissionAction.READ]: true,
320
+ [PermissionAction.CREATE]: true,
321
+ [PermissionAction.UPDATE]: true,
322
+ [PermissionAction.REMOVE]: true,
323
+ },
324
+ },
325
+ contentEditor: {
326
+ [PermissionResource.CONTENT_TYPE]: {
327
+ [PermissionAction.READ]: true,
328
+ },
329
+ [PermissionResource.CONTENT_ITEM]: {
330
+ [PermissionAction.READ]: true,
331
+ [PermissionAction.CREATE]: true,
332
+ [PermissionAction.UPDATE]: true,
333
+ [PermissionAction.REMOVE]: true,
334
+ },
335
+ [PermissionResource.CONTENT_VALUE]: {
336
+ [PermissionAction.READ]: true,
337
+ [PermissionAction.CREATE]: true,
338
+ [PermissionAction.UPDATE]: true,
339
+ [PermissionAction.REMOVE]: true,
340
+ },
341
+ [PermissionResource.ASSET]: {
342
+ [PermissionAction.READ]: true,
343
+ [PermissionAction.CREATE]: true,
344
+ [PermissionAction.UPDATE]: true,
345
+ [PermissionAction.REMOVE]: true,
346
+ },
347
+ [PermissionResource.TAG]: {
348
+ [PermissionAction.READ]: true,
349
+ [PermissionAction.CREATE]: true,
350
+ [PermissionAction.UPDATE]: true,
351
+ [PermissionAction.REMOVE]: true,
352
+ },
353
+ [PermissionResource.CONTENT_EXPERIENCE]: {
354
+ [PermissionAction.READ]: true,
355
+ [PermissionAction.CREATE]: true,
356
+ [PermissionAction.UPDATE]: true,
357
+ [PermissionAction.REMOVE]: true,
358
+ },
359
+ [PermissionResource.EXPERIENCE_COMPONENT]: {
360
+ [PermissionAction.READ]: true,
361
+ [PermissionAction.CREATE]: true,
362
+ [PermissionAction.UPDATE]: true,
363
+ [PermissionAction.REMOVE]: true,
364
+ },
365
+ [PermissionResource.BUILDING_BLOCK]: {
366
+ [PermissionAction.READ]: true,
367
+ },
368
+ [PermissionResource.BUILDING_BLOCK_FIELD]: {
369
+ [PermissionAction.READ]: true,
370
+ },
371
+ [PermissionResource.BUILDING_BLOCK_FIELD_FULFILLMENT]: {
372
+ [PermissionAction.READ]: true,
373
+ [PermissionAction.CREATE]: true,
374
+ [PermissionAction.UPDATE]: true,
375
+ [PermissionAction.REMOVE]: true,
376
+ },
377
+ [PermissionResource.PATH_PART]: {
378
+ [PermissionAction.READ]: true,
379
+ [PermissionAction.CREATE]: true,
380
+ [PermissionAction.UPDATE]: true,
381
+ [PermissionAction.REMOVE]: true,
382
+ },
383
+ [PermissionResource.CONTENT_FIELD]: {
384
+ [PermissionAction.READ]: true,
385
+ },
386
+ [PermissionResource.CHANNEL]: {
387
+ [PermissionAction.READ]: true,
388
+ [PermissionAction.CREATE]: true,
389
+ [PermissionAction.UPDATE]: true,
390
+ [PermissionAction.REMOVE]: true,
391
+ },
392
+ [PermissionResource.THEME]: {
393
+ [PermissionAction.READ]: true,
394
+ },
395
+ [PermissionResource.TEMPLATE]: {
396
+ [PermissionAction.READ]: true,
397
+ },
398
+ [PermissionResource.BLUEPRINT]: {
399
+ [PermissionAction.READ]: true,
400
+ [PermissionAction.CREATE]: true,
401
+ [PermissionAction.UPDATE]: true,
402
+ [PermissionAction.REMOVE]: true,
403
+ },
404
+ [PermissionResource.GRID]: {
405
+ [PermissionAction.READ]: true,
406
+ [PermissionAction.CREATE]: true,
407
+ [PermissionAction.UPDATE]: true,
408
+ [PermissionAction.REMOVE]: true,
409
+ },
410
+ [PermissionResource.GRID_DEFINITION]: {
411
+ [PermissionAction.READ]: true,
412
+ },
413
+ [PermissionResource.GRID_PLACEMENT]: {
414
+ [PermissionAction.READ]: true,
415
+ [PermissionAction.CREATE]: true,
416
+ [PermissionAction.UPDATE]: true,
417
+ [PermissionAction.REMOVE]: true,
418
+ },
419
+ [PermissionResource.ORGANIZATION]: {
420
+ [PermissionAction.READ]: true,
421
+ },
422
+ [PermissionResource.ROLE]: {
423
+ [PermissionAction.READ]: true,
424
+ },
425
+ [PermissionResource.ROLE_ASSIGNMENT]: {
426
+ [PermissionAction.READ]: true,
427
+ },
428
+ [PermissionResource.INVITATION]: {
429
+ [PermissionAction.READ]: true,
430
+ },
431
+ [PermissionResource.API_TOKEN]: {
432
+ [PermissionAction.READ]: true,
433
+ },
434
+ [PermissionResource.LANGUAGE]: {
435
+ [PermissionAction.READ]: true,
436
+ },
437
+ [PermissionResource.ISSUE]: {
438
+ [PermissionAction.READ]: true,
439
+ [PermissionAction.CREATE]: true,
440
+ [PermissionAction.UPDATE]: true,
441
+ [PermissionAction.REMOVE]: true,
442
+ },
443
+ [PermissionResource.PUBLICATION]: {
444
+ [PermissionAction.READ]: true,
445
+ [PermissionAction.CREATE]: true,
446
+ [PermissionAction.REMOVE]: true,
447
+ },
448
+ [PermissionResource.SCHEDULED_PUBLICATION]: {
449
+ [PermissionAction.READ]: true,
450
+ [PermissionAction.CREATE]: true,
451
+ [PermissionAction.UPDATE]: true,
452
+ [PermissionAction.REMOVE]: true,
453
+ },
454
+ [PermissionResource.CONTENT_VALIDATION_RULE]: {
455
+ [PermissionAction.READ]: true,
456
+ },
457
+ },
458
+ contentViewer: {
459
+ [PermissionResource.CONTENT_TYPE]: {
460
+ [PermissionAction.READ]: true,
461
+ },
462
+ [PermissionResource.CONTENT_ITEM]: {
463
+ [PermissionAction.READ]: true,
464
+ },
465
+ [PermissionResource.CONTENT_VALUE]: {
466
+ [PermissionAction.READ]: true,
467
+ },
468
+ [PermissionResource.ASSET]: {
469
+ [PermissionAction.READ]: true,
470
+ },
471
+ [PermissionResource.TAG]: {
472
+ [PermissionAction.READ]: true,
473
+ },
474
+ [PermissionResource.CONTENT_EXPERIENCE]: {
475
+ [PermissionAction.READ]: true,
476
+ },
477
+ [PermissionResource.EXPERIENCE_COMPONENT]: {
478
+ [PermissionAction.READ]: true,
479
+ },
480
+ [PermissionResource.BUILDING_BLOCK]: {
481
+ [PermissionAction.READ]: true,
482
+ },
483
+ [PermissionResource.BUILDING_BLOCK_FIELD]: {
484
+ [PermissionAction.READ]: true,
485
+ },
486
+ [PermissionResource.BUILDING_BLOCK_FIELD_FULFILLMENT]: {
487
+ [PermissionAction.READ]: true,
488
+ },
489
+ [PermissionResource.PATH_PART]: {
490
+ [PermissionAction.READ]: true,
491
+ },
492
+ [PermissionResource.CONTENT_FIELD]: {
493
+ [PermissionAction.READ]: true,
494
+ },
495
+ [PermissionResource.CHANNEL]: {
496
+ [PermissionAction.READ]: true,
497
+ },
498
+ [PermissionResource.THEME]: {
499
+ [PermissionAction.READ]: true,
500
+ },
501
+ [PermissionResource.TEMPLATE]: {
502
+ [PermissionAction.READ]: true,
503
+ },
504
+ [PermissionResource.BLUEPRINT]: {
505
+ [PermissionAction.READ]: true,
506
+ },
507
+ [PermissionResource.GRID]: {
508
+ [PermissionAction.READ]: true,
509
+ },
510
+ [PermissionResource.GRID_DEFINITION]: {
511
+ [PermissionAction.READ]: true,
512
+ },
513
+ [PermissionResource.GRID_PLACEMENT]: {
514
+ [PermissionAction.READ]: true,
515
+ },
516
+ [PermissionResource.ORGANIZATION]: {
517
+ [PermissionAction.READ]: true,
518
+ },
519
+ [PermissionResource.ROLE]: {
520
+ [PermissionAction.READ]: true,
521
+ },
522
+ [PermissionResource.ROLE_ASSIGNMENT]: {
523
+ [PermissionAction.READ]: true,
524
+ },
525
+ [PermissionResource.INVITATION]: {
526
+ [PermissionAction.READ]: true,
527
+ },
528
+ [PermissionResource.API_TOKEN]: {
529
+ [PermissionAction.READ]: true,
530
+ },
531
+ [PermissionResource.LANGUAGE]: {
532
+ [PermissionAction.READ]: true,
533
+ },
534
+ [PermissionResource.ISSUE]: {
535
+ [PermissionAction.READ]: true,
536
+ },
537
+ [PermissionResource.PUBLICATION]: {
538
+ [PermissionAction.READ]: true,
539
+ },
540
+ [PermissionResource.SCHEDULED_PUBLICATION]: {
541
+ [PermissionAction.READ]: true,
542
+ },
543
+ [PermissionResource.CONTENT_VALIDATION_RULE]: {
544
+ [PermissionAction.READ]: true,
545
+ },
546
+ },
547
+ cdaApiToken: {
548
+ [PermissionResource.CONTENT_EXPERIENCE]: {
549
+ [PermissionAction.READ]: true,
550
+ },
551
+ [PermissionResource.CONTENT_ITEM]: {
552
+ [PermissionAction.READ]: true,
553
+ },
554
+ [PermissionResource.TAG]: {
555
+ [PermissionAction.READ]: true,
556
+ },
557
+ [PermissionResource.LANGUAGE]: {
558
+ [PermissionAction.READ]: true,
559
+ },
560
+ [PermissionResource.PATH_PART]: {
561
+ [PermissionAction.READ]: true,
562
+ },
563
+ },
564
+ };
565
+
566
+ /**
567
+ * Merges permissions with additive OR semantics.
568
+ * @remarks Missing or false entries never clear an existing true grant.
569
+ */
570
+ export function mergePermissions(...permissionsList: Permissions[]): Permissions {
571
+ const result: Permissions = {};
572
+
573
+ for (const permissions of permissionsList) {
574
+ for (const [resource, actions] of Object.entries(permissions) as [
575
+ keyof Permissions,
576
+ Permissions[keyof Permissions],
577
+ ][]) {
578
+ if (actions == null) {
579
+ continue;
580
+ }
581
+
582
+ const mergedActions: Record<string, boolean> = {
583
+ ...(result[resource] as Record<string, boolean> | undefined),
584
+ };
585
+ for (const [action, granted] of Object.entries(actions)) {
586
+ if (granted === true) {
587
+ mergedActions[action] = true;
588
+ }
589
+ }
590
+ result[resource] = mergedActions as Permissions[keyof Permissions];
591
+ }
592
+ }
593
+
594
+ return result;
595
+ }
596
+
597
+ /**
598
+ * Returns a copy of {@link permissions} containing only `true` grants.
599
+ */
600
+ export function compactPermissions(permissions: Permissions): Permissions {
601
+ const result: Permissions = {};
602
+
603
+ for (const [resource, actions] of Object.entries(permissions) as [
604
+ keyof Permissions,
605
+ Permissions[keyof Permissions],
606
+ ][]) {
607
+ if (actions == null) {
608
+ continue;
609
+ }
610
+
611
+ const grantedActions: Record<string, true> = {};
612
+ for (const [action, granted] of Object.entries(actions)) {
613
+ if (granted === true) {
614
+ grantedActions[action] = true;
615
+ }
616
+ }
617
+
618
+ if (Object.keys(grantedActions).length > 0) {
619
+ result[resource] = grantedActions as Permissions[keyof Permissions];
620
+ }
621
+ }
622
+
623
+ return result;
624
+ }
625
+
626
+ /**
627
+ * Whether {@link permissions} is a valid permissions object against {@link Permissions}.
628
+ * @remarks Requires at least one `true` grant. Unknown resources or actions are invalid.
629
+ */
630
+ export function isValidPermissions(permissions: unknown): permissions is Permissions {
631
+ if (permissions === null || typeof permissions !== "object" || Array.isArray(permissions)) {
632
+ return false;
633
+ }
634
+
635
+ let hasGrantedPermission = false;
636
+
637
+ for (const [resource, actions] of Object.entries(permissions)) {
638
+ if (
639
+ !PERMISSION_RESOURCES.has(resource) ||
640
+ actions === null ||
641
+ typeof actions !== "object" ||
642
+ Array.isArray(actions)
643
+ ) {
644
+ return false;
645
+ }
646
+
647
+ const allowedActions = new Set<string>(RESOURCE_ACTIONS[resource as PermissionResource]);
648
+
649
+ for (const [action, granted] of Object.entries(actions as Record<string, unknown>)) {
650
+ if (!allowedActions.has(action)) {
651
+ return false;
652
+ }
653
+
654
+ if (granted === true) {
655
+ hasGrantedPermission = true;
656
+ continue;
657
+ }
658
+
659
+ if (granted !== false && granted != null) {
660
+ return false;
661
+ }
662
+ }
663
+ }
664
+
665
+ return hasGrantedPermission;
666
+ }
667
+
668
+ /**
669
+ * Returns the permissions from {@link permissions} that are not granted by {@link availablePermissions}.
670
+ */
671
+ export function getMissingPermissions(availablePermissions: Permissions, permissions: Permissions): Permissions {
672
+ const result: Permissions = {};
673
+
674
+ for (const [resource, actions] of Object.entries(permissions)) {
675
+ if (actions == null) {
676
+ continue;
677
+ }
678
+
679
+ for (const [action, granted] of Object.entries(actions)) {
680
+ if (
681
+ granted === true &&
682
+ availablePermissions[resource as PermissionResource]?.[action as PermissionAction] !== true
683
+ ) {
684
+ result[resource as PermissionResource] ??= {};
685
+ result[resource as PermissionResource]![action as PermissionAction] = true;
686
+ }
687
+ }
688
+ }
689
+
690
+ return result;
691
+ }
692
+
693
+ /**
694
+ * Whether {@link availablePermissions} contains all granted permissions in {@link permissions}.
695
+ */
696
+ export function hasPermissions(availablePermissions: Permissions, permissions: Permissions): boolean {
697
+ return Object.keys(getMissingPermissions(availablePermissions, permissions)).length === 0;
698
+ }
@@ -1,7 +0,0 @@
1
- export const enum ActionType {
2
- MANAGE = "manage",
3
- CREATE = "create",
4
- READ = "read",
5
- UPDATE = "update",
6
- REMOVE = "remove",
7
- }