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