@nocobase/plugin-acl 0.8.1-alpha.4 → 0.9.0-alpha.2

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 (41) hide show
  1. package/LICENSE +661 -201
  2. package/lib/actions/role-check.js +2 -0
  3. package/lib/actions/role-collections.js +39 -12
  4. package/lib/collections/roles.js +4 -0
  5. package/lib/collections/rolesResourcesActions.js +2 -1
  6. package/lib/collections/users.js +1 -0
  7. package/lib/migrations/20221214072638-set-role-snippets.d.ts +5 -0
  8. package/lib/migrations/20221214072638-set-role-snippets.js +49 -0
  9. package/lib/model/RoleModel.js +1 -0
  10. package/lib/model/RoleResourceActionModel.js +9 -3
  11. package/lib/server.js +393 -28
  12. package/package.json +7 -13
  13. package/src/__tests__/acl.test.ts +0 -548
  14. package/src/__tests__/association-field.test.ts +0 -308
  15. package/src/__tests__/configuration.test.ts +0 -74
  16. package/src/__tests__/middleware.test.ts +0 -228
  17. package/src/__tests__/own.test.ts +0 -133
  18. package/src/__tests__/prepare.ts +0 -20
  19. package/src/__tests__/role-check.test.ts +0 -41
  20. package/src/__tests__/role-resource.test.ts +0 -189
  21. package/src/__tests__/role-user.test.ts +0 -123
  22. package/src/__tests__/role.test.ts +0 -99
  23. package/src/__tests__/scope.test.ts +0 -59
  24. package/src/__tests__/setCurrentRole.test.ts +0 -83
  25. package/src/__tests__/users.test.ts +0 -52
  26. package/src/actions/available-actions.ts +0 -18
  27. package/src/actions/role-check.ts +0 -41
  28. package/src/actions/role-collections.ts +0 -65
  29. package/src/actions/user-setDefaultRole.ts +0 -45
  30. package/src/collections/roles-users.ts +0 -6
  31. package/src/collections/roles.ts +0 -79
  32. package/src/collections/rolesResources.ts +0 -31
  33. package/src/collections/rolesResourcesActions.ts +0 -28
  34. package/src/collections/rolesResourcesScopes.ts +0 -23
  35. package/src/collections/users.ts +0 -30
  36. package/src/index.ts +0 -2
  37. package/src/middlewares/setCurrentRole.ts +0 -32
  38. package/src/model/RoleModel.ts +0 -21
  39. package/src/model/RoleResourceActionModel.ts +0 -88
  40. package/src/model/RoleResourceModel.ts +0 -74
  41. package/src/server.ts +0 -463
