@nocobase/plugin-acl 0.10.0-alpha.5 → 0.11.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/client.d.ts +3 -0
  2. package/client.js +1 -0
  3. package/lib/client/index.d.ts +5 -0
  4. package/lib/client/index.js +22 -0
  5. package/lib/{actions → server/actions}/role-check.js +0 -3
  6. package/lib/server/index.d.ts +1 -0
  7. package/lib/server/index.js +13 -0
  8. package/lib/server/middlewares/setCurrentRole.d.ts +2 -0
  9. package/lib/{middlewares → server/middlewares}/setCurrentRole.js +16 -15
  10. package/lib/{server.js → server/server.js} +15 -16
  11. package/package.json +26 -9
  12. package/server.d.ts +3 -0
  13. package/server.js +1 -0
  14. package/src/client/index.ts +8 -0
  15. package/src/index.ts +1 -0
  16. package/src/server/__tests__/acl.test.ts +835 -0
  17. package/src/server/__tests__/actions.test.ts +141 -0
  18. package/src/server/__tests__/association-field.test.ts +413 -0
  19. package/src/server/__tests__/configuration.test.ts +70 -0
  20. package/src/server/__tests__/list-action.test.ts +446 -0
  21. package/src/server/__tests__/middleware.test.ts +210 -0
  22. package/src/server/__tests__/own.test.ts +124 -0
  23. package/src/server/__tests__/prepare.ts +20 -0
  24. package/src/server/__tests__/role-check.test.ts +46 -0
  25. package/src/server/__tests__/role-resource.test.ts +177 -0
  26. package/src/server/__tests__/role-user.test.ts +127 -0
  27. package/src/server/__tests__/role.test.ts +118 -0
  28. package/src/server/__tests__/scope.test.ts +55 -0
  29. package/src/server/__tests__/setCurrentRole.test.ts +86 -0
  30. package/src/server/__tests__/snippets.test.ts +35 -0
  31. package/src/server/__tests__/users.test.ts +136 -0
  32. package/src/server/__tests__/write-role-to-acl.test.ts +41 -0
  33. package/src/server/actions/available-actions.ts +18 -0
  34. package/src/server/actions/role-check.ts +50 -0
  35. package/src/server/actions/role-collections.ts +95 -0
  36. package/src/server/actions/user-setDefaultRole.ts +47 -0
  37. package/src/server/collections/roles-users.ts +8 -0
  38. package/src/server/collections/roles.ts +89 -0
  39. package/src/server/collections/rolesResources.ts +33 -0
  40. package/src/server/collections/rolesResourcesActions.ts +31 -0
  41. package/src/server/collections/rolesResourcesScopes.ts +25 -0
  42. package/src/server/collections/users.ts +31 -0
  43. package/src/server/index.ts +1 -0
  44. package/src/server/middlewares/setCurrentRole.ts +35 -0
  45. package/src/server/migrations/20221214072638-set-role-snippets.ts +23 -0
  46. package/src/server/model/RoleModel.ts +23 -0
  47. package/src/server/model/RoleResourceActionModel.ts +95 -0
  48. package/src/server/model/RoleResourceModel.ts +74 -0
  49. package/src/server/server.ts +854 -0
  50. package/lib/middlewares/setCurrentRole.d.ts +0 -1
  51. /package/lib/{actions → server/actions}/available-actions.d.ts +0 -0
  52. /package/lib/{actions → server/actions}/available-actions.js +0 -0
  53. /package/lib/{actions → server/actions}/role-check.d.ts +0 -0
  54. /package/lib/{actions → server/actions}/role-collections.d.ts +0 -0
  55. /package/lib/{actions → server/actions}/role-collections.js +0 -0
  56. /package/lib/{actions → server/actions}/user-setDefaultRole.d.ts +0 -0
  57. /package/lib/{actions → server/actions}/user-setDefaultRole.js +0 -0
  58. /package/lib/{collections → server/collections}/roles-users.d.ts +0 -0
  59. /package/lib/{collections → server/collections}/roles-users.js +0 -0
  60. /package/lib/{collections → server/collections}/roles.d.ts +0 -0
  61. /package/lib/{collections → server/collections}/roles.js +0 -0
  62. /package/lib/{collections → server/collections}/rolesResources.d.ts +0 -0
  63. /package/lib/{collections → server/collections}/rolesResources.js +0 -0
  64. /package/lib/{collections → server/collections}/rolesResourcesActions.d.ts +0 -0
  65. /package/lib/{collections → server/collections}/rolesResourcesActions.js +0 -0
  66. /package/lib/{collections → server/collections}/rolesResourcesScopes.d.ts +0 -0
  67. /package/lib/{collections → server/collections}/rolesResourcesScopes.js +0 -0
  68. /package/lib/{collections → server/collections}/users.d.ts +0 -0
  69. /package/lib/{collections → server/collections}/users.js +0 -0
  70. /package/lib/{migrations → server/migrations}/20221214072638-set-role-snippets.d.ts +0 -0
  71. /package/lib/{migrations → server/migrations}/20221214072638-set-role-snippets.js +0 -0
  72. /package/lib/{model → server/model}/RoleModel.d.ts +0 -0
  73. /package/lib/{model → server/model}/RoleModel.js +0 -0
  74. /package/lib/{model → server/model}/RoleResourceActionModel.d.ts +0 -0
  75. /package/lib/{model → server/model}/RoleResourceActionModel.js +0 -0
  76. /package/lib/{model → server/model}/RoleResourceModel.d.ts +0 -0
  77. /package/lib/{model → server/model}/RoleResourceModel.js +0 -0
  78. /package/lib/{server.d.ts → server/server.d.ts} +0 -0
