@nocobase/plugin-acl 0.11.1-alpha.5 → 0.12.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 (96) hide show
  1. package/client.d.ts +2 -3
  2. package/client.js +1 -1
  3. package/dist/client/index.js +11 -0
  4. package/{lib/server → dist}/index.d.ts +1 -0
  5. package/dist/index.js +18 -0
  6. package/{src/server/actions/available-actions.ts → dist/server/actions/available-actions.js} +7 -5
  7. package/{src/server/actions/role-check.ts → dist/server/actions/role-check.js} +17 -22
  8. package/dist/server/actions/role-collections.js +53 -0
  9. package/dist/server/actions/user-setDefaultRole.js +43 -0
  10. package/dist/server/collections/roles-users.js +10 -0
  11. package/dist/server/collections/roles.js +103 -0
  12. package/dist/server/collections/rolesResources.js +35 -0
  13. package/dist/server/collections/rolesResourcesActions.js +33 -0
  14. package/dist/server/collections/rolesResourcesScopes.js +27 -0
  15. package/dist/server/collections/users.js +35 -0
  16. package/dist/server/index.js +11 -0
  17. package/dist/server/middlewares/setCurrentRole.js +31 -0
  18. package/dist/server/migrations/20221214072638-set-role-snippets.js +25 -0
  19. package/dist/server/model/RoleModel.js +23 -0
  20. package/dist/server/model/RoleResourceActionModel.js +64 -0
  21. package/dist/server/model/RoleResourceModel.js +55 -0
  22. package/dist/server/server.js +709 -0
  23. package/package.json +12 -21
  24. package/server.d.ts +3 -3
  25. package/server.js +1 -1
  26. package/lib/client/index.js +0 -22
  27. package/lib/index.js +0 -13
  28. package/lib/server/actions/available-actions.js +0 -34
  29. package/lib/server/actions/role-check.js +0 -77
  30. package/lib/server/actions/role-collections.js +0 -98
  31. package/lib/server/actions/user-setDefaultRole.js +0 -52
  32. package/lib/server/collections/roles-users.js +0 -16
  33. package/lib/server/collections/roles.js +0 -92
  34. package/lib/server/collections/rolesResources.js +0 -31
  35. package/lib/server/collections/rolesResourcesActions.js +0 -31
  36. package/lib/server/collections/rolesResourcesScopes.js +0 -25
  37. package/lib/server/collections/users.js +0 -41
  38. package/lib/server/index.js +0 -13
  39. package/lib/server/middlewares/setCurrentRole.js +0 -45
  40. package/lib/server/migrations/20221214072638-set-role-snippets.js +0 -43
  41. package/lib/server/model/RoleModel.js +0 -35
  42. package/lib/server/model/RoleResourceActionModel.js +0 -91
  43. package/lib/server/model/RoleResourceModel.js +0 -106
  44. package/lib/server/server.js +0 -947
  45. package/src/client/index.ts +0 -8
  46. package/src/index.ts +0 -1
  47. package/src/server/__tests__/acl.test.ts +0 -835
  48. package/src/server/__tests__/actions.test.ts +0 -141
  49. package/src/server/__tests__/association-field.test.ts +0 -413
  50. package/src/server/__tests__/configuration.test.ts +0 -70
  51. package/src/server/__tests__/list-action.test.ts +0 -446
  52. package/src/server/__tests__/middleware.test.ts +0 -210
  53. package/src/server/__tests__/own.test.ts +0 -124
  54. package/src/server/__tests__/prepare.ts +0 -20
  55. package/src/server/__tests__/role-check.test.ts +0 -46
  56. package/src/server/__tests__/role-resource.test.ts +0 -177
  57. package/src/server/__tests__/role-user.test.ts +0 -127
  58. package/src/server/__tests__/role.test.ts +0 -118
  59. package/src/server/__tests__/scope.test.ts +0 -55
  60. package/src/server/__tests__/setCurrentRole.test.ts +0 -86
  61. package/src/server/__tests__/snippets.test.ts +0 -35
  62. package/src/server/__tests__/users.test.ts +0 -136
  63. package/src/server/__tests__/write-role-to-acl.test.ts +0 -41
  64. package/src/server/actions/role-collections.ts +0 -95
  65. package/src/server/actions/user-setDefaultRole.ts +0 -47
  66. package/src/server/collections/roles-users.ts +0 -8
  67. package/src/server/collections/roles.ts +0 -101
  68. package/src/server/collections/rolesResources.ts +0 -33
  69. package/src/server/collections/rolesResourcesActions.ts +0 -31
  70. package/src/server/collections/rolesResourcesScopes.ts +0 -25
  71. package/src/server/collections/users.ts +0 -31
  72. package/src/server/index.ts +0 -1
  73. package/src/server/middlewares/setCurrentRole.ts +0 -35
  74. package/src/server/migrations/20221214072638-set-role-snippets.ts +0 -23
  75. package/src/server/model/RoleModel.ts +0 -23
  76. package/src/server/model/RoleResourceActionModel.ts +0 -95
  77. package/src/server/model/RoleResourceModel.ts +0 -74
  78. package/src/server/server.ts +0 -854
  79. /package/{lib → dist}/client/index.d.ts +0 -0
  80. /package/{lib → dist}/server/actions/available-actions.d.ts +0 -0
  81. /package/{lib → dist}/server/actions/role-check.d.ts +0 -0
  82. /package/{lib → dist}/server/actions/role-collections.d.ts +0 -0
  83. /package/{lib → dist}/server/actions/user-setDefaultRole.d.ts +0 -0
  84. /package/{lib → dist}/server/collections/roles-users.d.ts +0 -0
  85. /package/{lib → dist}/server/collections/roles.d.ts +0 -0
  86. /package/{lib → dist}/server/collections/rolesResources.d.ts +0 -0
  87. /package/{lib → dist}/server/collections/rolesResourcesActions.d.ts +0 -0
  88. /package/{lib → dist}/server/collections/rolesResourcesScopes.d.ts +0 -0
  89. /package/{lib → dist}/server/collections/users.d.ts +0 -0
  90. /package/{lib → dist/server}/index.d.ts +0 -0
  91. /package/{lib → dist}/server/middlewares/setCurrentRole.d.ts +0 -0
  92. /package/{lib → dist}/server/migrations/20221214072638-set-role-snippets.d.ts +0 -0
  93. /package/{lib → dist}/server/model/RoleModel.d.ts +0 -0
  94. /package/{lib → dist}/server/model/RoleResourceActionModel.d.ts +0 -0
  95. /package/{lib → dist}/server/model/RoleResourceModel.d.ts +0 -0
  96. /package/{lib → dist}/server/server.d.ts +0 -0