@@ -1,74 +0,0 @@
1
- import { ACL, ACLResource, ACLRole } from '@nocobase/acl';
2
- import { Model } from '@nocobase/database';
3
- import { AssociationFieldsActions, GrantHelper } from '../server';
4
- import { RoleResourceActionModel } from './RoleResourceActionModel';
5
-
6
- export class RoleResourceModel extends Model {
7
- async revoke(options: { role: ACLRole; resourceName: string; grantHelper: GrantHelper }) {
8
- const { role, resourceName, grantHelper } = options;
9
- role.revokeResource(resourceName);
10
-
11
- const targetActions = grantHelper.resourceTargetActionMap.get(`${role.name}.${resourceName}`) || [];
12
-
13
- for (const targetAction of targetActions) {
14
- const targetActionResource = (grantHelper.targetActionResourceMap.get(targetAction) || []).filter(
15
- (item) => `${role.name}.${resourceName}` !== item,
16
- );
17
-
18
- grantHelper.targetActionResourceMap.set(targetAction, targetActionResource);
19
-
20
- if (targetActionResource.length == 0) {
21
- role.revokeAction(targetAction);
22
- }
23
- }
24
-
25
- grantHelper.resourceTargetActionMap.set(`${role.name}.${resourceName}`, []);
26
- }
27
-
28
- async writeToACL(options: {
29
- acl: ACL;
30
- associationFieldsActions: AssociationFieldsActions;
31
- grantHelper: GrantHelper;
32
- transaction: any;
33
- }) {
34
- const { acl, associationFieldsActions, grantHelper } = options;
35
- const resourceName = this.get('name') as string;
36
- const roleName = this.get('roleName') as string;
37
- const role = acl.getRole(roleName);
38
-
39
- if (!role) {
40
- console.log(`${roleName} role does not exist`);
41
- return;
42
- }
43
-
44
- // revoke resource of role
45
- await this.revoke({ role, resourceName, grantHelper });
46
-
47
- // @ts-ignore
48
- if (this.usingActionsConfig === false) {
49
- return;
50
- }
51
-
52
- const resource = new ACLResource({
53
- role,
54
- name: resourceName,
55
- });
56
-
57
- role.resources.set(resourceName, resource);
58
-
59
- // @ts-ignore
60
- const actions: RoleResourceActionModel[] = await this.getActions({
61
- transaction: options.transaction,
62
- });
63
-
64
- for (const action of actions) {
65
- await action.writeToACL({
66
- acl,
67
- role,
68
- resourceName,
69
- associationFieldsActions,
70
- grantHelper: options.grantHelper,
71
- });
72
- }
73
- }
74
- }
package/src/server.ts DELETED
@@ -1,463 +0,0 @@
1
- import { Context } from '@nocobase/actions';
2
- import { Collection } from '@nocobase/database';
3
- import { Plugin } from '@nocobase/server';
4
- import { resolve } from 'path';
5
- import { availableActionResource } from './actions/available-actions';
6
- import { checkAction } from './actions/role-check';
7
- import { roleCollectionsResource } from './actions/role-collections';
8
- import { setDefaultRole } from './actions/user-setDefaultRole';
9
- import { setCurrentRole } from './middlewares/setCurrentRole';
10
- import { RoleModel } from './model/RoleModel';
11
- import { RoleResourceActionModel } from './model/RoleResourceActionModel';
12
- import { RoleResourceModel } from './model/RoleResourceModel';
13
-
14
- export interface AssociationFieldAction {
15
- associationActions: string[];
16
- targetActions?: string[];
17
- }
18
-
19
- interface AssociationFieldActions {
20
- [availableActionName: string]: AssociationFieldAction;
21
- }
22
-
23
- export interface AssociationFieldsActions {
24
- [associationType: string]: AssociationFieldActions;
25
- }
26
-
27
- export class GrantHelper {
28
- resourceTargetActionMap = new Map<string, string[]>();
29
- targetActionResourceMap = new Map<string, string[]>();
30
-
31
- constructor() {}
32
- }
33
-
34
- export class PluginACL extends Plugin {
35
- // association field actions config
36
-
37
- associationFieldsActions: AssociationFieldsActions = {};
38
-
39
- grantHelper = new GrantHelper();
40
-
41
- get acl() {
42
- return this.app.acl;
43
- }
44
-
45
- registerAssociationFieldAction(associationType: string, value: AssociationFieldActions) {
46
- this.associationFieldsActions[associationType] = value;
47
- }
48
-
49
- registerAssociationFieldsActions() {
50
- // if grant create action to role, it should
51
- // also grant add action and association target's view action
52
- this.registerAssociationFieldAction('linkTo', {
53
- view: {
54
- associationActions: ['list', 'get'],
55
- },
56
- create: {
57
- associationActions: ['add'],
58
- targetActions: ['view'],
59
- },
60
- update: {
61
- associationActions: ['add', 'remove', 'toggle'],
62
- targetActions: ['view'],
63
- },
64
- });
65
-
66
- this.registerAssociationFieldAction('attachments', {
67
- view: {
68
- associationActions: ['list', 'get'],
69
- },
70
- add: {
71
- associationActions: ['upload', 'add'],
72
- },
73
- update: {
74
- associationActions: ['update', 'add', 'remove', 'toggle'],
75
- },
76
- });
77
-
78
- this.registerAssociationFieldAction('subTable', {
79
- view: {
80
- associationActions: ['list', 'get'],
81
- },
82
- create: {
83
- associationActions: ['create'],
84
- },
85
- update: {
86
- associationActions: ['update', 'destroy'],
87
- },
88
- });
89
- }
90
-
91
- async writeResourceToACL(resourceModel: RoleResourceModel, transaction) {
92
- await resourceModel.writeToACL({
93
- acl: this.acl,
94
- associationFieldsActions: this.associationFieldsActions,
95
- transaction: transaction,
96
- grantHelper: this.grantHelper,
97
- });
98
- }
99
-
100
- async writeActionToACL(actionModel: RoleResourceActionModel, transaction) {
101
- const resource = actionModel.get('resource') as RoleResourceModel;
102
- const role = this.acl.getRole(resource.get('roleName') as string);
103
- await actionModel.writeToACL({
104
- acl: this.acl,
105
- role,
106
- resourceName: resource.get('name') as string,
107
- associationFieldsActions: this.associationFieldsActions,
108
- grantHelper: this.grantHelper,
109
- });
110
- }
111
-
112
- async writeRolesToACL() {
113
- const roles = (await this.app.db.getRepository('roles').find({
114
- appends: ['resources', 'resources.actions'],
115
- })) as RoleModel[];
116
-
117
- for (const role of roles) {
118
- role.writeToAcl({ acl: this.acl });
119
- for (const resource of role.get('resources') as RoleResourceModel[]) {
120
- await this.writeResourceToACL(resource, null);
121
- }
122
- }
123
- }
124
-
125
- async beforeLoad() {
126
- this.app.db.registerModels({
127
- RoleResourceActionModel,
128
- RoleResourceModel,
129
- RoleModel,
130
- });
131
-
132
- this.registerAssociationFieldsActions();
133
-
134
- this.app.resourcer.define(availableActionResource);
135
- this.app.resourcer.define(roleCollectionsResource);
136
-
137
- this.app.resourcer.registerActionHandler('roles:check', checkAction);
138
-
139
- this.app.resourcer.registerActionHandler(`users:setDefaultRole`, setDefaultRole);
140
-
141
- this.db.on('users.afterCreateWithAssociations', async (model, options) => {
142
- const { transaction } = options;
143
- const repository = this.app.db.getRepository('roles');
144
- const defaultRole = await repository.findOne({
145
- filter: {
146
- default: true,
147
- },
148
- transaction,
149
- });
150
- if (defaultRole && (await model.countRoles({ transaction })) == 0) {
151
- await model.addRoles(defaultRole, { transaction });
152
- }
153
- });
154
-
155
- this.app.db.on('roles.afterSaveWithAssociations', async (model, options) => {
156
- const { transaction } = options;
157
-
158
- model.writeToAcl({
159
- acl: this.acl,
160
- });
161
-
162
- for (const resource of (await model.getResources({ transaction })) as RoleResourceModel[]) {
163
- await this.writeResourceToACL(resource, transaction);
164
- }
165
-
166
- // model is default
167
- if (model.get('default')) {
168
- await this.app.db.getRepository('roles').update({
169
- values: {
170
- default: false,
171
- },
172
- filter: {
173
- 'name.$ne': model.get('name'),
174
- },
175
- hooks: false,
176
- transaction,
177
- });
178
- }
179
- });
180
-
181
- this.app.db.on('roles.afterDestroy', (model) => {
182
- const roleName = model.get('name');
183
- this.acl.removeRole(roleName);
184
- });
185
-
186
- this.app.db.on('rolesResources.afterSaveWithAssociations', async (model: RoleResourceModel, options) => {
187
- await this.writeResourceToACL(model, options.transaction);
188
- });
189
-
190
- this.app.db.on('rolesResourcesActions.afterUpdateWithAssociations', async (model, options) => {
191
- const { transaction } = options;
192
- const resource = await model.getResource({
193
- transaction,
194
- });
195
-
196
- await this.writeResourceToACL(resource, transaction);
197
- });
198
-
199
- this.app.db.on('rolesResources.afterDestroy', async (model, options) => {
200
- const role = this.acl.getRole(model.get('roleName'));
201
- if (role) {
202
- role.revokeResource(model.get('name'));
203
- }
204
- });
205
-
206
- this.app.db.on('collections.afterDestroy', async (model, options) => {
207
- const { transaction } = options;
208
- await this.app.db.getRepository('rolesResources').destroy({
209
- filter: {
210
- name: model.get('name'),
211
- },
212
- transaction,
213
- });
214
- });
215
-
216
- this.app.db.on('fields.afterCreate', async (model, options) => {
217
- const { transaction } = options;
218
-
219
- const collectionName = model.get('collectionName');
220
- const fieldName = model.get('name');
221
-
222
- const resourceActions = (await this.app.db.getRepository('rolesResourcesActions').find({
223
- filter: {
224
- 'resource.name': collectionName,
225
- },
226
- transaction,
227
- appends: ['resource'],
228
- })) as RoleResourceActionModel[];
229
-
230
- for (const resourceAction of resourceActions) {
231
- const fields = resourceAction.get('fields') as string[];
232
- const newFields = [...fields, fieldName];
233
-
234
- await this.app.db.getRepository('rolesResourcesActions').update({
235
- filterByTk: resourceAction.get('id') as number,
236
- values: {
237
- fields: newFields,
238
- },
239
- transaction,
240
- });
241
- }
242
- });
243
-
244
- this.app.db.on('fields.afterDestroy', async (model, options) => {
245
- const collectionName = model.get('collectionName');
246
- const fieldName = model.get('name');
247
-
248
- const resourceActions = await this.app.db.getRepository('rolesResourcesActions').find({
249
- filter: {
250
- 'resource.name': collectionName,
251
- 'fields.$anyOf': [fieldName],
252
- },
253
- transaction: options.transaction,
254
- });
255
-
256
- for (const resourceAction of resourceActions) {
257
- const fields = resourceAction.get('fields') as string[];
258
- const newFields = fields.filter((field) => field != fieldName);
259
-
260
- await this.app.db.getRepository('rolesResourcesActions').update({
261
- filterByTk: resourceAction.get('id') as number,
262
- values: {
263
- fields: newFields,
264
- },
265
- transaction: options.transaction,
266
- });
267
- }
268
- });
269
-
270
- // sync database role data to acl
271
- this.app.on('afterLoad', async (app, options) => {
272
- if (options?.method === 'install') {
273
- return;
274
- }
275
- const exists = await this.app.db.collectionExistsInDb('roles');
276
- if (exists) {
277
- await this.writeRolesToACL();
278
- }
279
- });
280
-
281
- this.app.on('afterInstall', async (app, options) => {
282
- const exists = await this.app.db.collectionExistsInDb('roles');
283
- if (exists) {
284
- await this.writeRolesToACL();
285
- }
286
- });
287
-
288
- this.app.on('afterInstallPlugin', async (plugin) => {
289
- if (plugin.getName() !== 'users') {
290
- return;
291
- }
292
- const User = this.db.getCollection('users');
293
- await User.repository.update({
294
- values: {
295
- roles: ['root', 'admin', 'member'],
296
- },
297
- forceUpdate: true,
298
- });
299
-
300
- const RolesUsers = this.db.getCollection('rolesUsers');
301
- await RolesUsers.repository.update({
302
- filter: {
303
- userId: 1,
304
- roleName: 'root',
305
- },
306
- values: {
307
- default: true,
308
- },
309
- });
310
- });
311
-
312
- this.app.on('beforeInstallPlugin', async (plugin) => {
313
- if (plugin.getName() !== 'users') {
314
- return;
315
- }
316
- const roles = this.app.db.getRepository('roles');
317
- await roles.createMany({
318
- records: [
319
- {
320
- name: 'root',
321
- title: '{{t("Root")}}',
322
- hidden: true,
323
- },
324
- {
325
- name: 'admin',
326
- title: '{{t("Admin")}}',
327
- allowConfigure: true,
328
- allowNewMenu: true,
329
- strategy: { actions: ['create', 'view', 'update', 'destroy'] },
330
- },
331
- {
332
- name: 'member',
333
- title: '{{t("Member")}}',
334
- allowNewMenu: true,
335
- strategy: { actions: ['view', 'update:own', 'destroy:own', 'create'] },
336
- default: true,
337
- },
338
- ],
339
- });
340
- const rolesResourcesScopes = this.app.db.getRepository('rolesResourcesScopes');
341
- await rolesResourcesScopes.createMany({
342
- records: [
343
- {
344
- key: 'all',
345
- name: '{{t("All records")}}',
346
- scope: {},
347
- },
348
- {
349
- key: 'own',
350
- name: '{{t("Own records")}}',
351
- scope: {
352
- createdById: '{{ ctx.state.currentUser.id }}',
353
- },
354
- },
355
- ],
356
- });
357
- });
358
-
359
- this.app.resourcer.use(setCurrentRole, { tag: 'setCurrentRole', before: 'acl', after: 'parseToken' });
360
-
361
- this.app.acl.allow('users', 'setDefaultRole', 'loggedIn');
362
-
363
- this.app.acl.allow('roles', 'check', 'loggedIn');
364
- this.app.acl.allow('roles', ['create', 'update', 'destroy'], 'allowConfigure');
365
-
366
- this.app.acl.allow('roles.menuUiSchemas', ['set', 'toggle', 'list'], 'allowConfigure');
367
-
368
- this.app.acl.allow('*', '*', (ctx) => {
369
- return ctx.state.currentRole === 'root';
370
- });
371
-
372
- this.app.resourcer.use(async (ctx, next) => {
373
- const { actionName, resourceName, params } = ctx.action;
374
- const { showAnonymous } = params || {};
375
- if (actionName === 'list' && resourceName === 'roles') {
376
- if (!showAnonymous) {
377
- ctx.action.mergeParams({
378
- filter: {
379
- 'name.$ne': 'anonymous',
380
- },
381
- });
382
- }
383
- }
384
- if (actionName === 'update' && resourceName === 'roles.resources') {
385
- ctx.action.mergeParams({
386
- updateAssociationValues: ['actions'],
387
- });
388
- }
389
- await next();
390
- });
391
-
392
- this.app.acl.use(async (ctx: Context, next) => {
393
- const { actionName, resourceName } = ctx.action;
394
- if (actionName === 'get' || actionName === 'list') {
395
- if (!Array.isArray(ctx?.permission?.can?.params?.fields)) {
396
- return next();
397
- }
398
- let collection: Collection;
399
- if (resourceName.includes('.')) {
400
- const [collectionName, associationName] = resourceName.split('.');
401
- const field = ctx.db.getCollection(collectionName)?.getField?.(associationName);
402
- if (field.target) {
403
- collection = ctx.db.getCollection(field.target);
404
- }
405
- } else {
406
- collection = ctx.db.getCollection(resourceName);
407
- }
408
- if (collection && collection.hasField('createdById')) {
409
- ctx.permission.can.params.fields.push('createdById');
410
- }
411
- }
412
- return next();
413
- });
414
-
415
- const parseJsonTemplate = this.app.acl.parseJsonTemplate;
416
- this.app.acl.use(async (ctx: Context, next) => {
417
- const { actionName, resourceName, resourceOf } = ctx.action;
418
- if (resourceName.includes('.') && resourceOf) {
419
- if (!ctx?.permission?.can?.params) {
420
- return next();
421
- }
422
- // 关联数据去掉 filter
423
- delete ctx.permission.can.params.filter;
424
- // 关联数据能不能处理取决于 source 是否有权限
425
- const [collectionName] = resourceName.split('.');
426
- const action = ctx.can({ resource: collectionName, action: actionName });
427
- const availableAction = this.app.acl.getAvailableAction(actionName);
428
- if (availableAction?.options?.onNewRecord) {
429
- if (action) {
430
- ctx.permission.skip = true;
431
- } else {
432
- ctx.permission.can = false;
433
- }
434
- } else {
435
- const filter = parseJsonTemplate(action?.params?.filter || {}, ctx);
436
- const sourceInstance = await ctx.db.getRepository(collectionName).findOne({
437
- filterByTk: resourceOf,
438
- filter,
439
- });
440
- if (!sourceInstance) {
441
- ctx.permission.can = false;
442
- }
443
- }
444
- }
445
- await next();
446
- });
447
- }
448
-
449
- async install() {
450
- const repo = this.db.getRepository<any>('collections');
451
- if (repo) {
452
- await repo.db2cm('roles');
453
- }
454
- }
455
-
456
- async load() {
457
- await this.app.db.import({
458
- directory: resolve(__dirname, 'collections'),
459
- });
460
- }
461
- }
462
-
463
- export default PluginACL;