@lightdash/common 0.1936.2 → 0.1937.1

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 (157) hide show
  1. package/dist/cjs/authorization/index.d.ts +8 -4
  2. package/dist/cjs/authorization/index.d.ts.map +1 -1
  3. package/dist/cjs/authorization/index.js +26 -3
  4. package/dist/cjs/authorization/index.js.map +1 -1
  5. package/dist/cjs/authorization/index.mock.d.ts +2 -0
  6. package/dist/cjs/authorization/index.mock.d.ts.map +1 -1
  7. package/dist/cjs/authorization/index.mock.js +2 -0
  8. package/dist/cjs/authorization/index.mock.js.map +1 -1
  9. package/dist/cjs/authorization/organizationMemberAbility.mock.d.ts.map +1 -1
  10. package/dist/cjs/authorization/organizationMemberAbility.mock.js +1 -0
  11. package/dist/cjs/authorization/organizationMemberAbility.mock.js.map +1 -1
  12. package/dist/cjs/authorization/parseScopes.d.ts +3 -2
  13. package/dist/cjs/authorization/parseScopes.d.ts.map +1 -1
  14. package/dist/cjs/authorization/parseScopes.js +17 -8
  15. package/dist/cjs/authorization/parseScopes.js.map +1 -1
  16. package/dist/cjs/authorization/parseScopes.test.js +9 -14
  17. package/dist/cjs/authorization/parseScopes.test.js.map +1 -1
  18. package/dist/cjs/authorization/projectMemberAbility.mock.d.ts.map +1 -1
  19. package/dist/cjs/authorization/projectMemberAbility.mock.js +1 -0
  20. package/dist/cjs/authorization/projectMemberAbility.mock.js.map +1 -1
  21. package/dist/cjs/authorization/roleToScopeMapping.d.ts +19 -0
  22. package/dist/cjs/authorization/roleToScopeMapping.d.ts.map +1 -0
  23. package/dist/cjs/authorization/roleToScopeMapping.js +163 -0
  24. package/dist/cjs/authorization/roleToScopeMapping.js.map +1 -0
  25. package/dist/cjs/authorization/roleToScopeMapping.test.d.ts +2 -0
  26. package/dist/cjs/authorization/roleToScopeMapping.test.d.ts.map +1 -0
  27. package/dist/cjs/authorization/roleToScopeMapping.test.js +549 -0
  28. package/dist/cjs/authorization/roleToScopeMapping.test.js.map +1 -0
  29. package/dist/cjs/authorization/roleToScopeMapping.testUtils.d.ts +1393 -0
  30. package/dist/cjs/authorization/roleToScopeMapping.testUtils.d.ts.map +1 -0
  31. package/dist/cjs/authorization/roleToScopeMapping.testUtils.js +329 -0
  32. package/dist/cjs/authorization/roleToScopeMapping.testUtils.js.map +1 -0
  33. package/dist/cjs/authorization/scopeAbilityBuilder.d.ts +14 -8
  34. package/dist/cjs/authorization/scopeAbilityBuilder.d.ts.map +1 -1
  35. package/dist/cjs/authorization/scopeAbilityBuilder.js +7 -7
  36. package/dist/cjs/authorization/scopeAbilityBuilder.js.map +1 -1
  37. package/dist/cjs/authorization/scopeAbilityBuilder.test.js +258 -185
  38. package/dist/cjs/authorization/scopeAbilityBuilder.test.js.map +1 -1
  39. package/dist/cjs/authorization/scopes.d.ts.map +1 -1
  40. package/dist/cjs/authorization/scopes.js +132 -187
  41. package/dist/cjs/authorization/scopes.js.map +1 -1
  42. package/dist/cjs/ee/AiAgent/schemas/tools/index.d.ts +1 -0
  43. package/dist/cjs/ee/AiAgent/schemas/tools/index.d.ts.map +1 -1
  44. package/dist/cjs/ee/AiAgent/schemas/tools/index.js +1 -0
  45. package/dist/cjs/ee/AiAgent/schemas/tools/index.js.map +1 -1
  46. package/dist/cjs/ee/AiAgent/schemas/tools/toolSearchFieldValuesArgs.d.ts +2845 -0
  47. package/dist/cjs/ee/AiAgent/schemas/tools/toolSearchFieldValuesArgs.d.ts.map +1 -0
  48. package/dist/cjs/ee/AiAgent/schemas/tools/toolSearchFieldValuesArgs.js +41 -0
  49. package/dist/cjs/ee/AiAgent/schemas/tools/toolSearchFieldValuesArgs.js.map +1 -0
  50. package/dist/cjs/types/organizationMemberProfile.d.ts +1 -0
  51. package/dist/cjs/types/organizationMemberProfile.d.ts.map +1 -1
  52. package/dist/cjs/types/organizationMemberProfile.js.map +1 -1
  53. package/dist/cjs/types/projectMemberProfile.d.ts +1 -0
  54. package/dist/cjs/types/projectMemberProfile.d.ts.map +1 -1
  55. package/dist/cjs/types/scopes.d.ts +19 -9
  56. package/dist/cjs/types/scopes.d.ts.map +1 -1
  57. package/dist/cjs/types/search.d.ts +20 -0
  58. package/dist/cjs/types/search.d.ts.map +1 -1
  59. package/dist/cjs/types/search.js.map +1 -1
  60. package/dist/cjs/types/user.d.ts +1 -0
  61. package/dist/cjs/types/user.d.ts.map +1 -1
  62. package/dist/cjs/types/user.js.map +1 -1
  63. package/dist/esm/authorization/index.d.ts +8 -4
  64. package/dist/esm/authorization/index.d.ts.map +1 -1
  65. package/dist/esm/authorization/index.js +26 -3
  66. package/dist/esm/authorization/index.js.map +1 -1
  67. package/dist/esm/authorization/index.mock.d.ts +2 -0
  68. package/dist/esm/authorization/index.mock.d.ts.map +1 -1
  69. package/dist/esm/authorization/index.mock.js +2 -0
  70. package/dist/esm/authorization/index.mock.js.map +1 -1
  71. package/dist/esm/authorization/organizationMemberAbility.mock.d.ts.map +1 -1
  72. package/dist/esm/authorization/organizationMemberAbility.mock.js +1 -0
  73. package/dist/esm/authorization/organizationMemberAbility.mock.js.map +1 -1
  74. package/dist/esm/authorization/parseScopes.d.ts +3 -2
  75. package/dist/esm/authorization/parseScopes.d.ts.map +1 -1
  76. package/dist/esm/authorization/parseScopes.js +15 -7
  77. package/dist/esm/authorization/parseScopes.js.map +1 -1
  78. package/dist/esm/authorization/parseScopes.test.js +9 -14
  79. package/dist/esm/authorization/parseScopes.test.js.map +1 -1
  80. package/dist/esm/authorization/projectMemberAbility.mock.d.ts.map +1 -1
  81. package/dist/esm/authorization/projectMemberAbility.mock.js +1 -0
  82. package/dist/esm/authorization/projectMemberAbility.mock.js.map +1 -1
  83. package/dist/esm/authorization/roleToScopeMapping.d.ts +19 -0
  84. package/dist/esm/authorization/roleToScopeMapping.d.ts.map +1 -0
  85. package/dist/esm/authorization/roleToScopeMapping.js +157 -0
  86. package/dist/esm/authorization/roleToScopeMapping.js.map +1 -0
  87. package/dist/esm/authorization/roleToScopeMapping.test.d.ts +2 -0
  88. package/dist/esm/authorization/roleToScopeMapping.test.d.ts.map +1 -0
  89. package/dist/esm/authorization/roleToScopeMapping.test.js +547 -0
  90. package/dist/esm/authorization/roleToScopeMapping.test.js.map +1 -0
  91. package/dist/esm/authorization/roleToScopeMapping.testUtils.d.ts +1393 -0
  92. package/dist/esm/authorization/roleToScopeMapping.testUtils.d.ts.map +1 -0
  93. package/dist/esm/authorization/roleToScopeMapping.testUtils.js +319 -0
  94. package/dist/esm/authorization/roleToScopeMapping.testUtils.js.map +1 -0
  95. package/dist/esm/authorization/scopeAbilityBuilder.d.ts +14 -8
  96. package/dist/esm/authorization/scopeAbilityBuilder.d.ts.map +1 -1
  97. package/dist/esm/authorization/scopeAbilityBuilder.js +7 -7
  98. package/dist/esm/authorization/scopeAbilityBuilder.js.map +1 -1
  99. package/dist/esm/authorization/scopeAbilityBuilder.test.js +259 -186
  100. package/dist/esm/authorization/scopeAbilityBuilder.test.js.map +1 -1
  101. package/dist/esm/authorization/scopes.d.ts.map +1 -1
  102. package/dist/esm/authorization/scopes.js +132 -187
  103. package/dist/esm/authorization/scopes.js.map +1 -1
  104. package/dist/esm/ee/AiAgent/schemas/tools/index.d.ts +1 -0
  105. package/dist/esm/ee/AiAgent/schemas/tools/index.d.ts.map +1 -1
  106. package/dist/esm/ee/AiAgent/schemas/tools/index.js +1 -0
  107. package/dist/esm/ee/AiAgent/schemas/tools/index.js.map +1 -1
  108. package/dist/esm/ee/AiAgent/schemas/tools/toolSearchFieldValuesArgs.d.ts +2845 -0
  109. package/dist/esm/ee/AiAgent/schemas/tools/toolSearchFieldValuesArgs.d.ts.map +1 -0
  110. package/dist/esm/ee/AiAgent/schemas/tools/toolSearchFieldValuesArgs.js +38 -0
  111. package/dist/esm/ee/AiAgent/schemas/tools/toolSearchFieldValuesArgs.js.map +1 -0
  112. package/dist/esm/types/organizationMemberProfile.d.ts +1 -0
  113. package/dist/esm/types/organizationMemberProfile.d.ts.map +1 -1
  114. package/dist/esm/types/organizationMemberProfile.js.map +1 -1
  115. package/dist/esm/types/projectMemberProfile.d.ts +1 -0
  116. package/dist/esm/types/projectMemberProfile.d.ts.map +1 -1
  117. package/dist/esm/types/scopes.d.ts +19 -9
  118. package/dist/esm/types/scopes.d.ts.map +1 -1
  119. package/dist/esm/types/search.d.ts +20 -0
  120. package/dist/esm/types/search.d.ts.map +1 -1
  121. package/dist/esm/types/search.js.map +1 -1
  122. package/dist/esm/types/user.d.ts +1 -0
  123. package/dist/esm/types/user.d.ts.map +1 -1
  124. package/dist/esm/types/user.js.map +1 -1
  125. package/dist/tsconfig.types.tsbuildinfo +1 -1
  126. package/dist/types/authorization/index.d.ts +8 -4
  127. package/dist/types/authorization/index.d.ts.map +1 -1
  128. package/dist/types/authorization/index.mock.d.ts +2 -0
  129. package/dist/types/authorization/index.mock.d.ts.map +1 -1
  130. package/dist/types/authorization/organizationMemberAbility.mock.d.ts.map +1 -1
  131. package/dist/types/authorization/parseScopes.d.ts +3 -2
  132. package/dist/types/authorization/parseScopes.d.ts.map +1 -1
  133. package/dist/types/authorization/projectMemberAbility.mock.d.ts.map +1 -1
  134. package/dist/types/authorization/roleToScopeMapping.d.ts +19 -0
  135. package/dist/types/authorization/roleToScopeMapping.d.ts.map +1 -0
  136. package/dist/types/authorization/roleToScopeMapping.test.d.ts +2 -0
  137. package/dist/types/authorization/roleToScopeMapping.test.d.ts.map +1 -0
  138. package/dist/types/authorization/roleToScopeMapping.testUtils.d.ts +1393 -0
  139. package/dist/types/authorization/roleToScopeMapping.testUtils.d.ts.map +1 -0
  140. package/dist/types/authorization/scopeAbilityBuilder.d.ts +14 -8
  141. package/dist/types/authorization/scopeAbilityBuilder.d.ts.map +1 -1
  142. package/dist/types/authorization/scopes.d.ts.map +1 -1
  143. package/dist/types/ee/AiAgent/schemas/tools/index.d.ts +1 -0
  144. package/dist/types/ee/AiAgent/schemas/tools/index.d.ts.map +1 -1
  145. package/dist/types/ee/AiAgent/schemas/tools/toolSearchFieldValuesArgs.d.ts +2845 -0
  146. package/dist/types/ee/AiAgent/schemas/tools/toolSearchFieldValuesArgs.d.ts.map +1 -0
  147. package/dist/types/types/organizationMemberProfile.d.ts +1 -0
  148. package/dist/types/types/organizationMemberProfile.d.ts.map +1 -1
  149. package/dist/types/types/projectMemberProfile.d.ts +1 -0
  150. package/dist/types/types/projectMemberProfile.d.ts.map +1 -1
  151. package/dist/types/types/scopes.d.ts +19 -9
  152. package/dist/types/types/scopes.d.ts.map +1 -1
  153. package/dist/types/types/search.d.ts +20 -0
  154. package/dist/types/types/search.d.ts.map +1 -1
  155. package/dist/types/types/user.d.ts +1 -0
  156. package/dist/types/types/user.d.ts.map +1 -1
  157. package/package.json +1 -1
