@lightdash/common 0.2242.2 → 0.2243.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.
Files changed (43) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/authorization/roleToScopeMapping.js +1 -1
  3. package/dist/cjs/authorization/roleToScopeMapping.js.map +1 -1
  4. package/dist/cjs/authorization/scopeAbilityBuilder.test.js +1214 -1210
  5. package/dist/cjs/authorization/scopeAbilityBuilder.test.js.map +1 -1
  6. package/dist/cjs/authorization/scopes.d.ts.map +1 -1
  7. package/dist/cjs/authorization/scopes.js +3 -5
  8. package/dist/cjs/authorization/scopes.js.map +1 -1
  9. package/dist/cjs/types/scopes.d.ts +3 -2
  10. package/dist/cjs/types/scopes.d.ts.map +1 -1
  11. package/dist/cjs/utils/item.d.ts +0 -1
  12. package/dist/cjs/utils/item.d.ts.map +1 -1
  13. package/dist/cjs/utils/item.js +1 -10
  14. package/dist/cjs/utils/item.js.map +1 -1
  15. package/dist/esm/.tsbuildinfo +1 -1
  16. package/dist/esm/authorization/roleToScopeMapping.js +1 -1
  17. package/dist/esm/authorization/roleToScopeMapping.js.map +1 -1
  18. package/dist/esm/authorization/scopeAbilityBuilder.test.js +1214 -1210
  19. package/dist/esm/authorization/scopeAbilityBuilder.test.js.map +1 -1
  20. package/dist/esm/authorization/scopes.d.ts.map +1 -1
  21. package/dist/esm/authorization/scopes.js +3 -5
  22. package/dist/esm/authorization/scopes.js.map +1 -1
  23. package/dist/esm/types/scopes.d.ts +3 -2
  24. package/dist/esm/types/scopes.d.ts.map +1 -1
  25. package/dist/esm/utils/item.d.ts +0 -1
  26. package/dist/esm/utils/item.d.ts.map +1 -1
  27. package/dist/esm/utils/item.js +0 -8
  28. package/dist/esm/utils/item.js.map +1 -1
  29. package/dist/types/.tsbuildinfo +1 -1
  30. package/dist/types/authorization/roleToScopeMapping.js +1 -1
  31. package/dist/types/authorization/roleToScopeMapping.js.map +1 -1
  32. package/dist/types/authorization/scopeAbilityBuilder.test.js +1214 -1210
  33. package/dist/types/authorization/scopeAbilityBuilder.test.js.map +1 -1
  34. package/dist/types/authorization/scopes.d.ts.map +1 -1
  35. package/dist/types/authorization/scopes.js +3 -5
  36. package/dist/types/authorization/scopes.js.map +1 -1
  37. package/dist/types/types/scopes.d.ts +3 -2
  38. package/dist/types/types/scopes.d.ts.map +1 -1
  39. package/dist/types/utils/item.d.ts +0 -1
  40. package/dist/types/utils/item.d.ts.map +1 -1
  41. package/dist/types/utils/item.js +0 -8
  42. package/dist/types/utils/item.js.map +1 -1
  43. package/package.json +1 -1
@@ -3,1345 +3,1349 @@ import { ProjectType } from '../types/projects';
3
3
  import { SpaceMemberRole } from '../types/space';
4
4
  import { buildAbilityFromScopes } from './scopeAbilityBuilder';
