@lightdash/common 0.1930.3 → 0.1932.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 (78) hide show
  1. package/dist/cjs/authorization/parseScopes.d.ts +8 -0
  2. package/dist/cjs/authorization/parseScopes.d.ts.map +1 -0
  3. package/dist/cjs/authorization/parseScopes.js +27 -0
  4. package/dist/cjs/authorization/parseScopes.js.map +1 -0
  5. package/dist/cjs/authorization/parseScopes.test.d.ts +2 -0
  6. package/dist/cjs/authorization/parseScopes.test.d.ts.map +1 -0
  7. package/dist/cjs/authorization/parseScopes.test.js +109 -0
  8. package/dist/cjs/authorization/parseScopes.test.js.map +1 -0
  9. package/dist/cjs/authorization/scopeAbilityBuilder.d.ts +23 -0
  10. package/dist/cjs/authorization/scopeAbilityBuilder.d.ts.map +1 -0
  11. package/dist/cjs/authorization/scopeAbilityBuilder.js +58 -0
  12. package/dist/cjs/authorization/scopeAbilityBuilder.js.map +1 -0
  13. package/dist/cjs/authorization/scopeAbilityBuilder.test.d.ts +2 -0
  14. package/dist/cjs/authorization/scopeAbilityBuilder.test.d.ts.map +1 -0
  15. package/dist/cjs/authorization/scopeAbilityBuilder.test.js +955 -0
  16. package/dist/cjs/authorization/scopeAbilityBuilder.test.js.map +1 -0
  17. package/dist/cjs/authorization/scopes.d.ts +8 -0
  18. package/dist/cjs/authorization/scopes.d.ts.map +1 -0
  19. package/dist/cjs/authorization/scopes.js +633 -0
  20. package/dist/cjs/authorization/scopes.js.map +1 -0
  21. package/dist/cjs/types/scopes.d.ts +30 -3
  22. package/dist/cjs/types/scopes.d.ts.map +1 -1
  23. package/dist/cjs/types/scopes.js.map +1 -1
  24. package/dist/cjs/types/search.d.ts +1 -0
  25. package/dist/cjs/types/search.d.ts.map +1 -1
  26. package/dist/cjs/types/search.js.map +1 -1
  27. package/dist/esm/authorization/parseScopes.d.ts +8 -0
  28. package/dist/esm/authorization/parseScopes.d.ts.map +1 -0
  29. package/dist/esm/authorization/parseScopes.js +22 -0
  30. package/dist/esm/authorization/parseScopes.js.map +1 -0
  31. package/dist/esm/authorization/parseScopes.test.d.ts +2 -0
  32. package/dist/esm/authorization/parseScopes.test.d.ts.map +1 -0
  33. package/dist/esm/authorization/parseScopes.test.js +107 -0
  34. package/dist/esm/authorization/parseScopes.test.js.map +1 -0
  35. package/dist/esm/authorization/scopeAbilityBuilder.d.ts +23 -0
  36. package/dist/esm/authorization/scopeAbilityBuilder.d.ts.map +1 -0
  37. package/dist/esm/authorization/scopeAbilityBuilder.js +54 -0
  38. package/dist/esm/authorization/scopeAbilityBuilder.js.map +1 -0
  39. package/dist/esm/authorization/scopeAbilityBuilder.test.d.ts +2 -0
  40. package/dist/esm/authorization/scopeAbilityBuilder.test.d.ts.map +1 -0
  41. package/dist/esm/authorization/scopeAbilityBuilder.test.js +953 -0
  42. package/dist/esm/authorization/scopeAbilityBuilder.test.js.map +1 -0
  43. package/dist/esm/authorization/scopes.d.ts +8 -0
  44. package/dist/esm/authorization/scopes.d.ts.map +1 -0
  45. package/dist/esm/authorization/scopes.js +628 -0
  46. package/dist/esm/authorization/scopes.js.map +1 -0
  47. package/dist/esm/types/scopes.d.ts +30 -3
  48. package/dist/esm/types/scopes.d.ts.map +1 -1
  49. package/dist/esm/types/scopes.js.map +1 -1
  50. package/dist/esm/types/search.d.ts +1 -0
  51. package/dist/esm/types/search.d.ts.map +1 -1
  52. package/dist/esm/types/search.js.map +1 -1
  53. package/dist/tsconfig.types.tsbuildinfo +1 -1
  54. package/dist/types/authorization/parseScopes.d.ts +8 -0
  55. package/dist/types/authorization/parseScopes.d.ts.map +1 -0
  56. package/dist/types/authorization/parseScopes.test.d.ts +2 -0
  57. package/dist/types/authorization/parseScopes.test.d.ts.map +1 -0
  58. package/dist/types/authorization/scopeAbilityBuilder.d.ts +23 -0
  59. package/dist/types/authorization/scopeAbilityBuilder.d.ts.map +1 -0
  60. package/dist/types/authorization/scopeAbilityBuilder.test.d.ts +2 -0
  61. package/dist/types/authorization/scopeAbilityBuilder.test.d.ts.map +1 -0
  62. package/dist/types/authorization/scopes.d.ts +8 -0
  63. package/dist/types/authorization/scopes.d.ts.map +1 -0
  64. package/dist/types/types/scopes.d.ts +30 -3
  65. package/dist/types/types/scopes.d.ts.map +1 -1
  66. package/dist/types/types/search.d.ts +1 -0
  67. package/dist/types/types/search.d.ts.map +1 -1
  68. package/package.json +1 -1
  69. package/dist/cjs/authorization/scopes/index.d.ts +0 -5
  70. package/dist/cjs/authorization/scopes/index.d.ts.map +0 -1
  71. package/dist/cjs/authorization/scopes/index.js +0 -372
  72. package/dist/cjs/authorization/scopes/index.js.map +0 -1
  73. package/dist/esm/authorization/scopes/index.d.ts +0 -5
  74. package/dist/esm/authorization/scopes/index.d.ts.map +0 -1
  75. package/dist/esm/authorization/scopes/index.js +0 -368
  76. package/dist/esm/authorization/scopes/index.js.map +0 -1
  77. package/dist/types/authorization/scopes/index.d.ts +0 -5
  78. package/dist/types/authorization/scopes/index.d.ts.map +0 -1