@@ -1,21 +1,28 @@
1
- import { subject } from '@casl/ability';
1
+ import { Ability, AbilityBuilder, subject } from '@casl/ability';
2
2
  import { ProjectType } from '../types/projects';
3
3
  import { SpaceMemberRole } from '../types/space';
4
4
  import { buildAbilityFromScopes } from './scopeAbilityBuilder';
5
5
  describe('scopeAbilityBuilder', () => {
6
6
  describe('buildAbilityFromScopes', () => {
7
7
  const baseContext = {
8
- organizationUuid: 'org-123',
9
8
  isEnterprise: false,
10
9
  organizationRole: 'admin',
11
10
  projectUuid: 'project-123',
11
+ userUuid: 'user1',
12
12
  scopes: [],
13
13
  };
14
+ const baseContextWithOrg = {
15
+ ...baseContext,
16
+ projectUuid: undefined,
17
+ organizationUuid: 'org-123',
18
+ };
14
19
  it('should build ability with organization view permissions', () => {
15
- const ability = buildAbilityFromScopes({
16
- ...baseContext,
20
+ const builder = new AbilityBuilder(Ability);
21
+ buildAbilityFromScopes({
22
+ ...baseContextWithOrg,
17
23
  scopes: ['view:organization'],
18
- });
24
+ }, builder);
25
+ const ability = builder.build();
19
26
  expect(ability.can('view', subject('Organization', {
20
27
  organizationUuid: 'org-123',
21
28
  projectUuid: 'project-123',
@@ -26,10 +33,12 @@ describe('scopeAbilityBuilder', () => {
26
33
  }))).toBe(false);
27
34
  });
28
35
  it('should build ability with dashboard view permissions', () => {
29
- const ability = buildAbilityFromScopes({
36
+ const builder = new AbilityBuilder(Ability);
37
+ buildAbilityFromScopes({
30
38
  ...baseContext,
31
39
  scopes: ['view:dashboard'],
32
- });
40
+ }, builder);
41
+ const ability = builder.build();
33
42
  // Should be able to view public dashboards
34
43
  expect(ability.can('view', subject('Dashboard', {
35
44
  organizationUuid: 'org-123',
@@ -48,10 +57,12 @@ describe('scopeAbilityBuilder', () => {
48
57
  ...baseContext,
49
58
  userUuid: 'user-456',
50
59
  };
51
- const ability = buildAbilityFromScopes({
60
+ const builder = new AbilityBuilder(Ability);
61
+ buildAbilityFromScopes({
52
62
  ...contextWithUser,
53
63
  scopes: ['view:dashboard'],
54
- });
64
+ }, builder);
65
+ const ability = builder.build();
55
66
  // Can view dashboards with user access
56
67
  expect(ability.can('view', subject('Dashboard', {
57
68
  organizationUuid: 'org-123',
@@ -64,20 +75,24 @@ describe('scopeAbilityBuilder', () => {
64
75
  ...baseContext,
65
76
  projectUuid: 'project-789',
66
77
  };
67
- const ability = buildAbilityFromScopes({
78
+ const builder = new AbilityBuilder(Ability);
79
+ buildAbilityFromScopes({
68
80
  ...projectContext,
69
81
  scopes: ['view:project'],
70
- });
82
+ }, builder);
83
+ const ability = builder.build();
71
84
  expect(ability.can('view', subject('Project', {
72
85
  organizationUuid: 'org-123',
73
86
  projectUuid: 'project-789',
74
87
  }))).toBe(true);
75
88
  });
76
89
  it('should build ability with project creation permissions and type restrictions', () => {
77
- const ability = buildAbilityFromScopes({
78
- ...baseContext,
90
+ const builder = new AbilityBuilder(Ability);
91
+ buildAbilityFromScopes({
92
+ ...baseContextWithOrg,
79
93
  scopes: ['create:project'],
80
- });
94
+ }, builder);
95
+ const ability = builder.build();
81
96
  // Can create preview projects
82
97
  expect(ability.can('create', subject('Project', {
83
98
  organizationUuid: 'org-123',
@@ -91,13 +106,15 @@ describe('scopeAbilityBuilder', () => {
91
106
  });
92
107
  it('should build ability with editor permissions for dashboards', () => {
93
108
  const editorContext = {
94
- ...baseContext,
109
+ ...baseContextWithOrg,
95
110
  userUuid: 'user-456',
96
111
  };
97
- const ability = buildAbilityFromScopes({
112
+ const builder = new AbilityBuilder(Ability);
113
+ buildAbilityFromScopes({
98
114
  ...editorContext,
99
115
  scopes: ['manage:dashboard'],
100
- });
116
+ }, builder);
117
+ const ability = builder.build();
101
118
  // Can manage dashboards where user is editor
102
119
  expect(ability.can('manage', subject('Dashboard', {
103
120
  organizationUuid: 'org-123',
@@ -111,13 +128,15 @@ describe('scopeAbilityBuilder', () => {
111
128
  });
112
129
  it('should build ability with admin permissions for spaces', () => {
113
130
  const adminContext = {
114
- ...baseContext,
131
+ ...baseContextWithOrg,
115
132
  userUuid: 'user-456',
116
133
  };
117
- const ability = buildAbilityFromScopes({
134
+ const builder = new AbilityBuilder(Ability);
135
+ buildAbilityFromScopes({
118
136
  ...adminContext,
119
137
  scopes: ['manage:space'],
120
- });
138
+ }, builder);
139
+ const ability = builder.build();
121
140
  // Can manage spaces where user is admin
122
141
  expect(ability.can('manage', subject('Space', {
123
142
  organizationUuid: 'org-123',
@@ -134,10 +153,12 @@ describe('scopeAbilityBuilder', () => {
134
153
  ...baseContext,
135
154
  userUuid: 'user-456',
136
155
  };
137
- const ability = buildAbilityFromScopes({
156
+ const builder = new AbilityBuilder(Ability);
157
+ buildAbilityFromScopes({
138
158
  ...userContext,
139
159
  scopes: ['view:job_status'],
140
- });
160
+ }, builder);
161
+ const ability = builder.build();
141
162
  // Can view job status created by the user
142
163
  expect(ability.can('view', subject('JobStatus', {
143
164
  createdByUserUuid: 'user-456',
@@ -153,10 +174,12 @@ describe('scopeAbilityBuilder', () => {
153
174
  userUuid: 'user-456',
154
175
  isEnterprise: true,
155
176
  };
156
- const ability = buildAbilityFromScopes({
177
+ const builder = new AbilityBuilder(Ability);
178
+ buildAbilityFromScopes({
157
179
  ...userContext,
158
180
  scopes: ['manage:ai_agent_thread'],
159
- });
181
+ }, builder);
182
+ const ability = builder.build();
160
183
  // Can manage user's own AI agent threads
161
184
  expect(ability.can('manage', subject('AiAgentThread', {
162
185
  organizationUuid: 'org-123',
@@ -170,10 +193,12 @@ describe('scopeAbilityBuilder', () => {
170
193
  });
171
194
  it('should build ability with basic permissions for scopes without custom logic', () => {
172
195
  // These scopes don't have custom applyConditions
173
- const ability = buildAbilityFromScopes({
196
+ const builder = new AbilityBuilder(Ability);
197
+ buildAbilityFromScopes({
174
198
  ...baseContext,
175
199
  scopes: ['view:analytics', 'manage:tags'],
176
- });
200
+ }, builder);
201
+ const ability = builder.build();
177
202
  expect(ability.can('view', subject('Analytics', {
178
203
  organizationUuid: 'org-123',
179
204
  projectUuid: 'project-123',
@@ -184,7 +209,9 @@ describe('scopeAbilityBuilder', () => {
184
209
  }))).toBe(true);
185
210
  });
186
211
  it('should handle unknown scopes gracefully', () => {
187
- const ability = buildAbilityFromScopes(baseContext);
212
+ const builder = new AbilityBuilder(Ability);
213
+ buildAbilityFromScopes(baseContext, builder);
214
+ const ability = builder.build();
188
215
  // Unknown scope should not add any abilities
189
216
  expect(ability.rules.length).toBe(0);
190
217
  });
@@ -193,15 +220,16 @@ describe('scopeAbilityBuilder', () => {
193
220
  ...baseContext,
194
221
  isEnterprise: false,
195
222
  };
196
- const ability = buildAbilityFromScopes(nonEnterpriseContext);
223
+ const builder = new AbilityBuilder(Ability);
224
+ buildAbilityFromScopes(nonEnterpriseContext, builder);
225
+ const ability = builder.build();
197
226
  // Enterprise scope should not add abilities in non-enterprise context
198
227
  expect(ability.rules.length).toBe(0);
199
228
  });
200
229
  it('should build a complete ability from multiple scopes', () => {
201
230
  const context = {
202
- organizationUuid: 'org-123',
203
231
  userUuid: 'user-456',
204
- projectUuid: 'project-789',
232
+ organizationUuid: 'org-123',
205
233
  isEnterprise: false,
206
234
  organizationRole: 'admin',
207
235
  scopes: [
@@ -210,7 +238,9 @@ describe('scopeAbilityBuilder', () => {
210
238
  'view:project',
211
239
  ],
212
240
  };
213
- const ability = buildAbilityFromScopes(context);
241
+ const builder = new AbilityBuilder(Ability);
242
+ buildAbilityFromScopes(context, builder);
243
+ const ability = builder.build();
214
244
  // Check that all abilities were applied
215
245
  expect(ability.can('view', subject('Dashboard', {
216
246
  organizationUuid: 'org-123',
@@ -239,7 +269,9 @@ describe('scopeAbilityBuilder', () => {
239
269
  userUuid: 'user-456',
240
270
  scopes: ['manage:organization', 'manage:saved_chart'],
241
271
  };
242
- const ability = buildAbilityFromScopes(contextWithOrgManage);
272
+ const builder = new AbilityBuilder(Ability);
273
+ buildAbilityFromScopes(contextWithOrgManage, builder);
274
+ const ability = builder.build();
243
275
  // Should have organization-wide permissions for saved charts
244
276
  expect(ability.can('manage', subject('SavedChart', {
245
277
  organizationUuid: 'org-123',
@@ -258,7 +290,9 @@ describe('scopeAbilityBuilder', () => {
258
290
  userUuid: 'user-456',
259
291
  scopes: ['manage:saved_chart'],
260
292
  };
261
- const ability = buildAbilityFromScopes(contextWithoutOrgManage);
293
+ const builder = new AbilityBuilder(Ability);
294
+ buildAbilityFromScopes(contextWithoutOrgManage, builder);
295
+ const ability = builder.build();
262
296
  // Should require user access restrictions
263
297
  expect(ability.can('manage', subject('SavedChart', {
264
298
  organizationUuid: 'org-123',
@@ -283,7 +317,9 @@ describe('scopeAbilityBuilder', () => {
283
317
  userUuid: 'user-456',
284
318
  scopes: ['manage:project', 'manage:space'],
285
319
  };
286
- const ability = buildAbilityFromScopes(contextWithProjectManage);
320
+ const builder = new AbilityBuilder(Ability);
321
+ buildAbilityFromScopes(contextWithProjectManage, builder);
322
+ const ability = builder.build();
287
323
  // Should allow managing public spaces when user has project management
288
324
  expect(ability.can('manage', subject('Space', {
289
325
  organizationUuid: 'org-123',
@@ -314,13 +350,17 @@ describe('scopeAbilityBuilder', () => {
314
350
  scopes: ['promote:dashboard'],
315
351
  };
316
352
  // Test dashboard promotion with organization management
317
- const abilityWithOrg = buildAbilityFromScopes(contextWithOrgManage);
353
+ const builder = new AbilityBuilder(Ability);
354
+ buildAbilityFromScopes(contextWithOrgManage, builder);
355
+ const abilityWithOrg = builder.build();
318
356
  expect(abilityWithOrg.can('promote', subject('Dashboard', {
319
357
  organizationUuid: 'org-123',
320
358
  projectUuid: 'project-123',
321
359
  }))).toBe(true);
322
360
  // Test dashboard promotion without organization management
323
- const abilityWithoutOrg = buildAbilityFromScopes(contextWithoutOrgManage);
361
+ const builderWithoutOrg = new AbilityBuilder(Ability);
362
+ buildAbilityFromScopes(contextWithoutOrgManage, builderWithoutOrg);
363
+ const abilityWithoutOrg = builderWithoutOrg.build();
324
364
  expect(abilityWithoutOrg.can('promote', subject('Dashboard', {
325
365
  organizationUuid: 'org-123',
326
366
  projectUuid: 'project-123',
@@ -341,18 +381,21 @@ describe('scopeAbilityBuilder', () => {
341
381
  });
342
382
  describe('edge cases and error handling', () => {
343
383
  it('should handle empty scope array', () => {
344
- const ability = buildAbilityFromScopes(baseContext);
384
+ const builder = new AbilityBuilder(Ability);
385
+ buildAbilityFromScopes(baseContext, builder);
386
+ const ability = builder.build();
345
387
  expect(ability.rules.length).toBe(0);
346
388
  });
347
389
  it('should handle undefined userUuid in context', () => {
348
390
  const contextWithoutUser = {
349
391
  ...baseContext,
350
- userUuid: undefined,
351
392
  };
352
- const ability = buildAbilityFromScopes({
393
+ const builder = new AbilityBuilder(Ability);
394
+ buildAbilityFromScopes({
353
395
  ...contextWithoutUser,
354
396
  scopes: ['view:dashboard'],
355
- });
397
+ }, builder);
398
+ const ability = builder.build();
356
399
  // Should only allow viewing public dashboards
357
400
  expect(ability.can('view', subject('Dashboard', {
358
401
  organizationUuid: 'org-123',
@@ -365,62 +408,35 @@ describe('scopeAbilityBuilder', () => {
365
408
  isPrivate: true,
366
409
  }))).toBe(false);
367
410
  });
368
- it('should handle missing projectUuid in context', () => {
369
- const contextWithoutProject = {
370
- ...baseContext,
371
- projectUuid: '',
372
- };
373
- const ability = buildAbilityFromScopes({
374
- ...contextWithoutProject,
375
- scopes: ['view:dashboard'],
376
- });
377
- // Should work with public dashboards without project restriction
378
- expect(ability.can('view', subject('Dashboard', {
379
- organizationUuid: 'org-123',
380
- isPrivate: false,
381
- }))).toBe(true);
382
- });
383
411
  it('should handle mixed valid and invalid scopes', () => {
384
- // Should throw error for invalid scopes
385
- expect(() => {
386
- buildAbilityFromScopes({
387
- ...baseContext,
388
- scopes: [
389
- 'view:dashboard',
390
- 'view:project',
391
- 'invalid:scope',
392
- ],
393
- });
394
- }).toThrow('Invalid scope: invalid:Scope. Please check the scope name and try again.');
395
- // Should work with only valid scopes
396
- const ability = buildAbilityFromScopes({
412
+ const builder = new AbilityBuilder(Ability);
413
+ buildAbilityFromScopes({
397
414
  ...baseContext,
398
- scopes: ['view:dashboard', 'view:project'],
399
- });
400
- // Should apply valid scopes
401
- expect(ability.can('view', subject('Dashboard', {
402
- organizationUuid: 'org-123',
403
- projectUuid: 'project-123',
404
- isPrivate: false,
405
- }))).toBe(true);
406
- expect(ability.can('view', subject('Project', {
407
- organizationUuid: 'org-123',
408
- projectUuid: 'project-123',
409
- }))).toBe(true);
410
- // Should have rules from valid scopes
411
- expect(ability.rules.length).toBeGreaterThan(0);
415
+ scopes: [
416
+ 'view:dashboard',
417
+ 'view:project',
418
+ 'invalid:scope',
419
+ ],
420
+ }, builder);
421
+ const ability = builder.build();
422
+ // We have 3 valid rules, 2 for dashboard and 1 for project, dropping the invalid scope
423
+ expect(ability.rules.length).toBe(3);
424
+ expect(ability.rules.filter((r) => r.subject === 'Dashboard')).toHaveLength(2);
425
+ expect(ability.rules.find((r) => r.subject === 'Project')).toBeDefined();
412
426
  });
413
427
  });
414
428
  describe('cross-boundary access tests', () => {
415
429
  it('should not allow access to resources from different organizations', () => {
416
- const ability = buildAbilityFromScopes({
430
+ const builder = new AbilityBuilder(Ability);
431
+ buildAbilityFromScopes({
417
432
  ...baseContext,
418
433
  scopes: [
419
434
  'view:dashboard',
420
435
  'manage:saved_chart',
421
436
  'view:space',
422
437
  ],
423
- });
438
+ }, builder);
439
+ const ability = builder.build();
424
440
  // Should not access dashboard from different org
425
441
  expect(ability.can('view', subject('Dashboard', {
426
442
  organizationUuid: 'different-org',
@@ -437,10 +453,12 @@ describe('scopeAbilityBuilder', () => {
437
453
  }))).toBe(false);
438
454
  });
439
455
  it('should not allow access to resources from different projects', () => {
440
- const ability = buildAbilityFromScopes({
456
+ const builder = new AbilityBuilder(Ability);
457
+ buildAbilityFromScopes({
441
458
  ...baseContext,
442
459
  scopes: ['view:saved_chart'],
443
- });
460
+ }, builder);
461
+ const ability = builder.build();
444
462
  // Should not access saved chart from different project
445
463
  expect(ability.can('view', subject('SavedChart', {
446
464
  organizationUuid: 'org-123',
@@ -452,13 +470,15 @@ describe('scopeAbilityBuilder', () => {
452
470
  describe('private resource access with space roles', () => {
453
471
  it('should handle viewer role access to private resources', () => {
454
472
  const contextWithUser = {
455
- ...baseContext,
473
+ ...baseContextWithOrg,
456
474
  userUuid: 'user-456',
457
475
  };
458
- const ability = buildAbilityFromScopes({
476
+ const builder = new AbilityBuilder(Ability);
477
+ buildAbilityFromScopes({
459
478
  ...contextWithUser,
460
479
  scopes: ['view:dashboard'],
461
- });
480
+ }, builder);
481
+ const ability = builder.build();
462
482
  // Can view private dashboard with viewer access
463
483
  expect(ability.can('view', subject('Dashboard', {
464
484
  organizationUuid: 'org-123',
@@ -490,13 +510,15 @@ describe('scopeAbilityBuilder', () => {
490
510
  });
491
511
  it('should handle editor role for managing resources', () => {
492
512
  const contextWithUser = {
493
- ...baseContext,
513
+ ...baseContextWithOrg,
494
514
  userUuid: 'user-456',
495
515
  };
496
- const ability = buildAbilityFromScopes({
516
+ const builder = new AbilityBuilder(Ability);
517
+ buildAbilityFromScopes({
497
518
  ...contextWithUser,
498
519
  scopes: ['manage:dashboard'],
499
- });
520
+ }, builder);
521
+ const ability = builder.build();
500
522
  // Can manage dashboard with editor role
501
523
  expect(ability.can('manage', subject('Dashboard', {
502
524
  organizationUuid: 'org-123',
@@ -538,13 +560,15 @@ describe('scopeAbilityBuilder', () => {
538
560
  ...baseContext,
539
561
  userUuid: 'user-456',
540
562
  };
541
- const ability = buildAbilityFromScopes({
563
+ const builder = new AbilityBuilder(Ability);
564
+ buildAbilityFromScopes({
542
565
  ...contextWithUser,
543
- scopes: ['manage:space'],
544
- });
566
+ scopes: ['manage:space@assigned'],
567
+ }, builder);
568
+ const ability = builder.build();
545
569
  // Can manage space with admin role
546
570
  expect(ability.can('manage', subject('Space', {
547
- organizationUuid: 'org-123',
571
+ projectUuid: baseContext.projectUuid,
548
572
  access: [
549
573
  {
550
574
  userUuid: 'user-456',
@@ -582,10 +606,12 @@ describe('scopeAbilityBuilder', () => {
582
606
  ...baseContext,
583
607
  userUuid: 'user-456',
584
608
  };
585
- const ability = buildAbilityFromScopes({
609
+ const builder = new AbilityBuilder(Ability);
610
+ buildAbilityFromScopes({
586
611
  ...contextWithUser,
587
612
  scopes: ['view:job'],
588
- });
613
+ }, builder);
614
+ const ability = builder.build();
589
615
  // Can view own jobs
590
616
  expect(ability.can('view', subject('Job', {
591
617
  userUuid: 'user-456',
@@ -596,14 +622,12 @@ describe('scopeAbilityBuilder', () => {
596
622
  }))).toBe(false);
597
623
  });
598
624
  it('should handle view:job_status permissions for organization context', () => {
599
- const contextWithoutUser = {
625
+ const builder = new AbilityBuilder(Ability);
626
+ buildAbilityFromScopes({
600
627
  ...baseContext,
601
- userUuid: undefined,
602
- };
603
- const ability = buildAbilityFromScopes({
604
- ...contextWithoutUser,
605
628
  scopes: ['view:job_status'],
606
- });
629
+ }, builder);
630
+ const ability = builder.build();
607
631
  // Cannot view job status without user context when no manage:Organization scope
608
632
  expect(ability.can('view', subject('JobStatus', {
609
633
  organizationUuid: 'org-123',
@@ -614,14 +638,12 @@ describe('scopeAbilityBuilder', () => {
614
638
  }))).toBe(false);
615
639
  });
616
640
  it('should handle view:job_status permissions with manage:Organization scope', () => {
617
- const contextWithoutUser = {
618
- ...baseContext,
619
- userUuid: undefined,
620
- };
621
- const ability = buildAbilityFromScopes({
622
- ...contextWithoutUser,
641
+ const builder = new AbilityBuilder(Ability);
642
+ buildAbilityFromScopes({
643
+ ...baseContextWithOrg,
623
644
  scopes: ['view:job_status', 'manage:organization'],
624
- });
645
+ }, builder);
646
+ const ability = builder.build();
625
647
  // Can view all job status in organization when manage:Organization scope is present
626
648
  expect(ability.can('view', subject('JobStatus', {
627
649
  organizationUuid: 'org-123',
@@ -636,10 +658,12 @@ describe('scopeAbilityBuilder', () => {
636
658
  ...baseContext,
637
659
  userUuid: 'user-456',
638
660
  };
639
- const ability = buildAbilityFromScopes({
661
+ const builder = new AbilityBuilder(Ability);
662
+ buildAbilityFromScopes({
640
663
  ...contextWithUser,
641
664
  scopes: ['view:job_status'],
642
- });
665
+ }, builder);
666
+ const ability = builder.build();
643
667
  // Can view own job status
644
668
  expect(ability.can('view', subject('JobStatus', {
645
669
  createdByUserUuid: 'user-456',
@@ -652,10 +676,12 @@ describe('scopeAbilityBuilder', () => {
652
676
  });
653
677
  describe('semantic viewer permissions', () => {
654
678
  it('should handle view:semantic_viewer permissions', () => {
655
- const ability = buildAbilityFromScopes({
679
+ const builder = new AbilityBuilder(Ability);
680
+ buildAbilityFromScopes({
656
681
  ...baseContext,
657
682
  scopes: ['view:semantic_viewer'],
658
- });
683
+ }, builder);
684
+ const ability = builder.build();
659
685
  expect(ability.can('view', subject('SemanticViewer', {
660
686
  organizationUuid: 'org-123',
661
687
  projectUuid: 'project-123',
@@ -663,14 +689,19 @@ describe('scopeAbilityBuilder', () => {
663
689
  });
664
690
  it('should handle manage:semantic_viewer with organization scope', () => {
665
691
  const contextWithOrgManage = {
666
- ...baseContext,
692
+ ...baseContextWithOrg,
667
693
  userUuid: 'user-456',
668
694
  scopes: ['manage:organization'],
669
695
  };
670
- const ability = buildAbilityFromScopes({
696
+ const builder = new AbilityBuilder(Ability);
697
+ buildAbilityFromScopes({
671
698
  ...contextWithOrgManage,
672
- scopes: ['manage:organization', 'manage:semantic_viewer'],
673
- });
699
+ scopes: [
700
+ 'manage:organization',
701
+ 'manage:semantic_viewer',
702
+ ],
703
+ }, builder);
704
+ const ability = builder.build();
674
705
  // Can manage semantic viewer organization-wide
675
706
  expect(ability.can('manage', subject('SemanticViewer', {
676
707
  organizationUuid: 'org-123',
@@ -678,13 +709,15 @@ describe('scopeAbilityBuilder', () => {
678
709
  });
679
710
  it('should handle manage:semantic_viewer with editor role', () => {
680
711
  const contextWithUser = {
681
- ...baseContext,
712
+ ...baseContextWithOrg,
682
713
  userUuid: 'user-456',
683
714
  };
684
- const ability = buildAbilityFromScopes({
715
+ const builder = new AbilityBuilder(Ability);
716
+ buildAbilityFromScopes({
685
717
  ...contextWithUser,
686
718
  scopes: ['manage:semantic_viewer'],
687
- });
719
+ }, builder);
720
+ const ability = builder.build();
688
721
  // Can manage semantic viewer with editor role
689
722
  expect(ability.can('manage', subject('SemanticViewer', {
690
723
  organizationUuid: 'org-123',
@@ -709,10 +742,12 @@ describe('scopeAbilityBuilder', () => {
709
742
  });
710
743
  describe('create space permissions', () => {
711
744
  it('should handle create:space permissions', () => {
712
- const ability = buildAbilityFromScopes({
745
+ const builder = new AbilityBuilder(Ability);
746
+ buildAbilityFromScopes({
713
747
  ...baseContext,
714
748
  scopes: ['create:space'],
715
- });
749
+ }, builder);
750
+ const ability = builder.build();
716
751
  expect(ability.can('create', subject('Space', {
717
752
  organizationUuid: 'org-123',
718
753
  projectUuid: 'project-123',
@@ -721,20 +756,24 @@ describe('scopeAbilityBuilder', () => {
721
756
  });
722
757
  describe('export permissions', () => {
723
758
  it('should handle export csv permissions', () => {
724
- const ability = buildAbilityFromScopes({
759
+ const builder = new AbilityBuilder(Ability);
760
+ buildAbilityFromScopes({
725
761
  ...baseContext,
726
762
  scopes: ['manage:export_csv'],
727
- });
763
+ }, builder);
764
+ const ability = builder.build();
728
765
  expect(ability.can('manage', subject('ExportCsv', {
729
766
  organizationUuid: 'org-123',
730
767
  projectUuid: 'project-123',
731
768
  }))).toBe(true);
732
769
  });
733
770
  it('should handle change csv results permissions', () => {
734
- const ability = buildAbilityFromScopes({
771
+ const builder = new AbilityBuilder(Ability);
772
+ buildAbilityFromScopes({
735
773
  ...baseContext,
736
774
  scopes: ['manage:change_csv_results'],
737
- });
775
+ }, builder);
776
+ const ability = builder.build();
738
777
  expect(ability.can('manage', subject('ChangeCsvResults', {
739
778
  organizationUuid: 'org-123',
740
779
  projectUuid: 'project-123',
@@ -743,10 +782,12 @@ describe('scopeAbilityBuilder', () => {
743
782
  });
744
783
  describe('underlying data permissions', () => {
745
784
  it('should handle view:underlying_data permissions', () => {
746
- const ability = buildAbilityFromScopes({
785
+ const builder = new AbilityBuilder(Ability);
786
+ buildAbilityFromScopes({
747
787
  ...baseContext,
748
788
  scopes: ['view:underlying_data'],
749
- });
789
+ }, builder);
790
+ const ability = builder.build();
750
791
  expect(ability.can('view', subject('UnderlyingData', {
751
792
  organizationUuid: 'org-123',
752
793
  projectUuid: 'project-123',
@@ -755,20 +796,24 @@ describe('scopeAbilityBuilder', () => {
755
796
  });
756
797
  describe('sql runner and custom sql permissions', () => {
757
798
  it('should handle manage:sql_runner permissions', () => {
758
- const ability = buildAbilityFromScopes({
799
+ const builder = new AbilityBuilder(Ability);
800
+ buildAbilityFromScopes({
759
801
  ...baseContext,
760
802
  scopes: ['manage:sql_runner'],
761
- });
803
+ }, builder);
804
+ const ability = builder.build();
762
805
  expect(ability.can('manage', subject('SqlRunner', {
763
806
  organizationUuid: 'org-123',
764
807
  projectUuid: 'project-123',
765
808
  }))).toBe(true);
766
809
  });
767
810
  it('should handle manage:custom_sql permissions', () => {
768
- const ability = buildAbilityFromScopes({
811
+ const builder = new AbilityBuilder(Ability);
812
+ buildAbilityFromScopes({
769
813
  ...baseContext,
770
814
  scopes: ['manage:custom_sql'],
771
- });
815
+ }, builder);
816
+ const ability = builder.build();
772
817
  expect(ability.can('manage', subject('CustomSql', {
773
818
  organizationUuid: 'org-123',
774
819
  projectUuid: 'project-123',
@@ -776,33 +821,31 @@ describe('scopeAbilityBuilder', () => {
776
821
  });
777
822
  });
778
823
  describe('project delete permissions', () => {
779
- it('should handle delete:project with projectUuid', () => {
780
- const contextWithProject = {
824
+ it('should handle delete:project@self permissions', () => {
825
+ const builder = new AbilityBuilder(Ability);
826
+ buildAbilityFromScopes({
781
827
  ...baseContext,
782
- projectUuid: 'project-456',
783
- };
784
- const ability = buildAbilityFromScopes({
785
- ...contextWithProject,
786
- scopes: ['delete:project'],
787
- });
828
+ userUuid: 'user-456',
829
+ scopes: ['delete:project@self'],
830
+ }, builder);
831
+ const ability = builder.build();
788
832
  // Can delete specific project
789
833
  expect(ability.can('delete', subject('Project', {
790
- projectUuid: 'project-456',
834
+ createdByUserUuid: 'user-456',
835
+ type: ProjectType.PREVIEW,
791
836
  }))).toBe(true);
792
- // Cannot delete different project
793
837
  expect(ability.can('delete', subject('Project', {
794
- projectUuid: 'different-project',
838
+ createdByUserUuid: 'different-user',
839
+ type: ProjectType.PREVIEW,
795
840
  }))).toBe(false);
796
841
  });
797
842
  it('should handle delete:project for preview projects', () => {
798
- const contextWithoutProject = {
799
- ...baseContext,
800
- projectUuid: '',
801
- };
802
- const ability = buildAbilityFromScopes({
803
- ...contextWithoutProject,
843
+ const builder = new AbilityBuilder(Ability);
844
+ buildAbilityFromScopes({
845
+ ...baseContextWithOrg,
804
846
  scopes: ['delete:project'],
805
- });
847
+ }, builder);
848
+ const ability = builder.build();
806
849
  // Can delete preview projects in organization
807
850
  expect(ability.can('delete', subject('Project', {
808
851
  organizationUuid: 'org-123',
@@ -817,20 +860,24 @@ describe('scopeAbilityBuilder', () => {
817
860
  });
818
861
  describe('pinned items permissions', () => {
819
862
  it('should handle view:pinned_items permissions', () => {
820
- const ability = buildAbilityFromScopes({
863
+ const builder = new AbilityBuilder(Ability);
864
+ buildAbilityFromScopes({
821
865
  ...baseContext,
822
866
  scopes: ['view:pinned_items'],
823
- });
867
+ }, builder);
868
+ const ability = builder.build();
824
869
  expect(ability.can('view', subject('PinnedItems', {
825
870
  organizationUuid: 'org-123',
826
871
  projectUuid: 'project-123',
827
872
  }))).toBe(true);
828
873
  });
829
874
  it('should handle manage:pinned_items permissions', () => {
830
- const ability = buildAbilityFromScopes({
875
+ const builder = new AbilityBuilder(Ability);
876
+ buildAbilityFromScopes({
831
877
  ...baseContext,
832
878
  scopes: ['manage:pinned_items'],
833
- });
879
+ }, builder);
880
+ const ability = builder.build();
834
881
  expect(ability.can('manage', subject('PinnedItems', {
835
882
  organizationUuid: 'org-123',
836
883
  projectUuid: 'project-123',
@@ -839,10 +886,12 @@ describe('scopeAbilityBuilder', () => {
839
886
  });
840
887
  describe('explore permissions', () => {
841
888
  it('should handle manage:explore permissions', () => {
842
- const ability = buildAbilityFromScopes({
889
+ const builder = new AbilityBuilder(Ability);
890
+ buildAbilityFromScopes({
843
891
  ...baseContext,
844
892
  scopes: ['manage:explore'],
845
- });
893
+ }, builder);
894
+ const ability = builder.build();
846
895
  expect(ability.can('manage', subject('Explore', {
847
896
  organizationUuid: 'org-123',
848
897
  projectUuid: 'project-123',
@@ -851,10 +900,12 @@ describe('scopeAbilityBuilder', () => {
851
900
  });
852
901
  describe('virtual view permissions', () => {
853
902
  it('should handle create:virtual_view permissions', () => {
854
- const ability = buildAbilityFromScopes({
903
+ const builder = new AbilityBuilder(Ability);
904
+ buildAbilityFromScopes({
855
905
  ...baseContext,
856
906
  scopes: ['create:virtual_view'],
857
- });
907
+ }, builder);
908
+ const ability = builder.build();
858
909
  expect(ability.can('create', subject('VirtualView', {
859
910
  organizationUuid: 'org-123',
860
911
  projectUuid: 'project-123',
@@ -866,10 +917,12 @@ describe('scopeAbilityBuilder', () => {
866
917
  }))).toBe(false);
867
918
  });
868
919
  it('should handle delete:virtual_view permissions', () => {
869
- const ability = buildAbilityFromScopes({
920
+ const builder = new AbilityBuilder(Ability);
921
+ buildAbilityFromScopes({
870
922
  ...baseContext,
871
923
  scopes: ['delete:virtual_view'],
872
- });
924
+ }, builder);
925
+ const ability = builder.build();
873
926
  expect(ability.can('delete', subject('VirtualView', {
874
927
  organizationUuid: 'org-123',
875
928
  projectUuid: 'project-123',
@@ -881,10 +934,12 @@ describe('scopeAbilityBuilder', () => {
881
934
  }))).toBe(false);
882
935
  });
883
936
  it('should handle manage:virtual_view permissions for both create and delete', () => {
884
- const ability = buildAbilityFromScopes({
937
+ const builder = new AbilityBuilder(Ability);
938
+ buildAbilityFromScopes({
885
939
  ...baseContext,
886
940
  scopes: ['manage:virtual_view'],
887
- });
941
+ }, builder);
942
+ const ability = builder.build();
888
943
  // Should be able to manage (create and delete)
889
944
  expect(ability.can('manage', subject('VirtualView', {
890
945
  organizationUuid: 'org-123',
@@ -901,14 +956,16 @@ describe('scopeAbilityBuilder', () => {
901
956
  }))).toBe(true);
902
957
  });
903
958
  it('should not allow virtual view actions for different organizations', () => {
904
- const ability = buildAbilityFromScopes({
905
- ...baseContext,
959
+ const builder = new AbilityBuilder(Ability);
960
+ buildAbilityFromScopes({
961
+ ...baseContextWithOrg,
906
962
  scopes: [
907
963
  'create:virtual_view',
908
964
  'delete:virtual_view',
909
965
  'manage:virtual_view',
910
966
  ],
911
- });
967
+ }, builder);
968
+ const ability = builder.build();
912
969
  // Should not access virtual views from different org
913
970
  expect(ability.can('create', subject('VirtualView', {
914
971
  organizationUuid: 'different-org',
@@ -924,14 +981,16 @@ describe('scopeAbilityBuilder', () => {
924
981
  }))).toBe(false);
925
982
  });
926
983
  it('should allow virtual view actions for different projects within same organization', () => {
927
- const ability = buildAbilityFromScopes({
928
- ...baseContext,
984
+ const builder = new AbilityBuilder(Ability);
985
+ buildAbilityFromScopes({
986
+ ...baseContextWithOrg,
929
987
  scopes: [
930
988
  'create:virtual_view',
931
989
  'delete:virtual_view',
932
990
  'manage:virtual_view',
933
991
  ],
934
- });
992
+ }, builder);
993
+ const ability = builder.build();
935
994
  // Virtual view permissions are organization-scoped, not project-scoped
936
995
  // So they should work across different projects within the same org
937
996
  expect(ability.can('create', subject('VirtualView', {
@@ -950,25 +1009,29 @@ describe('scopeAbilityBuilder', () => {
950
1009
  });
951
1010
  describe('organization member profile permissions', () => {
952
1011
  it('should handle view:organization_member_profile permissions', () => {
953
- const ability = buildAbilityFromScopes({
954
- ...baseContext,
1012
+ const builder = new AbilityBuilder(Ability);
1013
+ buildAbilityFromScopes({
1014
+ ...baseContextWithOrg,
955
1015
  scopes: ['view:organization_member_profile'],
956
- });
1016
+ }, builder);
1017
+ const ability = builder.build();
957
1018
  expect(ability.can('view', subject('OrganizationMemberProfile', {
958
1019
  organizationUuid: 'org-123',
959
1020
  projectUuid: 'project-123',
960
1021
  }))).toBe(true);
961
- // Cannot view profiles from different organization
1022
+ // Cannot view profiles from different project
962
1023
  expect(ability.can('view', subject('OrganizationMemberProfile', {
963
1024
  organizationUuid: 'different-org',
964
1025
  projectUuid: 'project-123',
965
1026
  }))).toBe(false);
966
1027
  });
967
1028
  it('should handle manage:organization_member_profile permissions', () => {
968
- const ability = buildAbilityFromScopes({
1029
+ const builder = new AbilityBuilder(Ability);
1030
+ buildAbilityFromScopes({
969
1031
  ...baseContext,
970
1032
  scopes: ['manage:organization_member_profile'],
971
- });
1033
+ }, builder);
1034
+ const ability = builder.build();
972
1035
  expect(ability.can('manage', subject('OrganizationMemberProfile', {
973
1036
  organizationUuid: 'org-123',
974
1037
  projectUuid: 'project-123',
@@ -977,7 +1040,8 @@ describe('scopeAbilityBuilder', () => {
977
1040
  });
978
1041
  describe('personal access token permissions', () => {
979
1042
  it('should allow managing PAT when enabled and user has allowed role', () => {
980
- const ability = buildAbilityFromScopes({
1043
+ const builder = new AbilityBuilder(Ability);
1044
+ buildAbilityFromScopes({
981
1045
  ...baseContext,
982
1046
  isEnterprise: true,
983
1047
  organizationRole: 'admin',
@@ -988,11 +1052,13 @@ describe('scopeAbilityBuilder', () => {
988
1052
  allowedOrgRoles: ['admin', 'developer'],
989
1053
  },
990
1054
  },
991
- });
1055
+ }, builder);
1056
+ const ability = builder.build();
992
1057
  expect(ability.can('manage', 'PersonalAccessToken')).toBe(true);
993
1058
  });
994
1059
  it('should not allow managing PAT when disabled', () => {
995
- const ability = buildAbilityFromScopes({
1060
+ const builder = new AbilityBuilder(Ability);
1061
+ buildAbilityFromScopes({
996
1062
  ...baseContext,
997
1063
  isEnterprise: true,
998
1064
  organizationRole: 'admin',
@@ -1003,11 +1069,13 @@ describe('scopeAbilityBuilder', () => {
1003
1069
  allowedOrgRoles: ['admin', 'developer'],
1004
1070
  },
1005
1071
  },
1006
- });
1072
+ }, builder);
1073
+ const ability = builder.build();
1007
1074
  expect(ability.can('manage', subject('PersonalAccessToken', {}))).toBe(false);
1008
1075
  });
1009
1076
  it('should not allow managing PAT when user role not in allowed roles', () => {
1010
- const ability = buildAbilityFromScopes({
1077
+ const builder = new AbilityBuilder(Ability);
1078
+ buildAbilityFromScopes({
1011
1079
  ...baseContext,
1012
1080
  isEnterprise: true,
1013
1081
  organizationRole: 'developer',
@@ -1018,21 +1086,25 @@ describe('scopeAbilityBuilder', () => {
1018
1086
  allowedOrgRoles: ['admin'],
1019
1087
  },
1020
1088
  },
1021
- });
1089
+ }, builder);
1090
+ const ability = builder.build();
1022
1091
  expect(ability.can('manage', subject('PersonalAccessToken', {}))).toBe(false);
1023
1092
  });
1024
1093
  it('should not allow managing PAT when no permissions config provided', () => {
1025
- const ability = buildAbilityFromScopes({
1094
+ const builder = new AbilityBuilder(Ability);
1095
+ buildAbilityFromScopes({
1026
1096
  ...baseContext,
1027
1097
  isEnterprise: true,
1028
1098
  organizationRole: 'admin',
1029
1099
  scopes: ['manage:personal_access_token'],
1030
1100
  // No permissionsConfig provided
1031
- });
1101
+ }, builder);
1102
+ const ability = builder.build();
1032
1103
  expect(ability.can('manage', subject('PersonalAccessToken', {}))).toBe(false);
1033
1104
  });
1034
1105
  it('should not allow managing PAT when no organization role provided', () => {
1035
- const ability = buildAbilityFromScopes({
1106
+ const builder = new AbilityBuilder(Ability);
1107
+ buildAbilityFromScopes({
1036
1108
  ...baseContext,
1037
1109
  isEnterprise: true,
1038
1110
  organizationRole: '', // Empty organization role
@@ -1043,7 +1115,8 @@ describe('scopeAbilityBuilder', () => {
1043
1115
  allowedOrgRoles: ['admin', 'developer'],
1044
1116
  },
1045
1117
  },
1046
- });
1118
+ }, builder);
1119
+ const ability = builder.build();
1047
1120
  expect(ability.can('manage', subject('PersonalAccessToken', {}))).toBe(false);
1048
1121
  });
1049
1122
  });