@lightdash/common 0.1947.0 → 0.1948.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 (50) hide show
  1. package/dist/cjs/authorization/projectMemberAbility.d.ts.map +1 -1
  2. package/dist/cjs/authorization/projectMemberAbility.js +0 -4
  3. package/dist/cjs/authorization/projectMemberAbility.js.map +1 -1
  4. package/dist/cjs/authorization/roleToScopeMapping.d.ts.map +1 -1
  5. package/dist/cjs/authorization/roleToScopeMapping.js +11 -10
  6. package/dist/cjs/authorization/roleToScopeMapping.js.map +1 -1
  7. package/dist/cjs/authorization/roleToScopeParity.test.d.ts +2 -0
  8. package/dist/cjs/authorization/roleToScopeParity.test.d.ts.map +1 -0
  9. package/dist/cjs/authorization/roleToScopeParity.test.js +195 -0
  10. package/dist/cjs/authorization/roleToScopeParity.test.js.map +1 -0
  11. package/dist/cjs/authorization/scopeAbilityBuilder.d.ts.map +1 -1
  12. package/dist/cjs/authorization/scopeAbilityBuilder.js +10 -7
  13. package/dist/cjs/authorization/scopeAbilityBuilder.js.map +1 -1
  14. package/dist/cjs/authorization/scopeAbilityBuilder.test.js +312 -89
  15. package/dist/cjs/authorization/scopeAbilityBuilder.test.js.map +1 -1
  16. package/dist/cjs/authorization/scopes.d.ts.map +1 -1
  17. package/dist/cjs/authorization/scopes.js +102 -90
  18. package/dist/cjs/authorization/scopes.js.map +1 -1
  19. package/dist/cjs/types/scopes.d.ts +2 -2
  20. package/dist/cjs/types/scopes.d.ts.map +1 -1
  21. package/dist/esm/authorization/projectMemberAbility.d.ts.map +1 -1
  22. package/dist/esm/authorization/projectMemberAbility.js +0 -4
  23. package/dist/esm/authorization/projectMemberAbility.js.map +1 -1
  24. package/dist/esm/authorization/roleToScopeMapping.d.ts.map +1 -1
  25. package/dist/esm/authorization/roleToScopeMapping.js +11 -10
  26. package/dist/esm/authorization/roleToScopeMapping.js.map +1 -1
  27. package/dist/esm/authorization/roleToScopeParity.test.d.ts +2 -0
  28. package/dist/esm/authorization/roleToScopeParity.test.d.ts.map +1 -0
  29. package/dist/esm/authorization/roleToScopeParity.test.js +193 -0
  30. package/dist/esm/authorization/roleToScopeParity.test.js.map +1 -0
  31. package/dist/esm/authorization/scopeAbilityBuilder.d.ts.map +1 -1
  32. package/dist/esm/authorization/scopeAbilityBuilder.js +10 -7
  33. package/dist/esm/authorization/scopeAbilityBuilder.js.map +1 -1
  34. package/dist/esm/authorization/scopeAbilityBuilder.test.js +312 -89
  35. package/dist/esm/authorization/scopeAbilityBuilder.test.js.map +1 -1
  36. package/dist/esm/authorization/scopes.d.ts.map +1 -1
  37. package/dist/esm/authorization/scopes.js +102 -90
  38. package/dist/esm/authorization/scopes.js.map +1 -1
  39. package/dist/esm/types/scopes.d.ts +2 -2
  40. package/dist/esm/types/scopes.d.ts.map +1 -1
  41. package/dist/tsconfig.types.tsbuildinfo +1 -1
  42. package/dist/types/authorization/projectMemberAbility.d.ts.map +1 -1
  43. package/dist/types/authorization/roleToScopeMapping.d.ts.map +1 -1
  44. package/dist/types/authorization/roleToScopeParity.test.d.ts +2 -0
  45. package/dist/types/authorization/roleToScopeParity.test.d.ts.map +1 -0
  46. package/dist/types/authorization/scopeAbilityBuilder.d.ts.map +1 -1
  47. package/dist/types/authorization/scopes.d.ts.map +1 -1
  48. package/dist/types/types/scopes.d.ts +2 -2
  49. package/dist/types/types/scopes.d.ts.map +1 -1
  50. package/package.json +1 -1