@@ -0,0 +1,446 @@
1
+ import { Database } from '@nocobase/database';
2
+ import { MockServer } from '@nocobase/test';
3
+ import { prepareApp } from './prepare';
4
+
5
+ describe('list action with acl', () => {
6
+ let app: MockServer;
7
+
8
+ let Post;
9
+
10
+ beforeEach(async () => {
11
+ app = await prepareApp();
12
+
13
+ Post = app.db.collection({
14
+ name: 'posts',
15
+ fields: [
16
+ { type: 'string', name: 'title' },
17
+ {
18
+ type: 'bigInt',
19
+ name: 'createdById',
20
+ },
21
+ {
22
+ type: 'belongsTo',
23
+ name: 'createdBy',
24
+ target: 'users',
25
+ },
26
+ ],
27
+ });
28
+
29
+ await app.db.sync();
30
+ });
31
+
32
+ afterEach(async () => {
33
+ await app.destroy();
34
+ });
35
+
36
+ it('should list with meta permission that has difference primary key', async () => {
37
+ const userRole = app.acl.define({
38
+ role: 'user',
39
+ });
40
+
41
+ app.acl.addFixedParams('tests', 'destroy', () => {
42
+ return {
43
+ filter: {
44
+ $and: [{ 'name.$ne': 't1' }, { 'name.$ne': 't2' }],
45
+ },
46
+ };
47
+ });
48
+
49
+ userRole.grantAction('tests:view', {});
50
+
51
+ userRole.grantAction('tests:update', {
52
+ own: true,
53
+ });
54
+
55
+ userRole.grantAction('tests:destroy', {});
56
+
57
+ const Test = app.db.collection({
58
+ name: 'tests',
59
+ fields: [
60
+ { type: 'string', name: 'name', primaryKey: true },
61
+ {
62
+ type: 'bigInt',
63
+ name: 'createdById',
64
+ },
65
+ ],
66
+ autoGenId: false,
67
+ filterTargetKey: 'name',
68
+ });
69
+
70
+ await app.db.sync();
71
+
72
+ await Test.repository.create({
73
+ values: [
74
+ { name: 't1', createdById: 1 },
75
+ { name: 't2', createdById: 1 },
76
+ { name: 't3', createdById: 2 },
77
+ ],
78
+ });
79
+
80
+ app.resourcer.use(
81
+ (ctx, next) => {
82
+ ctx.state.currentRole = 'user';
83
+ ctx.state.currentUser = {
84
+ id: 1,
85
+ };
86
+
87
+ return next();
88
+ },
89
+ {
90
+ before: 'acl',
91
+ },
92
+ );
93
+
94
+ //@ts-ignore
95
+ const response = await app.agent().set('X-With-ACL-Meta', true).resource('tests').list({});
96
+
97
+ const data = response.body;
98
+ expect(data.meta.allowedActions.view).toEqual(['t1', 't2', 't3']);
99
+ expect(data.meta.allowedActions.update).toEqual(['t1', 't2']);
100
+ expect(data.meta.allowedActions.destroy).toEqual(['t3']);
101
+ });
102
+
103
+ it('should list items meta permissions by association field', async () => {
104
+ const userRole = app.acl.define({
105
+ role: 'user',
106
+ });
107
+
108
+ userRole.grantAction('posts:view', {});
109
+
110
+ userRole.grantAction('posts:update', {
111
+ filter: {
112
+ 'createdBy.id': '{{ ctx.state.currentUser.id }}',
113
+ },
114
+ });
115
+
116
+ await Post.repository.create({
117
+ values: [
118
+ { title: 'p1', createdById: 1 },
119
+ { title: 'p2', createdById: 1 },
120
+ { title: 'p3', createdById: 2 },
121
+ ],
122
+ });
123
+
124
+ app.resourcer.use(
125
+ (ctx, next) => {
126
+ ctx.state.currentRole = 'user';
127
+ ctx.state.currentUser = {
128
+ id: 1,
129
+ };
130
+
131
+ return next();
132
+ },
133
+ {
134
+ before: 'acl',
135
+ },
136
+ );
137
+
138
+ const response = await (app as any).agent().set('X-With-ACL-Meta', true).resource('posts').list();
139
+ const data = response.body;
140
+ expect(data.meta.allowedActions.view).toEqual([1, 2, 3]);
141
+ expect(data.meta.allowedActions.update).toEqual([1, 2]);
142
+ expect(data.meta.allowedActions.destroy).toEqual([]);
143
+ });
144
+
145
+ it('should list items with meta permission', async () => {
146
+ const userRole = app.acl.define({
147
+ role: 'user',
148
+ });
149
+
150
+ userRole.grantAction('posts:view', {});
151
+
152
+ userRole.grantAction('posts:update', {
153
+ own: true,
154
+ });
155
+
156
+ await Post.repository.create({
157
+ values: [
158
+ { title: 'p1', createdById: 1 },
159
+ { title: 'p2', createdById: 1 },
160
+ { title: 'p3', createdById: 2 },
161
+ ],
162
+ });
163
+
164
+ app.resourcer.use(
165
+ (ctx, next) => {
166
+ ctx.state.currentRole = 'user';
167
+ ctx.state.currentUser = {
168
+ id: 1,
169
+ };
170
+
171
+ return next();
172
+ },
173
+ {
174
+ before: 'acl',
175
+ },
176
+ );
177
+
178
+ // @ts-ignore
179
+ const response = await app.agent().set('X-With-ACL-Meta', true).resource('posts').list({});
180
+
181
+ const data = response.body;
182
+ expect(data.meta.allowedActions.view).toEqual([1, 2, 3]);
183
+ expect(data.meta.allowedActions.update).toEqual([1, 2]);
184
+ expect(data.meta.allowedActions.destroy).toEqual([]);
185
+ });
186
+
187
+ it('should response item permission when request get action', async () => {
188
+ const userRole = app.acl.define({
189
+ role: 'user',
190
+ });
191
+
192
+ userRole.grantAction('posts:view', {});
193
+
194
+ userRole.grantAction('posts:update', {
195
+ own: true,
196
+ });
197
+
198
+ await Post.repository.create({
199
+ values: [
200
+ { title: 'p1', createdById: 1 },
201
+ { title: 'p2', createdById: 1 },
202
+ { title: 'p3', createdById: 2 },
203
+ ],
204
+ });
205
+
206
+ app.resourcer.use(
207
+ (ctx, next) => {
208
+ ctx.state.currentRole = 'user';
209
+ ctx.state.currentUser = {
210
+ id: 1,
211
+ };
212
+
213
+ return next();
214
+ },
215
+ {
216
+ before: 'acl',
217
+ },
218
+ );
219
+
220
+ // @ts-ignore
221
+ const getResponse = await app.agent().set('X-With-ACL-Meta', true).resource('posts').get({
222
+ filterByTk: 1,
223
+ });
224
+
225
+ const getBody = getResponse.body;
226
+
227
+ expect(getBody.meta.allowedActions).toBeDefined();
228
+ });
229
+ });
230
+
231
+ describe('list association action with acl', () => {
232
+ let app;
233
+ let db: Database;
234
+
235
+ afterEach(async () => {
236
+ await app.destroy();
237
+ });
238
+
239
+ beforeEach(async () => {
240
+ app = await prepareApp();
241
+ db = app.db;
242
+
243
+ app.db.collection({
244
+ name: 'posts',
245
+ fields: [
246
+ {
247
+ type: 'string',
248
+ name: 'title',
249
+ },
250
+ {
251
+ type: 'hasMany',
252
+ name: 'comments',
253
+ },
254
+ ],
255
+ });
256
+
257
+ app.db.collection({
258
+ name: 'comments',
259
+ fields: [
260
+ {
261
+ type: 'string',
262
+ name: 'content',
263
+ },
264
+ {
265
+ type: 'belongsTo',
266
+ name: 'post',
267
+ },
268
+ ],
269
+ });
270
+
271
+ await app.db.sync();
272
+ });
273
+
274
+ it('should list allowedActions', async () => {
275
+ await db.getRepository('roles').create({
276
+ values: {
277
+ name: 'newRole',
278
+ },
279
+ });
280
+
281
+ const user = await db.getRepository('users').create({
282
+ values: {
283
+ roles: ['newRole'],
284
+ },
285
+ });
286
+
287
+ await db.getRepository('roles.resources', 'newRole').create({
288
+ values: {
289
+ name: 'posts',
290
+ usingActionConfig: true,
291
+ actions: [
292
+ {
293
+ name: 'view',
294
+ fields: ['title', 'comments'],
295
+ },
296
+ {
297
+ name: 'create',
298
+ fields: ['title', 'comments'],
299
+ },
300
+ ],
301
+ },
302
+ });
303
+
304
+ const userPlugin = app.getPlugin('users');
305
+ const userAgent = app.agent().login(user).set('X-With-ACL-Meta', true);
306
+
307
+ await userAgent.resource('posts').create({
308
+ values: {
309
+ title: 'post1',
310
+ comments: [{ content: 'comment1' }, { content: 'comment2' }],
311
+ },
312
+ });
313
+
314
+ const response = await userAgent.resource('posts').list({});
315
+ expect(response.statusCode).toEqual(200);
316
+
317
+ const commentsResponse = await userAgent.resource('posts.comments', 1).list({});
318
+ const data = commentsResponse.body;
319
+
320
+ /**
321
+ * allowedActions.view == [1]
322
+ * allowedActions.update = []
323
+ * allowedActions.destroy = []
324
+ */
325
+ expect(data['meta']['allowedActions']).toBeDefined();
326
+ expect(data['meta']['allowedActions'].view).toContain(1);
327
+ expect(data['meta']['allowedActions'].view).toContain(2);
328
+ });
329
+
330
+ it('tree list action allowActions', async () => {
331
+ await db.getRepository('roles').create({
332
+ values: {
333
+ name: 'newRole',
334
+ },
335
+ });
336
+
337
+ const user = await db.getRepository('users').create({
338
+ values: {
339
+ roles: ['newRole'],
340
+ },
341
+ });
342
+
343
+ const userPlugin = app.getPlugin('users');
344
+ const agent = app.agent().login(user).set('X-With-ACL-Meta', true);
345
+ app.acl.allow('table_a', ['*']);
346
+ app.acl.allow('collections', ['*']);
347
+
348
+ await agent.resource('collections').create({
349
+ values: {
350
+ autoGenId: true,
351
+ createdBy: false,
352
+ updatedBy: false,
353
+ createdAt: false,
354
+ updatedAt: false,
355
+ sortable: false,
356
+ name: 'table_a',
357
+ template: 'tree',
358
+ tree: 'adjacency-list',
359
+ fields: [
360
+ {
361
+ interface: 'integer',
362
+ name: 'parentId',
363
+ type: 'bigInt',
364
+ isForeignKey: true,
365
+ uiSchema: {
366
+ type: 'number',
367
+ title: '{{t("Parent ID")}}',
368
+ 'x-component': 'InputNumber',
369
+ 'x-read-pretty': true,
370
+ },
371
+ target: 'table_a',
372
+ },
373
+ {
374
+ interface: 'm2o',
375
+ type: 'belongsTo',
376
+ name: 'parent',
377
+ treeParent: true,
378
+ foreignKey: 'parentId',
379
+ uiSchema: {
380
+ title: '{{t("Parent")}}',
381
+ 'x-component': 'AssociationField',
382
+ 'x-component-props': { multiple: false, fieldNames: { label: 'id', value: 'id' } },
383
+ },
384
+ target: 'table_a',
385
+ },
386
+ {
387
+ interface: 'o2m',
388
+ type: 'hasMany',
389
+ name: 'children',
390
+ foreignKey: 'parentId',
391
+ uiSchema: {
392
+ title: '{{t("Children")}}',
393
+ 'x-component': 'RecordPicker',
394
+ 'x-component-props': { multiple: true, fieldNames: { label: 'id', value: 'id' } },
395
+ },
396
+ treeChildren: true,
397
+ target: 'table_a',
398
+ },
399
+ {
400
+ name: 'id',
401
+ type: 'bigInt',
402
+ autoIncrement: true,
403
+ primaryKey: true,
404
+ allowNull: false,
405
+ uiSchema: { type: 'number', title: '{{t("ID")}}', 'x-component': 'InputNumber', 'x-read-pretty': true },
406
+ interface: 'id',
407
+ },
408
+ ],
409
+ title: 'table_a',
410
+ },
411
+ });
412
+
413
+ await agent.resource('table_a').create({
414
+ values: {},
415
+ });
416
+
417
+ await agent.resource('table_a').create({
418
+ values: {
419
+ parent: {
420
+ id: 1,
421
+ },
422
+ },
423
+ });
424
+
425
+ await agent.resource('table_a').create({
426
+ values: {},
427
+ });
428
+
429
+ await agent.resource('table_a').create({
430
+ values: {
431
+ parent: {
432
+ id: 3,
433
+ },
434
+ },
435
+ });
436
+
437
+ const res = await agent.resource('table_a').list({
438
+ filter: JSON.stringify({
439
+ parentId: null,
440
+ }),
441
+ tree: true,
442
+ });
443
+
444
+ expect(res.body.meta.allowedActions.view.sort()).toMatchObject([1, 2, 3, 4]);
445
+ });
446
+ });
@@ -0,0 +1,210 @@
1
+ import { ACL } from '@nocobase/acl';
2
+ import { Database, Model } from '@nocobase/database';
3
+ import UsersPlugin from '@nocobase/plugin-users';
4
+ import { MockServer } from '@nocobase/test';
5
+ import { prepareApp } from './prepare';
6
+
7
+ describe('middleware', () => {
8
+ let app: MockServer;
9
+ let role: Model;
10
+ let db: Database;
11
+ let acl: ACL;
12
+ let admin;
13
+ let adminAgent;
14
+
15
+ beforeEach(async () => {
16
+ app = await prepareApp();
17
+ db = app.db;
18
+ acl = app.acl;
19
+
20
+ role = await db.getRepository('roles').findOne({
21
+ filter: {
22
+ name: 'admin',
23
+ },
24
+ });
25
+
26
+ const UserRepo = db.getCollection('users').repository;
27
+ admin = await UserRepo.create({
28
+ values: {
29
+ roles: ['admin'],
30
+ },
31
+ });
32
+
33
+ const userPlugin = app.getPlugin('users') as UsersPlugin;
34
+ adminAgent = app.agent().login(admin);
35
+
36
+ await db.getRepository('collections').create({
37
+ values: {
38
+ name: 'posts',
39
+ },
40
+ context: {},
41
+ });
42
+
43
+ await db.getRepository('collections.fields', 'posts').create({
44
+ values: {
45
+ name: 'title',
46
+ type: 'string',
47
+ },
48
+ context: {},
49
+ });
50
+
51
+ await db.getRepository('collections.fields', 'posts').create({
52
+ values: {
53
+ name: 'description',
54
+ type: 'string',
55
+ },
56
+ context: {},
57
+ });
58
+
59
+ await db.getRepository('collections.fields', 'posts').create({
60
+ values: {
61
+ name: 'createdById',
62
+ type: 'integer',
63
+ },
64
+ context: {},
65
+ });
66
+ });
67
+
68
+ afterEach(async () => {
69
+ await app.destroy();
70
+ });
71
+
72
+ it('should throw 403 when no permission', async () => {
73
+ const response = await app.agent().resource('posts').create({
74
+ values: {},
75
+ });
76
+
77
+ expect(response.statusCode).toEqual(403);
78
+ });
79
+
80
+ it('should return 200 when role has permission', async () => {
81
+ await db.getRepository('roles').update({
82
+ filterByTk: 'admin',
83
+ values: {
84
+ strategy: {
85
+ actions: ['create:all'],
86
+ },
87
+ },
88
+ });
89
+
90
+ const response = await adminAgent.resource('posts').create({
91
+ values: {},
92
+ });
93
+
94
+ expect(response.statusCode).toEqual(200);
95
+ });
96
+
97
+ it('should limit fields on view actions', async () => {
98
+ await adminAgent.resource('roles.resources', role.get('name')).create({
99
+ values: {
100
+ name: 'posts',
101
+ usingActionsConfig: true,
102
+ actions: [
103
+ {
104
+ name: 'create',
105
+ fields: ['title', 'description'],
106
+ },
107
+ {
108
+ name: 'view',
109
+ fields: ['title'],
110
+ },
111
+ ],
112
+ },
113
+ });
114
+
115
+ await adminAgent.resource('posts').create({
116
+ values: {
117
+ title: 'post-title',
118
+ description: 'post-description',
119
+ },
120
+ });
121
+
122
+ const post = await db.getRepository('posts').findOne();
123
+ expect(post.get('title')).toEqual('post-title');
124
+ expect(post.get('description')).toEqual('post-description');
125
+
126
+ const response = await adminAgent.resource('posts').list({});
127
+ expect(response.statusCode).toEqual(200);
128
+
129
+ const [data] = response.body.data;
130
+
131
+ expect(data['id']).not.toBeUndefined();
132
+ expect(data['title']).toEqual('post-title');
133
+ expect(data['description']).toBeUndefined();
134
+ });
135
+
136
+ it('should parse template value on action params', async () => {
137
+ const res = await adminAgent.resource('rolesResourcesScopes').create({
138
+ values: {
139
+ name: 'own',
140
+ scope: {
141
+ createdById: '{{ ctx.state.currentUser.id }}',
142
+ },
143
+ },
144
+ });
145
+
146
+ await adminAgent.resource('roles.resources', role.get('name')).create({
147
+ values: {
148
+ name: 'posts',
149
+ usingActionsConfig: true,
150
+ actions: [
151
+ {
152
+ name: 'create',
153
+ fields: ['title', 'description', 'createdById'],
154
+ },
155
+ {
156
+ name: 'view',
157
+ fields: ['title'],
158
+ scope: res.body.data.id,
159
+ },
160
+ ],
161
+ },
162
+ });
163
+
164
+ await adminAgent.resource('posts').create({
165
+ values: {
166
+ title: 't1',
167
+ description: 'd1',
168
+ createdById: 1,
169
+ },
170
+ });
171
+
172
+ await adminAgent.resource('posts').create({
173
+ values: {
174
+ title: 't2',
175
+ description: 'p2',
176
+ createdById: 2,
177
+ },
178
+ });
179
+
180
+ const response = await adminAgent.resource('posts').list();
181
+ const data = response.body.data;
182
+ expect(data.length).toEqual(1);
183
+ });
184
+
185
+ it('should change fields params to whitelist in create action', async () => {
186
+ await adminAgent.resource('roles.resources', role.get('name')).create({
187
+ values: {
188
+ name: 'posts',
189
+ usingActionsConfig: true,
190
+ actions: [
191
+ {
192
+ name: 'create',
193
+ fields: ['title'],
194
+ },
195
+ ],
196
+ },
197
+ });
198
+
199
+ await adminAgent.resource('posts').create({
200
+ values: {
201
+ title: 'post-title',
202
+ description: 'post-description',
203
+ },
204
+ });
205
+
206
+ const post = await db.getRepository('posts').findOne();
207
+ expect(post.get('title')).toEqual('post-title');
208
+ expect(post.get('description')).toBeNull();
209
+ });
210
+ });