5
5
  describe('scopeAbilityBuilder', () => {
6
- describe('buildAbilityFromScopes', () => {
7
- const baseContext = {
8
- isEnterprise: false,
9
- organizationRole: 'admin',
6
+ const baseContext = {
7
+ isEnterprise: false,
8
+ organizationRole: 'admin',
9
+ projectUuid: 'project-123',
10
+ userUuid: 'user1',
11
+ scopes: [],
12
+ };
13
+ const baseContextWithOrg = {
14
+ ...baseContext,
15
+ projectUuid: undefined,
16
+ organizationUuid: 'org-123',
17
+ };
18
+ it('should build ability with organization view permissions', () => {
19
+ const builder = new AbilityBuilder(Ability);
20
+ buildAbilityFromScopes({
21
+ ...baseContextWithOrg,
22
+ scopes: ['view:Organization'],
23
+ }, builder);
24
+ const ability = builder.build();
25
+ expect(ability.can('view', subject('Organization', {
26
+ organizationUuid: 'org-123',
27
+ projectUuid: 'project-123',
28
+ }))).toBe(true);
29
+ expect(ability.can('view', subject('Organization', {
30
+ organizationUuid: 'different-org',
31
+ projectUuid: 'project-123',
32
+ }))).toBe(false);
33
+ });
34
+ it('should build ability with dashboard view permissions', () => {
35
+ const builder = new AbilityBuilder(Ability);
36
+ buildAbilityFromScopes({
37
+ ...baseContext,
38
+ scopes: ['view:Dashboard'],
39
+ }, builder);
40
+ const ability = builder.build();
41
+ // Should be able to view public dashboards
42
+ expect(ability.can('view', subject('Dashboard', {
43
+ organizationUuid: 'org-123',
10
44
  projectUuid: 'project-123',
11
- userUuid: 'user1',
12
- scopes: [],
45
+ isPrivate: false,
46
+ }))).toBe(true);
47
+ // Should not be able to view private dashboards without user context
48
+ expect(ability.can('view', subject('Dashboard', {
49
+ organizationUuid: 'org-123',
50
+ projectUuid: 'project-123',
51
+ isPrivate: true,
52
+ }))).toBe(false);
53
+ });
54
+ it('should build ability with dashboard permissions for user with space access', () => {
55
+ const contextWithUser = {
56
+ ...baseContext,
57
+ userUuid: 'user-456',
58
+ };
59
+ const builder = new AbilityBuilder(Ability);
60
+ buildAbilityFromScopes({
61
+ ...contextWithUser,
62
+ scopes: ['view:Dashboard'],
63
+ }, builder);
64
+ const ability = builder.build();
65
+ // Can view dashboards with user access
66
+ expect(ability.can('view', subject('Dashboard', {
67
+ organizationUuid: 'org-123',
68
+ projectUuid: 'project-123',
69
+ access: [{ userUuid: 'user-456' }],
70
+ }))).toBe(true);
71
+ });
72
+ it('should build ability with project-scoped permissions', () => {
73
+ const projectContext = {
74
+ ...baseContext,
75
+ projectUuid: 'project-789',
76
+ };
77
+ const builder = new AbilityBuilder(Ability);
78
+ buildAbilityFromScopes({
79
+ ...projectContext,
80
+ scopes: ['view:Project'],
81
+ }, builder);
82
+ const ability = builder.build();
83
+ expect(ability.can('view', subject('Project', {
84
+ organizationUuid: 'org-123',
85
+ projectUuid: 'project-789',
86
+ }))).toBe(true);
87
+ });
88
+ it('should build ability with project preview creation permissions', () => {
89
+ const builder = new AbilityBuilder(Ability);
90
+ buildAbilityFromScopes({
91
+ ...baseContext,
92
+ scopes: ['create:Project@preview'],
93
+ }, builder);
94
+ const ability = builder.build();
95
+ // Can create preview projects
96
+ expect(ability.can('create', subject('Project', {
97
+ upstreamProjectUuid: 'project-123',
98
+ type: ProjectType.PREVIEW,
99
+ }))).toBe(true);
100
+ // Cannot create default projects with basic scope
101
+ expect(ability.can('create', subject('Project', {
102
+ organizationUuid: 'org-123',
103
+ type: ProjectType.DEFAULT,
104
+ }))).toBe(false);
105
+ });
106
+ it('cannot create projects by default', () => {
107
+ const builder = new AbilityBuilder(Ability);
108
+ buildAbilityFromScopes(baseContext, builder);
109
+ const ability = builder.build();
110
+ expect(ability.can('create', subject('Project', {
111
+ upstreamProjectUuid: 'project-123',
112
+ type: ProjectType.PREVIEW,
113
+ }))).toBe(false);
114
+ expect(ability.can('create', subject('Project', {
115
+ organizationUuid: 'org-123',
116
+ type: ProjectType.DEFAULT,
117
+ }))).toBe(false);
118
+ });
119
+ it('should build ability with editor permissions for dashboards', () => {
120
+ const editorContext = {
121
+ ...baseContextWithOrg,
122
+ userUuid: 'user-456',
123
+ };
124
+ const builder = new AbilityBuilder(Ability);
125
+ buildAbilityFromScopes({
126
+ ...editorContext,
127
+ scopes: ['manage:Dashboard'],
128
+ }, builder);
129
+ const ability = builder.build();
130
+ // Can manage dashboards where user is editor
131
+ expect(ability.can('manage', subject('Dashboard', {
132
+ organizationUuid: 'org-123',
133
+ access: [
134
+ {
135
+ userUuid: 'user-456',
136
+ role: SpaceMemberRole.EDITOR,
137
+ },
138
+ ],
139
+ }))).toBe(true);
140
+ });
141
+ it('should build ability with admin permissions for spaces', () => {
142
+ const adminContext = {
143
+ ...baseContextWithOrg,
144
+ userUuid: 'user-456',
145
+ };
146
+ const builder = new AbilityBuilder(Ability);
147
+ buildAbilityFromScopes({
148
+ ...adminContext,
149
+ scopes: ['manage:Space'],
150
+ }, builder);
151
+ const ability = builder.build();
152
+ // Can manage spaces where user is admin
153
+ expect(ability.can('manage', subject('Space', {
154
+ organizationUuid: 'org-123',
155
+ access: [
156
+ {
157
+ userUuid: 'user-456',
158
+ role: SpaceMemberRole.ADMIN,
159
+ },
160
+ ],
161
+ }))).toBe(true);
162
+ });
163
+ it('should build ability with user-specific job status permissions', () => {
164
+ const userContext = {
165
+ ...baseContext,
166
+ userUuid: 'user-456',
167
+ };
168
+ const builder = new AbilityBuilder(Ability);
169
+ buildAbilityFromScopes({
170
+ ...userContext,
171
+ scopes: ['view:JobStatus@self'],
172
+ }, builder);
173
+ const ability = builder.build();
174
+ // Can view job status created by the user
175
+ expect(ability.can('view', subject('JobStatus', {
176
+ createdByUserUuid: 'user-456',
177
+ }))).toBe(true);
178
+ // Cannot view job status created by another user
179
+ expect(ability.can('view', subject('JobStatus', {
180
+ createdByUserUuid: 'other-user',
181
+ }))).toBe(false);
182
+ });
183
+ it('should build ability with AI agent thread permissions for enterprise users', () => {
184
+ const userContext = {
185
+ ...baseContext,
186
+ userUuid: 'user-456',
187
+ isEnterprise: true,
13
188
  };
14
- const baseContextWithOrg = {
189
+ const builder = new AbilityBuilder(Ability);
190
+ buildAbilityFromScopes({
191
+ ...userContext,
192
+ scopes: ['manage:AiAgentThread@self'],
193
+ }, builder);
194
+ const ability = builder.build();
195
+ // Can manage user's own AI agent threads
196
+ expect(ability.can('manage', subject('AiAgentThread', {
197
+ projectUuid: 'project-123',
198
+ userUuid: 'user-456',
199
+ }))).toBe(true);
200
+ // Cannot manage another user's threads
201
+ expect(ability.can('manage', subject('AiAgentThread', {
202
+ projectUuid: 'project-123',
203
+ userUuid: 'other-user',
204
+ }))).toBe(false);
205
+ });
206
+ it('should build ability with basic permissions for scopes without custom logic', () => {
207
+ // These scopes don't have custom applyConditions
208
+ const builder = new AbilityBuilder(Ability);
209
+ buildAbilityFromScopes({
15
210
  ...baseContext,
16
- projectUuid: undefined,
211
+ scopes: ['view:Analytics', 'manage:Tags'],
212
+ }, builder);
213
+ const ability = builder.build();
214
+ expect(ability.can('view', subject('Analytics', {
215
+ organizationUuid: 'org-123',
216
+ projectUuid: 'project-123',
217
+ }))).toBe(true);
218
+ expect(ability.can('manage', subject('Tags', {
219
+ organizationUuid: 'org-123',
220
+ projectUuid: 'project-123',
221
+ }))).toBe(true);
222
+ });
223
+ it('should handle unknown scopes gracefully', () => {
224
+ const builder = new AbilityBuilder(Ability);
225
+ buildAbilityFromScopes(baseContext, builder);
226
+ const ability = builder.build();
227
+ // Unknown scope should not add any abilities
228
+ expect(ability.rules.length).toBe(0);
229
+ });
230
+ it('should handle enterprise scopes when not enterprise', () => {
231
+ const nonEnterpriseContext = {
232
+ ...baseContext,
233
+ isEnterprise: false,
234
+ };
235
+ const builder = new AbilityBuilder(Ability);
236
+ buildAbilityFromScopes(nonEnterpriseContext, builder);
237
+ const ability = builder.build();
238
+ // Enterprise scope should not add abilities in non-enterprise context
239
+ expect(ability.rules.length).toBe(0);
240
+ });
241
+ it('should build a complete ability from multiple scopes', () => {
242
+ const context = {
243
+ userUuid: 'user-456',
17
244
  organizationUuid: 'org-123',
245
+ isEnterprise: false,
246
+ organizationRole: 'admin',
247
+ scopes: ['view:Dashboard', 'manage:SavedChart', 'view:Project'],
18
248
  };
19
- it('should build ability with organization view permissions', () => {
249
+ const builder = new AbilityBuilder(Ability);
250
+ buildAbilityFromScopes(context, builder);
251
+ const ability = builder.build();
252
+ // Check that all abilities were applied
253
+ expect(ability.can('view', subject('Dashboard', {
254
+ organizationUuid: 'org-123',
255
+ projectUuid: 'project-789',
256
+ isPrivate: false,
257
+ }))).toBe(true);
258
+ // Should be able to manage saved charts with proper access
259
+ expect(ability.can('manage', subject('SavedChart', {
260
+ organizationUuid: 'org-123',
261
+ access: [
262
+ {
263
+ userUuid: 'user-456',
264
+ role: SpaceMemberRole.EDITOR,
265
+ },
266
+ ],
267
+ }))).toBe(true);
268
+ expect(ability.can('view', subject('Project', {
269
+ organizationUuid: 'org-123',
270
+ projectUuid: 'project-789',
271
+ }))).toBe(true);
272
+ });
273
+ describe('scope dependency checks', () => {
274
+ it('should apply organization-level permissions when manage:organization scope is present', () => {
275
+ const contextWithOrgManage = {
276
+ ...baseContext,
277
+ userUuid: 'user-456',
278
+ scopes: ['manage:Organization', 'manage:SavedChart'],
279
+ };
20
280
  const builder = new AbilityBuilder(Ability);
21
- buildAbilityFromScopes({
22
- ...baseContextWithOrg,
23
- scopes: ['view:Organization'],
24
- }, builder);
281
+ buildAbilityFromScopes(contextWithOrgManage, builder);
25
282
  const ability = builder.build();
26
- expect(ability.can('view', subject('Organization', {
283
+ // Should have organization-wide permissions for saved charts
284
+ expect(ability.can('manage', subject('SavedChart', {
27
285
  organizationUuid: 'org-123',
28
286
  projectUuid: 'project-123',
29
287
  }))).toBe(true);
30
- expect(ability.can('view', subject('Organization', {
31
- organizationUuid: 'different-org',
32
- projectUuid: 'project-123',
33
- }))).toBe(false);
34
- });
35
- it('should build ability with dashboard view permissions', () => {
36
- const builder = new AbilityBuilder(Ability);
37
- buildAbilityFromScopes({
38
- ...baseContext,
39
- scopes: ['view:Dashboard'],
40
- }, builder);
41
- const ability = builder.build();
42
- // Should be able to view public dashboards
43
- expect(ability.can('view', subject('Dashboard', {
288
+ // Should not require user access restrictions
289
+ expect(ability.can('manage', subject('SavedChart', {
44
290
  organizationUuid: 'org-123',
45
291
  projectUuid: 'project-123',
46
- isPrivate: false,
292
+ access: [{ userUuid: 'other-user' }],
47
293
  }))).toBe(true);
48
- // Should not be able to view private dashboards without user context
49
- expect(ability.can('view', subject('Dashboard', {
50
- organizationUuid: 'org-123',
51
- projectUuid: 'project-123',
52
- isPrivate: true,
53
- }))).toBe(false);
54
294
  });
55
- it('should build ability with dashboard permissions for user with space access', () => {
56
- const contextWithUser = {
295
+ it('should apply user-restricted permissions when manage:organization scope is not present', () => {
296
+ const contextWithoutOrgManage = {
57
297
  ...baseContext,
58
298
  userUuid: 'user-456',
299
+ scopes: ['manage:SavedChart@space'],
59
300
  };
60
301
  const builder = new AbilityBuilder(Ability);
61
- buildAbilityFromScopes({
62
- ...contextWithUser,
63
- scopes: ['view:Dashboard'],
64
- }, builder);
302
+ buildAbilityFromScopes(contextWithoutOrgManage, builder);
65
303
  const ability = builder.build();
66
- // Can view dashboards with user access
67
- expect(ability.can('view', subject('Dashboard', {
304
+ // Should require user access restrictions
305
+ expect(ability.can('manage', subject('SavedChart', {
68
306
  organizationUuid: 'org-123',
69
307
  projectUuid: 'project-123',
70
- access: [{ userUuid: 'user-456' }],
71
- }))).toBe(true);
72
- });
73
- it('should build ability with project-scoped permissions', () => {
74
- const projectContext = {
75
- ...baseContext,
76
- projectUuid: 'project-789',
77
- };
78
- const builder = new AbilityBuilder(Ability);
79
- buildAbilityFromScopes({
80
- ...projectContext,
81
- scopes: ['view:Project'],
82
- }, builder);
83
- const ability = builder.build();
84
- expect(ability.can('view', subject('Project', {
85
- organizationUuid: 'org-123',
86
- projectUuid: 'project-789',
87
- }))).toBe(true);
88
- });
89
- it('should build ability with project creation permissions and type restrictions', () => {
90
- const builder = new AbilityBuilder(Ability);
91
- buildAbilityFromScopes({
92
- ...baseContextWithOrg,
93
- scopes: ['create:Project'],
94
- }, builder);
95
- const ability = builder.build();
96
- // Can create preview projects
97
- expect(ability.can('create', subject('Project', {
98
- organizationUuid: 'org-123',
99
- type: ProjectType.PREVIEW,
308
+ access: [
309
+ {
310
+ userUuid: 'user-456',
311
+ role: SpaceMemberRole.EDITOR,
312
+ },
313
+ ],
100
314
  }))).toBe(true);
101
- // Cannot create default projects with basic scope
102
- expect(ability.can('create', subject('Project', {
315
+ // Should not allow access to other users' charts
316
+ expect(ability.can('manage', subject('SavedChart', {
103
317
  organizationUuid: 'org-123',
104
- type: ProjectType.DEFAULT,
318
+ projectUuid: 'project-123',
319
+ access: [{ userUuid: 'other-user' }],
105
320
  }))).toBe(false);
106
321
  });
107
- it('should build ability with editor permissions for dashboards', () => {
108
- const editorContext = {
109
- ...baseContextWithOrg,
322
+ it('should handle space management with different scope combinations', () => {
323
+ const contextWithProjectManage = {
324
+ ...baseContext,
110
325
  userUuid: 'user-456',
326
+ scopes: ['manage:Project', 'manage:Space'],
111
327
  };
112
328
  const builder = new AbilityBuilder(Ability);
113
- buildAbilityFromScopes({
114
- ...editorContext,
115
- scopes: ['manage:Dashboard'],
116
- }, builder);
329
+ buildAbilityFromScopes(contextWithProjectManage, builder);
117
330
  const ability = builder.build();
118
- // Can manage dashboards where user is editor
119
- expect(ability.can('manage', subject('Dashboard', {
331
+ // Should allow managing public spaces when user has project management
332
+ expect(ability.can('manage', subject('Space', {
333
+ organizationUuid: 'org-123',
334
+ projectUuid: 'project-123',
335
+ isPrivate: false,
336
+ }))).toBe(true);
337
+ // Should still allow managing spaces where user is admin
338
+ expect(ability.can('manage', subject('Space', {
120
339
  organizationUuid: 'org-123',
340
+ projectUuid: 'project-123',
121
341
  access: [
122
342
  {
123
343
  userUuid: 'user-456',
124
- role: SpaceMemberRole.EDITOR,
344
+ role: SpaceMemberRole.ADMIN,
125
345
  },
126
346
  ],
127
347
  }))).toBe(true);
128
348
  });
129
- it('should build ability with admin permissions for spaces', () => {
130
- const adminContext = {
131
- ...baseContextWithOrg,
349
+ it('should handle promotion permissions based on organization scope', () => {
350
+ const contextWithOrgManage = {
351
+ ...baseContext,
132
352
  userUuid: 'user-456',
353
+ scopes: ['manage:Organization', 'promote:Dashboard'],
133
354
  };
355
+ const contextWithoutOrgManage = {
356
+ ...baseContext,
357
+ userUuid: 'user-456',
358
+ scopes: ['promote:Dashboard@space'],
359
+ };
360
+ // Test dashboard promotion with organization management
134
361
  const builder = new AbilityBuilder(Ability);
135
- buildAbilityFromScopes({
136
- ...adminContext,
137
- scopes: ['manage:Space'],
138
- }, builder);
139
- const ability = builder.build();
140
- // Can manage spaces where user is admin
141
- expect(ability.can('manage', subject('Space', {
362
+ buildAbilityFromScopes(contextWithOrgManage, builder);
363
+ const abilityWithOrg = builder.build();
364
+ expect(abilityWithOrg.can('promote', subject('Dashboard', {
365
+ organizationUuid: 'org-123',
366
+ projectUuid: 'project-123',
367
+ }))).toBe(true);
368
+ // Test dashboard promotion without organization management
369
+ const builderWithoutOrg = new AbilityBuilder(Ability);
370
+ buildAbilityFromScopes(contextWithoutOrgManage, builderWithoutOrg);
371
+ const abilityWithoutOrg = builderWithoutOrg.build();
372
+ expect(abilityWithoutOrg.can('promote', subject('Dashboard', {
142
373
  organizationUuid: 'org-123',
374
+ projectUuid: 'project-123',
143
375
  access: [
144
376
  {
145
377
  userUuid: 'user-456',
146
- role: SpaceMemberRole.ADMIN,
378
+ role: SpaceMemberRole.EDITOR,
147
379
  },
148
380
  ],
149
381
  }))).toBe(true);
382
+ // Should not allow promotion without proper access
383
+ expect(abilityWithoutOrg.can('promote', subject('Dashboard', {
384
+ organizationUuid: 'org-123',
385
+ projectUuid: 'project-123',
386
+ access: [{ userUuid: 'other-user' }],
387
+ }))).toBe(false);
150
388
  });
151
- it('should build ability with user-specific job status permissions', () => {
152
- const userContext = {
389
+ });
390
+ describe('AI agent thread permissions with modifiers', () => {
391
+ it('should handle view:ai_agent_thread@self permissions', () => {
392
+ const contextWithUser = {
153
393
  ...baseContext,
154
394
  userUuid: 'user-456',
395
+ isEnterprise: true,
155
396
  };
156
397
  const builder = new AbilityBuilder(Ability);
157
398
  buildAbilityFromScopes({
158
- ...userContext,
159
- scopes: ['view:JobStatus@self'],
399
+ ...contextWithUser,
400
+ scopes: ['view:AiAgentThread@self'],
160
401
  }, builder);
161
402
  const ability = builder.build();
162
- // Can view job status created by the user
163
- expect(ability.can('view', subject('JobStatus', {
164
- createdByUserUuid: 'user-456',
403
+ // Can view own AI agent threads
404
+ expect(ability.can('view', subject('AiAgentThread', {
405
+ projectUuid: 'project-123',
406
+ userUuid: 'user-456',
165
407
  }))).toBe(true);
166
- // Cannot view job status created by another user
167
- expect(ability.can('view', subject('JobStatus', {
168
- createdByUserUuid: 'other-user',
408
+ // Cannot view other users' threads
409
+ expect(ability.can('view', subject('AiAgentThread', {
410
+ projectUuid: 'project-123',
411
+ userUuid: 'other-user',
169
412
  }))).toBe(false);
170
413
  });
171
- it('should build ability with AI agent thread permissions for enterprise users', () => {
172
- const userContext = {
414
+ it('should handle manage:ai_agent_thread@self permissions', () => {
415
+ const contextWithUser = {
173
416
  ...baseContext,
174
417
  userUuid: 'user-456',
175
418
  isEnterprise: true,
176
419
  };
177
420
  const builder = new AbilityBuilder(Ability);
178
421
  buildAbilityFromScopes({
179
- ...userContext,
422
+ ...contextWithUser,
423
+ userUuid: 'user-456',
180
424
  scopes: ['manage:AiAgentThread@self'],
181
425
  }, builder);
182
426
  const ability = builder.build();
183
- // Can manage user's own AI agent threads
427
+ // Can manage own AI agent threads
184
428
  expect(ability.can('manage', subject('AiAgentThread', {
185
429
  projectUuid: 'project-123',
186
430
  userUuid: 'user-456',
187
431
  }))).toBe(true);
188
- // Cannot manage another user's threads
432
+ // Cannot manage other users' threads
189
433
  expect(ability.can('manage', subject('AiAgentThread', {
190
- projectUuid: 'project-123',
191
434
  userUuid: 'other-user',
192
435
  }))).toBe(false);
193
436
  });
194
- it('should build ability with basic permissions for scopes without custom logic', () => {
195
- // These scopes don't have custom applyConditions
437
+ it('should handle view:ai_agent_thread permissions for all threads', () => {
196
438
  const builder = new AbilityBuilder(Ability);
197
439
  buildAbilityFromScopes({
198
- ...baseContext,
199
- scopes: ['view:Analytics', 'manage:Tags'],
440
+ ...baseContextWithOrg,
441
+ isEnterprise: true,
442
+ scopes: ['view:AiAgentThread'],
200
443
  }, builder);
201
444
  const ability = builder.build();
202
- expect(ability.can('view', subject('Analytics', {
445
+ // Can view any AI agent thread
446
+ expect(ability.can('view', subject('AiAgentThread', {
203
447
  organizationUuid: 'org-123',
204
- projectUuid: 'project-123',
448
+ userUuid: 'any-user',
205
449
  }))).toBe(true);
206
- expect(ability.can('manage', subject('Tags', {
450
+ });
451
+ it('should handle manage:ai_agent_thread permissions for all threads', () => {
452
+ const builder = new AbilityBuilder(Ability);
453
+ buildAbilityFromScopes({
454
+ ...baseContextWithOrg,
455
+ isEnterprise: true,
456
+ scopes: ['manage:AiAgentThread'],
457
+ }, builder);
458
+ const ability = builder.build();
459
+ // Can manage any AI agent thread
460
+ expect(ability.can('manage', subject('AiAgentThread', {
207
461
  organizationUuid: 'org-123',
208
- projectUuid: 'project-123',
462
+ userUuid: 'any-user',
209
463
  }))).toBe(true);
210
464
  });
211
- it('should handle unknown scopes gracefully', () => {
465
+ });
466
+ describe('edge cases and error handling', () => {
467
+ it('should handle empty scope array', () => {
212
468
  const builder = new AbilityBuilder(Ability);
213
469
  buildAbilityFromScopes(baseContext, builder);
214
470
  const ability = builder.build();
215
- // Unknown scope should not add any abilities
216
471
  expect(ability.rules.length).toBe(0);
217
472
  });
218
- it('should handle enterprise scopes when not enterprise', () => {
219
- const nonEnterpriseContext = {
473
+ it('should handle undefined userUuid in context', () => {
474
+ const contextWithoutUser = {
220
475
  ...baseContext,
221
- isEnterprise: false,
222
476
  };
223
477
  const builder = new AbilityBuilder(Ability);
224
- buildAbilityFromScopes(nonEnterpriseContext, builder);
478
+ buildAbilityFromScopes({
479
+ ...contextWithoutUser,
480
+ scopes: ['view:Dashboard'],
481
+ }, builder);
225
482
  const ability = builder.build();
226
- // Enterprise scope should not add abilities in non-enterprise context
227
- expect(ability.rules.length).toBe(0);
228
- });
229
- it('should build a complete ability from multiple scopes', () => {
230
- const context = {
231
- userUuid: 'user-456',
483
+ // Should only allow viewing public dashboards
484
+ expect(ability.can('view', subject('Dashboard', {
232
485
  organizationUuid: 'org-123',
233
- isEnterprise: false,
234
- organizationRole: 'admin',
235
- scopes: ['view:Dashboard', 'manage:SavedChart', 'view:Project'],
236
- };
486
+ projectUuid: 'project-123',
487
+ isPrivate: false,
488
+ }))).toBe(true);
489
+ expect(ability.can('view', subject('Dashboard', {
490
+ organizationUuid: 'org-123',
491
+ projectUuid: 'project-123',
492
+ isPrivate: true,
493
+ }))).toBe(false);
494
+ });
495
+ it('should handle mixed valid and invalid scopes', () => {
496
+ const builder = new AbilityBuilder(Ability);
497
+ buildAbilityFromScopes({
498
+ ...baseContext,
499
+ scopes: ['view:Dashboard', 'view:Project', 'invalid:scope'],
500
+ }, builder);
501
+ const ability = builder.build();
502
+ // We have 3 valid rules, 2 for dashboard and 1 for project, dropping the invalid scope
503
+ expect(ability.rules.length).toBe(3);
504
+ expect(ability.rules.filter((r) => r.subject === 'Dashboard')).toHaveLength(2);
505
+ expect(ability.rules.find((r) => r.subject === 'Project')).toBeDefined();
506
+ });
507
+ });
508
+ describe('cross-boundary access tests', () => {
509
+ it('should not allow access to resources from different organizations', () => {
237
510
  const builder = new AbilityBuilder(Ability);
238
- buildAbilityFromScopes(context, builder);
511
+ buildAbilityFromScopes({
512
+ ...baseContext,
513
+ scopes: [
514
+ 'view:Dashboard',
515
+ 'manage:SavedChart',
516
+ 'view:Space',
517
+ ],
518
+ }, builder);
239
519
  const ability = builder.build();
240
- // Check that all abilities were applied
520
+ // Should not access dashboard from different org
241
521
  expect(ability.can('view', subject('Dashboard', {
242
- organizationUuid: 'org-123',
243
- projectUuid: 'project-789',
522
+ organizationUuid: 'different-org',
244
523
  isPrivate: false,
245
- }))).toBe(true);
246
- // Should be able to manage saved charts with proper access
524
+ }))).toBe(false);
525
+ // Should not manage saved chart from different org
247
526
  expect(ability.can('manage', subject('SavedChart', {
527
+ organizationUuid: 'different-org',
528
+ }))).toBe(false);
529
+ // Should not view space from different org
530
+ expect(ability.can('view', subject('Space', {
531
+ organizationUuid: 'different-org',
532
+ isPrivate: false,
533
+ }))).toBe(false);
534
+ });
535
+ it('should not allow access to resources from different projects', () => {
536
+ const builder = new AbilityBuilder(Ability);
537
+ buildAbilityFromScopes({
538
+ ...baseContext,
539
+ scopes: ['view:SavedChart'],
540
+ }, builder);
541
+ const ability = builder.build();
542
+ // Should not access saved chart from different project
543
+ expect(ability.can('view', subject('SavedChart', {
544
+ organizationUuid: 'org-123',
545
+ projectUuid: 'different-project',
546
+ isPrivate: false,
547
+ }))).toBe(false);
548
+ });
549
+ });
550
+ describe('private resource access with space roles', () => {
551
+ it('should handle viewer role access to private resources', () => {
552
+ const contextWithUser = {
553
+ ...baseContextWithOrg,
554
+ userUuid: 'user-456',
555
+ };
556
+ const builder = new AbilityBuilder(Ability);
557
+ buildAbilityFromScopes({
558
+ ...contextWithUser,
559
+ scopes: ['view:Dashboard'],
560
+ }, builder);
561
+ const ability = builder.build();
562
+ // Can view private dashboard with viewer access
563
+ expect(ability.can('view', subject('Dashboard', {
248
564
  organizationUuid: 'org-123',
565
+ isPrivate: true,
249
566
  access: [
250
567
  {
251
568
  userUuid: 'user-456',
252
- role: SpaceMemberRole.EDITOR,
569
+ role: SpaceMemberRole.VIEWER,
253
570
  },
254
571
  ],
255
572
  }))).toBe(true);
256
- expect(ability.can('view', subject('Project', {
573
+ // Cannot view private dashboard without access
574
+ expect(ability.can('view', subject('Dashboard', {
257
575
  organizationUuid: 'org-123',
258
- projectUuid: 'project-789',
259
- }))).toBe(true);
576
+ isPrivate: true,
577
+ access: [],
578
+ }))).toBe(false);
579
+ // Cannot view private dashboard with access for another user
580
+ expect(ability.can('view', subject('Dashboard', {
581
+ organizationUuid: 'org-123',
582
+ isPrivate: true,
583
+ access: [
584
+ {
585
+ userUuid: 'other-user',
586
+ role: SpaceMemberRole.VIEWER,
587
+ },
588
+ ],
589
+ }))).toBe(false);
260
590
  });
261
- describe('scope dependency checks', () => {
262
- it('should apply organization-level permissions when manage:organization scope is present', () => {
263
- const contextWithOrgManage = {
264
- ...baseContext,
265
- userUuid: 'user-456',
266
- scopes: ['manage:Organization', 'manage:SavedChart'],
267
- };
268
- const builder = new AbilityBuilder(Ability);
269
- buildAbilityFromScopes(contextWithOrgManage, builder);
270
- const ability = builder.build();
271
- // Should have organization-wide permissions for saved charts
272
- expect(ability.can('manage', subject('SavedChart', {
273
- organizationUuid: 'org-123',
274
- projectUuid: 'project-123',
275
- }))).toBe(true);
276
- // Should not require user access restrictions
277
- expect(ability.can('manage', subject('SavedChart', {
278
- organizationUuid: 'org-123',
279
- projectUuid: 'project-123',
280
- access: [{ userUuid: 'other-user' }],
281
- }))).toBe(true);
282
- });
283
- it('should apply user-restricted permissions when manage:organization scope is not present', () => {
284
- const contextWithoutOrgManage = {
285
- ...baseContext,
286
- userUuid: 'user-456',
287
- scopes: ['manage:SavedChart@space'],
288
- };
289
- const builder = new AbilityBuilder(Ability);
290
- buildAbilityFromScopes(contextWithoutOrgManage, builder);
291
- const ability = builder.build();
292
- // Should require user access restrictions
293
- expect(ability.can('manage', subject('SavedChart', {
294
- organizationUuid: 'org-123',
295
- projectUuid: 'project-123',
296
- access: [
297
- {
298
- userUuid: 'user-456',
299
- role: SpaceMemberRole.EDITOR,
300
- },
301
- ],
302
- }))).toBe(true);
303
- // Should not allow access to other users' charts
304
- expect(ability.can('manage', subject('SavedChart', {
305
- organizationUuid: 'org-123',
306
- projectUuid: 'project-123',
307
- access: [{ userUuid: 'other-user' }],
308
- }))).toBe(false);
309
- });
310
- it('should handle space management with different scope combinations', () => {
311
- const contextWithProjectManage = {
312
- ...baseContext,
313
- userUuid: 'user-456',
314
- scopes: ['manage:Project', 'manage:Space'],
315
- };
316
- const builder = new AbilityBuilder(Ability);
317
- buildAbilityFromScopes(contextWithProjectManage, builder);
318
- const ability = builder.build();
319
- // Should allow managing public spaces when user has project management
320
- expect(ability.can('manage', subject('Space', {
321
- organizationUuid: 'org-123',
322
- projectUuid: 'project-123',
323
- isPrivate: false,
324
- }))).toBe(true);
325
- // Should still allow managing spaces where user is admin
326
- expect(ability.can('manage', subject('Space', {
327
- organizationUuid: 'org-123',
328
- projectUuid: 'project-123',
329
- access: [
330
- {
331
- userUuid: 'user-456',
332
- role: SpaceMemberRole.ADMIN,
333
- },
334
- ],
335
- }))).toBe(true);
336
- });
337
- it('should handle promotion permissions based on organization scope', () => {
338
- const contextWithOrgManage = {
339
- ...baseContext,
340
- userUuid: 'user-456',
341
- scopes: ['manage:Organization', 'promote:Dashboard'],
342
- };
343
- const contextWithoutOrgManage = {
344
- ...baseContext,
345
- userUuid: 'user-456',
346
- scopes: ['promote:Dashboard@space'],
347
- };
348
- // Test dashboard promotion with organization management
349
- const builder = new AbilityBuilder(Ability);
350
- buildAbilityFromScopes(contextWithOrgManage, builder);
351
- const abilityWithOrg = builder.build();
352
- expect(abilityWithOrg.can('promote', subject('Dashboard', {
353
- organizationUuid: 'org-123',
354
- projectUuid: 'project-123',
355
- }))).toBe(true);
356
- // Test dashboard promotion without organization management
357
- const builderWithoutOrg = new AbilityBuilder(Ability);
358
- buildAbilityFromScopes(contextWithoutOrgManage, builderWithoutOrg);
359
- const abilityWithoutOrg = builderWithoutOrg.build();
360
- expect(abilityWithoutOrg.can('promote', subject('Dashboard', {
361
- organizationUuid: 'org-123',
362
- projectUuid: 'project-123',
363
- access: [
364
- {
365
- userUuid: 'user-456',
366
- role: SpaceMemberRole.EDITOR,
367
- },
368
- ],
369
- }))).toBe(true);
370
- // Should not allow promotion without proper access
371
- expect(abilityWithoutOrg.can('promote', subject('Dashboard', {
372
- organizationUuid: 'org-123',
373
- projectUuid: 'project-123',
374
- access: [{ userUuid: 'other-user' }],
375
- }))).toBe(false);
376
- });
591
+ it('should handle editor role for managing resources', () => {
592
+ const contextWithUser = {
593
+ ...baseContextWithOrg,
594
+ userUuid: 'user-456',
595
+ };
596
+ const builder = new AbilityBuilder(Ability);
597
+ buildAbilityFromScopes({
598
+ ...contextWithUser,
599
+ scopes: ['manage:Dashboard@space'],
600
+ }, builder);
601
+ const ability = builder.build();
602
+ // Can manage dashboard with editor role
603
+ expect(ability.can('manage', subject('Dashboard', {
604
+ organizationUuid: 'org-123',
605
+ access: [
606
+ {
607
+ userUuid: 'user-456',
608
+ role: SpaceMemberRole.EDITOR,
609
+ },
610
+ ],
611
+ }))).toBe(true);
612
+ // Can manage dashboard with admin role
613
+ expect(ability.can('manage', subject('Dashboard', {
614
+ organizationUuid: 'org-123',
615
+ access: [
616
+ {
617
+ userUuid: 'user-456',
618
+ role: SpaceMemberRole.ADMIN,
619
+ },
620
+ ],
621
+ }))).toBe(true);
622
+ // Cannot manage dashboard with viewer role
623
+ expect(ability.can('manage', subject('Dashboard', {
624
+ organizationUuid: 'org-123',
625
+ access: [
626
+ {
627
+ userUuid: 'user-456',
628
+ role: SpaceMemberRole.VIEWER,
629
+ },
630
+ ],
631
+ }))).toBe(false);
632
+ // Cannot manage dashboard without access
633
+ expect(ability.can('manage', subject('Dashboard', {
634
+ organizationUuid: 'org-123',
635
+ access: [],
636
+ }))).toBe(false);
637
+ });
638
+ it('should handle space admin role for managing spaces', () => {
639
+ const contextWithUser = {
640
+ ...baseContext,
641
+ userUuid: 'user-456',
642
+ };
643
+ const builder = new AbilityBuilder(Ability);
644
+ buildAbilityFromScopes({
645
+ ...contextWithUser,
646
+ scopes: ['manage:Space@assigned'],
647
+ }, builder);
648
+ const ability = builder.build();
649
+ // Can manage space with admin role
650
+ expect(ability.can('manage', subject('Space', {
651
+ projectUuid: baseContext.projectUuid,
652
+ access: [
653
+ {
654
+ userUuid: 'user-456',
655
+ role: SpaceMemberRole.ADMIN,
656
+ },
657
+ ],
658
+ }))).toBe(true);
659
+ // Cannot manage space with editor role
660
+ expect(ability.can('manage', subject('Space', {
661
+ organizationUuid: 'org-123',
662
+ isPrivate: true,
663
+ access: [
664
+ {
665
+ userUuid: 'user-456',
666
+ role: SpaceMemberRole.EDITOR,
667
+ },
668
+ ],
669
+ }))).toBe(false);
670
+ // Cannot manage space with viewer role
671
+ expect(ability.can('manage', subject('Space', {
672
+ organizationUuid: 'org-123',
673
+ isPrivate: true,
674
+ access: [
675
+ {
676
+ userUuid: 'user-456',
677
+ role: SpaceMemberRole.VIEWER,
678
+ },
679
+ ],
680
+ }))).toBe(false);
681
+ });
682
+ });
683
+ describe('job and job status permissions', () => {
684
+ it('should handle view:job@self permissions', () => {
685
+ const contextWithUser = {
686
+ ...baseContext,
687
+ userUuid: 'user-456',
688
+ };
689
+ const builder = new AbilityBuilder(Ability);
690
+ buildAbilityFromScopes({
691
+ ...contextWithUser,
692
+ scopes: ['view:Job@self'],
693
+ }, builder);
694
+ const ability = builder.build();
695
+ // Can view own jobs
696
+ expect(ability.can('view', subject('Job', {
697
+ userUuid: 'user-456',
698
+ }))).toBe(true);
699
+ // Cannot view other users' jobs without manage permission
700
+ expect(ability.can('view', subject('Job', {
701
+ userUuid: 'other-user',
702
+ }))).toBe(false);
703
+ });
704
+ it('should handle view:job_status@self permissions for user context', () => {
705
+ const builder = new AbilityBuilder(Ability);
706
+ buildAbilityFromScopes({
707
+ ...baseContext,
708
+ userUuid: 'user-456',
709
+ scopes: ['view:JobStatus@self'],
710
+ }, builder);
711
+ const ability = builder.build();
712
+ // Can view own job status
713
+ expect(ability.can('view', subject('JobStatus', {
714
+ createdByUserUuid: 'user-456',
715
+ }))).toBe(true);
716
+ // Cannot view other users' job status
717
+ expect(ability.can('view', subject('JobStatus', {
718
+ createdByUserUuid: 'other-user',
719
+ }))).toBe(false);
720
+ });
721
+ it('should handle view:job_status permissions for all job status', () => {
722
+ const builder = new AbilityBuilder(Ability);
723
+ buildAbilityFromScopes({
724
+ ...baseContextWithOrg,
725
+ scopes: ['view:JobStatus'],
726
+ }, builder);
727
+ const ability = builder.build();
728
+ // Can view all job status in organization
729
+ expect(ability.can('view', subject('JobStatus', {
730
+ organizationUuid: 'org-123',
731
+ }))).toBe(true);
732
+ // Cannot view job status from another organization
733
+ expect(ability.can('view', subject('JobStatus', {
734
+ organizationUuid: 'different-org',
735
+ }))).toBe(false);
736
+ });
737
+ it('should handle view:job permissions for all jobs', () => {
738
+ const builder = new AbilityBuilder(Ability);
739
+ buildAbilityFromScopes({
740
+ ...baseContext,
741
+ scopes: ['view:Job'],
742
+ }, builder);
743
+ const ability = builder.build();
744
+ // Can view any job
745
+ expect(ability.can('view', subject('Job', {
746
+ organizationUuid: 'org-123',
747
+ projectUuid: 'project-123',
748
+ userUuid: 'any-user',
749
+ }))).toBe(true);
750
+ });
751
+ });
752
+ describe('space-based permissions modifiers', () => {
753
+ it('should handle manage:dashboard@space permissions', () => {
754
+ const contextWithUser = {
755
+ ...baseContextWithOrg,
756
+ userUuid: 'user-456',
757
+ };
758
+ const builder = new AbilityBuilder(Ability);
759
+ buildAbilityFromScopes({
760
+ ...contextWithUser,
761
+ scopes: ['manage:Dashboard@space'],
762
+ }, builder);
763
+ const ability = builder.build();
764
+ // Can manage dashboard with editor role
765
+ expect(ability.can('manage', subject('Dashboard', {
766
+ organizationUuid: 'org-123',
767
+ access: [
768
+ {
769
+ userUuid: 'user-456',
770
+ role: SpaceMemberRole.EDITOR,
771
+ },
772
+ ],
773
+ }))).toBe(true);
774
+ // Can manage dashboard with admin role
775
+ expect(ability.can('manage', subject('Dashboard', {
776
+ organizationUuid: 'org-123',
777
+ access: [
778
+ {
779
+ userUuid: 'user-456',
780
+ role: SpaceMemberRole.ADMIN,
781
+ },
782
+ ],
783
+ }))).toBe(true);
784
+ // Cannot manage dashboard with viewer role
785
+ expect(ability.can('manage', subject('Dashboard', {
786
+ organizationUuid: 'org-123',
787
+ access: [
788
+ {
789
+ userUuid: 'user-456',
790
+ role: SpaceMemberRole.VIEWER,
791
+ },
792
+ ],
793
+ }))).toBe(false);
794
+ // Cannot manage dashboard without access
795
+ expect(ability.can('manage', subject('Dashboard', {
796
+ organizationUuid: 'org-123',
797
+ access: [],
798
+ }))).toBe(false);
799
+ });
800
+ it('should handle manage:saved_chart@space permissions', () => {
801
+ const contextWithUser = {
802
+ ...baseContext,
803
+ userUuid: 'user-456',
804
+ };
805
+ const builder = new AbilityBuilder(Ability);
806
+ buildAbilityFromScopes({
807
+ ...contextWithUser,
808
+ scopes: ['manage:SavedChart@space'],
809
+ }, builder);
810
+ const ability = builder.build();
811
+ // Can manage saved chart with editor role
812
+ expect(ability.can('manage', subject('SavedChart', {
813
+ projectUuid: 'project-123',
814
+ access: [
815
+ {
816
+ userUuid: 'user-456',
817
+ role: SpaceMemberRole.EDITOR,
818
+ },
819
+ ],
820
+ }))).toBe(true);
821
+ // Can manage saved chart with admin role
822
+ expect(ability.can('manage', subject('SavedChart', {
823
+ projectUuid: 'project-123',
824
+ access: [
825
+ {
826
+ userUuid: 'user-456',
827
+ role: SpaceMemberRole.ADMIN,
828
+ },
829
+ ],
830
+ }))).toBe(true);
831
+ // Cannot manage without proper access
832
+ expect(ability.can('manage', subject('SavedChart', {
833
+ projectUuid: 'project-123',
834
+ access: [
835
+ {
836
+ userUuid: 'other-user',
837
+ role: SpaceMemberRole.EDITOR,
838
+ },
839
+ ],
840
+ }))).toBe(false);
841
+ });
842
+ it('should handle promote:dashboard@space permissions', () => {
843
+ const contextWithUser = {
844
+ ...baseContext,
845
+ userUuid: 'user-456',
846
+ };
847
+ const builder = new AbilityBuilder(Ability);
848
+ buildAbilityFromScopes({
849
+ ...contextWithUser,
850
+ scopes: ['promote:Dashboard@space'],
851
+ }, builder);
852
+ const ability = builder.build();
853
+ // Can promote dashboard with editor access
854
+ expect(ability.can('promote', subject('Dashboard', {
855
+ projectUuid: 'project-123',
856
+ access: [
857
+ {
858
+ userUuid: 'user-456',
859
+ role: SpaceMemberRole.EDITOR,
860
+ },
861
+ ],
862
+ }))).toBe(true);
863
+ // Cannot promote without editor access
864
+ expect(ability.can('promote', subject('Dashboard', {
865
+ projectUuid: 'project-123',
866
+ access: [
867
+ {
868
+ userUuid: 'user-456',
869
+ role: SpaceMemberRole.VIEWER,
870
+ },
871
+ ],
872
+ }))).toBe(false);
873
+ });
874
+ it('should handle manage:semantic_viewer@space permissions', () => {
875
+ const contextWithUser = {
876
+ ...baseContextWithOrg,
877
+ userUuid: 'user-456',
878
+ };
879
+ const builder = new AbilityBuilder(Ability);
880
+ buildAbilityFromScopes({
881
+ ...contextWithUser,
882
+ scopes: ['manage:SemanticViewer@space'],
883
+ }, builder);
884
+ const ability = builder.build();
885
+ // Can manage semantic viewer with editor role
886
+ expect(ability.can('manage', subject('SemanticViewer', {
887
+ organizationUuid: 'org-123',
888
+ access: [
889
+ {
890
+ userUuid: 'user-456',
891
+ role: SpaceMemberRole.EDITOR,
892
+ },
893
+ ],
894
+ }))).toBe(true);
895
+ // Cannot manage without editor role
896
+ expect(ability.can('manage', subject('SemanticViewer', {
897
+ organizationUuid: 'org-123',
898
+ access: [
899
+ {
900
+ userUuid: 'user-456',
901
+ role: SpaceMemberRole.VIEWER,
902
+ },
903
+ ],
904
+ }))).toBe(false);
905
+ });
906
+ });
907
+ describe('semantic viewer permissions', () => {
908
+ it('should handle view:semantic_viewer permissions', () => {
909
+ const builder = new AbilityBuilder(Ability);
910
+ buildAbilityFromScopes({
911
+ ...baseContext,
912
+ scopes: ['view:SemanticViewer'],
913
+ }, builder);
914
+ const ability = builder.build();
915
+ expect(ability.can('view', subject('SemanticViewer', {
916
+ organizationUuid: 'org-123',
917
+ projectUuid: 'project-123',
918
+ }))).toBe(true);
919
+ });
920
+ it('should handle manage:semantic_viewer with organization scope', () => {
921
+ const contextWithOrgManage = {
922
+ ...baseContextWithOrg,
923
+ userUuid: 'user-456',
924
+ scopes: ['manage:Organization'],
925
+ };
926
+ const builder = new AbilityBuilder(Ability);
927
+ buildAbilityFromScopes({
928
+ ...contextWithOrgManage,
929
+ scopes: ['manage:Organization', 'manage:SemanticViewer'],
930
+ }, builder);
931
+ const ability = builder.build();
932
+ // Can manage semantic viewer organization-wide
933
+ expect(ability.can('manage', subject('SemanticViewer', {
934
+ organizationUuid: 'org-123',
935
+ }))).toBe(true);
936
+ });
937
+ it('should handle manage:semantic_viewer with editor role', () => {
938
+ const contextWithUser = {
939
+ ...baseContextWithOrg,
940
+ userUuid: 'user-456',
941
+ };
942
+ const builder = new AbilityBuilder(Ability);
943
+ buildAbilityFromScopes({
944
+ ...contextWithUser,
945
+ scopes: ['manage:SemanticViewer@space'],
946
+ }, builder);
947
+ const ability = builder.build();
948
+ // Can manage semantic viewer with editor role
949
+ expect(ability.can('manage', subject('SemanticViewer', {
950
+ organizationUuid: 'org-123',
951
+ access: [
952
+ {
953
+ userUuid: 'user-456',
954
+ role: SpaceMemberRole.EDITOR,
955
+ },
956
+ ],
957
+ }))).toBe(true);
958
+ // Cannot manage without proper access
959
+ expect(ability.can('manage', subject('SemanticViewer', {
960
+ organizationUuid: 'org-123',
961
+ access: [
962
+ {
963
+ userUuid: 'user-456',
964
+ role: SpaceMemberRole.VIEWER,
965
+ },
966
+ ],
967
+ }))).toBe(false);
377
968
  });
378
- describe('AI agent thread permissions with modifiers', () => {
379
- it('should handle view:ai_agent_thread@self permissions', () => {
380
- const contextWithUser = {
381
- ...baseContext,
382
- userUuid: 'user-456',
383
- isEnterprise: true,
384
- };
385
- const builder = new AbilityBuilder(Ability);
386
- buildAbilityFromScopes({
387
- ...contextWithUser,
388
- scopes: ['view:AiAgentThread@self'],
389
- }, builder);
390
- const ability = builder.build();
391
- // Can view own AI agent threads
392
- expect(ability.can('view', subject('AiAgentThread', {
393
- projectUuid: 'project-123',
394
- userUuid: 'user-456',
395
- }))).toBe(true);
396
- // Cannot view other users' threads
397
- expect(ability.can('view', subject('AiAgentThread', {
398
- projectUuid: 'project-123',
399
- userUuid: 'other-user',
400
- }))).toBe(false);
401
- });
402
- it('should handle manage:ai_agent_thread@self permissions', () => {
403
- const contextWithUser = {
404
- ...baseContext,
405
- userUuid: 'user-456',
406
- isEnterprise: true,
407
- };
408
- const builder = new AbilityBuilder(Ability);
409
- buildAbilityFromScopes({
410
- ...contextWithUser,
411
- userUuid: 'user-456',
412
- scopes: ['manage:AiAgentThread@self'],
413
- }, builder);
414
- const ability = builder.build();
415
- // Can manage own AI agent threads
416
- expect(ability.can('manage', subject('AiAgentThread', {
417
- projectUuid: 'project-123',
418
- userUuid: 'user-456',
419
- }))).toBe(true);
420
- // Cannot manage other users' threads
421
- expect(ability.can('manage', subject('AiAgentThread', {
422
- userUuid: 'other-user',
423
- }))).toBe(false);
424
- });
425
- it('should handle view:ai_agent_thread permissions for all threads', () => {
426
- const builder = new AbilityBuilder(Ability);
427
- buildAbilityFromScopes({
428
- ...baseContextWithOrg,
429
- isEnterprise: true,
430
- scopes: ['view:AiAgentThread'],
431
- }, builder);
432
- const ability = builder.build();
433
- // Can view any AI agent thread
434
- expect(ability.can('view', subject('AiAgentThread', {
435
- organizationUuid: 'org-123',
436
- userUuid: 'any-user',
437
- }))).toBe(true);
438
- });
439
- it('should handle manage:ai_agent_thread permissions for all threads', () => {
440
- const builder = new AbilityBuilder(Ability);
441
- buildAbilityFromScopes({
442
- ...baseContextWithOrg,
443
- isEnterprise: true,
444
- scopes: ['manage:AiAgentThread'],
445
- }, builder);
446
- const ability = builder.build();
447
- // Can manage any AI agent thread
448
- expect(ability.can('manage', subject('AiAgentThread', {
449
- organizationUuid: 'org-123',
450
- userUuid: 'any-user',
451
- }))).toBe(true);
452
- });
969
+ });
970
+ describe('create space permissions', () => {
971
+ it('should handle create:space permissions', () => {
972
+ const builder = new AbilityBuilder(Ability);
973
+ buildAbilityFromScopes({
974
+ ...baseContext,
975
+ scopes: ['create:Space'],
976
+ }, builder);
977
+ const ability = builder.build();
978
+ expect(ability.can('create', subject('Space', {
979
+ organizationUuid: 'org-123',
980
+ projectUuid: 'project-123',
981
+ }))).toBe(true);
453
982
  });
454
- describe('edge cases and error handling', () => {
455
- it('should handle empty scope array', () => {
456
- const builder = new AbilityBuilder(Ability);
457
- buildAbilityFromScopes(baseContext, builder);
458
- const ability = builder.build();
459
- expect(ability.rules.length).toBe(0);
460
- });
461
- it('should handle undefined userUuid in context', () => {
462
- const contextWithoutUser = {
463
- ...baseContext,
464
- };
465
- const builder = new AbilityBuilder(Ability);
466
- buildAbilityFromScopes({
467
- ...contextWithoutUser,
468
- scopes: ['view:Dashboard'],
469
- }, builder);
470
- const ability = builder.build();
471
- // Should only allow viewing public dashboards
472
- expect(ability.can('view', subject('Dashboard', {
473
- organizationUuid: 'org-123',
474
- projectUuid: 'project-123',
475
- isPrivate: false,
476
- }))).toBe(true);
477
- expect(ability.can('view', subject('Dashboard', {
478
- organizationUuid: 'org-123',
479
- projectUuid: 'project-123',
480
- isPrivate: true,
481
- }))).toBe(false);
482
- });
483
- it('should handle mixed valid and invalid scopes', () => {
484
- const builder = new AbilityBuilder(Ability);
485
- buildAbilityFromScopes({
486
- ...baseContext,
487
- scopes: [
488
- 'view:Dashboard',
489
- 'view:Project',
490
- 'invalid:scope',
491
- ],
492
- }, builder);
493
- const ability = builder.build();
494
- // We have 3 valid rules, 2 for dashboard and 1 for project, dropping the invalid scope
495
- expect(ability.rules.length).toBe(3);
496
- expect(ability.rules.filter((r) => r.subject === 'Dashboard')).toHaveLength(2);
497
- expect(ability.rules.find((r) => r.subject === 'Project')).toBeDefined();
498
- });
983
+ });
984
+ describe('export permissions', () => {
985
+ it('should handle export csv permissions', () => {
986
+ const builder = new AbilityBuilder(Ability);
987
+ buildAbilityFromScopes({
988
+ ...baseContext,
989
+ scopes: ['manage:ExportCsv'],
990
+ }, builder);
991
+ const ability = builder.build();
992
+ expect(ability.can('manage', subject('ExportCsv', {
993
+ organizationUuid: 'org-123',
994
+ projectUuid: 'project-123',
995
+ }))).toBe(true);
499
996
  });
500
- describe('cross-boundary access tests', () => {
501
- it('should not allow access to resources from different organizations', () => {
502
- const builder = new AbilityBuilder(Ability);
503
- buildAbilityFromScopes({
504
- ...baseContext,
505
- scopes: [
506
- 'view:Dashboard',
507
- 'manage:SavedChart',
508
- 'view:Space',
509
- ],
510
- }, builder);
511
- const ability = builder.build();
512
- // Should not access dashboard from different org
513
- expect(ability.can('view', subject('Dashboard', {
514
- organizationUuid: 'different-org',
515
- isPrivate: false,
516
- }))).toBe(false);
517
- // Should not manage saved chart from different org
518
- expect(ability.can('manage', subject('SavedChart', {
519
- organizationUuid: 'different-org',
520
- }))).toBe(false);
521
- // Should not view space from different org
522
- expect(ability.can('view', subject('Space', {
523
- organizationUuid: 'different-org',
524
- isPrivate: false,
525
- }))).toBe(false);
526
- });
527
- it('should not allow access to resources from different projects', () => {
528
- const builder = new AbilityBuilder(Ability);
529
- buildAbilityFromScopes({
530
- ...baseContext,
531
- scopes: ['view:SavedChart'],
532
- }, builder);
533
- const ability = builder.build();
534
- // Should not access saved chart from different project
535
- expect(ability.can('view', subject('SavedChart', {
536
- organizationUuid: 'org-123',
537
- projectUuid: 'different-project',
538
- isPrivate: false,
539
- }))).toBe(false);
540
- });
997
+ it('should handle change csv results permissions', () => {
998
+ const builder = new AbilityBuilder(Ability);
999
+ buildAbilityFromScopes({
1000
+ ...baseContext,
1001
+ scopes: ['manage:ChangeCsvResults'],
1002
+ }, builder);
1003
+ const ability = builder.build();
1004
+ expect(ability.can('manage', subject('ChangeCsvResults', {
1005
+ organizationUuid: 'org-123',
1006
+ projectUuid: 'project-123',
1007
+ }))).toBe(true);
541
1008
  });
542
- describe('private resource access with space roles', () => {
543
- it('should handle viewer role access to private resources', () => {
544
- const contextWithUser = {
545
- ...baseContextWithOrg,
546
- userUuid: 'user-456',
547
- };
548
- const builder = new AbilityBuilder(Ability);
549
- buildAbilityFromScopes({
550
- ...contextWithUser,
551
- scopes: ['view:Dashboard'],
552
- }, builder);
553
- const ability = builder.build();
554
- // Can view private dashboard with viewer access
555
- expect(ability.can('view', subject('Dashboard', {
556
- organizationUuid: 'org-123',
557
- isPrivate: true,
558
- access: [
559
- {
560
- userUuid: 'user-456',
561
- role: SpaceMemberRole.VIEWER,
562
- },
563
- ],
564
- }))).toBe(true);
565
- // Cannot view private dashboard without access
566
- expect(ability.can('view', subject('Dashboard', {
567
- organizationUuid: 'org-123',
568
- isPrivate: true,
569
- access: [],
570
- }))).toBe(false);
571
- // Cannot view private dashboard with access for another user
572
- expect(ability.can('view', subject('Dashboard', {
573
- organizationUuid: 'org-123',
574
- isPrivate: true,
575
- access: [
576
- {
577
- userUuid: 'other-user',
578
- role: SpaceMemberRole.VIEWER,
579
- },
580
- ],
581
- }))).toBe(false);
582
- });
583
- it('should handle editor role for managing resources', () => {
584
- const contextWithUser = {
585
- ...baseContextWithOrg,
586
- userUuid: 'user-456',
587
- };
588
- const builder = new AbilityBuilder(Ability);
589
- buildAbilityFromScopes({
590
- ...contextWithUser,
591
- scopes: ['manage:Dashboard@space'],
592
- }, builder);
593
- const ability = builder.build();
594
- // Can manage dashboard with editor role
595
- expect(ability.can('manage', subject('Dashboard', {
596
- organizationUuid: 'org-123',
597
- access: [
598
- {
599
- userUuid: 'user-456',
600
- role: SpaceMemberRole.EDITOR,
601
- },
602
- ],
603
- }))).toBe(true);
604
- // Can manage dashboard with admin role
605
- expect(ability.can('manage', subject('Dashboard', {
606
- organizationUuid: 'org-123',
607
- access: [
608
- {
609
- userUuid: 'user-456',
610
- role: SpaceMemberRole.ADMIN,
611
- },
612
- ],
613
- }))).toBe(true);
614
- // Cannot manage dashboard with viewer role
615
- expect(ability.can('manage', subject('Dashboard', {
616
- organizationUuid: 'org-123',
617
- access: [
618
- {
619
- userUuid: 'user-456',
620
- role: SpaceMemberRole.VIEWER,
621
- },
622
- ],
623
- }))).toBe(false);
624
- // Cannot manage dashboard without access
625
- expect(ability.can('manage', subject('Dashboard', {
626
- organizationUuid: 'org-123',
627
- access: [],
628
- }))).toBe(false);
629
- });
630
- it('should handle space admin role for managing spaces', () => {
631
- const contextWithUser = {
632
- ...baseContext,
633
- userUuid: 'user-456',
634
- };
635
- const builder = new AbilityBuilder(Ability);
636
- buildAbilityFromScopes({
637
- ...contextWithUser,
638
- scopes: ['manage:Space@assigned'],
639
- }, builder);
640
- const ability = builder.build();
641
- // Can manage space with admin role
642
- expect(ability.can('manage', subject('Space', {
643
- projectUuid: baseContext.projectUuid,
644
- access: [
645
- {
646
- userUuid: 'user-456',
647
- role: SpaceMemberRole.ADMIN,
648
- },
649
- ],
650
- }))).toBe(true);
651
- // Cannot manage space with editor role
652
- expect(ability.can('manage', subject('Space', {
653
- organizationUuid: 'org-123',
654
- isPrivate: true,
655
- access: [
656
- {
657
- userUuid: 'user-456',
658
- role: SpaceMemberRole.EDITOR,
659
- },
660
- ],
661
- }))).toBe(false);
662
- // Cannot manage space with viewer role
663
- expect(ability.can('manage', subject('Space', {
664
- organizationUuid: 'org-123',
665
- isPrivate: true,
666
- access: [
667
- {
668
- userUuid: 'user-456',
669
- role: SpaceMemberRole.VIEWER,
670
- },
671
- ],
672
- }))).toBe(false);
673
- });
1009
+ });
1010
+ describe('underlying data permissions', () => {
1011
+ it('should handle view:underlying_data permissions', () => {
1012
+ const builder = new AbilityBuilder(Ability);
1013
+ buildAbilityFromScopes({
1014
+ ...baseContext,
1015
+ scopes: ['view:UnderlyingData'],
1016
+ }, builder);
1017
+ const ability = builder.build();
1018
+ expect(ability.can('view', subject('UnderlyingData', {
1019
+ organizationUuid: 'org-123',
1020
+ projectUuid: 'project-123',
1021
+ }))).toBe(true);
674
1022
  });
675
- describe('job and job status permissions', () => {
676
- it('should handle view:job@self permissions', () => {
677
- const contextWithUser = {
678
- ...baseContext,
679
- userUuid: 'user-456',
680
- };
681
- const builder = new AbilityBuilder(Ability);
682
- buildAbilityFromScopes({
683
- ...contextWithUser,
684
- scopes: ['view:Job@self'],
685
- }, builder);
686
- const ability = builder.build();
687
- // Can view own jobs
688
- expect(ability.can('view', subject('Job', {
689
- userUuid: 'user-456',
690
- }))).toBe(true);
691
- // Cannot view other users' jobs without manage permission
692
- expect(ability.can('view', subject('Job', {
693
- userUuid: 'other-user',
694
- }))).toBe(false);
695
- });
696
- it('should handle view:job_status@self permissions for user context', () => {
697
- const builder = new AbilityBuilder(Ability);
698
- buildAbilityFromScopes({
699
- ...baseContext,
700
- userUuid: 'user-456',
701
- scopes: ['view:JobStatus@self'],
702
- }, builder);
703
- const ability = builder.build();
704
- // Can view own job status
705
- expect(ability.can('view', subject('JobStatus', {
706
- createdByUserUuid: 'user-456',
707
- }))).toBe(true);
708
- // Cannot view other users' job status
709
- expect(ability.can('view', subject('JobStatus', {
710
- createdByUserUuid: 'other-user',
711
- }))).toBe(false);
712
- });
713
- it('should handle view:job_status permissions for all job status', () => {
714
- const builder = new AbilityBuilder(Ability);
715
- buildAbilityFromScopes({
716
- ...baseContextWithOrg,
717
- scopes: ['view:JobStatus'],
718
- }, builder);
719
- const ability = builder.build();
720
- // Can view all job status in organization
721
- expect(ability.can('view', subject('JobStatus', {
722
- organizationUuid: 'org-123',
723
- }))).toBe(true);
724
- // Cannot view job status from another organization
725
- expect(ability.can('view', subject('JobStatus', {
726
- organizationUuid: 'different-org',
727
- }))).toBe(false);
728
- });
729
- it('should handle view:job permissions for all jobs', () => {
730
- const builder = new AbilityBuilder(Ability);
731
- buildAbilityFromScopes({
732
- ...baseContext,
733
- scopes: ['view:Job'],
734
- }, builder);
735
- const ability = builder.build();
736
- // Can view any job
737
- expect(ability.can('view', subject('Job', {
738
- organizationUuid: 'org-123',
739
- projectUuid: 'project-123',
740
- userUuid: 'any-user',
741
- }))).toBe(true);
742
- });
1023
+ });
1024
+ describe('sql runner and custom sql permissions', () => {
1025
+ it('should handle manage:sql_runner permissions', () => {
1026
+ const builder = new AbilityBuilder(Ability);
1027
+ buildAbilityFromScopes({
1028
+ ...baseContext,
1029
+ scopes: ['manage:SqlRunner'],
1030
+ }, builder);
1031
+ const ability = builder.build();
1032
+ expect(ability.can('manage', subject('SqlRunner', {
1033
+ organizationUuid: 'org-123',
1034
+ projectUuid: 'project-123',
1035
+ }))).toBe(true);
743
1036
  });
744
- describe('space-based permissions modifiers', () => {
745
- it('should handle manage:dashboard@space permissions', () => {
746
- const contextWithUser = {
747
- ...baseContextWithOrg,
748
- userUuid: 'user-456',
749
- };
750
- const builder = new AbilityBuilder(Ability);
751
- buildAbilityFromScopes({
752
- ...contextWithUser,
753
- scopes: ['manage:Dashboard@space'],
754
- }, builder);
755
- const ability = builder.build();
756
- // Can manage dashboard with editor role
757
- expect(ability.can('manage', subject('Dashboard', {
758
- organizationUuid: 'org-123',
759
- access: [
760
- {
761
- userUuid: 'user-456',
762
- role: SpaceMemberRole.EDITOR,
763
- },
764
- ],
765
- }))).toBe(true);
766
- // Can manage dashboard with admin role
767
- expect(ability.can('manage', subject('Dashboard', {
768
- organizationUuid: 'org-123',
769
- access: [
770
- {
771
- userUuid: 'user-456',
772
- role: SpaceMemberRole.ADMIN,
773
- },
774
- ],
775
- }))).toBe(true);
776
- // Cannot manage dashboard with viewer role
777
- expect(ability.can('manage', subject('Dashboard', {
778
- organizationUuid: 'org-123',
779
- access: [
780
- {
781
- userUuid: 'user-456',
782
- role: SpaceMemberRole.VIEWER,
783
- },
784
- ],
785
- }))).toBe(false);
786
- // Cannot manage dashboard without access
787
- expect(ability.can('manage', subject('Dashboard', {
788
- organizationUuid: 'org-123',
789
- access: [],
790
- }))).toBe(false);
791
- });
792
- it('should handle manage:saved_chart@space permissions', () => {
793
- const contextWithUser = {
794
- ...baseContext,
795
- userUuid: 'user-456',
796
- };
797
- const builder = new AbilityBuilder(Ability);
798
- buildAbilityFromScopes({
799
- ...contextWithUser,
800
- scopes: ['manage:SavedChart@space'],
801
- }, builder);
802
- const ability = builder.build();
803
- // Can manage saved chart with editor role
804
- expect(ability.can('manage', subject('SavedChart', {
805
- projectUuid: 'project-123',
806
- access: [
807
- {
808
- userUuid: 'user-456',
809
- role: SpaceMemberRole.EDITOR,
810
- },
811
- ],
812
- }))).toBe(true);
813
- // Can manage saved chart with admin role
814
- expect(ability.can('manage', subject('SavedChart', {
815
- projectUuid: 'project-123',
816
- access: [
817
- {
818
- userUuid: 'user-456',
819
- role: SpaceMemberRole.ADMIN,
820
- },
821
- ],
822
- }))).toBe(true);
823
- // Cannot manage without proper access
824
- expect(ability.can('manage', subject('SavedChart', {
825
- projectUuid: 'project-123',
826
- access: [
827
- {
828
- userUuid: 'other-user',
829
- role: SpaceMemberRole.EDITOR,
830
- },
831
- ],
832
- }))).toBe(false);
833
- });
834
- it('should handle promote:dashboard@space permissions', () => {
835
- const contextWithUser = {
836
- ...baseContext,
837
- userUuid: 'user-456',
838
- };
839
- const builder = new AbilityBuilder(Ability);
840
- buildAbilityFromScopes({
841
- ...contextWithUser,
842
- scopes: ['promote:Dashboard@space'],
843
- }, builder);
844
- const ability = builder.build();
845
- // Can promote dashboard with editor access
846
- expect(ability.can('promote', subject('Dashboard', {
847
- projectUuid: 'project-123',
848
- access: [
849
- {
850
- userUuid: 'user-456',
851
- role: SpaceMemberRole.EDITOR,
852
- },
853
- ],
854
- }))).toBe(true);
855
- // Cannot promote without editor access
856
- expect(ability.can('promote', subject('Dashboard', {
857
- projectUuid: 'project-123',
858
- access: [
859
- {
860
- userUuid: 'user-456',
861
- role: SpaceMemberRole.VIEWER,
862
- },
863
- ],
864
- }))).toBe(false);
865
- });
866
- it('should handle manage:semantic_viewer@space permissions', () => {
867
- const contextWithUser = {
868
- ...baseContextWithOrg,
869
- userUuid: 'user-456',
870
- };
871
- const builder = new AbilityBuilder(Ability);
872
- buildAbilityFromScopes({
873
- ...contextWithUser,
874
- scopes: ['manage:SemanticViewer@space'],
875
- }, builder);
876
- const ability = builder.build();
877
- // Can manage semantic viewer with editor role
878
- expect(ability.can('manage', subject('SemanticViewer', {
879
- organizationUuid: 'org-123',
880
- access: [
881
- {
882
- userUuid: 'user-456',
883
- role: SpaceMemberRole.EDITOR,
884
- },
885
- ],
886
- }))).toBe(true);
887
- // Cannot manage without editor role
888
- expect(ability.can('manage', subject('SemanticViewer', {
889
- organizationUuid: 'org-123',
890
- access: [
891
- {
892
- userUuid: 'user-456',
893
- role: SpaceMemberRole.VIEWER,
894
- },
895
- ],
896
- }))).toBe(false);
897
- });
1037
+ it('should handle manage:custom_sql permissions', () => {
1038
+ const builder = new AbilityBuilder(Ability);
1039
+ buildAbilityFromScopes({
1040
+ ...baseContext,
1041
+ scopes: ['manage:CustomSql'],
1042
+ }, builder);
1043
+ const ability = builder.build();
1044
+ expect(ability.can('manage', subject('CustomSql', {
1045
+ organizationUuid: 'org-123',
1046
+ projectUuid: 'project-123',
1047
+ }))).toBe(true);
898
1048
  });
899
- describe('semantic viewer permissions', () => {
900
- it('should handle view:semantic_viewer permissions', () => {
901
- const builder = new AbilityBuilder(Ability);
902
- buildAbilityFromScopes({
903
- ...baseContext,
904
- scopes: ['view:SemanticViewer'],
905
- }, builder);
906
- const ability = builder.build();
907
- expect(ability.can('view', subject('SemanticViewer', {
908
- organizationUuid: 'org-123',
909
- projectUuid: 'project-123',
910
- }))).toBe(true);
911
- });
912
- it('should handle manage:semantic_viewer with organization scope', () => {
913
- const contextWithOrgManage = {
914
- ...baseContextWithOrg,
915
- userUuid: 'user-456',
916
- scopes: ['manage:Organization'],
917
- };
918
- const builder = new AbilityBuilder(Ability);
919
- buildAbilityFromScopes({
920
- ...contextWithOrgManage,
921
- scopes: [
922
- 'manage:Organization',
923
- 'manage:SemanticViewer',
924
- ],
925
- }, builder);
926
- const ability = builder.build();
927
- // Can manage semantic viewer organization-wide
928
- expect(ability.can('manage', subject('SemanticViewer', {
929
- organizationUuid: 'org-123',
930
- }))).toBe(true);
931
- });
932
- it('should handle manage:semantic_viewer with editor role', () => {
933
- const contextWithUser = {
934
- ...baseContextWithOrg,
935
- userUuid: 'user-456',
936
- };
937
- const builder = new AbilityBuilder(Ability);
938
- buildAbilityFromScopes({
939
- ...contextWithUser,
940
- scopes: ['manage:SemanticViewer@space'],
941
- }, builder);
942
- const ability = builder.build();
943
- // Can manage semantic viewer with editor role
944
- expect(ability.can('manage', subject('SemanticViewer', {
945
- organizationUuid: 'org-123',
946
- access: [
947
- {
948
- userUuid: 'user-456',
949
- role: SpaceMemberRole.EDITOR,
950
- },
951
- ],
952
- }))).toBe(true);
953
- // Cannot manage without proper access
954
- expect(ability.can('manage', subject('SemanticViewer', {
955
- organizationUuid: 'org-123',
956
- access: [
957
- {
958
- userUuid: 'user-456',
959
- role: SpaceMemberRole.VIEWER,
960
- },
961
- ],
962
- }))).toBe(false);
963
- });
1049
+ });
1050
+ describe('project delete permissions', () => {
1051
+ it('should handle delete:project@self permissions', () => {
1052
+ const builder = new AbilityBuilder(Ability);
1053
+ buildAbilityFromScopes({
1054
+ ...baseContext,
1055
+ userUuid: 'user-456',
1056
+ scopes: ['delete:Project@self'],
1057
+ }, builder);
1058
+ const ability = builder.build();
1059
+ // Can delete specific project
1060
+ expect(ability.can('delete', subject('Project', {
1061
+ createdByUserUuid: 'user-456',
1062
+ type: ProjectType.PREVIEW,
1063
+ }))).toBe(true);
1064
+ expect(ability.can('delete', subject('Project', {
1065
+ createdByUserUuid: 'different-user',
1066
+ type: ProjectType.PREVIEW,
1067
+ }))).toBe(false);
1068
+ });
1069
+ it('should handle delete:project@self for own preview projects', () => {
1070
+ const builder = new AbilityBuilder(Ability);
1071
+ buildAbilityFromScopes({
1072
+ ...baseContext,
1073
+ userUuid: 'user-456',
1074
+ scopes: ['delete:Project@self'],
1075
+ }, builder);
1076
+ const ability = builder.build();
1077
+ // Can delete preview projects in a project
1078
+ expect(ability.can('delete', subject('Project', {
1079
+ createdByUserUuid: 'user-456',
1080
+ type: ProjectType.PREVIEW,
1081
+ }))).toBe(true);
1082
+ // Cannot delete default projects
1083
+ expect(ability.can('delete', subject('Project', {
1084
+ createdByUserUuid: 'user-456',
1085
+ type: ProjectType.DEFAULT,
1086
+ }))).toBe(false);
1087
+ });
1088
+ });
1089
+ describe('pinned items permissions', () => {
1090
+ it('should handle view:pinned_items permissions', () => {
1091
+ const builder = new AbilityBuilder(Ability);
1092
+ buildAbilityFromScopes({
1093
+ ...baseContext,
1094
+ scopes: ['view:PinnedItems'],
1095
+ }, builder);
1096
+ const ability = builder.build();
1097
+ expect(ability.can('view', subject('PinnedItems', {
1098
+ organizationUuid: 'org-123',
1099
+ projectUuid: 'project-123',
1100
+ }))).toBe(true);
964
1101
  });
965
- describe('create space permissions', () => {
966
- it('should handle create:space permissions', () => {
967
- const builder = new AbilityBuilder(Ability);
968
- buildAbilityFromScopes({
969
- ...baseContext,
970
- scopes: ['create:Space'],
971
- }, builder);
972
- const ability = builder.build();
973
- expect(ability.can('create', subject('Space', {
974
- organizationUuid: 'org-123',
975
- projectUuid: 'project-123',
976
- }))).toBe(true);
977
- });
1102
+ it('should handle manage:pinned_items permissions', () => {
1103
+ const builder = new AbilityBuilder(Ability);
1104
+ buildAbilityFromScopes({
1105
+ ...baseContext,
1106
+ scopes: ['manage:PinnedItems'],
1107
+ }, builder);
1108
+ const ability = builder.build();
1109
+ expect(ability.can('manage', subject('PinnedItems', {
1110
+ organizationUuid: 'org-123',
1111
+ projectUuid: 'project-123',
1112
+ }))).toBe(true);
978
1113
  });
979
- describe('export permissions', () => {
980
- it('should handle export csv permissions', () => {
981
- const builder = new AbilityBuilder(Ability);
982
- buildAbilityFromScopes({
983
- ...baseContext,
984
- scopes: ['manage:ExportCsv'],
985
- }, builder);
986
- const ability = builder.build();
987
- expect(ability.can('manage', subject('ExportCsv', {
988
- organizationUuid: 'org-123',
989
- projectUuid: 'project-123',
990
- }))).toBe(true);
991
- });
992
- it('should handle change csv results permissions', () => {
993
- const builder = new AbilityBuilder(Ability);
994
- buildAbilityFromScopes({
995
- ...baseContext,
996
- scopes: ['manage:ChangeCsvResults'],
997
- }, builder);
998
- const ability = builder.build();
999
- expect(ability.can('manage', subject('ChangeCsvResults', {
1000
- organizationUuid: 'org-123',
1001
- projectUuid: 'project-123',
1002
- }))).toBe(true);
1003
- });
1114
+ });
1115
+ describe('explore permissions', () => {
1116
+ it('should handle manage:explore permissions', () => {
1117
+ const builder = new AbilityBuilder(Ability);
1118
+ buildAbilityFromScopes({
1119
+ ...baseContext,
1120
+ scopes: ['manage:Explore'],
1121
+ }, builder);
1122
+ const ability = builder.build();
1123
+ expect(ability.can('manage', subject('Explore', {
1124
+ organizationUuid: 'org-123',
1125
+ projectUuid: 'project-123',
1126
+ }))).toBe(true);
1004
1127
  });
1005
- describe('underlying data permissions', () => {
1006
- it('should handle view:underlying_data permissions', () => {
1007
- const builder = new AbilityBuilder(Ability);
1008
- buildAbilityFromScopes({
1009
- ...baseContext,
1010
- scopes: ['view:UnderlyingData'],
1011
- }, builder);
1012
- const ability = builder.build();
1013
- expect(ability.can('view', subject('UnderlyingData', {
1014
- organizationUuid: 'org-123',
1015
- projectUuid: 'project-123',
1016
- }))).toBe(true);
1017
- });
1128
+ });
1129
+ describe('virtual view permissions', () => {
1130
+ it('should handle create:virtual_view permissions', () => {
1131
+ const builder = new AbilityBuilder(Ability);
1132
+ buildAbilityFromScopes({
1133
+ ...baseContext,
1134
+ scopes: ['create:VirtualView'],
1135
+ }, builder);
1136
+ const ability = builder.build();
1137
+ expect(ability.can('create', subject('VirtualView', {
1138
+ organizationUuid: 'org-123',
1139
+ projectUuid: 'project-123',
1140
+ }))).toBe(true);
1141
+ // Should not be able to delete with create scope
1142
+ expect(ability.can('delete', subject('VirtualView', {
1143
+ organizationUuid: 'org-123',
1144
+ projectUuid: 'project-123',
1145
+ }))).toBe(false);
1018
1146
  });
1019
- describe('sql runner and custom sql permissions', () => {
1020
- it('should handle manage:sql_runner permissions', () => {
1021
- const builder = new AbilityBuilder(Ability);
1022
- buildAbilityFromScopes({
1023
- ...baseContext,
1024
- scopes: ['manage:SqlRunner'],
1025
- }, builder);
1026
- const ability = builder.build();
1027
- expect(ability.can('manage', subject('SqlRunner', {
1028
- organizationUuid: 'org-123',
1029
- projectUuid: 'project-123',
1030
- }))).toBe(true);
1031
- });
1032
- it('should handle manage:custom_sql permissions', () => {
1033
- const builder = new AbilityBuilder(Ability);
1034
- buildAbilityFromScopes({
1035
- ...baseContext,
1036
- scopes: ['manage:CustomSql'],
1037
- }, builder);
1038
- const ability = builder.build();
1039
- expect(ability.can('manage', subject('CustomSql', {
1040
- organizationUuid: 'org-123',
1041
- projectUuid: 'project-123',
1042
- }))).toBe(true);
1043
- });
1147
+ it('should handle delete:virtual_view permissions', () => {
1148
+ const builder = new AbilityBuilder(Ability);
1149
+ buildAbilityFromScopes({
1150
+ ...baseContext,
1151
+ scopes: ['delete:VirtualView'],
1152
+ }, builder);
1153
+ const ability = builder.build();
1154
+ expect(ability.can('delete', subject('VirtualView', {
1155
+ organizationUuid: 'org-123',
1156
+ projectUuid: 'project-123',
1157
+ }))).toBe(true);
1158
+ // Should not be able to create with delete scope
1159
+ expect(ability.can('create', subject('VirtualView', {
1160
+ organizationUuid: 'org-123',
1161
+ projectUuid: 'project-123',
1162
+ }))).toBe(false);
1044
1163
  });
1045
- describe('project delete permissions', () => {
1046
- it('should handle delete:project@self permissions', () => {
1047
- const builder = new AbilityBuilder(Ability);
1048
- buildAbilityFromScopes({
1049
- ...baseContext,
1050
- userUuid: 'user-456',
1051
- scopes: ['delete:Project@self'],
1052
- }, builder);
1053
- const ability = builder.build();
1054
- // Can delete specific project
1055
- expect(ability.can('delete', subject('Project', {
1056
- createdByUserUuid: 'user-456',
1057
- type: ProjectType.PREVIEW,
1058
- }))).toBe(true);
1059
- expect(ability.can('delete', subject('Project', {
1060
- createdByUserUuid: 'different-user',
1061
- type: ProjectType.PREVIEW,
1062
- }))).toBe(false);
1063
- });
1064
- it('should handle delete:project@self for own preview projects', () => {
1065
- const builder = new AbilityBuilder(Ability);
1066
- buildAbilityFromScopes({
1067
- ...baseContext,
1068
- userUuid: 'user-456',
1069
- scopes: ['delete:Project@self'],
1070
- }, builder);
1071
- const ability = builder.build();
1072
- // Can delete preview projects in a project
1073
- expect(ability.can('delete', subject('Project', {
1074
- createdByUserUuid: 'user-456',
1075
- type: ProjectType.PREVIEW,
1076
- }))).toBe(true);
1077
- // Cannot delete default projects
1078
- expect(ability.can('delete', subject('Project', {
1079
- createdByUserUuid: 'user-456',
1080
- type: ProjectType.DEFAULT,
1081
- }))).toBe(false);
1082
- });
1164
+ it('should handle manage:virtual_view permissions for both create and delete', () => {
1165
+ const builder = new AbilityBuilder(Ability);
1166
+ buildAbilityFromScopes({
1167
+ ...baseContext,
1168
+ scopes: ['manage:VirtualView'],
1169
+ }, builder);
1170
+ const ability = builder.build();
1171
+ // Should be able to manage (create and delete)
1172
+ expect(ability.can('manage', subject('VirtualView', {
1173
+ organizationUuid: 'org-123',
1174
+ projectUuid: 'project-123',
1175
+ }))).toBe(true);
1176
+ // Manage scope should allow both create and delete actions
1177
+ expect(ability.can('create', subject('VirtualView', {
1178
+ organizationUuid: 'org-123',
1179
+ projectUuid: 'project-123',
1180
+ }))).toBe(true);
1181
+ expect(ability.can('delete', subject('VirtualView', {
1182
+ organizationUuid: 'org-123',
1183
+ projectUuid: 'project-123',
1184
+ }))).toBe(true);
1083
1185
  });
1084
- describe('pinned items permissions', () => {
1085
- it('should handle view:pinned_items permissions', () => {
1086
- const builder = new AbilityBuilder(Ability);
1087
- buildAbilityFromScopes({
1088
- ...baseContext,
1089
- scopes: ['view:PinnedItems'],
1090
- }, builder);
1091
- const ability = builder.build();
1092
- expect(ability.can('view', subject('PinnedItems', {
1093
- organizationUuid: 'org-123',
1094
- projectUuid: 'project-123',
1095
- }))).toBe(true);
1096
- });
1097
- it('should handle manage:pinned_items permissions', () => {
1098
- const builder = new AbilityBuilder(Ability);
1099
- buildAbilityFromScopes({
1100
- ...baseContext,
1101
- scopes: ['manage:PinnedItems'],
1102
- }, builder);
1103
- const ability = builder.build();
1104
- expect(ability.can('manage', subject('PinnedItems', {
1105
- organizationUuid: 'org-123',
1106
- projectUuid: 'project-123',
1107
- }))).toBe(true);
1108
- });
1186
+ it('should not allow virtual view actions for different organizations', () => {
1187
+ const builder = new AbilityBuilder(Ability);
1188
+ buildAbilityFromScopes({
1189
+ ...baseContextWithOrg,
1190
+ scopes: [
1191
+ 'create:VirtualView',
1192
+ 'delete:VirtualView',
1193
+ 'manage:VirtualView',
1194
+ ],
1195
+ }, builder);
1196
+ const ability = builder.build();
1197
+ // Should not access virtual views from different org
1198
+ expect(ability.can('create', subject('VirtualView', {
1199
+ organizationUuid: 'different-org',
1200
+ projectUuid: 'project-123',
1201
+ }))).toBe(false);
1202
+ expect(ability.can('delete', subject('VirtualView', {
1203
+ organizationUuid: 'different-org',
1204
+ projectUuid: 'project-123',
1205
+ }))).toBe(false);
1206
+ expect(ability.can('manage', subject('VirtualView', {
1207
+ organizationUuid: 'different-org',
1208
+ projectUuid: 'project-123',
1209
+ }))).toBe(false);
1109
1210
  });
1110
- describe('explore permissions', () => {
1111
- it('should handle manage:explore permissions', () => {
1112
- const builder = new AbilityBuilder(Ability);
1113
- buildAbilityFromScopes({
1114
- ...baseContext,
1115
- scopes: ['manage:Explore'],
1116
- }, builder);
1117
- const ability = builder.build();
1118
- expect(ability.can('manage', subject('Explore', {
1119
- organizationUuid: 'org-123',
1120
- projectUuid: 'project-123',
1121
- }))).toBe(true);
1122
- });
1211
+ it('should allow virtual view actions for different projects within same organization', () => {
1212
+ const builder = new AbilityBuilder(Ability);
1213
+ buildAbilityFromScopes({
1214
+ ...baseContextWithOrg,
1215
+ scopes: [
1216
+ 'create:VirtualView',
1217
+ 'delete:VirtualView',
1218
+ 'manage:VirtualView',
1219
+ ],
1220
+ }, builder);
1221
+ const ability = builder.build();
1222
+ // Virtual view permissions are organization-scoped, not project-scoped
1223
+ // So they should work across different projects within the same org
1224
+ expect(ability.can('create', subject('VirtualView', {
1225
+ organizationUuid: 'org-123',
1226
+ projectUuid: 'different-project',
1227
+ }))).toBe(true);
1228
+ expect(ability.can('delete', subject('VirtualView', {
1229
+ organizationUuid: 'org-123',
1230
+ projectUuid: 'different-project',
1231
+ }))).toBe(true);
1232
+ expect(ability.can('manage', subject('VirtualView', {
1233
+ organizationUuid: 'org-123',
1234
+ projectUuid: 'different-project',
1235
+ }))).toBe(true);
1123
1236
  });
1124
- describe('virtual view permissions', () => {
1125
- it('should handle create:virtual_view permissions', () => {
1126
- const builder = new AbilityBuilder(Ability);
1127
- buildAbilityFromScopes({
1128
- ...baseContext,
1129
- scopes: ['create:VirtualView'],
1130
- }, builder);
1131
- const ability = builder.build();
1132
- expect(ability.can('create', subject('VirtualView', {
1133
- organizationUuid: 'org-123',
1134
- projectUuid: 'project-123',
1135
- }))).toBe(true);
1136
- // Should not be able to delete with create scope
1137
- expect(ability.can('delete', subject('VirtualView', {
1138
- organizationUuid: 'org-123',
1139
- projectUuid: 'project-123',
1140
- }))).toBe(false);
1141
- });
1142
- it('should handle delete:virtual_view permissions', () => {
1143
- const builder = new AbilityBuilder(Ability);
1144
- buildAbilityFromScopes({
1145
- ...baseContext,
1146
- scopes: ['delete:VirtualView'],
1147
- }, builder);
1148
- const ability = builder.build();
1149
- expect(ability.can('delete', subject('VirtualView', {
1150
- organizationUuid: 'org-123',
1151
- projectUuid: 'project-123',
1152
- }))).toBe(true);
1153
- // Should not be able to create with delete scope
1154
- expect(ability.can('create', subject('VirtualView', {
1155
- organizationUuid: 'org-123',
1156
- projectUuid: 'project-123',
1157
- }))).toBe(false);
1158
- });
1159
- it('should handle manage:virtual_view permissions for both create and delete', () => {
1160
- const builder = new AbilityBuilder(Ability);
1161
- buildAbilityFromScopes({
1162
- ...baseContext,
1163
- scopes: ['manage:VirtualView'],
1164
- }, builder);
1165
- const ability = builder.build();
1166
- // Should be able to manage (create and delete)
1167
- expect(ability.can('manage', subject('VirtualView', {
1168
- organizationUuid: 'org-123',
1169
- projectUuid: 'project-123',
1170
- }))).toBe(true);
1171
- // Manage scope should allow both create and delete actions
1172
- expect(ability.can('create', subject('VirtualView', {
1173
- organizationUuid: 'org-123',
1174
- projectUuid: 'project-123',
1175
- }))).toBe(true);
1176
- expect(ability.can('delete', subject('VirtualView', {
1177
- organizationUuid: 'org-123',
1178
- projectUuid: 'project-123',
1179
- }))).toBe(true);
1180
- });
1181
- it('should not allow virtual view actions for different organizations', () => {
1182
- const builder = new AbilityBuilder(Ability);
1183
- buildAbilityFromScopes({
1184
- ...baseContextWithOrg,
1185
- scopes: [
1186
- 'create:VirtualView',
1187
- 'delete:VirtualView',
1188
- 'manage:VirtualView',
1189
- ],
1190
- }, builder);
1191
- const ability = builder.build();
1192
- // Should not access virtual views from different org
1193
- expect(ability.can('create', subject('VirtualView', {
1194
- organizationUuid: 'different-org',
1195
- projectUuid: 'project-123',
1196
- }))).toBe(false);
1197
- expect(ability.can('delete', subject('VirtualView', {
1198
- organizationUuid: 'different-org',
1199
- projectUuid: 'project-123',
1200
- }))).toBe(false);
1201
- expect(ability.can('manage', subject('VirtualView', {
1202
- organizationUuid: 'different-org',
1203
- projectUuid: 'project-123',
1204
- }))).toBe(false);
1205
- });
1206
- it('should allow virtual view actions for different projects within same organization', () => {
1207
- const builder = new AbilityBuilder(Ability);
1208
- buildAbilityFromScopes({
1209
- ...baseContextWithOrg,
1210
- scopes: [
1211
- 'create:VirtualView',
1212
- 'delete:VirtualView',
1213
- 'manage:VirtualView',
1214
- ],
1215
- }, builder);
1216
- const ability = builder.build();
1217
- // Virtual view permissions are organization-scoped, not project-scoped
1218
- // So they should work across different projects within the same org
1219
- expect(ability.can('create', subject('VirtualView', {
1220
- organizationUuid: 'org-123',
1221
- projectUuid: 'different-project',
1222
- }))).toBe(true);
1223
- expect(ability.can('delete', subject('VirtualView', {
1224
- organizationUuid: 'org-123',
1225
- projectUuid: 'different-project',
1226
- }))).toBe(true);
1227
- expect(ability.can('manage', subject('VirtualView', {
1228
- organizationUuid: 'org-123',
1229
- projectUuid: 'different-project',
1230
- }))).toBe(true);
1231
- });
1237
+ });
1238
+ describe('organization member profile permissions', () => {
1239
+ it('should handle view:organization_member_profile permissions', () => {
1240
+ const builder = new AbilityBuilder(Ability);
1241
+ buildAbilityFromScopes({
1242
+ ...baseContextWithOrg,
1243
+ scopes: ['view:OrganizationMemberProfile'],
1244
+ }, builder);
1245
+ const ability = builder.build();
1246
+ expect(ability.can('view', subject('OrganizationMemberProfile', {
1247
+ organizationUuid: 'org-123',
1248
+ projectUuid: 'project-123',
1249
+ }))).toBe(true);
1250
+ // Cannot view profiles from different project
1251
+ expect(ability.can('view', subject('OrganizationMemberProfile', {
1252
+ organizationUuid: 'different-org',
1253
+ projectUuid: 'project-123',
1254
+ }))).toBe(false);
1232
1255
  });
1233
- describe('organization member profile permissions', () => {
1234
- it('should handle view:organization_member_profile permissions', () => {
1235
- const builder = new AbilityBuilder(Ability);
1236
- buildAbilityFromScopes({
1237
- ...baseContextWithOrg,
1238
- scopes: ['view:OrganizationMemberProfile'],
1239
- }, builder);
1240
- const ability = builder.build();
1241
- expect(ability.can('view', subject('OrganizationMemberProfile', {
1242
- organizationUuid: 'org-123',
1243
- projectUuid: 'project-123',
1244
- }))).toBe(true);
1245
- // Cannot view profiles from different project
1246
- expect(ability.can('view', subject('OrganizationMemberProfile', {
1247
- organizationUuid: 'different-org',
1248
- projectUuid: 'project-123',
1249
- }))).toBe(false);
1250
- });
1251
- it('should handle manage:organization_member_profile permissions', () => {
1252
- const builder = new AbilityBuilder(Ability);
1253
- buildAbilityFromScopes({
1254
- ...baseContext,
1255
- scopes: ['manage:OrganizationMemberProfile'],
1256
- }, builder);
1257
- const ability = builder.build();
1258
- expect(ability.can('manage', subject('OrganizationMemberProfile', {
1259
- organizationUuid: 'org-123',
1260
- projectUuid: 'project-123',
1261
- }))).toBe(true);
1262
- });
1256
+ it('should handle manage:organization_member_profile permissions', () => {
1257
+ const builder = new AbilityBuilder(Ability);
1258
+ buildAbilityFromScopes({
1259
+ ...baseContext,
1260
+ scopes: ['manage:OrganizationMemberProfile'],
1261
+ }, builder);
1262
+ const ability = builder.build();
1263
+ expect(ability.can('manage', subject('OrganizationMemberProfile', {
1264
+ organizationUuid: 'org-123',
1265
+ projectUuid: 'project-123',
1266
+ }))).toBe(true);
1263
1267
  });
1264
- describe('personal access token permissions', () => {
1265
- it('should allow managing PAT when enabled and user has allowed role', () => {
1266
- const builder = new AbilityBuilder(Ability);
1267
- buildAbilityFromScopes({
1268
- ...baseContext,
1269
- isEnterprise: true,
1270
- organizationRole: 'admin',
1271
- scopes: ['manage:PersonalAccessToken'],
1272
- permissionsConfig: {
1273
- pat: {
1274
- enabled: true,
1275
- allowedOrgRoles: ['admin', 'developer'],
1276
- },
1268
+ });
1269
+ describe('personal access token permissions', () => {
1270
+ it('should allow managing PAT when enabled and user has allowed role', () => {
1271
+ const builder = new AbilityBuilder(Ability);
1272
+ buildAbilityFromScopes({
1273
+ ...baseContext,
1274
+ isEnterprise: true,
1275
+ organizationRole: 'admin',
1276
+ scopes: ['manage:PersonalAccessToken'],
1277
+ permissionsConfig: {
1278
+ pat: {
1279
+ enabled: true,
1280
+ allowedOrgRoles: ['admin', 'developer'],
1277
1281
  },
1278
- }, builder);
1279
- const ability = builder.build();
1280
- expect(ability.can('manage', 'PersonalAccessToken')).toBe(true);
1281
- });
1282
- it('should not allow managing PAT when disabled', () => {
1283
- const builder = new AbilityBuilder(Ability);
1284
- buildAbilityFromScopes({
1285
- ...baseContext,
1286
- isEnterprise: true,
1287
- organizationRole: 'admin',
1288
- scopes: ['manage:PersonalAccessToken'],
1289
- permissionsConfig: {
1290
- pat: {
1291
- enabled: false,
1292
- allowedOrgRoles: ['admin', 'developer'],
1293
- },
1282
+ },
1283
+ }, builder);
1284
+ const ability = builder.build();
1285
+ expect(ability.can('manage', 'PersonalAccessToken')).toBe(true);
1286
+ });
1287
+ it('should not allow managing PAT when disabled', () => {
1288
+ const builder = new AbilityBuilder(Ability);
1289
+ buildAbilityFromScopes({
1290
+ ...baseContext,
1291
+ isEnterprise: true,
1292
+ organizationRole: 'admin',
1293
+ scopes: ['manage:PersonalAccessToken'],
1294
+ permissionsConfig: {
1295
+ pat: {
1296
+ enabled: false,
1297
+ allowedOrgRoles: ['admin', 'developer'],
1294
1298
  },
1295
- }, builder);
1296
- const ability = builder.build();
1297
- expect(ability.can('manage', subject('PersonalAccessToken', {}))).toBe(false);
1298
- });
1299
- it('should not allow managing PAT when user role not in allowed roles', () => {
1300
- const builder = new AbilityBuilder(Ability);
1301
- buildAbilityFromScopes({
1302
- ...baseContext,
1303
- isEnterprise: true,
1304
- organizationRole: 'developer',
1305
- scopes: ['manage:PersonalAccessToken'],
1306
- permissionsConfig: {
1307
- pat: {
1308
- enabled: true,
1309
- allowedOrgRoles: ['admin'],
1310
- },
1299
+ },
1300
+ }, builder);
1301
+ const ability = builder.build();
1302
+ expect(ability.can('manage', subject('PersonalAccessToken', {}))).toBe(false);
1303
+ });
1304
+ it('should not allow managing PAT when user role not in allowed roles', () => {
1305
+ const builder = new AbilityBuilder(Ability);
1306
+ buildAbilityFromScopes({
1307
+ ...baseContext,
1308
+ isEnterprise: true,
1309
+ organizationRole: 'developer',
1310
+ scopes: ['manage:PersonalAccessToken'],
1311
+ permissionsConfig: {
1312
+ pat: {
1313
+ enabled: true,
1314
+ allowedOrgRoles: ['admin'],
1311
1315
  },
1312
- }, builder);
1313
- const ability = builder.build();
1314
- expect(ability.can('manage', subject('PersonalAccessToken', {}))).toBe(false);
1315
- });
1316
- it('should not allow managing PAT when no permissions config provided', () => {
1317
- const builder = new AbilityBuilder(Ability);
1318
- buildAbilityFromScopes({
1319
- ...baseContext,
1320
- isEnterprise: true,
1321
- organizationRole: 'admin',
1322
- scopes: ['manage:PersonalAccessToken'],
1323
- // No permissionsConfig provided
1324
- }, builder);
1325
- const ability = builder.build();
1326
- expect(ability.can('manage', subject('PersonalAccessToken', {}))).toBe(false);
1327
- });
1328
- it('should not allow managing PAT when no organization role provided', () => {
1329
- const builder = new AbilityBuilder(Ability);
1330
- buildAbilityFromScopes({
1331
- ...baseContext,
1332
- isEnterprise: true,
1333
- organizationRole: '', // Empty organization role
1334
- scopes: ['manage:PersonalAccessToken'],
1335
- permissionsConfig: {
1336
- pat: {
1337
- enabled: true,
1338
- allowedOrgRoles: ['admin', 'developer'],
1339
- },
1316
+ },
1317
+ }, builder);
1318
+ const ability = builder.build();
1319
+ expect(ability.can('manage', subject('PersonalAccessToken', {}))).toBe(false);
1320
+ });
1321
+ it('should not allow managing PAT when no permissions config provided', () => {
1322
+ const builder = new AbilityBuilder(Ability);
1323
+ buildAbilityFromScopes({
1324
+ ...baseContext,
1325
+ isEnterprise: true,
1326
+ organizationRole: 'admin',
1327
+ scopes: ['manage:PersonalAccessToken'],
1328
+ // No permissionsConfig provided
1329
+ }, builder);
1330
+ const ability = builder.build();
1331
+ expect(ability.can('manage', subject('PersonalAccessToken', {}))).toBe(false);
1332
+ });
1333
+ it('should not allow managing PAT when no organization role provided', () => {
1334
+ const builder = new AbilityBuilder(Ability);
1335
+ buildAbilityFromScopes({
1336
+ ...baseContext,
1337
+ isEnterprise: true,
1338
+ organizationRole: '', // Empty organization role
1339
+ scopes: ['manage:PersonalAccessToken'],
1340
+ permissionsConfig: {
1341
+ pat: {
1342
+ enabled: true,
1343
+ allowedOrgRoles: ['admin', 'developer'],
1340
1344
  },
1341
- }, builder);
1342
- const ability = builder.build();
1343
- expect(ability.can('manage', subject('PersonalAccessToken', {}))).toBe(false);
1344
- });
1345
+ },
1346
+ }, builder);
1347
+ const ability = builder.build();
1348
+ expect(ability.can('manage', subject('PersonalAccessToken', {}))).toBe(false);
1345
1349
  });
1346
1350
  });
1347
1351
  });