@@ -20,7 +20,7 @@ describe('scopeAbilityBuilder', () => {
20
20
  const builder = new AbilityBuilder(Ability);
21
21
  buildAbilityFromScopes({
22
22
  ...baseContextWithOrg,
23
- scopes: ['view:organization'],
23
+ scopes: ['view:Organization'],
24
24
  }, builder);
25
25
  const ability = builder.build();
26
26
  expect(ability.can('view', subject('Organization', {
@@ -36,7 +36,7 @@ describe('scopeAbilityBuilder', () => {
36
36
  const builder = new AbilityBuilder(Ability);
37
37
  buildAbilityFromScopes({
38
38
  ...baseContext,
39
- scopes: ['view:dashboard'],
39
+ scopes: ['view:Dashboard'],
40
40
  }, builder);
41
41
  const ability = builder.build();
42
42
  // Should be able to view public dashboards
@@ -60,7 +60,7 @@ describe('scopeAbilityBuilder', () => {
60
60
  const builder = new AbilityBuilder(Ability);
61
61
  buildAbilityFromScopes({
62
62
  ...contextWithUser,
63
- scopes: ['view:dashboard'],
63
+ scopes: ['view:Dashboard'],
64
64
  }, builder);
65
65
  const ability = builder.build();
66
66
  // Can view dashboards with user access
@@ -78,7 +78,7 @@ describe('scopeAbilityBuilder', () => {
78
78
  const builder = new AbilityBuilder(Ability);
79
79
  buildAbilityFromScopes({
80
80
  ...projectContext,
81
- scopes: ['view:project'],
81
+ scopes: ['view:Project'],
82
82
  }, builder);
83
83
  const ability = builder.build();
84
84
  expect(ability.can('view', subject('Project', {
@@ -90,7 +90,7 @@ describe('scopeAbilityBuilder', () => {
90
90
  const builder = new AbilityBuilder(Ability);
91
91
  buildAbilityFromScopes({
92
92
  ...baseContextWithOrg,
93
- scopes: ['create:project'],
93
+ scopes: ['create:Project'],
94
94
  }, builder);
95
95
  const ability = builder.build();
96
96
  // Can create preview projects
@@ -112,7 +112,7 @@ describe('scopeAbilityBuilder', () => {
112
112
  const builder = new AbilityBuilder(Ability);
113
113
  buildAbilityFromScopes({
114
114
  ...editorContext,
115
- scopes: ['manage:dashboard'],
115
+ scopes: ['manage:Dashboard'],
116
116
  }, builder);
117
117
  const ability = builder.build();
118
118
  // Can manage dashboards where user is editor
@@ -134,7 +134,7 @@ describe('scopeAbilityBuilder', () => {
134
134
  const builder = new AbilityBuilder(Ability);
135
135
  buildAbilityFromScopes({
136
136
  ...adminContext,
137
- scopes: ['manage:space'],
137
+ scopes: ['manage:Space'],
138
138
  }, builder);
139
139
  const ability = builder.build();
140
140
  // Can manage spaces where user is admin
@@ -156,7 +156,7 @@ describe('scopeAbilityBuilder', () => {
156
156
  const builder = new AbilityBuilder(Ability);
157
157
  buildAbilityFromScopes({
158
158
  ...userContext,
159
- scopes: ['view:job_status'],
159
+ scopes: ['view:JobStatus@self'],
160
160
  }, builder);
161
161
  const ability = builder.build();
162
162
  // Can view job status created by the user
@@ -177,17 +177,17 @@ describe('scopeAbilityBuilder', () => {
177
177
  const builder = new AbilityBuilder(Ability);
178
178
  buildAbilityFromScopes({
179
179
  ...userContext,
180
- scopes: ['manage:ai_agent_thread'],
180
+ scopes: ['manage:AiAgentThread@self'],
181
181
  }, builder);
182
182
  const ability = builder.build();
183
183
  // Can manage user's own AI agent threads
184
184
  expect(ability.can('manage', subject('AiAgentThread', {
185
- organizationUuid: 'org-123',
185
+ projectUuid: 'project-123',
186
186
  userUuid: 'user-456',
187
187
  }))).toBe(true);
188
188
  // Cannot manage another user's threads
189
189
  expect(ability.can('manage', subject('AiAgentThread', {
190
- organizationUuid: 'org-123',
190
+ projectUuid: 'project-123',
191
191
  userUuid: 'other-user',
192
192
  }))).toBe(false);
193
193
  });
@@ -196,7 +196,7 @@ describe('scopeAbilityBuilder', () => {
196
196
  const builder = new AbilityBuilder(Ability);
197
197
  buildAbilityFromScopes({
198
198
  ...baseContext,
199
- scopes: ['view:analytics', 'manage:tags'],
199
+ scopes: ['view:Analytics', 'manage:Tags'],
200
200
  }, builder);
201
201
  const ability = builder.build();
202
202
  expect(ability.can('view', subject('Analytics', {
@@ -232,11 +232,7 @@ describe('scopeAbilityBuilder', () => {
232
232
  organizationUuid: 'org-123',
233
233
  isEnterprise: false,
234
234
  organizationRole: 'admin',
235
- scopes: [
236
- 'view:dashboard',
237
- 'manage:saved_chart',
238
- 'view:project',
239
- ],
235
+ scopes: ['view:Dashboard', 'manage:SavedChart', 'view:Project'],
240
236
  };
241
237
  const builder = new AbilityBuilder(Ability);
242
238
  buildAbilityFromScopes(context, builder);
@@ -267,7 +263,7 @@ describe('scopeAbilityBuilder', () => {
267
263
  const contextWithOrgManage = {
268
264
  ...baseContext,
269
265
  userUuid: 'user-456',
270
- scopes: ['manage:organization', 'manage:saved_chart'],
266
+ scopes: ['manage:Organization', 'manage:SavedChart'],
271
267
  };
272
268
  const builder = new AbilityBuilder(Ability);
273
269
  buildAbilityFromScopes(contextWithOrgManage, builder);
@@ -288,7 +284,7 @@ describe('scopeAbilityBuilder', () => {
288
284
  const contextWithoutOrgManage = {
289
285
  ...baseContext,
290
286
  userUuid: 'user-456',
291
- scopes: ['manage:saved_chart'],
287
+ scopes: ['manage:SavedChart@space'],
292
288
  };
293
289
  const builder = new AbilityBuilder(Ability);
294
290
  buildAbilityFromScopes(contextWithoutOrgManage, builder);
@@ -315,7 +311,7 @@ describe('scopeAbilityBuilder', () => {
315
311
  const contextWithProjectManage = {
316
312
  ...baseContext,
317
313
  userUuid: 'user-456',
318
- scopes: ['manage:project', 'manage:space'],
314
+ scopes: ['manage:Project', 'manage:Space'],
319
315
  };
320
316
  const builder = new AbilityBuilder(Ability);
321
317
  buildAbilityFromScopes(contextWithProjectManage, builder);
@@ -342,12 +338,12 @@ describe('scopeAbilityBuilder', () => {
342
338
  const contextWithOrgManage = {
343
339
  ...baseContext,
344
340
  userUuid: 'user-456',
345
- scopes: ['manage:organization', 'promote:dashboard'],
341
+ scopes: ['manage:Organization', 'promote:Dashboard'],
346
342
  };
347
343
  const contextWithoutOrgManage = {
348
344
  ...baseContext,
349
345
  userUuid: 'user-456',
350
- scopes: ['promote:dashboard'],
346
+ scopes: ['promote:Dashboard@space'],
351
347
  };
352
348
  // Test dashboard promotion with organization management
353
349
  const builder = new AbilityBuilder(Ability);
@@ -379,6 +375,82 @@ describe('scopeAbilityBuilder', () => {
379
375
  }))).toBe(false);
380
376
  });
381
377
  });
378
+ describe('AI agent thread permissions with modifiers', () => {
379
+ it('should handle view:ai_agent_thread@self permissions', () => {
380
+ const contextWithUser = {
381
+ ...baseContext,
382
+ userUuid: 'user-456',
383
+ isEnterprise: true,
384
+ };
385
+ const builder = new AbilityBuilder(Ability);
386
+ buildAbilityFromScopes({
387
+ ...contextWithUser,
388
+ scopes: ['view:AiAgentThread@self'],
389
+ }, builder);
390
+ const ability = builder.build();
391
+ // Can view own AI agent threads
392
+ expect(ability.can('view', subject('AiAgentThread', {
393
+ projectUuid: 'project-123',
394
+ userUuid: 'user-456',
395
+ }))).toBe(true);
396
+ // Cannot view other users' threads
397
+ expect(ability.can('view', subject('AiAgentThread', {
398
+ projectUuid: 'project-123',
399
+ userUuid: 'other-user',
400
+ }))).toBe(false);
401
+ });
402
+ it('should handle manage:ai_agent_thread@self permissions', () => {
403
+ const contextWithUser = {
404
+ ...baseContext,
405
+ userUuid: 'user-456',
406
+ isEnterprise: true,
407
+ };
408
+ const builder = new AbilityBuilder(Ability);
409
+ buildAbilityFromScopes({
410
+ ...contextWithUser,
411
+ userUuid: 'user-456',
412
+ scopes: ['manage:AiAgentThread@self'],
413
+ }, builder);
414
+ const ability = builder.build();
415
+ // Can manage own AI agent threads
416
+ expect(ability.can('manage', subject('AiAgentThread', {
417
+ projectUuid: 'project-123',
418
+ userUuid: 'user-456',
419
+ }))).toBe(true);
420
+ // Cannot manage other users' threads
421
+ expect(ability.can('manage', subject('AiAgentThread', {
422
+ userUuid: 'other-user',
423
+ }))).toBe(false);
424
+ });
425
+ it('should handle view:ai_agent_thread permissions for all threads', () => {
426
+ const builder = new AbilityBuilder(Ability);
427
+ buildAbilityFromScopes({
428
+ ...baseContextWithOrg,
429
+ isEnterprise: true,
430
+ scopes: ['view:AiAgentThread'],
431
+ }, builder);
432
+ const ability = builder.build();
433
+ // Can view any AI agent thread
434
+ expect(ability.can('view', subject('AiAgentThread', {
435
+ organizationUuid: 'org-123',
436
+ userUuid: 'any-user',
437
+ }))).toBe(true);
438
+ });
439
+ it('should handle manage:ai_agent_thread permissions for all threads', () => {
440
+ const builder = new AbilityBuilder(Ability);
441
+ buildAbilityFromScopes({
442
+ ...baseContextWithOrg,
443
+ isEnterprise: true,
444
+ scopes: ['manage:AiAgentThread'],
445
+ }, builder);
446
+ const ability = builder.build();
447
+ // Can manage any AI agent thread
448
+ expect(ability.can('manage', subject('AiAgentThread', {
449
+ organizationUuid: 'org-123',
450
+ userUuid: 'any-user',
451
+ }))).toBe(true);
452
+ });
453
+ });
382
454
  describe('edge cases and error handling', () => {
383
455
  it('should handle empty scope array', () => {
384
456
  const builder = new AbilityBuilder(Ability);
@@ -393,7 +465,7 @@ describe('scopeAbilityBuilder', () => {
393
465
  const builder = new AbilityBuilder(Ability);
394
466
  buildAbilityFromScopes({
395
467
  ...contextWithoutUser,
396
- scopes: ['view:dashboard'],
468
+ scopes: ['view:Dashboard'],
397
469
  }, builder);
398
470
  const ability = builder.build();
399
471
  // Should only allow viewing public dashboards
@@ -413,8 +485,8 @@ describe('scopeAbilityBuilder', () => {
413
485
  buildAbilityFromScopes({
414
486
  ...baseContext,
415
487
  scopes: [
416
- 'view:dashboard',
417
- 'view:project',
488
+ 'view:Dashboard',
489
+ 'view:Project',
418
490
  'invalid:scope',
419
491
  ],
420
492
  }, builder);
@@ -431,9 +503,9 @@ describe('scopeAbilityBuilder', () => {
431
503
  buildAbilityFromScopes({
432
504
  ...baseContext,
433
505
  scopes: [
434
- 'view:dashboard',
435
- 'manage:saved_chart',
436
- 'view:space',
506
+ 'view:Dashboard',
507
+ 'manage:SavedChart',
508
+ 'view:Space',
437
509
  ],
438
510
  }, builder);
439
511
  const ability = builder.build();
@@ -456,7 +528,7 @@ describe('scopeAbilityBuilder', () => {
456
528
  const builder = new AbilityBuilder(Ability);
457
529
  buildAbilityFromScopes({
458
530
  ...baseContext,
459
- scopes: ['view:saved_chart'],
531
+ scopes: ['view:SavedChart'],
460
532
  }, builder);
461
533
  const ability = builder.build();
462
534
  // Should not access saved chart from different project
@@ -476,7 +548,7 @@ describe('scopeAbilityBuilder', () => {
476
548
  const builder = new AbilityBuilder(Ability);
477
549
  buildAbilityFromScopes({
478
550
  ...contextWithUser,
479
- scopes: ['view:dashboard'],
551
+ scopes: ['view:Dashboard'],
480
552
  }, builder);
481
553
  const ability = builder.build();
482
554
  // Can view private dashboard with viewer access
@@ -516,7 +588,7 @@ describe('scopeAbilityBuilder', () => {
516
588
  const builder = new AbilityBuilder(Ability);
517
589
  buildAbilityFromScopes({
518
590
  ...contextWithUser,
519
- scopes: ['manage:dashboard'],
591
+ scopes: ['manage:Dashboard@space'],
520
592
  }, builder);
521
593
  const ability = builder.build();
522
594
  // Can manage dashboard with editor role
@@ -563,7 +635,7 @@ describe('scopeAbilityBuilder', () => {
563
635
  const builder = new AbilityBuilder(Ability);
564
636
  buildAbilityFromScopes({
565
637
  ...contextWithUser,
566
- scopes: ['manage:space@assigned'],
638
+ scopes: ['manage:Space@assigned'],
567
639
  }, builder);
568
640
  const ability = builder.build();
569
641
  // Can manage space with admin role
@@ -601,7 +673,7 @@ describe('scopeAbilityBuilder', () => {
601
673
  });
602
674
  });
603
675
  describe('job and job status permissions', () => {
604
- it('should handle view:job permissions', () => {
676
+ it('should handle view:job@self permissions', () => {
605
677
  const contextWithUser = {
606
678
  ...baseContext,
607
679
  userUuid: 'user-456',
@@ -609,7 +681,7 @@ describe('scopeAbilityBuilder', () => {
609
681
  const builder = new AbilityBuilder(Ability);
610
682
  buildAbilityFromScopes({
611
683
  ...contextWithUser,
612
- scopes: ['view:job'],
684
+ scopes: ['view:Job@self'],
613
685
  }, builder);
614
686
  const ability = builder.build();
615
687
  // Can view own jobs
@@ -621,30 +693,31 @@ describe('scopeAbilityBuilder', () => {
621
693
  userUuid: 'other-user',
622
694
  }))).toBe(false);
623
695
  });
624
- it('should handle view:job_status permissions for organization context', () => {
696
+ it('should handle view:job_status@self permissions for user context', () => {
625
697
  const builder = new AbilityBuilder(Ability);
626
698
  buildAbilityFromScopes({
627
699
  ...baseContext,
628
- scopes: ['view:job_status'],
700
+ userUuid: 'user-456',
701
+ scopes: ['view:JobStatus@self'],
629
702
  }, builder);
630
703
  const ability = builder.build();
631
- // Cannot view job status without user context when no manage:Organization scope
704
+ // Can view own job status
632
705
  expect(ability.can('view', subject('JobStatus', {
633
- organizationUuid: 'org-123',
634
- }))).toBe(false);
635
- // Cannot view job status from another organization
706
+ createdByUserUuid: 'user-456',
707
+ }))).toBe(true);
708
+ // Cannot view other users' job status
636
709
  expect(ability.can('view', subject('JobStatus', {
637
- organizationUuid: 'different-org',
710
+ createdByUserUuid: 'other-user',
638
711
  }))).toBe(false);
639
712
  });
640
- it('should handle view:job_status permissions with manage:Organization scope', () => {
713
+ it('should handle view:job_status permissions for all job status', () => {
641
714
  const builder = new AbilityBuilder(Ability);
642
715
  buildAbilityFromScopes({
643
716
  ...baseContextWithOrg,
644
- scopes: ['view:job_status', 'manage:organization'],
717
+ scopes: ['view:JobStatus'],
645
718
  }, builder);
646
719
  const ability = builder.build();
647
- // Can view all job status in organization when manage:Organization scope is present
720
+ // Can view all job status in organization
648
721
  expect(ability.can('view', subject('JobStatus', {
649
722
  organizationUuid: 'org-123',
650
723
  }))).toBe(true);
@@ -653,7 +726,70 @@ describe('scopeAbilityBuilder', () => {
653
726
  organizationUuid: 'different-org',
654
727
  }))).toBe(false);
655
728
  });
656
- it('should handle view:job_status permissions for user context', () => {
729
+ it('should handle view:job permissions for all jobs', () => {
730
+ const builder = new AbilityBuilder(Ability);
731
+ buildAbilityFromScopes({
732
+ ...baseContext,
733
+ scopes: ['view:Job'],
734
+ }, builder);
735
+ const ability = builder.build();
736
+ // Can view any job
737
+ expect(ability.can('view', subject('Job', {
738
+ organizationUuid: 'org-123',
739
+ projectUuid: 'project-123',
740
+ userUuid: 'any-user',
741
+ }))).toBe(true);
742
+ });
743
+ });
744
+ describe('space-based permissions modifiers', () => {
745
+ it('should handle manage:dashboard@space permissions', () => {
746
+ const contextWithUser = {
747
+ ...baseContextWithOrg,
748
+ userUuid: 'user-456',
749
+ };
750
+ const builder = new AbilityBuilder(Ability);
751
+ buildAbilityFromScopes({
752
+ ...contextWithUser,
753
+ scopes: ['manage:Dashboard@space'],
754
+ }, builder);
755
+ const ability = builder.build();
756
+ // Can manage dashboard with editor role
757
+ expect(ability.can('manage', subject('Dashboard', {
758
+ organizationUuid: 'org-123',
759
+ access: [
760
+ {
761
+ userUuid: 'user-456',
762
+ role: SpaceMemberRole.EDITOR,
763
+ },
764
+ ],
765
+ }))).toBe(true);
766
+ // Can manage dashboard with admin role
767
+ expect(ability.can('manage', subject('Dashboard', {
768
+ organizationUuid: 'org-123',
769
+ access: [
770
+ {
771
+ userUuid: 'user-456',
772
+ role: SpaceMemberRole.ADMIN,
773
+ },
774
+ ],
775
+ }))).toBe(true);
776
+ // Cannot manage dashboard with viewer role
777
+ expect(ability.can('manage', subject('Dashboard', {
778
+ organizationUuid: 'org-123',
779
+ access: [
780
+ {
781
+ userUuid: 'user-456',
782
+ role: SpaceMemberRole.VIEWER,
783
+ },
784
+ ],
785
+ }))).toBe(false);
786
+ // Cannot manage dashboard without access
787
+ expect(ability.can('manage', subject('Dashboard', {
788
+ organizationUuid: 'org-123',
789
+ access: [],
790
+ }))).toBe(false);
791
+ });
792
+ it('should handle manage:saved_chart@space permissions', () => {
657
793
  const contextWithUser = {
658
794
  ...baseContext,
659
795
  userUuid: 'user-456',
@@ -661,16 +797,102 @@ describe('scopeAbilityBuilder', () => {
661
797
  const builder = new AbilityBuilder(Ability);
662
798
  buildAbilityFromScopes({
663
799
  ...contextWithUser,
664
- scopes: ['view:job_status'],
800
+ scopes: ['manage:SavedChart@space'],
665
801
  }, builder);
666
802
  const ability = builder.build();
667
- // Can view own job status
668
- expect(ability.can('view', subject('JobStatus', {
669
- createdByUserUuid: 'user-456',
803
+ // Can manage saved chart with editor role
804
+ expect(ability.can('manage', subject('SavedChart', {
805
+ projectUuid: 'project-123',
806
+ access: [
807
+ {
808
+ userUuid: 'user-456',
809
+ role: SpaceMemberRole.EDITOR,
810
+ },
811
+ ],
670
812
  }))).toBe(true);
671
- // Cannot view other users' job status
672
- expect(ability.can('view', subject('JobStatus', {
673
- createdByUserUuid: 'other-user',
813
+ // Can manage saved chart with admin role
814
+ expect(ability.can('manage', subject('SavedChart', {
815
+ projectUuid: 'project-123',
816
+ access: [
817
+ {
818
+ userUuid: 'user-456',
819
+ role: SpaceMemberRole.ADMIN,
820
+ },
821
+ ],
822
+ }))).toBe(true);
823
+ // Cannot manage without proper access
824
+ expect(ability.can('manage', subject('SavedChart', {
825
+ projectUuid: 'project-123',
826
+ access: [
827
+ {
828
+ userUuid: 'other-user',
829
+ role: SpaceMemberRole.EDITOR,
830
+ },
831
+ ],
832
+ }))).toBe(false);
833
+ });
834
+ it('should handle promote:dashboard@space permissions', () => {
835
+ const contextWithUser = {
836
+ ...baseContext,
837
+ userUuid: 'user-456',
838
+ };
839
+ const builder = new AbilityBuilder(Ability);
840
+ buildAbilityFromScopes({
841
+ ...contextWithUser,
842
+ scopes: ['promote:Dashboard@space'],
843
+ }, builder);
844
+ const ability = builder.build();
845
+ // Can promote dashboard with editor access
846
+ expect(ability.can('promote', subject('Dashboard', {
847
+ projectUuid: 'project-123',
848
+ access: [
849
+ {
850
+ userUuid: 'user-456',
851
+ role: SpaceMemberRole.EDITOR,
852
+ },
853
+ ],
854
+ }))).toBe(true);
855
+ // Cannot promote without editor access
856
+ expect(ability.can('promote', subject('Dashboard', {
857
+ projectUuid: 'project-123',
858
+ access: [
859
+ {
860
+ userUuid: 'user-456',
861
+ role: SpaceMemberRole.VIEWER,
862
+ },
863
+ ],
864
+ }))).toBe(false);
865
+ });
866
+ it('should handle manage:semantic_viewer@space permissions', () => {
867
+ const contextWithUser = {
868
+ ...baseContextWithOrg,
869
+ userUuid: 'user-456',
870
+ };
871
+ const builder = new AbilityBuilder(Ability);
872
+ buildAbilityFromScopes({
873
+ ...contextWithUser,
874
+ scopes: ['manage:SemanticViewer@space'],
875
+ }, builder);
876
+ const ability = builder.build();
877
+ // Can manage semantic viewer with editor role
878
+ expect(ability.can('manage', subject('SemanticViewer', {
879
+ organizationUuid: 'org-123',
880
+ access: [
881
+ {
882
+ userUuid: 'user-456',
883
+ role: SpaceMemberRole.EDITOR,
884
+ },
885
+ ],
886
+ }))).toBe(true);
887
+ // Cannot manage without editor role
888
+ expect(ability.can('manage', subject('SemanticViewer', {
889
+ organizationUuid: 'org-123',
890
+ access: [
891
+ {
892
+ userUuid: 'user-456',
893
+ role: SpaceMemberRole.VIEWER,
894
+ },
895
+ ],
674
896
  }))).toBe(false);
675
897
  });
676
898
  });
@@ -679,7 +901,7 @@ describe('scopeAbilityBuilder', () => {
679
901
  const builder = new AbilityBuilder(Ability);
680
902
  buildAbilityFromScopes({
681
903
  ...baseContext,
682
- scopes: ['view:semantic_viewer'],
904
+ scopes: ['view:SemanticViewer'],
683
905
  }, builder);
684
906
  const ability = builder.build();
685
907
  expect(ability.can('view', subject('SemanticViewer', {
@@ -691,14 +913,14 @@ describe('scopeAbilityBuilder', () => {
691
913
  const contextWithOrgManage = {
692
914
  ...baseContextWithOrg,
693
915
  userUuid: 'user-456',
694
- scopes: ['manage:organization'],
916
+ scopes: ['manage:Organization'],
695
917
  };
696
918
  const builder = new AbilityBuilder(Ability);
697
919
  buildAbilityFromScopes({
698
920
  ...contextWithOrgManage,
699
921
  scopes: [
700
- 'manage:organization',
701
- 'manage:semantic_viewer',
922
+ 'manage:Organization',
923
+ 'manage:SemanticViewer',
702
924
  ],
703
925
  }, builder);
704
926
  const ability = builder.build();
@@ -715,7 +937,7 @@ describe('scopeAbilityBuilder', () => {
715
937
  const builder = new AbilityBuilder(Ability);
716
938
  buildAbilityFromScopes({
717
939
  ...contextWithUser,
718
- scopes: ['manage:semantic_viewer'],
940
+ scopes: ['manage:SemanticViewer@space'],
719
941
  }, builder);
720
942
  const ability = builder.build();
721
943
  // Can manage semantic viewer with editor role
@@ -745,7 +967,7 @@ describe('scopeAbilityBuilder', () => {
745
967
  const builder = new AbilityBuilder(Ability);
746
968
  buildAbilityFromScopes({
747
969
  ...baseContext,
748
- scopes: ['create:space'],
970
+ scopes: ['create:Space'],
749
971
  }, builder);
750
972
  const ability = builder.build();
751
973
  expect(ability.can('create', subject('Space', {
@@ -759,7 +981,7 @@ describe('scopeAbilityBuilder', () => {
759
981
  const builder = new AbilityBuilder(Ability);
760
982
  buildAbilityFromScopes({
761
983
  ...baseContext,
762
- scopes: ['manage:export_csv'],
984
+ scopes: ['manage:ExportCsv'],
763
985
  }, builder);
764
986
  const ability = builder.build();
765
987
  expect(ability.can('manage', subject('ExportCsv', {
@@ -771,7 +993,7 @@ describe('scopeAbilityBuilder', () => {
771
993
  const builder = new AbilityBuilder(Ability);
772
994
  buildAbilityFromScopes({
773
995
  ...baseContext,
774
- scopes: ['manage:change_csv_results'],
996
+ scopes: ['manage:ChangeCsvResults'],
775
997
  }, builder);
776
998
  const ability = builder.build();
777
999
  expect(ability.can('manage', subject('ChangeCsvResults', {
@@ -785,7 +1007,7 @@ describe('scopeAbilityBuilder', () => {
785
1007
  const builder = new AbilityBuilder(Ability);
786
1008
  buildAbilityFromScopes({
787
1009
  ...baseContext,
788
- scopes: ['view:underlying_data'],
1010
+ scopes: ['view:UnderlyingData'],
789
1011
  }, builder);
790
1012
  const ability = builder.build();
791
1013
  expect(ability.can('view', subject('UnderlyingData', {
@@ -799,7 +1021,7 @@ describe('scopeAbilityBuilder', () => {
799
1021
  const builder = new AbilityBuilder(Ability);
800
1022
  buildAbilityFromScopes({
801
1023
  ...baseContext,
802
- scopes: ['manage:sql_runner'],
1024
+ scopes: ['manage:SqlRunner'],
803
1025
  }, builder);
804
1026
  const ability = builder.build();
805
1027
  expect(ability.can('manage', subject('SqlRunner', {
@@ -811,7 +1033,7 @@ describe('scopeAbilityBuilder', () => {
811
1033
  const builder = new AbilityBuilder(Ability);
812
1034
  buildAbilityFromScopes({
813
1035
  ...baseContext,
814
- scopes: ['manage:custom_sql'],
1036
+ scopes: ['manage:CustomSql'],
815
1037
  }, builder);
816
1038
  const ability = builder.build();
817
1039
  expect(ability.can('manage', subject('CustomSql', {
@@ -826,7 +1048,7 @@ describe('scopeAbilityBuilder', () => {
826
1048
  buildAbilityFromScopes({
827
1049
  ...baseContext,
828
1050
  userUuid: 'user-456',
829
- scopes: ['delete:project@self'],
1051
+ scopes: ['delete:Project@self'],
830
1052
  }, builder);
831
1053
  const ability = builder.build();
832
1054
  // Can delete specific project
@@ -839,21 +1061,22 @@ describe('scopeAbilityBuilder', () => {
839
1061
  type: ProjectType.PREVIEW,
840
1062
  }))).toBe(false);
841
1063
  });
842
- it('should handle delete:project for preview projects', () => {
1064
+ it('should handle delete:project@self for own preview projects', () => {
843
1065
  const builder = new AbilityBuilder(Ability);
844
1066
  buildAbilityFromScopes({
845
- ...baseContextWithOrg,
846
- scopes: ['delete:project'],
1067
+ ...baseContext,
1068
+ userUuid: 'user-456',
1069
+ scopes: ['delete:Project@self'],
847
1070
  }, builder);
848
1071
  const ability = builder.build();
849
- // Can delete preview projects in organization
1072
+ // Can delete preview projects in a project
850
1073
  expect(ability.can('delete', subject('Project', {
851
- organizationUuid: 'org-123',
1074
+ createdByUserUuid: 'user-456',
852
1075
  type: ProjectType.PREVIEW,
853
1076
  }))).toBe(true);
854
1077
  // Cannot delete default projects
855
1078
  expect(ability.can('delete', subject('Project', {
856
- organizationUuid: 'org-123',
1079
+ createdByUserUuid: 'user-456',
857
1080
  type: ProjectType.DEFAULT,
858
1081
  }))).toBe(false);
859
1082
  });
@@ -863,7 +1086,7 @@ describe('scopeAbilityBuilder', () => {
863
1086
  const builder = new AbilityBuilder(Ability);
864
1087
  buildAbilityFromScopes({
865
1088
  ...baseContext,
866
- scopes: ['view:pinned_items'],
1089
+ scopes: ['view:PinnedItems'],
867
1090
  }, builder);
868
1091
  const ability = builder.build();
869
1092
  expect(ability.can('view', subject('PinnedItems', {
@@ -875,7 +1098,7 @@ describe('scopeAbilityBuilder', () => {
875
1098
  const builder = new AbilityBuilder(Ability);
876
1099
  buildAbilityFromScopes({
877
1100
  ...baseContext,
878
- scopes: ['manage:pinned_items'],
1101
+ scopes: ['manage:PinnedItems'],
879
1102
  }, builder);
880
1103
  const ability = builder.build();
881
1104
  expect(ability.can('manage', subject('PinnedItems', {
@@ -889,7 +1112,7 @@ describe('scopeAbilityBuilder', () => {
889
1112
  const builder = new AbilityBuilder(Ability);
890
1113
  buildAbilityFromScopes({
891
1114
  ...baseContext,
892
- scopes: ['manage:explore'],
1115
+ scopes: ['manage:Explore'],
893
1116
  }, builder);
894
1117
  const ability = builder.build();
895
1118
  expect(ability.can('manage', subject('Explore', {
@@ -903,7 +1126,7 @@ describe('scopeAbilityBuilder', () => {
903
1126
  const builder = new AbilityBuilder(Ability);
904
1127
  buildAbilityFromScopes({
905
1128
  ...baseContext,
906
- scopes: ['create:virtual_view'],
1129
+ scopes: ['create:VirtualView'],
907
1130
  }, builder);
908
1131
  const ability = builder.build();
909
1132
  expect(ability.can('create', subject('VirtualView', {
@@ -920,7 +1143,7 @@ describe('scopeAbilityBuilder', () => {
920
1143
  const builder = new AbilityBuilder(Ability);
921
1144
  buildAbilityFromScopes({
922
1145
  ...baseContext,
923
- scopes: ['delete:virtual_view'],
1146
+ scopes: ['delete:VirtualView'],
924
1147
  }, builder);
925
1148
  const ability = builder.build();
926
1149
  expect(ability.can('delete', subject('VirtualView', {
@@ -937,7 +1160,7 @@ describe('scopeAbilityBuilder', () => {
937
1160
  const builder = new AbilityBuilder(Ability);
938
1161
  buildAbilityFromScopes({
939
1162
  ...baseContext,
940
- scopes: ['manage:virtual_view'],
1163
+ scopes: ['manage:VirtualView'],
941
1164
  }, builder);
942
1165
  const ability = builder.build();
943
1166
  // Should be able to manage (create and delete)
@@ -960,9 +1183,9 @@ describe('scopeAbilityBuilder', () => {
960
1183
  buildAbilityFromScopes({
961
1184
  ...baseContextWithOrg,
962
1185
  scopes: [
963
- 'create:virtual_view',
964
- 'delete:virtual_view',
965
- 'manage:virtual_view',
1186
+ 'create:VirtualView',
1187
+ 'delete:VirtualView',
1188
+ 'manage:VirtualView',
966
1189
  ],
967
1190
  }, builder);
968
1191
  const ability = builder.build();
@@ -985,9 +1208,9 @@ describe('scopeAbilityBuilder', () => {
985
1208
  buildAbilityFromScopes({
986
1209
  ...baseContextWithOrg,
987
1210
  scopes: [
988
- 'create:virtual_view',
989
- 'delete:virtual_view',
990
- 'manage:virtual_view',
1211
+ 'create:VirtualView',
1212
+ 'delete:VirtualView',
1213
+ 'manage:VirtualView',
991
1214
  ],
992
1215
  }, builder);
993
1216
  const ability = builder.build();
@@ -1012,7 +1235,7 @@ describe('scopeAbilityBuilder', () => {
1012
1235
  const builder = new AbilityBuilder(Ability);
1013
1236
  buildAbilityFromScopes({
1014
1237
  ...baseContextWithOrg,
1015
- scopes: ['view:organization_member_profile'],
1238
+ scopes: ['view:OrganizationMemberProfile'],
1016
1239
  }, builder);
1017
1240
  const ability = builder.build();
1018
1241
  expect(ability.can('view', subject('OrganizationMemberProfile', {
@@ -1029,7 +1252,7 @@ describe('scopeAbilityBuilder', () => {
1029
1252
  const builder = new AbilityBuilder(Ability);
1030
1253
  buildAbilityFromScopes({
1031
1254
  ...baseContext,
1032
- scopes: ['manage:organization_member_profile'],
1255
+ scopes: ['manage:OrganizationMemberProfile'],
1033
1256
  }, builder);
1034
1257
  const ability = builder.build();
1035
1258
  expect(ability.can('manage', subject('OrganizationMemberProfile', {
@@ -1045,7 +1268,7 @@ describe('scopeAbilityBuilder', () => {
1045
1268
  ...baseContext,
1046
1269
  isEnterprise: true,
1047
1270
  organizationRole: 'admin',
1048
- scopes: ['manage:personal_access_token'],
1271
+ scopes: ['manage:PersonalAccessToken'],
1049
1272
  permissionsConfig: {
1050
1273
  pat: {
1051
1274
  enabled: true,
@@ -1062,7 +1285,7 @@ describe('scopeAbilityBuilder', () => {
1062
1285
  ...baseContext,
1063
1286
  isEnterprise: true,
1064
1287
  organizationRole: 'admin',
1065
- scopes: ['manage:personal_access_token'],
1288
+ scopes: ['manage:PersonalAccessToken'],
1066
1289
  permissionsConfig: {
1067
1290
  pat: {
1068
1291
  enabled: false,
@@ -1079,7 +1302,7 @@ describe('scopeAbilityBuilder', () => {
1079
1302
  ...baseContext,
1080
1303
  isEnterprise: true,
1081
1304
  organizationRole: 'developer',
1082
- scopes: ['manage:personal_access_token'],
1305
+ scopes: ['manage:PersonalAccessToken'],
1083
1306
  permissionsConfig: {
1084
1307
  pat: {
1085
1308
  enabled: true,
@@ -1096,7 +1319,7 @@ describe('scopeAbilityBuilder', () => {
1096
1319
  ...baseContext,
1097
1320
  isEnterprise: true,
1098
1321
  organizationRole: 'admin',
1099
- scopes: ['manage:personal_access_token'],
1322
+ scopes: ['manage:PersonalAccessToken'],
1100
1323
  // No permissionsConfig provided
1101
1324
  }, builder);
1102
1325
  const ability = builder.build();
@@ -1108,7 +1331,7 @@ describe('scopeAbilityBuilder', () => {
1108
1331
  ...baseContext,
1109
1332
  isEnterprise: true,
1110
1333
  organizationRole: '', // Empty organization role
1111
- scopes: ['manage:personal_access_token'],
1334
+ scopes: ['manage:PersonalAccessToken'],
1112
1335
  permissionsConfig: {
1113
1336
  pat: {
1114
1337
  enabled: true,