@@ -1,854 +0,0 @@
1
- import { NoPermissionError } from '@nocobase/acl';
2
- import { Context, utils as actionUtils } from '@nocobase/actions';
3
- import { Collection, RelationField, snakeCase } from '@nocobase/database';
4
- import { Plugin } from '@nocobase/server';
5
- import { lodash } from '@nocobase/utils';
6
- import { resolve } from 'path';
7
- import { availableActionResource } from './actions/available-actions';
8
- import { checkAction } from './actions/role-check';
9
- import { roleCollectionsResource } from './actions/role-collections';
10
- import { setDefaultRole } from './actions/user-setDefaultRole';
11
- import { setCurrentRole } from './middlewares/setCurrentRole';
12
- import { RoleModel } from './model/RoleModel';
13
- import { RoleResourceActionModel } from './model/RoleResourceActionModel';
14
- import { RoleResourceModel } from './model/RoleResourceModel';
15
-
16
- export interface AssociationFieldAction {
17
- associationActions: string[];
18
- targetActions?: string[];
19
- }
20
-
21
- interface AssociationFieldActions {
22
- [availableActionName: string]: AssociationFieldAction;
23
- }
24
-
25
- export interface AssociationFieldsActions {
26
- [associationType: string]: AssociationFieldActions;
27
- }
28
-
29
- export class GrantHelper {
30
- resourceTargetActionMap = new Map<string, string[]>();
31
- targetActionResourceMap = new Map<string, string[]>();
32
-
33
- constructor() {}
34
- }
35
-
36
- export class PluginACL extends Plugin {
37
- // association field actions config
38
-
39
- associationFieldsActions: AssociationFieldsActions = {};
40
-
41
- grantHelper = new GrantHelper();
42
-
43
- get acl() {
44
- return this.app.acl;
45
- }
46
-
47
- registerAssociationFieldAction(associationType: string, value: AssociationFieldActions) {
48
- this.associationFieldsActions[associationType] = value;
49
- }
50
-
51
- registerAssociationFieldsActions() {
52
- // if grant create action to role, it should
53
- // also grant add action and association target's view action
54
-
55
- this.registerAssociationFieldAction('hasOne', {
56
- view: {
57
- associationActions: ['list', 'get', 'view'],
58
- },
59
- create: {
60
- associationActions: ['create', 'set'],
61
- },
62
- update: {
63
- associationActions: ['update', 'remove', 'set'],
64
- },
65
- });
66
-
67
- this.registerAssociationFieldAction('hasMany', {
68
- view: {
69
- associationActions: ['list', 'get', 'view'],
70
- },
71
- create: {
72
- associationActions: ['create', 'set', 'add'],
73
- },
74
- update: {
75
- associationActions: ['update', 'remove', 'set'],
76
- },
77
- });
78
-
79
- this.registerAssociationFieldAction('belongsTo', {
80
- view: {
81
- associationActions: ['list', 'get', 'view'],
82
- },
83
- create: {
84
- associationActions: ['create', 'set'],
85
- },
86
- update: {
87
- associationActions: ['update', 'remove', 'set'],
88
- },
89
- });
90
-
91
- this.registerAssociationFieldAction('belongsToMany', {
92
- view: {
93
- associationActions: ['list', 'get', 'view'],
94
- },
95
- create: {
96
- associationActions: ['create', 'set', 'add'],
97
- },
98
- update: {
99
- associationActions: ['update', 'remove', 'set', 'toggle'],
100
- },
101
- });
102
- }
103
-
104
- async writeResourceToACL(resourceModel: RoleResourceModel, transaction) {
105
- await resourceModel.writeToACL({
106
- acl: this.acl,
107
- associationFieldsActions: this.associationFieldsActions,
108
- transaction: transaction,
109
- grantHelper: this.grantHelper,
110
- });
111
- }
112
-
113
- async writeActionToACL(actionModel: RoleResourceActionModel, transaction) {
114
- const resource = actionModel.get('resource') as RoleResourceModel;
115
- const role = this.acl.getRole(resource.get('roleName') as string);
116
- await actionModel.writeToACL({
117
- acl: this.acl,
118
- role,
119
- resourceName: resource.get('name') as string,
120
- associationFieldsActions: this.associationFieldsActions,
121
- grantHelper: this.grantHelper,
122
- });
123
- }
124
-
125
- async writeRolesToACL() {
126
- const roles = (await this.app.db.getRepository('roles').find({
127
- appends: ['resources', 'resources.actions'],
128
- })) as RoleModel[];
129
-
130
- for (const role of roles) {
131
- await this.writeRoleToACL(role);
132
- }
133
- }
134
-
135
- async writeRoleToACL(role: RoleModel, transaction: any = null) {
136
- role.writeToAcl({ acl: this.acl });
137
-
138
- let resources = role.get('resources') as RoleResourceModel[];
139
-
140
- if (!resources) {
141
- resources = await role.getResources({ transaction });
142
- }
143
-
144
- for (const resource of resources as RoleResourceModel[]) {
145
- await this.writeResourceToACL(resource, transaction);
146
- }
147
- }
148
-
149
- async beforeLoad() {
150
- this.db.addMigrations({
151
- namespace: this.name,
152
- directory: resolve(__dirname, './migrations'),
153
- context: {
154
- plugin: this,
155
- },
156
- });
157
-
158
- this.app.db.registerModels({
159
- RoleResourceActionModel,
160
- RoleResourceModel,
161
- RoleModel,
162
- });
163
-
164
- this.app.acl.registerSnippet({
165
- name: `pm.${this.name}.roles`,
166
- actions: [
167
- 'roles:*',
168
- 'roles.snippets:*',
169
- 'availableActions:list',
170
- 'roles.collections:list',
171
- 'roles.resources:*',
172
- 'uiSchemas:getProperties',
173
- 'roles.menuUiSchemas:*',
174
- ],
175
- });
176
-
177
- // change resource fields to association fields
178
- this.app.acl.beforeGrantAction((ctx) => {
179
- const actionName = this.app.acl.resolveActionAlias(ctx.actionName);
180
- const collection = this.app.db.getCollection(ctx.resourceName);
181
-
182
- if (!collection) {
183
- return;
184
- }
185
-
186
- const fieldsParams = ctx.params.fields;
187
-
188
- if (!fieldsParams) {
189
- return;
190
- }
191
-
192
- if (actionName == 'view' || actionName == 'export') {
193
- const associationsFields = fieldsParams.filter((fieldName) => {
194
- const field = collection.getField(fieldName);
195
- return field instanceof RelationField;
196
- });
197
-
198
- ctx.params = {
199
- ...ctx.params,
200
- fields: lodash.difference(fieldsParams, associationsFields),
201
- appends: associationsFields,
202
- };
203
- }
204
- });
205
-
206
- this.registerAssociationFieldsActions();
207
-
208
- this.app.resourcer.define(availableActionResource);
209
- this.app.resourcer.define(roleCollectionsResource);
210
-
211
- this.app.resourcer.registerActionHandler('roles:check', checkAction);
212
-
213
- this.app.resourcer.registerActionHandler(`users:setDefaultRole`, setDefaultRole);
214
-
215
- this.db.on('users.afterCreateWithAssociations', async (model, options) => {
216
- const { transaction } = options;
217
- const repository = this.app.db.getRepository('roles');
218
- const defaultRole = await repository.findOne({
219
- filter: {
220
- default: true,
221
- },
222
- transaction,
223
- });
224
-
225
- if (defaultRole && (await model.countRoles({ transaction })) == 0) {
226
- await model.addRoles(defaultRole, { transaction });
227
- }
228
- });
229
-
230
- this.app.on('acl:writeRoleToACL', async (roleModel: RoleModel) => {
231
- await this.writeRoleToACL(roleModel);
232
- });
233
-
234
- this.app.db.on('roles.afterSaveWithAssociations', async (model, options) => {
235
- const { transaction } = options;
236
-
237
- await this.writeRoleToACL(model, transaction);
238
-
239
- // model is default
240
- if (model.get('default')) {
241
- await this.app.db.getRepository('roles').update({
242
- values: {
243
- default: false,
244
- },
245
- filter: {
246
- 'name.$ne': model.get('name'),
247
- },
248
- hooks: false,
249
- transaction,
250
- });
251
- }
252
- });
253
-
254
- this.app.db.on('roles.afterDestroy', (model) => {
255
- const roleName = model.get('name');
256
- this.acl.removeRole(roleName);
257
- });
258
-
259
- this.app.db.on('rolesResources.afterSaveWithAssociations', async (model: RoleResourceModel, options) => {
260
- await this.writeResourceToACL(model, options.transaction);
261
- });
262
-
263
- this.app.db.on('rolesResourcesActions.afterUpdateWithAssociations', async (model, options) => {
264
- const { transaction } = options;
265
- const resource = await model.getResource({
266
- transaction,
267
- });
268
-
269
- await this.writeResourceToACL(resource, transaction);
270
- });
271
-
272
- this.app.db.on('rolesResources.afterDestroy', async (model, options) => {
273
- const role = this.acl.getRole(model.get('roleName'));
274
-
275
- if (role) {
276
- role.revokeResource(model.get('name'));
277
- }
278
- });
279
-
280
- this.app.db.on('collections.afterDestroy', async (model, options) => {
281
- const { transaction } = options;
282
- await this.app.db.getRepository('rolesResources').destroy({
283
- filter: {
284
- name: model.get('name'),
285
- },
286
- transaction,
287
- });
288
- });
289
-
290
- this.app.db.on('fields.afterCreate', async (model, options) => {
291
- const { transaction } = options;
292
-
293
- const collectionName = model.get('collectionName');
294
-
295
- const fieldName = model.get('name');
296
-
297
- const resourceActions = (await this.app.db.getRepository('rolesResourcesActions').find({
298
- filter: {
299
- 'resource.name': collectionName,
300
- },
301
- transaction,
302
- appends: ['resource'],
303
- })) as RoleResourceActionModel[];
304
-
305
- for (const resourceAction of resourceActions) {
306
- const fields = resourceAction.get('fields') as string[];
307
- const newFields = [...fields, fieldName];
308
-
309
- await this.app.db.getRepository('rolesResourcesActions').update({
310
- filterByTk: resourceAction.get('id') as number,
311
- values: {
312
- fields: newFields,
313
- },
314
- transaction,
315
- });
316
- }
317
- });
318
-
319
- this.app.db.on('fields.afterDestroy', async (model, options) => {
320
- const collectionName = model.get('collectionName');
321
- const fieldName = model.get('name');
322
-
323
- const resourceActions = await this.app.db.getRepository('rolesResourcesActions').find({
324
- filter: {
325
- 'resource.name': collectionName,
326
- 'fields.$anyOf': [fieldName],
327
- },
328
- transaction: options.transaction,
329
- });
330
-
331
- for (const resourceAction of resourceActions) {
332
- const fields = resourceAction.get('fields') as string[];
333
- const newFields = fields.filter((field) => field != fieldName);
334
-
335
- await this.app.db.getRepository('rolesResourcesActions').update({
336
- filterByTk: resourceAction.get('id') as number,
337
- values: {
338
- fields: newFields,
339
- },
340
- transaction: options.transaction,
341
- });
342
- }
343
- });
344
-
345
- // sync database role data to acl
346
- this.app.on('afterLoad', async (app, options) => {
347
- if (options?.method === 'install' || options?.method === 'upgrade') {
348
- return;
349
- }
350
- const exists = await this.app.db.collectionExistsInDb('roles');
351
- if (exists) {
352
- await this.writeRolesToACL();
353
- }
354
- });
355
-
356
- this.app.on('afterInstall', async (app, options) => {
357
- const exists = await this.app.db.collectionExistsInDb('roles');
358
- if (exists) {
359
- await this.writeRolesToACL();
360
- }
361
- });
362
-
363
- this.app.on('afterInstallPlugin', async (plugin) => {
364
- if (plugin.getName() !== 'users') {
365
- return;
366
- }
367
- const User = this.db.getCollection('users');
368
- await User.repository.update({
369
- values: {
370
- roles: ['root', 'admin', 'member'],
371
- },
372
- forceUpdate: true,
373
- });
374
-
375
- const RolesUsers = this.db.getCollection('rolesUsers');
376
- await RolesUsers.repository.update({
377
- filter: {
378
- userId: 1,
379
- roleName: 'root',
380
- },
381
- values: {
382
- default: true,
383
- },
384
- });
385
- });
386
-
387
- this.app.on('beforeInstallPlugin', async (plugin) => {
388
- if (plugin.getName() !== 'users') {
389
- return;
390
- }
391
- const roles = this.app.db.getRepository('roles');
392
- await roles.createMany({
393
- records: [
394
- {
395
- name: 'root',
396
- title: '{{t("Root")}}',
397
- hidden: true,
398
- snippets: ['ui.*', 'pm', 'pm.*'],
399
- },
400
- {
401
- name: 'admin',
402
- title: '{{t("Admin")}}',
403
- allowConfigure: true,
404
- allowNewMenu: true,
405
- strategy: { actions: ['create', 'view', 'update', 'destroy'] },
406
- snippets: ['ui.*', 'pm', 'pm.*'],
407
- },
408
- {
409
- name: 'member',
410
- title: '{{t("Member")}}',
411
- allowNewMenu: true,
412
- strategy: { actions: ['view', 'update:own', 'destroy:own', 'create'] },
413
- default: true,
414
- snippets: ['!ui.*', '!pm', '!pm.*'],
415
- },
416
- ],
417
- });
418
- const rolesResourcesScopes = this.app.db.getRepository('rolesResourcesScopes');
419
- await rolesResourcesScopes.createMany({
420
- records: [
421
- {
422
- key: 'all',
423
- name: '{{t("All records")}}',
424
- scope: {},
425
- },
426
- {
427
- key: 'own',
428
- name: '{{t("Own records")}}',
429
- scope: {
430
- createdById: '{{ ctx.state.currentUser.id }}',
431
- },
432
- },
433
- ],
434
- });
435
- });
436
-
437
- this.app.resourcer.use(setCurrentRole, { tag: 'setCurrentRole', before: 'acl', after: 'auth' });
438
-
439
- this.app.acl.allow('users', 'setDefaultRole', 'loggedIn');
440
- this.app.acl.allow('roles', 'check', 'loggedIn');
441
-
442
- this.app.acl.allow('*', '*', (ctx) => {
443
- return ctx.state.currentRole === 'root';
444
- });
445
-
446
- this.app.acl.addFixedParams('collections', 'destroy', () => {
447
- return {
448
- filter: {
449
- $and: [{ 'name.$ne': 'roles' }, { 'name.$ne': 'rolesUsers' }],
450
- },
451
- };
452
- });
453
-
454
- this.app.acl.addFixedParams('rolesResourcesScopes', 'destroy', () => {
455
- return {
456
- filter: {
457
- $and: [{ 'key.$ne': 'all' }, { 'key.$ne': 'own' }],
458
- },
459
- };
460
- });
461
-
462
- this.app.acl.addFixedParams('rolesResourcesScopes', 'update', () => {
463
- return {
464
- filter: {
465
- $and: [{ 'key.$ne': 'all' }, { 'key.$ne': 'own' }],
466
- },
467
- };
468
- });
469
-
470
- this.app.acl.addFixedParams('roles', 'destroy', () => {
471
- return {
472
- filter: {
473
- $and: [{ 'name.$ne': 'root' }, { 'name.$ne': 'admin' }, { 'name.$ne': 'member' }],
474
- },
475
- };
476
- });
477
-
478
- this.app.resourcer.use(async (ctx, next) => {
479
- const { actionName, resourceName, params } = ctx.action;
480
- const { showAnonymous } = params || {};
481
- if (actionName === 'list' && resourceName === 'roles') {
482
- if (!showAnonymous) {
483
- ctx.action.mergeParams({
484
- filter: {
485
- 'name.$ne': 'anonymous',
486
- },
487
- });
488
- }
489
- }
490
-
491
- if (actionName === 'update' && resourceName === 'roles.resources') {
492
- ctx.action.mergeParams({
493
- updateAssociationValues: ['actions'],
494
- });
495
- }
496
-
497
- await next();
498
- });
499
-
500
- this.app.acl.use(async (ctx: Context, next) => {
501
- const { actionName, resourceName } = ctx.action;
502
- if (actionName === 'get' || actionName === 'list') {
503
- if (!Array.isArray(ctx?.permission?.can?.params?.fields)) {
504
- return next();
505
- }
506
- let collection: Collection;
507
- if (resourceName.includes('.')) {
508
- const [collectionName, associationName] = resourceName.split('.');
509
- const field = ctx.db.getCollection(collectionName)?.getField?.(associationName);
510
- if (field.target) {
511
- collection = ctx.db.getCollection(field.target);
512
- }
513
- } else {
514
- collection = ctx.db.getCollection(resourceName);
515
- }
516
-
517
- if (collection && collection.hasField('createdById')) {
518
- ctx.permission.can.params.fields.push('createdById');
519
- }
520
- }
521
- return next();
522
- });
523
-
524
- const parseJsonTemplate = this.app.acl.parseJsonTemplate;
525
-
526
- this.app.acl.use(
527
- async (ctx: Context, next) => {
528
- const { actionName, resourceName, resourceOf } = ctx.action;
529
- // is association request
530
- if (resourceName.includes('.') && resourceOf) {
531
- if (!ctx?.permission?.can?.params) {
532
- return next();
533
- }
534
- // 关联数据去掉 filter
535
- delete ctx.permission.can.params.filter;
536
- // 关联数据能不能处理取决于 source 是否有权限
537
- const [collectionName] = resourceName.split('.');
538
- const action = ctx.can({ resource: collectionName, action: actionName });
539
-
540
- const availableAction = this.app.acl.getAvailableAction(actionName);
541
-
542
- if (availableAction?.options?.onNewRecord) {
543
- if (action) {
544
- ctx.permission.skip = true;
545
- } else {
546
- ctx.permission.can = false;
547
- }
548
- } else {
549
- const filter = await parseJsonTemplate(action?.params?.filter || {}, ctx);
550
- const sourceInstance = await ctx.db.getRepository(collectionName).findOne({
551
- filterByTk: resourceOf,
552
- filter,
553
- });
554
- if (!sourceInstance) {
555
- ctx.permission.can = false;
556
- }
557
- }
558
- }
559
- await next();
560
- },
561
- {
562
- before: 'core',
563
- },
564
- );
565
-
566
- // throw error when user has no fixed params permissions
567
- this.app.acl.use(
568
- async (ctx: any, next) => {
569
- const action = ctx.permission?.can?.action;
570
-
571
- if (action == 'destroy' && !ctx.action.resourceName.includes('.')) {
572
- const repository = actionUtils.getRepositoryFromParams(ctx);
573
-
574
- // params after merge with fixed params
575
- const filteredCount = await repository.count(ctx.permission.mergedParams);
576
-
577
- // params user requested
578
- const queryCount = await repository.count(ctx.permission.rawParams);
579
-
580
- if (queryCount > filteredCount) {
581
- ctx.throw(403, 'No permissions');
582
- return;
583
- }
584
- }
585
-
586
- await next();
587
- },
588
- {
589
- after: 'core',
590
- group: 'after',
591
- },
592
- );
593
-
594
- const withACLMeta = async (ctx: any, next) => {
595
- await next();
596
-
597
- if (!ctx.action || !ctx.get('X-With-ACL-Meta') || ctx.status !== 200) {
598
- return;
599
- }
600
-
601
- const { resourceName, actionName } = ctx.action;
602
-
603
- if (!['list', 'get'].includes(actionName)) {
604
- return;
605
- }
606
-
607
- const collection = ctx.db.getCollection(resourceName);
608
-
609
- if (!collection) {
610
- return;
611
- }
612
-
613
- const Model = collection.model;
614
-
615
- const primaryKeyField = Model.primaryKeyField || Model.primaryKeyAttribute;
616
-
617
- const dataPath = ctx.body?.rows ? 'body.rows' : 'body';
618
- let listData = lodash.get(ctx, dataPath);
619
-
620
- if (actionName == 'get') {
621
- listData = lodash.castArray(listData);
622
- }
623
-
624
- const inspectActions = ['view', 'update', 'destroy'];
625
-
626
- const actionsParams = [];
627
-
628
- for (const action of inspectActions) {
629
- const actionCtx: any = {
630
- db: ctx.db,
631
- action: {
632
- actionName: action,
633
- name: action,
634
- params: {},
635
- resourceName: ctx.action.resourceName,
636
- resourceOf: ctx.action.resourceOf,
637
- mergeParams() {},
638
- },
639
- state: {
640
- currentRole: ctx.state.currentRole,
641
- currentUser: (() => {
642
- if (!ctx.state.currentUser) {
643
- return null;
644
- }
645
- if (ctx.state.currentUser.toJSON) {
646
- return ctx.state.currentUser?.toJSON();
647
- }
648
-
649
- return ctx.state.currentUser;
650
- })(),
651
- },
652
- permission: {},
653
- throw(...args) {
654
- throw new NoPermissionError(...args);
655
- },
656
- };
657
-
658
- try {
659
- await this.app.acl.getActionParams(actionCtx);
660
- } catch (e) {
661
- if (e instanceof NoPermissionError) {
662
- continue;
663
- }
664
-
665
- throw e;
666
- }
667
-
668
- actionsParams.push([
669
- action,
670
- actionCtx.permission?.can === null && !actionCtx.permission.skip
671
- ? null
672
- : actionCtx.permission?.parsedParams || {},
673
- actionCtx,
674
- ]);
675
- }
676
-
677
- const ids = (() => {
678
- if (collection.options.tree) {
679
- if (listData.length == 0) return [];
680
- const getAllNodeIds = (data) => [data[primaryKeyField], ...(data.children || []).flatMap(getAllNodeIds)];
681
- return listData.map((tree) => getAllNodeIds(tree.toJSON())).flat();
682
- }
683
-
684
- return listData.map((item) => item[primaryKeyField]);
685
- })();
686
-
687
- const conditions = [];
688
-
689
- const allAllowed = [];
690
-
691
- for (const [action, params, actionCtx] of actionsParams) {
692
- if (!params) {
693
- continue;
694
- }
695
-
696
- if (lodash.isEmpty(params) || lodash.isEmpty(params.filter)) {
697
- allAllowed.push(action);
698
- continue;
699
- }
700
-
701
- const queryParams = collection.repository.buildQueryOptions({
702
- ...params,
703
- context: actionCtx,
704
- });
705
-
706
- const actionSql = ctx.db.sequelize.queryInterface.queryGenerator.selectQuery(
707
- Model.getTableName(),
708
- {
709
- where: (() => {
710
- const filterObj = queryParams.where;
711
-
712
- if (!this.db.options.underscored) {
713
- return filterObj;
714
- }
715
-
716
- const isAssociationKey = (key) => {
717
- return key.startsWith('$') && key.endsWith('$');
718
- };
719
-
720
- // change camelCase to snake_case
721
- const iterate = (rootObj, path = []) => {
722
- const obj = path.length == 0 ? rootObj : lodash.get(rootObj, path);
723
-
724
- if (Array.isArray(obj)) {
725
- for (let i = 0; i < obj.length; i++) {
726
- if (obj[i] === null) {
727
- continue;
728
- }
729
-
730
- if (typeof obj[i] === 'object') {
731
- iterate(rootObj, [...path, i]);
732
- }
733
- }
734
-
735
- return;
736
- }
737
-
738
- Reflect.ownKeys(obj).forEach((key) => {
739
- if (Array.isArray(obj) && key == 'length') {
740
- return;
741
- }
742
-
743
- if ((typeof obj[key] === 'object' && obj[key] !== null) || typeof obj[key] === 'symbol') {
744
- iterate(rootObj, [...path, key]);
745
- }
746
-
747
- if (typeof key === 'string' && key !== snakeCase(key)) {
748
- const setKey = isAssociationKey(key)
749
- ? (() => {
750
- const parts = key.split('.');
751
-
752
- parts[parts.length - 1] = lodash.snakeCase(parts[parts.length - 1]);
753
-
754
- const result = parts.join('.');
755
-
756
- return result.endsWith('$') ? result : `${result}$`;
757
- })()
758
- : snakeCase(key);
759
- const setValue = lodash.cloneDeep(obj[key]);
760
- lodash.unset(rootObj, [...path, key]);
761
-
762
- lodash.set(rootObj, [...path, setKey], setValue);
763
- }
764
- });
765
- };
766
-
767
- iterate(filterObj);
768
-
769
- return filterObj;
770
- })(),
771
- attributes: [primaryKeyField],
772
- includeIgnoreAttributes: false,
773
- },
774
- Model,
775
- );
776
-
777
- const whereCase = actionSql.match(/WHERE (.*?);/)[1];
778
-
779
- conditions.push({
780
- whereCase,
781
- action,
782
- include: queryParams.include,
783
- });
784
- }
785
-
786
- const results = await collection.model.findAll({
787
- where: {
788
- [primaryKeyField]: ids,
789
- },
790
- attributes: [
791
- primaryKeyField,
792
- ...conditions.map((condition) => {
793
- return [ctx.db.sequelize.literal(`CASE WHEN ${condition.whereCase} THEN 1 ELSE 0 END`), condition.action];
794
- }),
795
- ],
796
- include: conditions.map((condition) => condition.include).flat(),
797
- });
798
-
799
- const allowedActions = inspectActions
800
- .map((action) => {
801
- if (allAllowed.includes(action)) {
802
- return [action, ids];
803
- }
804
-
805
- return [action, results.filter((item) => Boolean(item.get(action))).map((item) => item.get(primaryKeyField))];
806
- })
807
- .reduce((acc, [action, ids]) => {
808
- acc[action] = ids;
809
- return acc;
810
- }, {});
811
-
812
- if (actionName == 'get') {
813
- ctx.bodyMeta = {
814
- ...(ctx.bodyMeta || {}),
815
- allowedActions: allowedActions,
816
- };
817
- }
818
-
819
- if (actionName == 'list') {
820
- ctx.body.allowedActions = allowedActions;
821
- }
822
- };
823
-
824
- // append allowedActions to list & get response
825
- this.app.use(
826
- async (ctx, next) => {
827
- try {
828
- await withACLMeta(ctx, next);
829
- } catch (error) {
830
- ctx.logger.error(error);
831
- }
832
- },
833
- { after: 'restApi', group: 'after' },
834
- );
835
- }
836
-
837
- async install() {
838
- const repo = this.db.getRepository<any>('collections');
839
- if (repo) {
840
- await repo.db2cm('roles');
841
- }
842
- }
843
-
844
- async load() {
845
- await this.importCollections(resolve(__dirname, 'collections'));
846
- this.db.extendCollection({
847
- name: 'rolesUischemas',
848
- namespace: 'acl.acl',
849
- duplicator: 'required',
850
- });
851
- }
852
- }
853
-
854
- export default PluginACL;