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