@@ -0,0 +1,955 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ability_1 = require("@casl/ability");
4
+ const projects_1 = require("../types/projects");
5
+ const space_1 = require("../types/space");
6
+ const scopeAbilityBuilder_1 = require("./scopeAbilityBuilder");
7
+ describe('scopeAbilityBuilder', () => {
8
+ describe('buildAbilityFromScopes', () => {
9
+ const baseContext = {
10
+ organizationUuid: 'org-123',
11
+ isEnterprise: false,
12
+ organizationRole: 'admin',
13
+ projectUuid: 'project-123',
14
+ scopes: [],
15
+ };
16
+ it('should build ability with organization view permissions', () => {
17
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
18
+ ...baseContext,
19
+ scopes: ['view:organization'],
20
+ });
21
+ expect(ability.can('view', (0, ability_1.subject)('Organization', {
22
+ organizationUuid: 'org-123',
23
+ projectUuid: 'project-123',
24
+ }))).toBe(true);
25
+ expect(ability.can('view', (0, ability_1.subject)('Organization', {
26
+ organizationUuid: 'different-org',
27
+ projectUuid: 'project-123',
28
+ }))).toBe(false);
29
+ });
30
+ it('should build ability with dashboard view permissions', () => {
31
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
32
+ ...baseContext,
33
+ scopes: ['view:dashboard'],
34
+ });
35
+ // Should be able to view public dashboards
36
+ expect(ability.can('view', (0, ability_1.subject)('Dashboard', {
37
+ organizationUuid: 'org-123',
38
+ projectUuid: 'project-123',
39
+ isPrivate: false,
40
+ }))).toBe(true);
41
+ // Should not be able to view private dashboards without user context
42
+ expect(ability.can('view', (0, ability_1.subject)('Dashboard', {
43
+ organizationUuid: 'org-123',
44
+ projectUuid: 'project-123',
45
+ isPrivate: true,
46
+ }))).toBe(false);
47
+ });
48
+ it('should build ability with dashboard permissions for user with space access', () => {
49
+ const contextWithUser = {
50
+ ...baseContext,
51
+ userUuid: 'user-456',
52
+ };
53
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
54
+ ...contextWithUser,
55
+ scopes: ['view:dashboard'],
56
+ });
57
+ // Can view dashboards with user access
58
+ expect(ability.can('view', (0, ability_1.subject)('Dashboard', {
59
+ organizationUuid: 'org-123',
60
+ projectUuid: 'project-123',
61
+ access: [{ userUuid: 'user-456' }],
62
+ }))).toBe(true);
63
+ });
64
+ it('should build ability with project-scoped permissions', () => {
65
+ const projectContext = {
66
+ ...baseContext,
67
+ projectUuid: 'project-789',
68
+ };
69
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
70
+ ...projectContext,
71
+ scopes: ['view:project'],
72
+ });
73
+ expect(ability.can('view', (0, ability_1.subject)('Project', {
74
+ organizationUuid: 'org-123',
75
+ projectUuid: 'project-789',
76
+ }))).toBe(true);
77
+ });
78
+ it('should build ability with project creation permissions and type restrictions', () => {
79
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
80
+ ...baseContext,
81
+ scopes: ['create:project'],
82
+ });
83
+ // Can create preview projects
84
+ expect(ability.can('create', (0, ability_1.subject)('Project', {
85
+ organizationUuid: 'org-123',
86
+ type: projects_1.ProjectType.PREVIEW,
87
+ }))).toBe(true);
88
+ // Cannot create default projects with basic scope
89
+ expect(ability.can('create', (0, ability_1.subject)('Project', {
90
+ organizationUuid: 'org-123',
91
+ type: projects_1.ProjectType.DEFAULT,
92
+ }))).toBe(false);
93
+ });
94
+ it('should build ability with editor permissions for dashboards', () => {
95
+ const editorContext = {
96
+ ...baseContext,
97
+ userUuid: 'user-456',
98
+ };
99
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
100
+ ...editorContext,
101
+ scopes: ['manage:dashboard'],
102
+ });
103
+ // Can manage dashboards where user is editor
104
+ expect(ability.can('manage', (0, ability_1.subject)('Dashboard', {
105
+ organizationUuid: 'org-123',
106
+ access: [
107
+ {
108
+ userUuid: 'user-456',
109
+ role: space_1.SpaceMemberRole.EDITOR,
110
+ },
111
+ ],
112
+ }))).toBe(true);
113
+ });
114
+ it('should build ability with admin permissions for spaces', () => {
115
+ const adminContext = {
116
+ ...baseContext,
117
+ userUuid: 'user-456',
118
+ };
119
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
120
+ ...adminContext,
121
+ scopes: ['manage:space'],
122
+ });
123
+ // Can manage spaces where user is admin
124
+ expect(ability.can('manage', (0, ability_1.subject)('Space', {
125
+ organizationUuid: 'org-123',
126
+ access: [
127
+ {
128
+ userUuid: 'user-456',
129
+ role: space_1.SpaceMemberRole.ADMIN,
130
+ },
131
+ ],
132
+ }))).toBe(true);
133
+ });
134
+ it('should build ability with user-specific job status permissions', () => {
135
+ const userContext = {
136
+ ...baseContext,
137
+ userUuid: 'user-456',
138
+ };
139
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
140
+ ...userContext,
141
+ scopes: ['view:job_status'],
142
+ });
143
+ // Can view job status created by the user
144
+ expect(ability.can('view', (0, ability_1.subject)('JobStatus', {
145
+ createdByUserUuid: 'user-456',
146
+ }))).toBe(true);
147
+ // Cannot view job status created by another user
148
+ expect(ability.can('view', (0, ability_1.subject)('JobStatus', {
149
+ createdByUserUuid: 'other-user',
150
+ }))).toBe(false);
151
+ });
152
+ it('should build ability with AI agent thread permissions for enterprise users', () => {
153
+ const userContext = {
154
+ ...baseContext,
155
+ userUuid: 'user-456',
156
+ isEnterprise: true,
157
+ };
158
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
159
+ ...userContext,
160
+ scopes: ['manage:ai_agent_thread'],
161
+ });
162
+ // Can manage user's own AI agent threads
163
+ expect(ability.can('manage', (0, ability_1.subject)('AiAgentThread', {
164
+ organizationUuid: 'org-123',
165
+ userUuid: 'user-456',
166
+ }))).toBe(true);
167
+ // Cannot manage another user's threads
168
+ expect(ability.can('manage', (0, ability_1.subject)('AiAgentThread', {
169
+ organizationUuid: 'org-123',
170
+ userUuid: 'other-user',
171
+ }))).toBe(false);
172
+ });
173
+ it('should build ability with basic permissions for scopes without custom logic', () => {
174
+ // These scopes don't have custom applyConditions
175
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
176
+ ...baseContext,
177
+ scopes: ['view:analytics', 'manage:tags'],
178
+ });
179
+ expect(ability.can('view', (0, ability_1.subject)('Analytics', {
180
+ organizationUuid: 'org-123',
181
+ projectUuid: 'project-123',
182
+ }))).toBe(true);
183
+ expect(ability.can('manage', (0, ability_1.subject)('Tags', {
184
+ organizationUuid: 'org-123',
185
+ projectUuid: 'project-123',
186
+ }))).toBe(true);
187
+ });
188
+ it('should handle unknown scopes gracefully', () => {
189
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)(baseContext);
190
+ // Unknown scope should not add any abilities
191
+ expect(ability.rules.length).toBe(0);
192
+ });
193
+ it('should handle enterprise scopes when not enterprise', () => {
194
+ const nonEnterpriseContext = {
195
+ ...baseContext,
196
+ isEnterprise: false,
197
+ };
198
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)(nonEnterpriseContext);
199
+ // Enterprise scope should not add abilities in non-enterprise context
200
+ expect(ability.rules.length).toBe(0);
201
+ });
202
+ it('should build a complete ability from multiple scopes', () => {
203
+ const context = {
204
+ organizationUuid: 'org-123',
205
+ userUuid: 'user-456',
206
+ projectUuid: 'project-789',
207
+ isEnterprise: false,
208
+ organizationRole: 'admin',
209
+ scopes: [
210
+ 'view:dashboard',
211
+ 'manage:saved_chart',
212
+ 'view:project',
213
+ ],
214
+ };
215
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)(context);
216
+ // Check that all abilities were applied
217
+ expect(ability.can('view', (0, ability_1.subject)('Dashboard', {
218
+ organizationUuid: 'org-123',
219
+ projectUuid: 'project-789',
220
+ isPrivate: false,
221
+ }))).toBe(true);
222
+ // Should be able to manage saved charts with proper access
223
+ expect(ability.can('manage', (0, ability_1.subject)('SavedChart', {
224
+ organizationUuid: 'org-123',
225
+ access: [
226
+ {
227
+ userUuid: 'user-456',
228
+ role: space_1.SpaceMemberRole.EDITOR,
229
+ },
230
+ ],
231
+ }))).toBe(true);
232
+ expect(ability.can('view', (0, ability_1.subject)('Project', {
233
+ organizationUuid: 'org-123',
234
+ projectUuid: 'project-789',
235
+ }))).toBe(true);
236
+ });
237
+ describe('scope dependency checks', () => {
238
+ it('should apply organization-level permissions when manage:organization scope is present', () => {
239
+ const contextWithOrgManage = {
240
+ ...baseContext,
241
+ userUuid: 'user-456',
242
+ scopes: ['manage:organization', 'manage:saved_chart'],
243
+ };
244
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)(contextWithOrgManage);
245
+ // Should have organization-wide permissions for saved charts
246
+ expect(ability.can('manage', (0, ability_1.subject)('SavedChart', {
247
+ organizationUuid: 'org-123',
248
+ projectUuid: 'project-123',
249
+ }))).toBe(true);
250
+ // Should not require user access restrictions
251
+ expect(ability.can('manage', (0, ability_1.subject)('SavedChart', {
252
+ organizationUuid: 'org-123',
253
+ projectUuid: 'project-123',
254
+ access: [{ userUuid: 'other-user' }],
255
+ }))).toBe(true);
256
+ });
257
+ it('should apply user-restricted permissions when manage:organization scope is not present', () => {
258
+ const contextWithoutOrgManage = {
259
+ ...baseContext,
260
+ userUuid: 'user-456',
261
+ scopes: ['manage:saved_chart'],
262
+ };
263
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)(contextWithoutOrgManage);
264
+ // Should require user access restrictions
265
+ expect(ability.can('manage', (0, ability_1.subject)('SavedChart', {
266
+ organizationUuid: 'org-123',
267
+ projectUuid: 'project-123',
268
+ access: [
269
+ {
270
+ userUuid: 'user-456',
271
+ role: space_1.SpaceMemberRole.EDITOR,
272
+ },
273
+ ],
274
+ }))).toBe(true);
275
+ // Should not allow access to other users' charts
276
+ expect(ability.can('manage', (0, ability_1.subject)('SavedChart', {
277
+ organizationUuid: 'org-123',
278
+ projectUuid: 'project-123',
279
+ access: [{ userUuid: 'other-user' }],
280
+ }))).toBe(false);
281
+ });
282
+ it('should handle space management with different scope combinations', () => {
283
+ const contextWithProjectManage = {
284
+ ...baseContext,
285
+ userUuid: 'user-456',
286
+ scopes: ['manage:project', 'manage:space'],
287
+ };
288
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)(contextWithProjectManage);
289
+ // Should allow managing public spaces when user has project management
290
+ expect(ability.can('manage', (0, ability_1.subject)('Space', {
291
+ organizationUuid: 'org-123',
292
+ projectUuid: 'project-123',
293
+ isPrivate: false,
294
+ }))).toBe(true);
295
+ // Should still allow managing spaces where user is admin
296
+ expect(ability.can('manage', (0, ability_1.subject)('Space', {
297
+ organizationUuid: 'org-123',
298
+ projectUuid: 'project-123',
299
+ access: [
300
+ {
301
+ userUuid: 'user-456',
302
+ role: space_1.SpaceMemberRole.ADMIN,
303
+ },
304
+ ],
305
+ }))).toBe(true);
306
+ });
307
+ it('should handle promotion permissions based on organization scope', () => {
308
+ const contextWithOrgManage = {
309
+ ...baseContext,
310
+ userUuid: 'user-456',
311
+ scopes: ['manage:organization', 'promote:dashboard'],
312
+ };
313
+ const contextWithoutOrgManage = {
314
+ ...baseContext,
315
+ userUuid: 'user-456',
316
+ scopes: ['promote:dashboard'],
317
+ };
318
+ // Test dashboard promotion with organization management
319
+ const abilityWithOrg = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)(contextWithOrgManage);
320
+ expect(abilityWithOrg.can('promote', (0, ability_1.subject)('Dashboard', {
321
+ organizationUuid: 'org-123',
322
+ projectUuid: 'project-123',
323
+ }))).toBe(true);
324
+ // Test dashboard promotion without organization management
325
+ const abilityWithoutOrg = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)(contextWithoutOrgManage);
326
+ expect(abilityWithoutOrg.can('promote', (0, ability_1.subject)('Dashboard', {
327
+ organizationUuid: 'org-123',
328
+ projectUuid: 'project-123',
329
+ access: [
330
+ {
331
+ userUuid: 'user-456',
332
+ role: space_1.SpaceMemberRole.EDITOR,
333
+ },
334
+ ],
335
+ }))).toBe(true);
336
+ // Should not allow promotion without proper access
337
+ expect(abilityWithoutOrg.can('promote', (0, ability_1.subject)('Dashboard', {
338
+ organizationUuid: 'org-123',
339
+ projectUuid: 'project-123',
340
+ access: [{ userUuid: 'other-user' }],
341
+ }))).toBe(false);
342
+ });
343
+ });
344
+ describe('edge cases and error handling', () => {
345
+ it('should handle empty scope array', () => {
346
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)(baseContext);
347
+ expect(ability.rules.length).toBe(0);
348
+ });
349
+ it('should handle undefined userUuid in context', () => {
350
+ const contextWithoutUser = {
351
+ ...baseContext,
352
+ userUuid: undefined,
353
+ };
354
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
355
+ ...contextWithoutUser,
356
+ scopes: ['view:dashboard'],
357
+ });
358
+ // Should only allow viewing public dashboards
359
+ expect(ability.can('view', (0, ability_1.subject)('Dashboard', {
360
+ organizationUuid: 'org-123',
361
+ projectUuid: 'project-123',
362
+ isPrivate: false,
363
+ }))).toBe(true);
364
+ expect(ability.can('view', (0, ability_1.subject)('Dashboard', {
365
+ organizationUuid: 'org-123',
366
+ projectUuid: 'project-123',
367
+ isPrivate: true,
368
+ }))).toBe(false);
369
+ });
370
+ it('should handle missing projectUuid in context', () => {
371
+ const contextWithoutProject = {
372
+ ...baseContext,
373
+ projectUuid: '',
374
+ };
375
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
376
+ ...contextWithoutProject,
377
+ scopes: ['view:dashboard'],
378
+ });
379
+ // Should work with public dashboards without project restriction
380
+ expect(ability.can('view', (0, ability_1.subject)('Dashboard', {
381
+ organizationUuid: 'org-123',
382
+ isPrivate: false,
383
+ }))).toBe(true);
384
+ });
385
+ it('should handle mixed valid and invalid scopes', () => {
386
+ // Should throw error for invalid scopes
387
+ expect(() => {
388
+ (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
389
+ ...baseContext,
390
+ scopes: [
391
+ 'view:dashboard',
392
+ 'view:project',
393
+ 'invalid:scope',
394
+ ],
395
+ });
396
+ }).toThrow('Invalid scope: invalid:Scope. Please check the scope name and try again.');
397
+ // Should work with only valid scopes
398
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
399
+ ...baseContext,
400
+ scopes: ['view:dashboard', 'view:project'],
401
+ });
402
+ // Should apply valid scopes
403
+ expect(ability.can('view', (0, ability_1.subject)('Dashboard', {
404
+ organizationUuid: 'org-123',
405
+ projectUuid: 'project-123',
406
+ isPrivate: false,
407
+ }))).toBe(true);
408
+ expect(ability.can('view', (0, ability_1.subject)('Project', {
409
+ organizationUuid: 'org-123',
410
+ projectUuid: 'project-123',
411
+ }))).toBe(true);
412
+ // Should have rules from valid scopes
413
+ expect(ability.rules.length).toBeGreaterThan(0);
414
+ });
415
+ });
416
+ describe('cross-boundary access tests', () => {
417
+ it('should not allow access to resources from different organizations', () => {
418
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
419
+ ...baseContext,
420
+ scopes: [
421
+ 'view:dashboard',
422
+ 'manage:saved_chart',
423
+ 'view:space',
424
+ ],
425
+ });
426
+ // Should not access dashboard from different org
427
+ expect(ability.can('view', (0, ability_1.subject)('Dashboard', {
428
+ organizationUuid: 'different-org',
429
+ isPrivate: false,
430
+ }))).toBe(false);
431
+ // Should not manage saved chart from different org
432
+ expect(ability.can('manage', (0, ability_1.subject)('SavedChart', {
433
+ organizationUuid: 'different-org',
434
+ }))).toBe(false);
435
+ // Should not view space from different org
436
+ expect(ability.can('view', (0, ability_1.subject)('Space', {
437
+ organizationUuid: 'different-org',
438
+ isPrivate: false,
439
+ }))).toBe(false);
440
+ });
441
+ it('should not allow access to resources from different projects', () => {
442
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
443
+ ...baseContext,
444
+ scopes: ['view:saved_chart'],
445
+ });
446
+ // Should not access saved chart from different project
447
+ expect(ability.can('view', (0, ability_1.subject)('SavedChart', {
448
+ organizationUuid: 'org-123',
449
+ projectUuid: 'different-project',
450
+ isPrivate: false,
451
+ }))).toBe(false);
452
+ });
453
+ });
454
+ describe('private resource access with space roles', () => {
455
+ it('should handle viewer role access to private resources', () => {
456
+ const contextWithUser = {
457
+ ...baseContext,
458
+ userUuid: 'user-456',
459
+ };
460
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
461
+ ...contextWithUser,
462
+ scopes: ['view:dashboard'],
463
+ });
464
+ // Can view private dashboard with viewer access
465
+ expect(ability.can('view', (0, ability_1.subject)('Dashboard', {
466
+ organizationUuid: 'org-123',
467
+ isPrivate: true,
468
+ access: [
469
+ {
470
+ userUuid: 'user-456',
471
+ role: space_1.SpaceMemberRole.VIEWER,
472
+ },
473
+ ],
474
+ }))).toBe(true);
475
+ // Cannot view private dashboard without access
476
+ expect(ability.can('view', (0, ability_1.subject)('Dashboard', {
477
+ organizationUuid: 'org-123',
478
+ isPrivate: true,
479
+ access: [],
480
+ }))).toBe(false);
481
+ // Cannot view private dashboard with access for another user
482
+ expect(ability.can('view', (0, ability_1.subject)('Dashboard', {
483
+ organizationUuid: 'org-123',
484
+ isPrivate: true,
485
+ access: [
486
+ {
487
+ userUuid: 'other-user',
488
+ role: space_1.SpaceMemberRole.VIEWER,
489
+ },
490
+ ],
491
+ }))).toBe(false);
492
+ });
493
+ it('should handle editor role for managing resources', () => {
494
+ const contextWithUser = {
495
+ ...baseContext,
496
+ userUuid: 'user-456',
497
+ };
498
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
499
+ ...contextWithUser,
500
+ scopes: ['manage:dashboard'],
501
+ });
502
+ // Can manage dashboard with editor role
503
+ expect(ability.can('manage', (0, ability_1.subject)('Dashboard', {
504
+ organizationUuid: 'org-123',
505
+ access: [
506
+ {
507
+ userUuid: 'user-456',
508
+ role: space_1.SpaceMemberRole.EDITOR,
509
+ },
510
+ ],
511
+ }))).toBe(true);
512
+ // Can manage dashboard with admin role
513
+ expect(ability.can('manage', (0, ability_1.subject)('Dashboard', {
514
+ organizationUuid: 'org-123',
515
+ access: [
516
+ {
517
+ userUuid: 'user-456',
518
+ role: space_1.SpaceMemberRole.ADMIN,
519
+ },
520
+ ],
521
+ }))).toBe(true);
522
+ // Cannot manage dashboard with viewer role
523
+ expect(ability.can('manage', (0, ability_1.subject)('Dashboard', {
524
+ organizationUuid: 'org-123',
525
+ access: [
526
+ {
527
+ userUuid: 'user-456',
528
+ role: space_1.SpaceMemberRole.VIEWER,
529
+ },
530
+ ],
531
+ }))).toBe(false);
532
+ // Cannot manage dashboard without access
533
+ expect(ability.can('manage', (0, ability_1.subject)('Dashboard', {
534
+ organizationUuid: 'org-123',
535
+ access: [],
536
+ }))).toBe(false);
537
+ });
538
+ it('should handle space admin role for managing spaces', () => {
539
+ const contextWithUser = {
540
+ ...baseContext,
541
+ userUuid: 'user-456',
542
+ };
543
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
544
+ ...contextWithUser,
545
+ scopes: ['manage:space'],
546
+ });
547
+ // Can manage space with admin role
548
+ expect(ability.can('manage', (0, ability_1.subject)('Space', {
549
+ organizationUuid: 'org-123',
550
+ access: [
551
+ {
552
+ userUuid: 'user-456',
553
+ role: space_1.SpaceMemberRole.ADMIN,
554
+ },
555
+ ],
556
+ }))).toBe(true);
557
+ // Cannot manage space with editor role
558
+ expect(ability.can('manage', (0, ability_1.subject)('Space', {
559
+ organizationUuid: 'org-123',
560
+ isPrivate: true,
561
+ access: [
562
+ {
563
+ userUuid: 'user-456',
564
+ role: space_1.SpaceMemberRole.EDITOR,
565
+ },
566
+ ],
567
+ }))).toBe(false);
568
+ // Cannot manage space with viewer role
569
+ expect(ability.can('manage', (0, ability_1.subject)('Space', {
570
+ organizationUuid: 'org-123',
571
+ isPrivate: true,
572
+ access: [
573
+ {
574
+ userUuid: 'user-456',
575
+ role: space_1.SpaceMemberRole.VIEWER,
576
+ },
577
+ ],
578
+ }))).toBe(false);
579
+ });
580
+ });
581
+ describe('job and job status permissions', () => {
582
+ it('should handle view:job permissions', () => {
583
+ const contextWithUser = {
584
+ ...baseContext,
585
+ userUuid: 'user-456',
586
+ };
587
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
588
+ ...contextWithUser,
589
+ scopes: ['view:job'],
590
+ });
591
+ // Can view own jobs
592
+ expect(ability.can('view', (0, ability_1.subject)('Job', {
593
+ userUuid: 'user-456',
594
+ }))).toBe(true);
595
+ // Cannot view other users' jobs without manage permission
596
+ expect(ability.can('view', (0, ability_1.subject)('Job', {
597
+ userUuid: 'other-user',
598
+ }))).toBe(false);
599
+ });
600
+ it('should handle view:job_status permissions for organization context', () => {
601
+ const contextWithoutUser = {
602
+ ...baseContext,
603
+ userUuid: undefined,
604
+ };
605
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
606
+ ...contextWithoutUser,
607
+ scopes: ['view:job_status'],
608
+ });
609
+ // Cannot view job status without user context when no manage:Organization scope
610
+ expect(ability.can('view', (0, ability_1.subject)('JobStatus', {
611
+ organizationUuid: 'org-123',
612
+ }))).toBe(false);
613
+ // Cannot view job status from another organization
614
+ expect(ability.can('view', (0, ability_1.subject)('JobStatus', {
615
+ organizationUuid: 'different-org',
616
+ }))).toBe(false);
617
+ });
618
+ it('should handle view:job_status permissions with manage:Organization scope', () => {
619
+ const contextWithoutUser = {
620
+ ...baseContext,
621
+ userUuid: undefined,
622
+ };
623
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
624
+ ...contextWithoutUser,
625
+ scopes: ['view:job_status', 'manage:organization'],
626
+ });
627
+ // Can view all job status in organization when manage:Organization scope is present
628
+ expect(ability.can('view', (0, ability_1.subject)('JobStatus', {
629
+ organizationUuid: 'org-123',
630
+ }))).toBe(true);
631
+ // Cannot view job status from another organization
632
+ expect(ability.can('view', (0, ability_1.subject)('JobStatus', {
633
+ organizationUuid: 'different-org',
634
+ }))).toBe(false);
635
+ });
636
+ it('should handle view:job_status permissions for user context', () => {
637
+ const contextWithUser = {
638
+ ...baseContext,
639
+ userUuid: 'user-456',
640
+ };
641
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
642
+ ...contextWithUser,
643
+ scopes: ['view:job_status'],
644
+ });
645
+ // Can view own job status
646
+ expect(ability.can('view', (0, ability_1.subject)('JobStatus', {
647
+ createdByUserUuid: 'user-456',
648
+ }))).toBe(true);
649
+ // Cannot view other users' job status
650
+ expect(ability.can('view', (0, ability_1.subject)('JobStatus', {
651
+ createdByUserUuid: 'other-user',
652
+ }))).toBe(false);
653
+ });
654
+ });
655
+ describe('semantic viewer permissions', () => {
656
+ it('should handle view:semantic_viewer permissions', () => {
657
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
658
+ ...baseContext,
659
+ scopes: ['view:semantic_viewer'],
660
+ });
661
+ expect(ability.can('view', (0, ability_1.subject)('SemanticViewer', {
662
+ organizationUuid: 'org-123',
663
+ projectUuid: 'project-123',
664
+ }))).toBe(true);
665
+ });
666
+ it('should handle manage:semantic_viewer with organization scope', () => {
667
+ const contextWithOrgManage = {
668
+ ...baseContext,
669
+ userUuid: 'user-456',
670
+ scopes: ['manage:organization'],
671
+ };
672
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
673
+ ...contextWithOrgManage,
674
+ scopes: ['manage:organization', 'manage:semantic_viewer'],
675
+ });
676
+ // Can manage semantic viewer organization-wide
677
+ expect(ability.can('manage', (0, ability_1.subject)('SemanticViewer', {
678
+ organizationUuid: 'org-123',
679
+ }))).toBe(true);
680
+ });
681
+ it('should handle manage:semantic_viewer with editor role', () => {
682
+ const contextWithUser = {
683
+ ...baseContext,
684
+ userUuid: 'user-456',
685
+ };
686
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
687
+ ...contextWithUser,
688
+ scopes: ['manage:semantic_viewer'],
689
+ });
690
+ // Can manage semantic viewer with editor role
691
+ expect(ability.can('manage', (0, ability_1.subject)('SemanticViewer', {
692
+ organizationUuid: 'org-123',
693
+ access: [
694
+ {
695
+ userUuid: 'user-456',
696
+ role: space_1.SpaceMemberRole.EDITOR,
697
+ },
698
+ ],
699
+ }))).toBe(true);
700
+ // Cannot manage without proper access
701
+ expect(ability.can('manage', (0, ability_1.subject)('SemanticViewer', {
702
+ organizationUuid: 'org-123',
703
+ access: [
704
+ {
705
+ userUuid: 'user-456',
706
+ role: space_1.SpaceMemberRole.VIEWER,
707
+ },
708
+ ],
709
+ }))).toBe(false);
710
+ });
711
+ });
712
+ describe('create space permissions', () => {
713
+ it('should handle create:space permissions', () => {
714
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
715
+ ...baseContext,
716
+ scopes: ['create:space'],
717
+ });
718
+ expect(ability.can('create', (0, ability_1.subject)('Space', {
719
+ organizationUuid: 'org-123',
720
+ projectUuid: 'project-123',
721
+ }))).toBe(true);
722
+ });
723
+ });
724
+ describe('export permissions', () => {
725
+ it('should handle export csv permissions', () => {
726
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
727
+ ...baseContext,
728
+ scopes: ['manage:export_csv'],
729
+ });
730
+ expect(ability.can('manage', (0, ability_1.subject)('ExportCsv', {
731
+ organizationUuid: 'org-123',
732
+ projectUuid: 'project-123',
733
+ }))).toBe(true);
734
+ });
735
+ it('should handle change csv results permissions', () => {
736
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
737
+ ...baseContext,
738
+ scopes: ['manage:change_csv_results'],
739
+ });
740
+ expect(ability.can('manage', (0, ability_1.subject)('ChangeCsvResults', {
741
+ organizationUuid: 'org-123',
742
+ projectUuid: 'project-123',
743
+ }))).toBe(true);
744
+ });
745
+ });
746
+ describe('underlying data permissions', () => {
747
+ it('should handle view:underlying_data permissions', () => {
748
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
749
+ ...baseContext,
750
+ scopes: ['view:underlying_data'],
751
+ });
752
+ expect(ability.can('view', (0, ability_1.subject)('UnderlyingData', {
753
+ organizationUuid: 'org-123',
754
+ projectUuid: 'project-123',
755
+ }))).toBe(true);
756
+ });
757
+ });
758
+ describe('sql runner and custom sql permissions', () => {
759
+ it('should handle manage:sql_runner permissions', () => {
760
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
761
+ ...baseContext,
762
+ scopes: ['manage:sql_runner'],
763
+ });
764
+ expect(ability.can('manage', (0, ability_1.subject)('SqlRunner', {
765
+ organizationUuid: 'org-123',
766
+ projectUuid: 'project-123',
767
+ }))).toBe(true);
768
+ });
769
+ it('should handle manage:custom_sql permissions', () => {
770
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
771
+ ...baseContext,
772
+ scopes: ['manage:custom_sql'],
773
+ });
774
+ expect(ability.can('manage', (0, ability_1.subject)('CustomSql', {
775
+ organizationUuid: 'org-123',
776
+ projectUuid: 'project-123',
777
+ }))).toBe(true);
778
+ });
779
+ });
780
+ describe('project delete permissions', () => {
781
+ it('should handle delete:project with projectUuid', () => {
782
+ const contextWithProject = {
783
+ ...baseContext,
784
+ projectUuid: 'project-456',
785
+ };
786
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
787
+ ...contextWithProject,
788
+ scopes: ['delete:project'],
789
+ });
790
+ // Can delete specific project
791
+ expect(ability.can('delete', (0, ability_1.subject)('Project', {
792
+ projectUuid: 'project-456',
793
+ }))).toBe(true);
794
+ // Cannot delete different project
795
+ expect(ability.can('delete', (0, ability_1.subject)('Project', {
796
+ projectUuid: 'different-project',
797
+ }))).toBe(false);
798
+ });
799
+ it('should handle delete:project for preview projects', () => {
800
+ const contextWithoutProject = {
801
+ ...baseContext,
802
+ projectUuid: '',
803
+ };
804
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
805
+ ...contextWithoutProject,
806
+ scopes: ['delete:project'],
807
+ });
808
+ // Can delete preview projects in organization
809
+ expect(ability.can('delete', (0, ability_1.subject)('Project', {
810
+ organizationUuid: 'org-123',
811
+ type: projects_1.ProjectType.PREVIEW,
812
+ }))).toBe(true);
813
+ // Cannot delete default projects
814
+ expect(ability.can('delete', (0, ability_1.subject)('Project', {
815
+ organizationUuid: 'org-123',
816
+ type: projects_1.ProjectType.DEFAULT,
817
+ }))).toBe(false);
818
+ });
819
+ });
820
+ describe('pinned items permissions', () => {
821
+ it('should handle view:pinned_items permissions', () => {
822
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
823
+ ...baseContext,
824
+ scopes: ['view:pinned_items'],
825
+ });
826
+ expect(ability.can('view', (0, ability_1.subject)('PinnedItems', {
827
+ organizationUuid: 'org-123',
828
+ projectUuid: 'project-123',
829
+ }))).toBe(true);
830
+ });
831
+ it('should handle manage:pinned_items permissions', () => {
832
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
833
+ ...baseContext,
834
+ scopes: ['manage:pinned_items'],
835
+ });
836
+ expect(ability.can('manage', (0, ability_1.subject)('PinnedItems', {
837
+ organizationUuid: 'org-123',
838
+ projectUuid: 'project-123',
839
+ }))).toBe(true);
840
+ });
841
+ });
842
+ describe('explore permissions', () => {
843
+ it('should handle manage:explore permissions', () => {
844
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
845
+ ...baseContext,
846
+ scopes: ['manage:explore'],
847
+ });
848
+ expect(ability.can('manage', (0, ability_1.subject)('Explore', {
849
+ organizationUuid: 'org-123',
850
+ projectUuid: 'project-123',
851
+ }))).toBe(true);
852
+ });
853
+ });
854
+ describe('organization member profile permissions', () => {
855
+ it('should handle view:organization_member_profile permissions', () => {
856
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
857
+ ...baseContext,
858
+ scopes: ['view:organization_member_profile'],
859
+ });
860
+ expect(ability.can('view', (0, ability_1.subject)('OrganizationMemberProfile', {
861
+ organizationUuid: 'org-123',
862
+ projectUuid: 'project-123',
863
+ }))).toBe(true);
864
+ // Cannot view profiles from different organization
865
+ expect(ability.can('view', (0, ability_1.subject)('OrganizationMemberProfile', {
866
+ organizationUuid: 'different-org',
867
+ projectUuid: 'project-123',
868
+ }))).toBe(false);
869
+ });
870
+ it('should handle manage:organization_member_profile permissions', () => {
871
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
872
+ ...baseContext,
873
+ scopes: ['manage:organization_member_profile'],
874
+ });
875
+ expect(ability.can('manage', (0, ability_1.subject)('OrganizationMemberProfile', {
876
+ organizationUuid: 'org-123',
877
+ projectUuid: 'project-123',
878
+ }))).toBe(true);
879
+ });
880
+ });
881
+ describe('personal access token permissions', () => {
882
+ it('should allow managing PAT when enabled and user has allowed role', () => {
883
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
884
+ ...baseContext,
885
+ isEnterprise: true,
886
+ organizationRole: 'admin',
887
+ scopes: ['manage:personal_access_token'],
888
+ permissionsConfig: {
889
+ pat: {
890
+ enabled: true,
891
+ allowedOrgRoles: ['admin', 'developer'],
892
+ },
893
+ },
894
+ });
895
+ expect(ability.can('manage', 'PersonalAccessToken')).toBe(true);
896
+ });
897
+ it('should not allow managing PAT when disabled', () => {
898
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
899
+ ...baseContext,
900
+ isEnterprise: true,
901
+ organizationRole: 'admin',
902
+ scopes: ['manage:personal_access_token'],
903
+ permissionsConfig: {
904
+ pat: {
905
+ enabled: false,
906
+ allowedOrgRoles: ['admin', 'developer'],
907
+ },
908
+ },
909
+ });
910
+ expect(ability.can('manage', (0, ability_1.subject)('PersonalAccessToken', {}))).toBe(false);
911
+ });
912
+ it('should not allow managing PAT when user role not in allowed roles', () => {
913
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
914
+ ...baseContext,
915
+ isEnterprise: true,
916
+ organizationRole: 'developer',
917
+ scopes: ['manage:personal_access_token'],
918
+ permissionsConfig: {
919
+ pat: {
920
+ enabled: true,
921
+ allowedOrgRoles: ['admin'],
922
+ },
923
+ },
924
+ });
925
+ expect(ability.can('manage', (0, ability_1.subject)('PersonalAccessToken', {}))).toBe(false);
926
+ });
927
+ it('should not allow managing PAT when no permissions config provided', () => {
928
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
929
+ ...baseContext,
930
+ isEnterprise: true,
931
+ organizationRole: 'admin',
932
+ scopes: ['manage:personal_access_token'],
933
+ // No permissionsConfig provided
934
+ });
935
+ expect(ability.can('manage', (0, ability_1.subject)('PersonalAccessToken', {}))).toBe(false);
936
+ });
937
+ it('should not allow managing PAT when no organization role provided', () => {
938
+ const ability = (0, scopeAbilityBuilder_1.buildAbilityFromScopes)({
939
+ ...baseContext,
940
+ isEnterprise: true,
941
+ organizationRole: '', // Empty organization role
942
+ scopes: ['manage:personal_access_token'],
943
+ permissionsConfig: {
944
+ pat: {
945
+ enabled: true,
946
+ allowedOrgRoles: ['admin', 'developer'],
947
+ },
948
+ },
949
+ });
950
+ expect(ability.can('manage', (0, ability_1.subject)('PersonalAccessToken', {}))).toBe(false);
951
+ });
952
+ });
953
+ });
954
+ });
955
+ //# sourceMappingURL=scopeAbilityBuilder.test.js.map