feat: acl optimization (#1136)
* fix: sort field with table dose not have primary key * feat: fixed params merger * chore(plugins/acl): fixed params * chore(plugins/acl): allowConfigure of collections * chore(plugins/china-region): disable actions other than list * chore(plugins/collection-manager): allowConfigure permission * chore(plugins/file-manager): acl fixed params * chore: acl fixed params * chore: rolesResourcesScopes onDelete cascade * fix: install error * chore: test * fix: root user fixed params * fix: role resource scope onDelete * chore: test * chore: test * fix: acl * chore: disable index.html cache * chore: disable index.html cache * test: destory user role * test: destory throught table * fix: test * fix: test * chore: add rolesUsers to fixed params * feat: permission logging * feat: permission logging * fix: test * fix: test * chore: disable grant target action * fix: appends with fields * fix: get action params * fix: associationActions * chore: change AssociationField using relation type * chore: typo * refactor: allow to skip * fix: prettier * chore: attachments association action * fix: allowConfigure condition * fix: deprecated allow * fix: please use skip instead * feat: table column aclcheck * chore: test * feat: throw error when detory no permission record * chore: test * chore: acl test * feat: field acl * chore: after action middleware * fix: destory permission check * chore: middleware use * fix: test * feat: filter match * feat: subform/subtable field acl check * feat: action permision by scope * feat: action permision by scope * feat: list action with allowedActions * chore: all allowed action * fix: pk error * fix: merge error * fix: create query sql * fix: skip permission * fix: scope with association field * feat: action acl fix * feat: action acl fix * fix: update submodule * Feat: setting center permission (#1214) * feat: add setting center permissions * feat: setting center permissions backlist * feat: setting center permissions BLACKLIST * feat: setting center permissions blacklist * feat: setting center permissions blacklist * feat: setting center permission * feat: configure plugin tab expand Co-authored-by: chenos <chenlinxh@gmail.com> * Feat :field acl (#1211) Co-authored-by: chenos <chenlinxh@gmail.com> * fix: build error * test: acl snippet * feat: set field * fix: test * fix: build error * fix: utils Dependency cycles * feat: general permissions * feat: delete pluginTabBlacklist * fix: test * feat: snippetManager allow method * feat: acl role snippetAllowed method * feat: array field repository * feat: ArrayFieldRepository * fix: test * fix: ci * fix: ci error * fix: add set parse * test: array field repository * chore: addSnippetPatten * fix: start * feat: sync role snippets * feat: snippets check * feat: snippets check * chore: acl role snippet api * fix: test * fix: test * refactor: acl role snippets * chore: registerACLSettingSnippet * chore: default snippets * feat: snippets match * feat: snippets check * feat: snippets check * feat: pm permision check * feat: pm permision check * feat: snippet pattern match * feat: pluginManagerToolbar check * feat: pluginManagerToolbar check * chore: snippets default value * feat: set role snippets migration * chore: snippets * feat: acl local * feat: acl local * feat: bookmask fix * feat: plugin-manger & ui-editor snippet * feat: set allowConfigure to false when upgrade to snippets * feat: destory action acl fix * feat: destory action acl fix * fix: association resource params merge * fix: ui editor snippet * feat: action acl fix * chore: move list meta middleware into plugins/acl * fix: test * feat: action acl fix * feat: action acl check fix * feat: plugins toolbar fix * feat: gitmodules * fix: subproject * chore: add avaiableActions to snippet * chore: change plugin-manager snippet * feat: configure action acl fix * feat: plugin tab acl check fix * chore: roles snippets * fix: add actions to snippet * feat: allowconfigure fix * fix: count with filterBy * fix: build error * feat: get action with allowedActions * feat: acl route check fix * feat: aclActionProvider fix * feat: actionscpe fix * feat: actionname alias * feat: setting center fix * feat: acl provider fix * fix: role collection * feat: associate resource acl * feat: associate resource acl * feat: redirect to 403 * feat: route redirct * feat: acl scope check by record * fix: fields appends fix * fix: fields appends fix * fix: fields appends fix * fix: allowedActions fix * fix: menu items * fix: rename * fix: improve code * fix: improve code * fix: improve code * fix: ctx?.data?.data * fix: styling * fix: allowAll after ignore scope * chore: allowConfigure condition * fix: collections.fields:* * fix: acl test * fix: update submodule * fix: acl test * fix: acl snippet * fix: updates * fix: only load history for logged-in users * fix: this.app.acl.registerSnippet * fix: downloadXlsxTemplate * fix: 404 * feat: allowedAction in association list response * fix: listData get * fix: test * fix: x-collection-field * fix: update record error * fix: calendar template * test: allow manager * fix: fetch action step * fix: update submodule * fix: refresh * fix: refresh * fix: rolesResourcesScopes * test: snippets * fix: snippets * fix: test * fix: omit filter.createdById * fix: improve code * fix: collections path * fix: test error * fix: upgrade error * fix: errors * fix: read allowed actions error * fix: kanban error * fix: error Co-authored-by: chenos <chenlinxh@gmail.com> Co-authored-by: katherinehhh <katherine_15995@163.com>
This commit is contained in:
		
							parent
							
								
									668f96fb25
								
							
						
					
					
						commit
						a614bc7de8
					
				@ -13,7 +13,9 @@
 | 
				
			|||||||
  "types": "./lib/index.d.ts",
 | 
					  "types": "./lib/index.d.ts",
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@nocobase/resourcer": "0.8.1-alpha.4",
 | 
					    "@nocobase/resourcer": "0.8.1-alpha.4",
 | 
				
			||||||
    "json-templates": "^4.2.0"
 | 
					    "@nocobase/utils": "0.8.1-alpha.4",
 | 
				
			||||||
 | 
					    "json-templates": "^4.2.0",
 | 
				
			||||||
 | 
					    "minimatch": "^5.1.1"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "repository": {
 | 
					  "repository": {
 | 
				
			||||||
    "type": "git",
 | 
					    "type": "git",
 | 
				
			||||||
 | 
				
			|||||||
@ -354,32 +354,4 @@ describe('acl', () => {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					 | 
				
			||||||
  it('should allow system config', () => {
 | 
					 | 
				
			||||||
    acl.setAvailableAction('create', {
 | 
					 | 
				
			||||||
      displayName: 'create',
 | 
					 | 
				
			||||||
      type: 'new-data',
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    acl.registerConfigResources(['roles']);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const role = acl.define({
 | 
					 | 
				
			||||||
      role: 'admin',
 | 
					 | 
				
			||||||
      strategy: {
 | 
					 | 
				
			||||||
        allowConfigure: true,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    expect(
 | 
					 | 
				
			||||||
      acl.can({
 | 
					 | 
				
			||||||
        role: 'admin',
 | 
					 | 
				
			||||||
        resource: 'roles',
 | 
					 | 
				
			||||||
        action: 'create',
 | 
					 | 
				
			||||||
      }),
 | 
					 | 
				
			||||||
    ).toMatchObject({
 | 
					 | 
				
			||||||
      role: 'admin',
 | 
					 | 
				
			||||||
      resource: 'roles',
 | 
					 | 
				
			||||||
      action: 'create',
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										19
									
								
								packages/core/acl/src/__tests__/allow-manager.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								packages/core/acl/src/__tests__/allow-manager.test.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					import { ACL } from '..';
 | 
				
			||||||
 | 
					import { AllowManager } from '../allow-manager';
 | 
				
			||||||
 | 
					describe('allow manager', () => {
 | 
				
			||||||
 | 
					  let acl: ACL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    acl = new ACL();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should allow star resource', async () => {
 | 
				
			||||||
 | 
					    const allowManager = new AllowManager(acl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    allowManager.allow('*', 'download', 'public');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(await allowManager.isAllowed('users', 'download', {})).toBeTruthy();
 | 
				
			||||||
 | 
					    expect(await allowManager.isAllowed('users', 'fake-method', {})).toBeFalsy();
 | 
				
			||||||
 | 
					    expect(await allowManager.isAllowed('users', 'other-method', {})).toBeFalsy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -40,6 +40,9 @@ describe('skip', () => {
 | 
				
			|||||||
        resourceName: 'users',
 | 
					        resourceName: 'users',
 | 
				
			||||||
        actionName: 'login',
 | 
					        actionName: 'login',
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      log: {
 | 
				
			||||||
 | 
					        info() {},
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      app: {
 | 
					      app: {
 | 
				
			||||||
        acl,
 | 
					        acl,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@ -49,6 +52,7 @@ describe('skip', () => {
 | 
				
			|||||||
    const nextFunc = jest.fn();
 | 
					    const nextFunc = jest.fn();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let skip = false;
 | 
					    let skip = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    acl.allow('users', 'login', (ctx) => {
 | 
					    acl.allow('users', 'login', (ctx) => {
 | 
				
			||||||
      return skip;
 | 
					      return skip;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -76,6 +80,9 @@ describe('skip', () => {
 | 
				
			|||||||
        resourceName: 'users',
 | 
					        resourceName: 'users',
 | 
				
			||||||
        actionName: 'login',
 | 
					        actionName: 'login',
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      log: {
 | 
				
			||||||
 | 
					        info() {},
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      app: {
 | 
					      app: {
 | 
				
			||||||
        acl,
 | 
					        acl,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										82
									
								
								packages/core/acl/src/__tests__/fixed-params.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								packages/core/acl/src/__tests__/fixed-params.test.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					import { ACL } from '../acl';
 | 
				
			||||||
 | 
					import FixedParamsManager from '../fixed-params-manager';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('fixed params', () => {
 | 
				
			||||||
 | 
					  it('should merge params', async () => {
 | 
				
			||||||
 | 
					    const fixedParamsManager = new FixedParamsManager();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fixedParamsManager.addParams('collections', 'destroy', () => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        filter: {
 | 
				
			||||||
 | 
					          'name.$ne': 'users',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fixedParamsManager.addParams('collections', 'destroy', () => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        filter: {
 | 
				
			||||||
 | 
					          'name.$ne': 'roles',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const params = fixedParamsManager.getParams('collections', 'destroy');
 | 
				
			||||||
 | 
					    expect(params).toEqual({
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        $and: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            'name.$ne': 'users',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            'name.$ne': 'roles',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should add fixed params to acl action', async () => {
 | 
				
			||||||
 | 
					    const acl = new ACL();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const adminRole = acl.define({
 | 
				
			||||||
 | 
					      role: 'admin',
 | 
				
			||||||
 | 
					      actions: {
 | 
				
			||||||
 | 
					        'collections:destroy': {},
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let canResult = acl.can({
 | 
				
			||||||
 | 
					      role: 'admin',
 | 
				
			||||||
 | 
					      resource: 'collections',
 | 
				
			||||||
 | 
					      action: 'destroy',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(canResult).toEqual({ role: 'admin', resource: 'collections', action: 'destroy' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    acl.addFixedParams('collections', 'destroy', () => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        filter: {
 | 
				
			||||||
 | 
					          'name.$ne': 'users',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    canResult = acl.can({
 | 
				
			||||||
 | 
					      role: 'admin',
 | 
				
			||||||
 | 
					      resource: 'collections',
 | 
				
			||||||
 | 
					      action: 'destroy',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(canResult).toEqual({
 | 
				
			||||||
 | 
					      role: 'admin',
 | 
				
			||||||
 | 
					      resource: 'collections',
 | 
				
			||||||
 | 
					      action: 'destroy',
 | 
				
			||||||
 | 
					      params: {
 | 
				
			||||||
 | 
					        filter: {
 | 
				
			||||||
 | 
					          'name.$ne': 'users',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										130
									
								
								packages/core/acl/src/__tests__/snippet.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								packages/core/acl/src/__tests__/snippet.test.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,130 @@
 | 
				
			|||||||
 | 
					import { ACL } from '..';
 | 
				
			||||||
 | 
					import SnippetManager from '../snippet-manager';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('acl snippet', () => {
 | 
				
			||||||
 | 
					  let acl: ACL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    acl = new ACL();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should get effective snipptes', () => {
 | 
				
			||||||
 | 
					    acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: 'sc.collection-manager.fields',
 | 
				
			||||||
 | 
					      actions: ['fields:list'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: 'sc.collection-manager.gi',
 | 
				
			||||||
 | 
					      actions: ['fields:list', 'gi:list'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: 'sc.collection-manager.collections',
 | 
				
			||||||
 | 
					      actions: ['collections:list'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const adminRole = acl.define({
 | 
				
			||||||
 | 
					      role: 'admin',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    adminRole.snippets.add('sc.*');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(adminRole.effectiveSnippets().allowed).toEqual([
 | 
				
			||||||
 | 
					      'sc.collection-manager.fields',
 | 
				
			||||||
 | 
					      'sc.collection-manager.gi',
 | 
				
			||||||
 | 
					      'sc.collection-manager.collections',
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    adminRole.snippets.add('!sc.collection-manager.gi');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(adminRole.effectiveSnippets().allowed).toEqual([
 | 
				
			||||||
 | 
					      'sc.collection-manager.fields',
 | 
				
			||||||
 | 
					      'sc.collection-manager.collections',
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should register snippet', () => {
 | 
				
			||||||
 | 
					    acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: 'sc.collection-manager.fields',
 | 
				
			||||||
 | 
					      actions: ['fields:list'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: 'sc.collection-manager.gi',
 | 
				
			||||||
 | 
					      actions: ['fields:list', 'gi:list'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: 'sc.collection-manager.collections',
 | 
				
			||||||
 | 
					      actions: ['collections:list'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const adminRole = acl.define({
 | 
				
			||||||
 | 
					      role: 'admin',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    adminRole.snippets.add('sc.*');
 | 
				
			||||||
 | 
					    expect(acl.can({ role: 'admin', resource: 'collections', action: 'list' })).not.toBeNull();
 | 
				
			||||||
 | 
					    expect(adminRole.snippetAllowed('collections:list')).toBe(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    adminRole.snippets.add('!sc.collection-manager.gi');
 | 
				
			||||||
 | 
					    expect(acl.can({ role: 'admin', resource: 'gi', action: 'list' })).toBeNull();
 | 
				
			||||||
 | 
					    expect(adminRole.snippetAllowed('gi:list')).toBe(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(acl.can({ role: 'admin', resource: 'fields', action: 'list' })).not.toBeNull();
 | 
				
			||||||
 | 
					    expect(adminRole.snippetAllowed('fields:list')).toBe(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(adminRole.snippetAllowed('other:list')).toBeNull();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('snippet manager', () => {
 | 
				
			||||||
 | 
					  describe('allow method', () => {
 | 
				
			||||||
 | 
					    it('should return true when allowed', () => {
 | 
				
			||||||
 | 
					      const snippetManager = new SnippetManager();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      snippetManager.register({
 | 
				
			||||||
 | 
					        name: 'sc.collection-manager.fields',
 | 
				
			||||||
 | 
					        actions: ['collections:list'],
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(snippetManager.allow('collections:list', 'sc.collection-manager.fields')).toBe(true);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should return true when allowed by wild match', () => {
 | 
				
			||||||
 | 
					      const snippetManager = new SnippetManager();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      snippetManager.register({
 | 
				
			||||||
 | 
					        name: 'sc.collection-manager.fields',
 | 
				
			||||||
 | 
					        actions: ['collections:*'],
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(snippetManager.allow('collections:list', 'sc.collection-manager.fields')).toBe(true);
 | 
				
			||||||
 | 
					      expect(snippetManager.allow('collections:destroy', 'sc.collection-manager.fields')).toBe(true);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should return false when match negated rule', () => {
 | 
				
			||||||
 | 
					      const snippetManager = new SnippetManager();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      snippetManager.register({
 | 
				
			||||||
 | 
					        name: 'sc.collection-manager.fields',
 | 
				
			||||||
 | 
					        actions: ['collections:*'],
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(snippetManager.allow('collections:list', 'sc.collection-manager.fields')).toBe(true);
 | 
				
			||||||
 | 
					      expect(snippetManager.allow('collections:destroy', '!sc.collection-manager.fields')).toBe(false);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should return null when not matched', () => {
 | 
				
			||||||
 | 
					      const snippetManager = new SnippetManager();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      snippetManager.register({
 | 
				
			||||||
 | 
					        name: 'sc.collection-manager.fields',
 | 
				
			||||||
 | 
					        actions: ['collections:*'],
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(snippetManager.allow('fields:list', 'sc.collection-manager.fields')).toBeNull();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -52,7 +52,7 @@ export class ACLAvailableStrategy {
 | 
				
			|||||||
    if (this.actionsAsObject?.hasOwnProperty(actionName)) {
 | 
					    if (this.actionsAsObject?.hasOwnProperty(actionName)) {
 | 
				
			||||||
      const predicateName = this.actionsAsObject[actionName];
 | 
					      const predicateName = this.actionsAsObject[actionName];
 | 
				
			||||||
      if (predicateName) {
 | 
					      if (predicateName) {
 | 
				
			||||||
        return predicate[predicateName];
 | 
					        return lodash.cloneDeep(predicate[predicateName]);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
@ -62,10 +62,6 @@ export class ACLAvailableStrategy {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  allow(resourceName: string, actionName: string) {
 | 
					  allow(resourceName: string, actionName: string) {
 | 
				
			||||||
    if (this.acl.isConfigResource(resourceName) && this.allowConfigure) {
 | 
					 | 
				
			||||||
      return true;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return this.matchAction(this.acl.resolveActionAlias(actionName));
 | 
					    return this.matchAction(this.acl.resolveActionAlias(actionName));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,8 @@
 | 
				
			|||||||
import { ACL, DefineOptions } from './acl';
 | 
					import { ACL, DefineOptions } from './acl';
 | 
				
			||||||
import { AvailableStrategyOptions } from './acl-available-strategy';
 | 
					import { ACLAvailableStrategy, AvailableStrategyOptions } from './acl-available-strategy';
 | 
				
			||||||
import { ACLResource } from './acl-resource';
 | 
					import { ACLResource } from './acl-resource';
 | 
				
			||||||
 | 
					import lodash from 'lodash';
 | 
				
			||||||
 | 
					import minimatch from 'minimatch';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface RoleActionParams {
 | 
					export interface RoleActionParams {
 | 
				
			||||||
  fields?: string[];
 | 
					  fields?: string[];
 | 
				
			||||||
@ -18,6 +20,7 @@ export interface ResourceActionsOptions {
 | 
				
			|||||||
export class ACLRole {
 | 
					export class ACLRole {
 | 
				
			||||||
  strategy: string | AvailableStrategyOptions;
 | 
					  strategy: string | AvailableStrategyOptions;
 | 
				
			||||||
  resources = new Map<string, ACLResource>();
 | 
					  resources = new Map<string, ACLResource>();
 | 
				
			||||||
 | 
					  snippets: Set<string> = new Set();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(public acl: ACL, public name: string) {}
 | 
					  constructor(public acl: ACL, public name: string) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -29,6 +32,16 @@ export class ACLRole {
 | 
				
			|||||||
    this.strategy = value;
 | 
					    this.strategy = value;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public getStrategy() {
 | 
				
			||||||
 | 
					    if (!this.strategy) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return lodash.isString(this.strategy)
 | 
				
			||||||
 | 
					      ? this.acl.availableStrategy.get(this.strategy)
 | 
				
			||||||
 | 
					      : new ACLAvailableStrategy(this.acl, this.strategy);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public getResourceActionsParams(resourceName: string) {
 | 
					  public getResourceActionsParams(resourceName: string) {
 | 
				
			||||||
    const resource = this.getResource(resourceName);
 | 
					    const resource = this.getResource(resourceName);
 | 
				
			||||||
    return resource.getActions();
 | 
					    return resource.getActions();
 | 
				
			||||||
@ -67,6 +80,64 @@ export class ACLRole {
 | 
				
			|||||||
    resource.removeAction(actionName);
 | 
					    resource.removeAction(actionName);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public effectiveSnippets(): { allowed: Array<string>; rejected: Array<string> } {
 | 
				
			||||||
 | 
					    const allowedSnippets = new Set<string>();
 | 
				
			||||||
 | 
					    const rejectedSnippets = new Set<string>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const availableSnippets = this.acl.snippetManager.snippets;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (let snippetRule of this.snippets) {
 | 
				
			||||||
 | 
					      const negated = snippetRule.startsWith('!');
 | 
				
			||||||
 | 
					      snippetRule = negated ? snippetRule.slice(1) : snippetRule;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      for (const [_, availableSnippet] of availableSnippets) {
 | 
				
			||||||
 | 
					        if (minimatch(availableSnippet.name, snippetRule)) {
 | 
				
			||||||
 | 
					          if (negated) {
 | 
				
			||||||
 | 
					            rejectedSnippets.add(availableSnippet.name);
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            allowedSnippets.add(availableSnippet.name);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // get difference of allowed and rejected snippets
 | 
				
			||||||
 | 
					    const effectiveSnippets = new Set([...allowedSnippets].filter((x) => !rejectedSnippets.has(x)));
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      allowed: [...effectiveSnippets],
 | 
				
			||||||
 | 
					      rejected: [...rejectedSnippets],
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public snippetAllowed(actionPath: string) {
 | 
				
			||||||
 | 
					    const effectiveSnippets = this.effectiveSnippets();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getActions = (snippets) => {
 | 
				
			||||||
 | 
					      return snippets.map((snippetName) => this.acl.snippetManager.snippets.get(snippetName).actions).flat();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const allowedActions = getActions(effectiveSnippets.allowed);
 | 
				
			||||||
 | 
					    const rejectedActions = getActions(effectiveSnippets.rejected);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const actionMatched = (actionPath, actionRule) => {
 | 
				
			||||||
 | 
					      return minimatch(actionPath, actionRule);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const action of allowedActions) {
 | 
				
			||||||
 | 
					      if (actionMatched(actionPath, action)) {
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const action of rejectedActions) {
 | 
				
			||||||
 | 
					      if (actionMatched(actionPath, action)) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public toJSON(): DefineOptions {
 | 
					  public toJSON(): DefineOptions {
 | 
				
			||||||
    const actions = {};
 | 
					    const actions = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -81,6 +152,7 @@ export class ACLRole {
 | 
				
			|||||||
      role: this.name,
 | 
					      role: this.name,
 | 
				
			||||||
      strategy: this.strategy,
 | 
					      strategy: this.strategy,
 | 
				
			||||||
      actions,
 | 
					      actions,
 | 
				
			||||||
 | 
					      snippets: Array.from(this.snippets),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import { Action } from '@nocobase/resourcer';
 | 
					import { Action } from '@nocobase/resourcer';
 | 
				
			||||||
import { Toposort, ToposortOptions } from '@nocobase/utils';
 | 
					import { assign, Toposort, ToposortOptions } from '@nocobase/utils';
 | 
				
			||||||
import EventEmitter from 'events';
 | 
					import EventEmitter from 'events';
 | 
				
			||||||
import parse from 'json-templates';
 | 
					import parse from 'json-templates';
 | 
				
			||||||
import compose from 'koa-compose';
 | 
					import compose from 'koa-compose';
 | 
				
			||||||
@ -8,6 +8,8 @@ import { ACLAvailableAction, AvailableActionOptions } from './acl-available-acti
 | 
				
			|||||||
import { ACLAvailableStrategy, AvailableStrategyOptions, predicate } from './acl-available-strategy';
 | 
					import { ACLAvailableStrategy, AvailableStrategyOptions, predicate } from './acl-available-strategy';
 | 
				
			||||||
import { ACLRole, ResourceActionsOptions, RoleActionParams } from './acl-role';
 | 
					import { ACLRole, ResourceActionsOptions, RoleActionParams } from './acl-role';
 | 
				
			||||||
import { AllowManager, ConditionFunc } from './allow-manager';
 | 
					import { AllowManager, ConditionFunc } from './allow-manager';
 | 
				
			||||||
 | 
					import FixedParamsManager, { Merger } from './fixed-params-manager';
 | 
				
			||||||
 | 
					import SnippetManager, { SnippetOptions } from './snippet-manager';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface CanResult {
 | 
					interface CanResult {
 | 
				
			||||||
  role: string;
 | 
					  role: string;
 | 
				
			||||||
@ -22,6 +24,7 @@ export interface DefineOptions {
 | 
				
			|||||||
  strategy?: string | AvailableStrategyOptions;
 | 
					  strategy?: string | AvailableStrategyOptions;
 | 
				
			||||||
  actions?: ResourceActionsOptions;
 | 
					  actions?: ResourceActionsOptions;
 | 
				
			||||||
  routes?: any;
 | 
					  routes?: any;
 | 
				
			||||||
 | 
					  snippets?: string[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ListenerContext {
 | 
					export interface ListenerContext {
 | 
				
			||||||
@ -39,14 +42,18 @@ interface CanArgs {
 | 
				
			|||||||
  role: string;
 | 
					  role: string;
 | 
				
			||||||
  resource: string;
 | 
					  resource: string;
 | 
				
			||||||
  action: string;
 | 
					  action: string;
 | 
				
			||||||
 | 
					  ctx?: any;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ACL extends EventEmitter {
 | 
					export class ACL extends EventEmitter {
 | 
				
			||||||
  protected availableActions = new Map<string, ACLAvailableAction>();
 | 
					  protected availableActions = new Map<string, ACLAvailableAction>();
 | 
				
			||||||
  protected availableStrategy = new Map<string, ACLAvailableStrategy>();
 | 
					  public availableStrategy = new Map<string, ACLAvailableStrategy>();
 | 
				
			||||||
 | 
					  protected fixedParamsManager = new FixedParamsManager();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected middlewares: Toposort<any>;
 | 
					  protected middlewares: Toposort<any>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public allowManager = new AllowManager(this);
 | 
					  public allowManager = new AllowManager(this);
 | 
				
			||||||
 | 
					  public snippetManager = new SnippetManager();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  roles = new Map<string, ACLRole>();
 | 
					  roles = new Map<string, ACLRole>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -86,7 +93,66 @@ export class ACL extends EventEmitter {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.middlewares.add(this.allowManager.aclMiddleware());
 | 
					    this.use(this.allowManager.aclMiddleware(), {
 | 
				
			||||||
 | 
					      tag: 'allow-manager',
 | 
				
			||||||
 | 
					      before: 'core',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.addCoreMiddleware();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected addCoreMiddleware() {
 | 
				
			||||||
 | 
					    const acl = this;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const filterParams = (ctx, resourceName, params) => {
 | 
				
			||||||
 | 
					      if (params?.filter?.createdById) {
 | 
				
			||||||
 | 
					        const collection = ctx.db.getCollection(resourceName);
 | 
				
			||||||
 | 
					        if (!collection || !collection.getField('createdById')) {
 | 
				
			||||||
 | 
					          return lodash.omit(params, 'filter.createdById');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return params;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.middlewares.add(
 | 
				
			||||||
 | 
					      async (ctx, next) => {
 | 
				
			||||||
 | 
					        const resourcerAction: Action = ctx.action;
 | 
				
			||||||
 | 
					        const { resourceName, actionName } = ctx.action;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const permission = ctx.permission;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ctx.log?.info && ctx.log.info('ctx permission', permission);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ((!permission.can || typeof permission.can !== 'object') && !permission.skip) {
 | 
				
			||||||
 | 
					          ctx.throw(403, 'No permissions');
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const params = permission.can?.params || acl.fixedParamsManager.getParams(resourceName, actionName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ctx.log?.info && ctx.log.info('acl params', params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (params && resourcerAction.mergeParams) {
 | 
				
			||||||
 | 
					          const filteredParams = filterParams(ctx, resourceName, params);
 | 
				
			||||||
 | 
					          const parsedParams = acl.parseJsonTemplate(filteredParams, ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          ctx.permission.parsedParams = parsedParams;
 | 
				
			||||||
 | 
					          ctx.log?.info && ctx.log.info('acl parsedParams', parsedParams);
 | 
				
			||||||
 | 
					          ctx.permission.rawParams = lodash.cloneDeep(resourcerAction.params);
 | 
				
			||||||
 | 
					          resourcerAction.mergeParams(parsedParams, {
 | 
				
			||||||
 | 
					            appends: (x, y) => lodash.intersection(x, y),
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          ctx.permission.mergedParams = lodash.cloneDeep(resourcerAction.params);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await next();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        tag: 'core',
 | 
				
			||||||
 | 
					        group: 'core',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  define(options: DefineOptions): ACLRole {
 | 
					  define(options: DefineOptions): ACLRole {
 | 
				
			||||||
@ -164,6 +230,28 @@ export class ACL extends EventEmitter {
 | 
				
			|||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const snippetAllowed = aclRole.snippetAllowed(`${resource}:${action}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (snippetAllowed === false) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const fixedParams = this.fixedParamsManager.getParams(resource, action);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const mergeParams = (result: CanResult) => {
 | 
				
			||||||
 | 
					      const params = result['params'] || {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const mergedParams = assign(params, fixedParams);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (Object.keys(mergedParams).length) {
 | 
				
			||||||
 | 
					        result['params'] = mergedParams;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        delete result['params'];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return result;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const aclResource = aclRole.getResource(resource);
 | 
					    const aclResource = aclRole.getResource(resource);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (aclResource) {
 | 
					    if (aclResource) {
 | 
				
			||||||
@ -171,39 +259,37 @@ export class ACL extends EventEmitter {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      if (actionParams) {
 | 
					      if (actionParams) {
 | 
				
			||||||
        // handle single action config
 | 
					        // handle single action config
 | 
				
			||||||
        return {
 | 
					        return mergeParams({
 | 
				
			||||||
          role,
 | 
					          role,
 | 
				
			||||||
          resource,
 | 
					          resource,
 | 
				
			||||||
          action,
 | 
					          action,
 | 
				
			||||||
          params: actionParams,
 | 
					          params: actionParams,
 | 
				
			||||||
        };
 | 
					        });
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        return null;
 | 
					        return null;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!aclRole.strategy) {
 | 
					    const roleStrategy = aclRole.getStrategy();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!roleStrategy && !snippetAllowed) {
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const roleStrategy = lodash.isString(aclRole.strategy)
 | 
					    let roleStrategyParams = roleStrategy?.allow(resource, this.resolveActionAlias(action));
 | 
				
			||||||
      ? this.availableStrategy.get(aclRole.strategy)
 | 
					 | 
				
			||||||
      : new ACLAvailableStrategy(this, aclRole.strategy);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!roleStrategy) {
 | 
					    if (!roleStrategyParams && snippetAllowed) {
 | 
				
			||||||
      return null;
 | 
					      roleStrategyParams = {};
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const roleStrategyParams = roleStrategy.allow(resource, this.resolveActionAlias(action));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (roleStrategyParams) {
 | 
					    if (roleStrategyParams) {
 | 
				
			||||||
      const result = { role, resource, action };
 | 
					      const result = { role, resource, action, params: {} };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (lodash.isPlainObject(roleStrategyParams)) {
 | 
					      if (lodash.isPlainObject(roleStrategyParams)) {
 | 
				
			||||||
        result['params'] = roleStrategyParams;
 | 
					        result['params'] = roleStrategyParams;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return result;
 | 
					      return mergeParams(result);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
@ -218,10 +304,17 @@ export class ACL extends EventEmitter {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  use(fn: any, options?: ToposortOptions) {
 | 
					  use(fn: any, options?: ToposortOptions) {
 | 
				
			||||||
    this.middlewares.add(fn, options);
 | 
					    this.middlewares.add(fn, {
 | 
				
			||||||
 | 
					      group: 'prep',
 | 
				
			||||||
 | 
					      ...options,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  allow(resourceName: string, actionNames: string[] | string, condition?: string | ConditionFunc) {
 | 
					  allow(resourceName: string, actionNames: string[] | string, condition?: string | ConditionFunc) {
 | 
				
			||||||
 | 
					    return this.skip(resourceName, actionNames, condition);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  skip(resourceName: string, actionNames: string[] | string, condition?: string | ConditionFunc) {
 | 
				
			||||||
    if (!Array.isArray(actionNames)) {
 | 
					    if (!Array.isArray(actionNames)) {
 | 
				
			||||||
      actionNames = [actionNames];
 | 
					      actionNames = [actionNames];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -242,23 +335,10 @@ export class ACL extends EventEmitter {
 | 
				
			|||||||
  middleware() {
 | 
					  middleware() {
 | 
				
			||||||
    const acl = this;
 | 
					    const acl = this;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const filterParams = (ctx, resourceName, params) => {
 | 
					 | 
				
			||||||
      if (params?.filter?.createdById) {
 | 
					 | 
				
			||||||
        const collection = ctx.db.getCollection(resourceName);
 | 
					 | 
				
			||||||
        if (collection && !collection.getField('createdById')) {
 | 
					 | 
				
			||||||
          return lodash.omit(params, 'filter.createdById');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return params;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return async function ACLMiddleware(ctx, next) {
 | 
					    return async function ACLMiddleware(ctx, next) {
 | 
				
			||||||
      const roleName = ctx.state.currentRole || 'anonymous';
 | 
					      const roleName = ctx.state.currentRole || 'anonymous';
 | 
				
			||||||
      const { resourceName, actionName } = ctx.action;
 | 
					      const { resourceName, actionName } = ctx.action;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const resourcerAction: Action = ctx.action;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      ctx.can = (options: Omit<CanArgs, 'role'>) => {
 | 
					      ctx.can = (options: Omit<CanArgs, 'role'>) => {
 | 
				
			||||||
        return acl.can({ role: roleName, ...options });
 | 
					        return acl.can({ role: roleName, ...options });
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
@ -267,28 +347,30 @@ export class ACL extends EventEmitter {
 | 
				
			|||||||
        can: ctx.can({ resource: resourceName, action: actionName }),
 | 
					        can: ctx.can({ resource: resourceName, action: actionName }),
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return compose(acl.middlewares.nodes)(ctx, async () => {
 | 
					      return compose(acl.middlewares.nodes)(ctx, next);
 | 
				
			||||||
        const permission = ctx.permission;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (permission.skip) {
 | 
					 | 
				
			||||||
          return next();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!permission.can || typeof permission.can !== 'object') {
 | 
					 | 
				
			||||||
          ctx.throw(403, 'No permissions');
 | 
					 | 
				
			||||||
          return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const { params } = permission.can;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (params) {
 | 
					 | 
				
			||||||
          const filteredParams = filterParams(ctx, resourceName, params);
 | 
					 | 
				
			||||||
          const parsedParams = acl.parseJsonTemplate(filteredParams, ctx);
 | 
					 | 
				
			||||||
          resourcerAction.mergeParams(parsedParams);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        await next();
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getActionParams(ctx) {
 | 
				
			||||||
 | 
					    const roleName = ctx.state.currentRole || 'anonymous';
 | 
				
			||||||
 | 
					    const { resourceName, actionName } = ctx.action;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ctx.can = (options: Omit<CanArgs, 'role'>) => {
 | 
				
			||||||
 | 
					      return this.can({ role: roleName, ...options });
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ctx.permission = {
 | 
				
			||||||
 | 
					      can: ctx.can({ resource: resourceName, action: actionName }),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await compose(this.middlewares.nodes)(ctx, async () => {});
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  addFixedParams(resource: string, action: string, merger: Merger) {
 | 
				
			||||||
 | 
					    this.fixedParamsManager.addParams(resource, action, merger);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  registerSnippet(snippet: SnippetOptions) {
 | 
				
			||||||
 | 
					    this.snippetManager.register(snippet);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -12,19 +12,22 @@ export class AllowManager {
 | 
				
			|||||||
      return ctx.state.currentUser;
 | 
					      return ctx.state.currentUser;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.registerAllowCondition('public', (ctx) => {
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.registerAllowCondition('allowConfigure', async (ctx) => {
 | 
					    this.registerAllowCondition('allowConfigure', async (ctx) => {
 | 
				
			||||||
      const roleName = ctx.state.currentRole;
 | 
					      const roleName = ctx.state.currentRole;
 | 
				
			||||||
      if (!roleName) {
 | 
					      if (!roleName) {
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const roleInstance = await ctx.db.getRepository('roles').findOne({
 | 
					      const role = acl.getRole(roleName);
 | 
				
			||||||
        filter: {
 | 
					      if (!role) {
 | 
				
			||||||
          name: roleName,
 | 
					        return false;
 | 
				
			||||||
        },
 | 
					      }
 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return roleInstance?.get('allowConfigure');
 | 
					      return role.getStrategy()?.allowConfigure;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -43,9 +46,11 @@ export class AllowManager {
 | 
				
			|||||||
    for (const fetchActionStep of fetchActionSteps) {
 | 
					    for (const fetchActionStep of fetchActionSteps) {
 | 
				
			||||||
      const resource = this.skipActions.get(fetchActionStep);
 | 
					      const resource = this.skipActions.get(fetchActionStep);
 | 
				
			||||||
      if (resource) {
 | 
					      if (resource) {
 | 
				
			||||||
        const condition = resource.get('*') || resource.get(actionName);
 | 
					        for (const fetchActionStep of ['*', actionName]) {
 | 
				
			||||||
        if (condition) {
 | 
					          const condition = resource.get(fetchActionStep);
 | 
				
			||||||
          results.push(typeof condition === 'string' ? this.registeredCondition.get(condition) : condition);
 | 
					          if (condition) {
 | 
				
			||||||
 | 
					            results.push(typeof condition === 'string' ? this.registeredCondition.get(condition) : condition);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -57,34 +62,40 @@ export class AllowManager {
 | 
				
			|||||||
    this.registeredCondition.set(name, condition);
 | 
					    this.registeredCondition.set(name, condition);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async isAllowed(resourceName: string, actionName: string, ctx: any) {
 | 
				
			||||||
 | 
					    const skippedConditions = this.getAllowedConditions(resourceName, actionName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const skippedCondition of skippedConditions) {
 | 
				
			||||||
 | 
					      if (skippedCondition) {
 | 
				
			||||||
 | 
					        let skipResult = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (typeof skippedCondition === 'function') {
 | 
				
			||||||
 | 
					          skipResult = await skippedCondition(ctx);
 | 
				
			||||||
 | 
					        } else if (skippedCondition) {
 | 
				
			||||||
 | 
					          skipResult = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (skipResult) {
 | 
				
			||||||
 | 
					          return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  aclMiddleware() {
 | 
					  aclMiddleware() {
 | 
				
			||||||
    return async (ctx, next) => {
 | 
					    return async (ctx, next) => {
 | 
				
			||||||
      const { resourceName, actionName } = ctx.action;
 | 
					      const { resourceName, actionName } = ctx.action;
 | 
				
			||||||
      const skippedConditions = ctx.app.acl.allowManager.getAllowedConditions(resourceName, actionName);
 | 
					      let skip = await this.acl.allowManager.isAllowed(resourceName, actionName, ctx);
 | 
				
			||||||
      let skip = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      for (const skippedCondition of skippedConditions) {
 | 
					 | 
				
			||||||
        if (skippedCondition) {
 | 
					 | 
				
			||||||
          let skipResult = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (typeof skippedCondition === 'function') {
 | 
					 | 
				
			||||||
            skipResult = await skippedCondition(ctx);
 | 
					 | 
				
			||||||
          } else if (skippedCondition) {
 | 
					 | 
				
			||||||
            skipResult = true;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (skipResult) {
 | 
					 | 
				
			||||||
            skip = true;
 | 
					 | 
				
			||||||
            break;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (skip) {
 | 
					      if (skip) {
 | 
				
			||||||
        ctx.permission = {
 | 
					        ctx.permission = {
 | 
				
			||||||
 | 
					          ...(ctx.permission || {}),
 | 
				
			||||||
          skip: true,
 | 
					          skip: true,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await next();
 | 
					      await next();
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										51
									
								
								packages/core/acl/src/fixed-params-manager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								packages/core/acl/src/fixed-params-manager.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					import { assign } from '@nocobase/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Context = any;
 | 
				
			||||||
 | 
					export type Merger = () => object;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ActionPath = string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SPLIT = ':';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class FixedParamsManager {
 | 
				
			||||||
 | 
					  merger = new Map<ActionPath, Array<Merger>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  addParams(resource: string, action: string, merger: Merger) {
 | 
				
			||||||
 | 
					    const path = this.getActionPath(resource, action);
 | 
				
			||||||
 | 
					    this.merger.set(path, [...this.getParamsMerger(resource, action), merger]);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getParamsMerger(resource: string, action: string) {
 | 
				
			||||||
 | 
					    const path = this.getActionPath(resource, action);
 | 
				
			||||||
 | 
					    return this.merger.get(path) || [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected getActionPath(resource: string, action: string) {
 | 
				
			||||||
 | 
					    return `${resource}${SPLIT}${action}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getParams(resource: string, action: string, extraParams: any = {}) {
 | 
				
			||||||
 | 
					    const results = {};
 | 
				
			||||||
 | 
					    for (const merger of this.getParamsMerger(resource, action)) {
 | 
				
			||||||
 | 
					      FixedParamsManager.mergeParams(results, merger());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (extraParams) {
 | 
				
			||||||
 | 
					      FixedParamsManager.mergeParams(results, extraParams);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return results;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static mergeParams(a: any, b: any) {
 | 
				
			||||||
 | 
					    assign(a, b, {
 | 
				
			||||||
 | 
					      filter: 'andMerge',
 | 
				
			||||||
 | 
					      fields: 'intersect',
 | 
				
			||||||
 | 
					      appends: 'union',
 | 
				
			||||||
 | 
					      except: 'union',
 | 
				
			||||||
 | 
					      whitelist: 'intersect',
 | 
				
			||||||
 | 
					      blacklist: 'intersect',
 | 
				
			||||||
 | 
					      sort: 'overwrite',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -3,4 +3,6 @@ export * from './acl-available-action';
 | 
				
			|||||||
export * from './acl-available-strategy';
 | 
					export * from './acl-available-strategy';
 | 
				
			||||||
export * from './acl-resource';
 | 
					export * from './acl-resource';
 | 
				
			||||||
export * from './acl-role';
 | 
					export * from './acl-role';
 | 
				
			||||||
 | 
					export * from './no-permission-error';
 | 
				
			||||||
export * from './skip-middleware';
 | 
					export * from './skip-middleware';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7
									
								
								packages/core/acl/src/no-permission-error.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								packages/core/acl/src/no-permission-error.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					class NoPermissionError extends Error {
 | 
				
			||||||
 | 
					  constructor(...args) {
 | 
				
			||||||
 | 
					    super(...args);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { NoPermissionError };
 | 
				
			||||||
							
								
								
									
										45
									
								
								packages/core/acl/src/snippet-manager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								packages/core/acl/src/snippet-manager.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					import { ACL } from './acl';
 | 
				
			||||||
 | 
					import minimatch from 'minimatch';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type SnippetOptions = {
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  actions: Array<string>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Snippet {
 | 
				
			||||||
 | 
					  constructor(public name: string, public actions: Array<string>) {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type SnippetGroup = {
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  snippets: SnippetOptions[];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SnippetManager {
 | 
				
			||||||
 | 
					  public snippets: Map<string, Snippet> = new Map();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  register(snippet: SnippetOptions) {
 | 
				
			||||||
 | 
					    this.snippets.set(snippet.name, snippet);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  allow(actionPath: string, snippetName: string) {
 | 
				
			||||||
 | 
					    const negated = snippetName.startsWith('!');
 | 
				
			||||||
 | 
					    snippetName = negated ? snippetName.slice(1) : snippetName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const snippet = this.snippets.get(snippetName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!snippet) {
 | 
				
			||||||
 | 
					      throw new Error(`Snippet ${snippetName} not found`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const matched = snippet.actions.some((action) => minimatch(actionPath, action));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (matched) {
 | 
				
			||||||
 | 
					      return negated ? false : true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default SnippetManager;
 | 
				
			||||||
@ -1,8 +1,8 @@
 | 
				
			|||||||
import { MockServer, mockServer } from './index';
 | 
					 | 
				
			||||||
import { registerActions } from '@nocobase/actions';
 | 
					import { registerActions } from '@nocobase/actions';
 | 
				
			||||||
 | 
					import { mockServer } from './index';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('destroy action', () => {
 | 
					describe('destroy action', () => {
 | 
				
			||||||
  let app: MockServer;
 | 
					  let app;
 | 
				
			||||||
  let Post;
 | 
					  let Post;
 | 
				
			||||||
  let Comment;
 | 
					  let Comment;
 | 
				
			||||||
  let Tag;
 | 
					  let Tag;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,10 @@
 | 
				
			|||||||
import { registerActions } from '@nocobase/actions';
 | 
					import { registerActions } from '@nocobase/actions';
 | 
				
			||||||
import { mockServer } from './index';
 | 
					import { mockServer as actionMockServer } from './index';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('list action', () => {
 | 
					describe('list action', () => {
 | 
				
			||||||
  let app;
 | 
					  let app;
 | 
				
			||||||
  beforeEach(async () => {
 | 
					  beforeEach(async () => {
 | 
				
			||||||
    app = mockServer();
 | 
					    app = actionMockServer();
 | 
				
			||||||
    registerActions(app);
 | 
					    registerActions(app);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const Post = app.collection({
 | 
					    const Post = app.collection({
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,8 @@
 | 
				
			|||||||
import { Context } from '..';
 | 
					 | 
				
			||||||
import { getRepositoryFromParams } from '../utils';
 | 
					import { getRepositoryFromParams } from '../utils';
 | 
				
			||||||
 | 
					import { Context } from '../index';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function destroy(ctx: Context, next) {
 | 
					export async function destroy(ctx: Context, next) {
 | 
				
			||||||
  const repository = getRepositoryFromParams(ctx);
 | 
					  const repository = getRepositoryFromParams(ctx);
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const { filterByTk, filter } = ctx.action.params;
 | 
					  const { filterByTk, filter } = ctx.action.params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const instance = await repository.destroy({
 | 
					  const instance = await repository.destroy({
 | 
				
			||||||
 | 
				
			|||||||
@ -57,10 +57,13 @@ async function listWithNonPaged(ctx: Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export async function list(ctx: Context, next) {
 | 
					export async function list(ctx: Context, next) {
 | 
				
			||||||
  const { paginate } = ctx.action.params;
 | 
					  const { paginate } = ctx.action.params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (paginate === false || paginate === 'false') {
 | 
					  if (paginate === false || paginate === 'false') {
 | 
				
			||||||
    await listWithNonPaged(ctx);
 | 
					    await listWithNonPaged(ctx);
 | 
				
			||||||
 | 
					    ctx.paginate = false;
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    await listWithPagination(ctx);
 | 
					    await listWithPagination(ctx);
 | 
				
			||||||
 | 
					    ctx.paginate = true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await next();
 | 
					  await next();
 | 
				
			||||||
 | 
				
			|||||||
@ -1,13 +1,15 @@
 | 
				
			|||||||
import { useFieldSchema } from '@formily/react';
 | 
					import { Schema, useFieldSchema } from '@formily/react';
 | 
				
			||||||
import { Spin } from 'antd';
 | 
					import { Spin } from 'antd';
 | 
				
			||||||
import React, { createContext, useContext } from 'react';
 | 
					import React, { createContext, useContext } from 'react';
 | 
				
			||||||
import { Redirect } from 'react-router-dom';
 | 
					import { Redirect } from 'react-router-dom';
 | 
				
			||||||
import { useAPIClient, useRequest } from '../api-client';
 | 
					import { useAPIClient, useRequest } from '../api-client';
 | 
				
			||||||
 | 
					import { useBlockRequestContext } from '../block-provider/BlockProvider';
 | 
				
			||||||
import { useCollection } from '../collection-manager';
 | 
					import { useCollection } from '../collection-manager';
 | 
				
			||||||
import { useRecordIsOwn } from '../record-provider';
 | 
					import { useResourceActionContext } from '../collection-manager/ResourceActionProvider';
 | 
				
			||||||
 | 
					import { useRecord } from '../record-provider';
 | 
				
			||||||
import { SchemaComponentOptions, useDesignable } from '../schema-component';
 | 
					import { SchemaComponentOptions, useDesignable } from '../schema-component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ACLContext = createContext(null);
 | 
					export const ACLContext = createContext<any>({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ACLProvider = (props) => {
 | 
					export const ACLProvider = (props) => {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
@ -19,7 +21,29 @@ export const ACLProvider = (props) => {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getRouteUrl = (props) => {
 | 
				
			||||||
 | 
					  if (props?.match) {
 | 
				
			||||||
 | 
					    return props.match;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return props && getRouteUrl(props?.children?.props);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getRouteAclCheck = (match, snippets) => {
 | 
				
			||||||
 | 
					  const { url, params } = match;
 | 
				
			||||||
 | 
					  if (url === '/admin/pm/list' || params?.pluginName || params?.name?.includes('settings')) {
 | 
				
			||||||
 | 
					    const pmAclCheck = url === '/admin/pm/list' && snippets.includes('pm');
 | 
				
			||||||
 | 
					    const pluginTabByName = params?.name.split('/');
 | 
				
			||||||
 | 
					    pluginTabByName.shift();
 | 
				
			||||||
 | 
					    const pluginName = params.pluginName || pluginTabByName[0];
 | 
				
			||||||
 | 
					    const tabName = params.tabName || pluginTabByName[1];
 | 
				
			||||||
 | 
					    const pluginTabSnippet = pluginName && tabName && `!pm.${pluginName}.${tabName}`;
 | 
				
			||||||
 | 
					    const pluginTabAclCheck = pluginTabSnippet && !snippets.includes(pluginTabSnippet);
 | 
				
			||||||
 | 
					    return pmAclCheck || pluginTabAclCheck;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
export const ACLRolesCheckProvider = (props) => {
 | 
					export const ACLRolesCheckProvider = (props) => {
 | 
				
			||||||
 | 
					  const route = getRouteUrl(props.children.props);
 | 
				
			||||||
  const { setDesignable } = useDesignable();
 | 
					  const { setDesignable } = useDesignable();
 | 
				
			||||||
  const api = useAPIClient();
 | 
					  const api = useAPIClient();
 | 
				
			||||||
  const result = useRequest(
 | 
					  const result = useRequest(
 | 
				
			||||||
@ -28,7 +52,7 @@ export const ACLRolesCheckProvider = (props) => {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      onSuccess(data) {
 | 
					      onSuccess(data) {
 | 
				
			||||||
        if (!data?.data?.allowConfigure && !data?.data?.allowAll) {
 | 
					        if (!data?.data?.snippets.includes('ui.*')) {
 | 
				
			||||||
          setDesignable(false);
 | 
					          setDesignable(false);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (data?.data?.role !== api.auth.role) {
 | 
					        if (data?.data?.role !== api.auth.role) {
 | 
				
			||||||
@ -48,9 +72,9 @@ export const ACLRolesCheckProvider = (props) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const useRoleRecheck = () => {
 | 
					export const useRoleRecheck = () => {
 | 
				
			||||||
  const ctx = useContext(ACLContext);
 | 
					  const ctx = useContext(ACLContext);
 | 
				
			||||||
  const { allowAll, allowConfigure } = useACLRoleContext();
 | 
					  const { allowAll } = useACLRoleContext();
 | 
				
			||||||
  return () => {
 | 
					  return () => {
 | 
				
			||||||
    if (allowAll || allowConfigure) {
 | 
					    if (allowAll) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    ctx.refresh();
 | 
					    ctx.refresh();
 | 
				
			||||||
@ -61,98 +85,191 @@ export const useACLContext = () => {
 | 
				
			|||||||
  return useContext(ACLContext);
 | 
					  return useContext(ACLContext);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useACLRoleContext = () => {
 | 
					export const ACLActionParamsContext = createContext<any>({});
 | 
				
			||||||
  const ctx = useContext(ACLContext);
 | 
					 | 
				
			||||||
  const data = ctx.data?.data;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useACLRolesCheck = () => {
 | 
				
			||||||
 | 
					  const ctx = useContext(ACLContext);
 | 
				
			||||||
 | 
					  const data = ctx?.data?.data;
 | 
				
			||||||
 | 
					  const getActionAlias = (actionPath: string) => {
 | 
				
			||||||
 | 
					    const actionName = actionPath.split(':').pop();
 | 
				
			||||||
 | 
					    return data?.actionAlias?.[actionName] || actionName;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    ...data,
 | 
					    data,
 | 
				
			||||||
    getActionParams(path: string, { skipOwnCheck, isOwn }) {
 | 
					    getActionAlias,
 | 
				
			||||||
      const [resourceName, act] = path.split(':');
 | 
					    inResources: (resourceName: string) => {
 | 
				
			||||||
      const currentAction = data?.actionAlias?.[act] || act;
 | 
					      return data?.resources?.includes?.(resourceName);
 | 
				
			||||||
      const hasResource = data?.resources?.includes(resourceName);
 | 
					    },
 | 
				
			||||||
      const params = data?.actions?.[`${resourceName}:${currentAction}`];
 | 
					    getResourceActionParams: (actionPath: string) => {
 | 
				
			||||||
      if (hasResource) {
 | 
					      const [resourceName] = actionPath.split(':');
 | 
				
			||||||
        if (!skipOwnCheck && params?.own) {
 | 
					      const actionAlias = getActionAlias(actionPath);
 | 
				
			||||||
          return isOwn ? params : null;
 | 
					      return data?.actions?.[`${resourceName}:${actionAlias}`] || data?.actions?.[actionPath];
 | 
				
			||||||
        }
 | 
					    },
 | 
				
			||||||
        return params;
 | 
					    getStrategyActionParams: (actionPath: string) => {
 | 
				
			||||||
      }
 | 
					      const actionAlias = getActionAlias(actionPath);
 | 
				
			||||||
      const strategyActions = data?.strategy?.actions || [];
 | 
					      const strategyAction = data?.strategy?.actions.find((action) => {
 | 
				
			||||||
      const strategyAction = strategyActions?.find((action) => {
 | 
					 | 
				
			||||||
        const [value] = action.split(':');
 | 
					        const [value] = action.split(':');
 | 
				
			||||||
        return value === currentAction;
 | 
					        return value === actionAlias;
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      if (!strategyAction) {
 | 
					      return strategyAction ? {} : null;
 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (skipOwnCheck) {
 | 
					 | 
				
			||||||
        return {};
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      const [, actionScope] = strategyAction.split(':');
 | 
					 | 
				
			||||||
      if (actionScope === 'own') {
 | 
					 | 
				
			||||||
        return isOwn;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return {};
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ACLAllowConfigure = (props) => {
 | 
					const getIgnoreScope = (options: any = {}) => {
 | 
				
			||||||
  const { allowAll, allowConfigure } = useACLRoleContext();
 | 
					  const { schema, recordPkValue } = options;
 | 
				
			||||||
  if (allowAll || allowConfigure) {
 | 
					  let ignoreScope = false;
 | 
				
			||||||
    return <>{props.children}</>;
 | 
					  if (options.ignoreScope) {
 | 
				
			||||||
 | 
					    ignoreScope = true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return null;
 | 
					  if (schema?.['x-acl-ignore-scope']) {
 | 
				
			||||||
 | 
					    ignoreScope = true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (schema?.['x-acl-action-props']?.['skipScopeCheck']) {
 | 
				
			||||||
 | 
					    ignoreScope = true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (!recordPkValue) {
 | 
				
			||||||
 | 
					    ignoreScope = true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return ignoreScope;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ACLActionParamsContext = createContext<any>({});
 | 
					const useAllowedActions = () => {
 | 
				
			||||||
 | 
					  const result = useBlockRequestContext() || { service: useResourceActionContext() };
 | 
				
			||||||
 | 
					  return result?.service?.data?.meta?.allowedActions;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const useResourceName = () => {
 | 
				
			||||||
 | 
					  const result = useBlockRequestContext() || { service: useResourceActionContext() };
 | 
				
			||||||
 | 
					  return result?.props?.resource || result?.service?.defaultRequest?.resource;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useACLRoleContext() {
 | 
				
			||||||
 | 
					  const { data, getActionAlias, inResources, getResourceActionParams, getStrategyActionParams } = useACLRolesCheck();
 | 
				
			||||||
 | 
					  const allowedActions = useAllowedActions();
 | 
				
			||||||
 | 
					  const verifyScope = (actionName: string, recordPkValue: any) => {
 | 
				
			||||||
 | 
					    const actionAlias = getActionAlias(actionName);
 | 
				
			||||||
 | 
					    if (!Array.isArray(allowedActions?.[actionAlias])) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return allowedActions[actionAlias].includes(recordPkValue);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    ...data,
 | 
				
			||||||
 | 
					    parseAction: (actionPath: string, options: any = {}) => {
 | 
				
			||||||
 | 
					      const [resourceName, actionName] = actionPath.split(':');
 | 
				
			||||||
 | 
					      if (!getIgnoreScope(options)) {
 | 
				
			||||||
 | 
					        const r = verifyScope(actionName, options.recordPkValue);
 | 
				
			||||||
 | 
					        if (r !== null) {
 | 
				
			||||||
 | 
					          return r ? {} : null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (data?.allowAll) {
 | 
				
			||||||
 | 
					        return {};
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (inResources(resourceName)) {
 | 
				
			||||||
 | 
					        return getResourceActionParams(actionPath);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return getStrategyActionParams(actionPath);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ACLCollectionProvider = (props) => {
 | 
					export const ACLCollectionProvider = (props) => {
 | 
				
			||||||
  const { allowAll, allowConfigure, getActionParams } = useACLRoleContext();
 | 
					  const { allowAll, parseAction } = useACLRoleContext();
 | 
				
			||||||
  const fieldSchema = useFieldSchema();
 | 
					  const schema = useFieldSchema();
 | 
				
			||||||
  const isOwn = useRecordIsOwn();
 | 
					  if (allowAll) {
 | 
				
			||||||
  if (allowAll || allowConfigure) {
 | 
					 | 
				
			||||||
    return <>{props.children}</>;
 | 
					    return <>{props.children}</>;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  const path = fieldSchema['x-acl-action'];
 | 
					  const actionPath = schema['x-acl-action'];
 | 
				
			||||||
  const skipScopeCheck = fieldSchema['x-acl-action-props']?.skipScopeCheck;
 | 
					  if (!actionPath) {
 | 
				
			||||||
  if (!path) {
 | 
					 | 
				
			||||||
    return <>{props.children}</>;
 | 
					    return <>{props.children}</>;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  const params = getActionParams(path, { isOwn, skipOwnCheck: skipScopeCheck === false ? false : true });
 | 
					  const params = parseAction(actionPath, { schema });
 | 
				
			||||||
  if (!params) {
 | 
					  if (!params) {
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
 | 
					  return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useACLActionParamsContext = () => {
 | 
				
			||||||
 | 
					  return useContext(ACLActionParamsContext);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useRecordPkValue = () => {
 | 
				
			||||||
 | 
					  const { getPrimaryKey } = useCollection();
 | 
				
			||||||
 | 
					  const record = useRecord();
 | 
				
			||||||
 | 
					  const primaryKey = getPrimaryKey();
 | 
				
			||||||
 | 
					  return record?.[primaryKey];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ACLActionProvider = (props) => {
 | 
					export const ACLActionProvider = (props) => {
 | 
				
			||||||
  const { name } = useCollection();
 | 
					  const recordPkValue = useRecordPkValue();
 | 
				
			||||||
  const fieldSchema = useFieldSchema();
 | 
					  const resource = useResourceName();
 | 
				
			||||||
  const isOwn = useRecordIsOwn();
 | 
					  const { parseAction } = useACLRoleContext();
 | 
				
			||||||
  const { allowAll, allowConfigure, getActionParams } = useACLRoleContext();
 | 
					  const schema = useFieldSchema();
 | 
				
			||||||
  if (!name || allowAll || allowConfigure) {
 | 
					  let actionPath = schema['x-acl-action'];
 | 
				
			||||||
 | 
					  if (!actionPath && resource && schema['x-action']) {
 | 
				
			||||||
 | 
					    actionPath = `${resource}:${schema['x-action']}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (!actionPath.includes(':')) {
 | 
				
			||||||
 | 
					    actionPath = `${resource}:${actionPath}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (!actionPath) {
 | 
				
			||||||
    return <>{props.children}</>;
 | 
					    return <>{props.children}</>;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  const actionName = fieldSchema['x-action'];
 | 
					  const params = parseAction(actionPath, { schema, recordPkValue });
 | 
				
			||||||
  const path = fieldSchema['x-acl-action'] || `${name}:${actionName}`;
 | 
					 | 
				
			||||||
  const skipScopeCheck = fieldSchema['x-acl-action-props']?.skipScopeCheck;
 | 
					 | 
				
			||||||
  const params = getActionParams(path, { skipOwnCheck: skipScopeCheck, isOwn });
 | 
					 | 
				
			||||||
  if (!params) {
 | 
					  if (!params) {
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
 | 
					  return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useACLFieldWhitelist = () => {
 | 
				
			||||||
 | 
					  const params = useContext(ACLActionParamsContext);
 | 
				
			||||||
 | 
					  const whitelist = []
 | 
				
			||||||
 | 
					    .concat(params?.whitelist || [])
 | 
				
			||||||
 | 
					    .concat(params?.fields || [])
 | 
				
			||||||
 | 
					    .concat(params?.appends || []);
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    whitelist,
 | 
				
			||||||
 | 
					    schemaInWhitelist(fieldSchema: Schema) {
 | 
				
			||||||
 | 
					      if (whitelist.length === 0) {
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (!fieldSchema) {
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (!fieldSchema['x-collection-field']) {
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const [, ...keys] = fieldSchema['x-collection-field'].split('.');
 | 
				
			||||||
 | 
					      return whitelist?.includes(keys.join('.'));
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ACLCollectionFieldProvider = (props) => {
 | 
					export const ACLCollectionFieldProvider = (props) => {
 | 
				
			||||||
 | 
					  const fieldSchema = useFieldSchema();
 | 
				
			||||||
 | 
					  const { allowAll } = useACLRoleContext();
 | 
				
			||||||
 | 
					  if (allowAll) {
 | 
				
			||||||
 | 
					    return <>{props.children}</>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (!fieldSchema['x-collection-field']) {
 | 
				
			||||||
 | 
					    return <>{props.children}</>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const { whitelist } = useACLFieldWhitelist();
 | 
				
			||||||
 | 
					  const allowed = whitelist.length > 0 ? whitelist.includes(fieldSchema.name) : true;
 | 
				
			||||||
 | 
					  if (!allowed) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  return <>{props.children}</>;
 | 
					  return <>{props.children}</>;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ACLMenuItemProvider = (props) => {
 | 
					export const ACLMenuItemProvider = (props) => {
 | 
				
			||||||
  const { allowAll, allowConfigure, allowMenuItemIds = [] } = useACLRoleContext();
 | 
					  const { allowAll, allowMenuItemIds = [], snippets } = useACLRoleContext();
 | 
				
			||||||
  const fieldSchema = useFieldSchema();
 | 
					  const fieldSchema = useFieldSchema();
 | 
				
			||||||
  if (allowAll || allowConfigure) {
 | 
					  if (allowAll || snippets.includes('ui.*')) {
 | 
				
			||||||
    return <>{props.children}</>;
 | 
					    return <>{props.children}</>;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  if (!fieldSchema['x-uid']) {
 | 
					  if (!fieldSchema['x-uid']) {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										148
									
								
								packages/core/client/src/acl/Configuration/ConfigureCenter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								packages/core/client/src/acl/Configuration/ConfigureCenter.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,148 @@
 | 
				
			|||||||
 | 
					import { Checkbox, message, Table } from 'antd';
 | 
				
			||||||
 | 
					import React, { createContext, useContext, useState } from 'react';
 | 
				
			||||||
 | 
					import { useTranslation } from 'react-i18next';
 | 
				
			||||||
 | 
					import { useAPIClient, useRequest } from '../../api-client';
 | 
				
			||||||
 | 
					import { useRecord } from '../../record-provider';
 | 
				
			||||||
 | 
					import { useCompile } from '../../schema-component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getParentKeys = (tree, func, path = []) => {
 | 
				
			||||||
 | 
					  if (!tree) return [];
 | 
				
			||||||
 | 
					  for (const data of tree) {
 | 
				
			||||||
 | 
					    path.push(data.key);
 | 
				
			||||||
 | 
					    if (func(data)) return path;
 | 
				
			||||||
 | 
					    if (data.children) {
 | 
				
			||||||
 | 
					      const findChildren = getParentKeys(data.children, func, path);
 | 
				
			||||||
 | 
					      if (findChildren.length) return findChildren;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    path.pop();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return [];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					const getChildrenKeys = (data = [], arr = []) => {
 | 
				
			||||||
 | 
					  for (let item of data) {
 | 
				
			||||||
 | 
					    arr.push(item.key);
 | 
				
			||||||
 | 
					    if (item.children && item.children.length) getChildrenKeys(item.children, arr);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return arr;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SettingMenuContext = createContext(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function useGetContext() {
 | 
				
			||||||
 | 
					  const [context, setContext] = useState(null);
 | 
				
			||||||
 | 
					  import('../../pm').then(({ SettingsCenterContext }) => {
 | 
				
			||||||
 | 
					    setContext(SettingsCenterContext);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return context;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SettingCenterProvider = (props) => {
 | 
				
			||||||
 | 
					  const context = useGetContext();
 | 
				
			||||||
 | 
					  const configureItems = context && useContext(context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return <SettingMenuContext.Provider value={configureItems}>{props.children}</SettingMenuContext.Provider>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const formatPluginTabs = (data) => {
 | 
				
			||||||
 | 
					  const tabs = [];
 | 
				
			||||||
 | 
					  for (const key in data) {
 | 
				
			||||||
 | 
					    const plugin = data?.[key];
 | 
				
			||||||
 | 
					    for (const tabKey in plugin?.tabs || {}) {
 | 
				
			||||||
 | 
					      const tab = plugin?.tabs[tabKey];
 | 
				
			||||||
 | 
					      tabs.push({
 | 
				
			||||||
 | 
					        pluginTitle: plugin.title,
 | 
				
			||||||
 | 
					        ...tab,
 | 
				
			||||||
 | 
					        key: `pm.${key}.${tabKey}`,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return tabs;
 | 
				
			||||||
 | 
					  const arr: any[] = Object.entries(data);
 | 
				
			||||||
 | 
					  const pluginsTabs = [];
 | 
				
			||||||
 | 
					  console.log(tabs);
 | 
				
			||||||
 | 
					  arr.forEach((v) => {
 | 
				
			||||||
 | 
					    const children = Object.entries(v[1].tabs).map((k: any) => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        key: 'pm.' + v[0] + '.' + k[0],
 | 
				
			||||||
 | 
					        title: k[1].title,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pluginsTabs.push({
 | 
				
			||||||
 | 
					      title: v[1].title,
 | 
				
			||||||
 | 
					      key: 'pm.' + v[0],
 | 
				
			||||||
 | 
					      children,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  return pluginsTabs;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SettingsCenterConfigure = () => {
 | 
				
			||||||
 | 
					  const record = useRecord();
 | 
				
			||||||
 | 
					  const api = useAPIClient();
 | 
				
			||||||
 | 
					  const pluginTags = useContext(SettingMenuContext);
 | 
				
			||||||
 | 
					  const items: any[] = (pluginTags && formatPluginTabs(pluginTags)) || [];
 | 
				
			||||||
 | 
					  const { t } = useTranslation();
 | 
				
			||||||
 | 
					  const compile = useCompile();
 | 
				
			||||||
 | 
					  const { loading, refresh, data } = useRequest({
 | 
				
			||||||
 | 
					    resource: 'roles.snippets',
 | 
				
			||||||
 | 
					    resourceOf: record.name,
 | 
				
			||||||
 | 
					    action: 'list',
 | 
				
			||||||
 | 
					    params: {
 | 
				
			||||||
 | 
					      paginate: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  const resource = api.resource('roles.snippets', record.name);
 | 
				
			||||||
 | 
					  const handleChange = async (checked, record) => {
 | 
				
			||||||
 | 
					    const childrenKeys = getChildrenKeys(record?.children, []);
 | 
				
			||||||
 | 
					    const totalKeys = childrenKeys.concat(record.key);
 | 
				
			||||||
 | 
					    if (!checked) {
 | 
				
			||||||
 | 
					      await resource.remove({
 | 
				
			||||||
 | 
					        values: totalKeys.map((v) => '!' + v),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      refresh();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      await resource.add({
 | 
				
			||||||
 | 
					        values: totalKeys.map((v) => '!' + v),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      refresh();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    message.success(t('Saved successfully'));
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    items?.length && (
 | 
				
			||||||
 | 
					      <Table
 | 
				
			||||||
 | 
					        loading={loading}
 | 
				
			||||||
 | 
					        rowKey={'key'}
 | 
				
			||||||
 | 
					        pagination={false}
 | 
				
			||||||
 | 
					        columns={[
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            dataIndex: 'title',
 | 
				
			||||||
 | 
					            title: t('Plugin tab name'),
 | 
				
			||||||
 | 
					            render: (value) => {
 | 
				
			||||||
 | 
					              return compile(value);
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            dataIndex: 'pluginTitle',
 | 
				
			||||||
 | 
					            title: t('Plugin name'),
 | 
				
			||||||
 | 
					            render: (value) => {
 | 
				
			||||||
 | 
					              return compile(value);
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            dataIndex: 'accessible',
 | 
				
			||||||
 | 
					            title: t('Accessible'),
 | 
				
			||||||
 | 
					            render: (_, record) => {
 | 
				
			||||||
 | 
					              const checked = !data?.data?.includes('!' + record.key);
 | 
				
			||||||
 | 
					              return !record.children && <Checkbox checked={checked} onChange={() => handleChange(checked, record)} />;
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ]}
 | 
				
			||||||
 | 
					        dataSource={items}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -1,9 +1,9 @@
 | 
				
			|||||||
import { Checkbox, message, Table } from 'antd';
 | 
					import { Checkbox, message, Table } from 'antd';
 | 
				
			||||||
 | 
					import { uniq } from 'lodash';
 | 
				
			||||||
import React, { useState } from 'react';
 | 
					import React, { useState } from 'react';
 | 
				
			||||||
import { useTranslation } from 'react-i18next';
 | 
					import { useTranslation } from 'react-i18next';
 | 
				
			||||||
import { useAPIClient, useRequest } from '../../api-client';
 | 
					import { useAPIClient, useRequest } from '../../api-client';
 | 
				
			||||||
import { useRecord } from '../../record-provider';
 | 
					import { useRecord } from '../../record-provider';
 | 
				
			||||||
import { uniq } from 'lodash';
 | 
					 | 
				
			||||||
import { useMenuItems } from './MenuItemsProvider';
 | 
					import { useMenuItems } from './MenuItemsProvider';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const findUids = (items) => {
 | 
					const findUids = (items) => {
 | 
				
			||||||
@ -117,7 +117,7 @@ export const MenuConfigure = () => {
 | 
				
			|||||||
                  message.success(t('Saved successfully'));
 | 
					                  message.success(t('Saved successfully'));
 | 
				
			||||||
                }}
 | 
					                }}
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
              {t('Accessible')}
 | 
					              {' '}{t('Accessible')}
 | 
				
			||||||
            </>
 | 
					            </>
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          render: (_, schema) => {
 | 
					          render: (_, schema) => {
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,61 @@
 | 
				
			|||||||
 | 
					import { message } from 'antd';
 | 
				
			||||||
 | 
					import React, { createContext, useContext, useState } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useTranslation } from 'react-i18next';
 | 
				
			||||||
 | 
					import { useAPIClient } from '../../api-client';
 | 
				
			||||||
 | 
					import { useRecord } from '../../record-provider';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SettingCenterPermissionProvider = (props) => {
 | 
				
			||||||
 | 
					  const { currentRecord } = useContext(PermissionContext);
 | 
				
			||||||
 | 
					  if (!currentRecord?.snippets?.includes('pm.*')) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return <div>{props.children}</div>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const PermissionContext = createContext<any>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const PermissionProvider = (props) => {
 | 
				
			||||||
 | 
					  const api = useAPIClient();
 | 
				
			||||||
 | 
					  const record = useRecord();
 | 
				
			||||||
 | 
					  const { t } = useTranslation();
 | 
				
			||||||
 | 
					  const { snippets } = record;
 | 
				
			||||||
 | 
					  snippets?.forEach((key) => {
 | 
				
			||||||
 | 
					    record[key] = true;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  const [currentRecord, setCurrentRecord] = useState(record);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <PermissionContext.Provider
 | 
				
			||||||
 | 
					      value={{
 | 
				
			||||||
 | 
					        currentRecord,
 | 
				
			||||||
 | 
					        update: async (field, form) => {
 | 
				
			||||||
 | 
					          const { path, value } = field.getState() as any;
 | 
				
			||||||
 | 
					          if (['ui.*', 'pm', 'pm.*'].includes(path)) {
 | 
				
			||||||
 | 
					            const resource = api.resource('roles.snippets', record.name);
 | 
				
			||||||
 | 
					            if (value) {
 | 
				
			||||||
 | 
					              await resource.add({
 | 
				
			||||||
 | 
					                values: [path],
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              await resource.remove({
 | 
				
			||||||
 | 
					                values: [path],
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            setCurrentRecord({ ...currentRecord, ...form.values, [path]: value });
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            await api.resource('roles').update({
 | 
				
			||||||
 | 
					              filterByTk: record.name,
 | 
				
			||||||
 | 
					              values: form.values,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            setCurrentRecord({ ...currentRecord, ...form.values });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          message.success(t('Saved successfully'));
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {props.children}
 | 
				
			||||||
 | 
					    </PermissionContext.Provider>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -1,31 +1,81 @@
 | 
				
			|||||||
import { onFieldChange } from '@formily/core';
 | 
					import { onFieldChange } from '@formily/core';
 | 
				
			||||||
import { message } from 'antd';
 | 
					import { connect } from '@formily/react';
 | 
				
			||||||
import React from 'react';
 | 
					import { Checkbox } from 'antd';
 | 
				
			||||||
 | 
					import uniq from 'lodash/uniq';
 | 
				
			||||||
 | 
					import React, { useContext } from 'react';
 | 
				
			||||||
import { useTranslation } from 'react-i18next';
 | 
					import { useTranslation } from 'react-i18next';
 | 
				
			||||||
import { useAPIClient, useRequest } from '../../api-client';
 | 
					import { useAPIClient, useRequest } from '../../api-client';
 | 
				
			||||||
import { useRecord } from '../../record-provider';
 | 
					 | 
				
			||||||
import { SchemaComponent } from '../../schema-component';
 | 
					import { SchemaComponent } from '../../schema-component';
 | 
				
			||||||
 | 
					import { PermissionContext } from './PermisionProvider';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SnippetCheckboxGroup = connect((props) => {
 | 
				
			||||||
 | 
					  const { t } = useTranslation();
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Checkbox.Group
 | 
				
			||||||
 | 
					      style={{
 | 
				
			||||||
 | 
					        width: '100%',
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      value={props.value}
 | 
				
			||||||
 | 
					      onChange={(values) => {
 | 
				
			||||||
 | 
					        const value = uniq([...(props.value || []), ...values])
 | 
				
			||||||
 | 
					          .filter((key) => key && !['!ui.*', '!pm', '!pm.*'].includes(key))
 | 
				
			||||||
 | 
					          .map((key) => {
 | 
				
			||||||
 | 
					            if (!['ui.*', 'pm', 'pm.*'].includes(key)) {
 | 
				
			||||||
 | 
					              return key;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (values?.includes(key)) {
 | 
				
			||||||
 | 
					              return key;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return `!${key}`;
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        for (const key of ['ui.*', 'pm', 'pm.*']) {
 | 
				
			||||||
 | 
					          if (!value.includes(key) && !value.includes(`!${key}`)) {
 | 
				
			||||||
 | 
					            value.push(`!${key}`);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        props.onChange(value);
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <div style={{ marginTop: 16 }}>
 | 
				
			||||||
 | 
					        <Checkbox value="ui.*">{t('Allows to configure interface')}</Checkbox>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div style={{ marginTop: 8 }}>
 | 
				
			||||||
 | 
					        <Checkbox value="pm">{t('Allows to install, activate, disable plugins')}</Checkbox>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div style={{ marginTop: 8 }}>
 | 
				
			||||||
 | 
					        <Checkbox value="pm.*">{t('Allows to configure plugins')}</Checkbox>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </Checkbox.Group>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const RoleConfigure = () => {
 | 
					export const RoleConfigure = () => {
 | 
				
			||||||
  const api = useAPIClient();
 | 
					  const { update, currentRecord } = useContext(PermissionContext);
 | 
				
			||||||
  const record = useRecord();
 | 
					 | 
				
			||||||
  const { t } = useTranslation();
 | 
					  const { t } = useTranslation();
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <SchemaComponent
 | 
					    <SchemaComponent
 | 
				
			||||||
 | 
					      components={{ SnippetCheckboxGroup }}
 | 
				
			||||||
      schema={{
 | 
					      schema={{
 | 
				
			||||||
        type: 'void',
 | 
					        type: 'void',
 | 
				
			||||||
        name: 'form',
 | 
					        name: 'form',
 | 
				
			||||||
        'x-component': 'Form',
 | 
					        'x-component': 'Form',
 | 
				
			||||||
        'x-component-props': {
 | 
					        'x-component-props': {
 | 
				
			||||||
          useValues: (options) => {
 | 
					          useValues: (options) => {
 | 
				
			||||||
 | 
					            const api = useAPIClient();
 | 
				
			||||||
            return useRequest(
 | 
					            return useRequest(
 | 
				
			||||||
              {
 | 
					              () =>
 | 
				
			||||||
                resource: 'roles',
 | 
					                api
 | 
				
			||||||
                action: 'get',
 | 
					                  .resource('roles')
 | 
				
			||||||
                params: {
 | 
					                  .get({
 | 
				
			||||||
                  filterByTk: record.name,
 | 
					                    filterByTk: currentRecord.name,
 | 
				
			||||||
                },
 | 
					                  })
 | 
				
			||||||
              },
 | 
					                  .then((res) => {
 | 
				
			||||||
 | 
					                    const record = res?.data?.data;
 | 
				
			||||||
 | 
					                    record.snippets?.forEach((key) => {
 | 
				
			||||||
 | 
					                      record[key] = true;
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                    return { data: record };
 | 
				
			||||||
 | 
					                  }),
 | 
				
			||||||
              options,
 | 
					              options,
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@ -34,24 +84,22 @@ export const RoleConfigure = () => {
 | 
				
			|||||||
              if (!form.modified) {
 | 
					              if (!form.modified) {
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
              await api.resource('roles').update({
 | 
					              await update(field, form);
 | 
				
			||||||
                filterByTk: record.name,
 | 
					 | 
				
			||||||
                values: form.values,
 | 
					 | 
				
			||||||
              });
 | 
					 | 
				
			||||||
              message.success(t('Saved successfully'));
 | 
					 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        properties: {
 | 
					        properties: {
 | 
				
			||||||
          allowConfigure: {
 | 
					          snippets: {
 | 
				
			||||||
            title: t('Configure permissions'),
 | 
					            title: t('Configure permissions'),
 | 
				
			||||||
 | 
					            type: 'boolean',
 | 
				
			||||||
            'x-decorator': 'FormItem',
 | 
					            'x-decorator': 'FormItem',
 | 
				
			||||||
            'x-component': 'Checkbox',
 | 
					            'x-component': 'SnippetCheckboxGroup',
 | 
				
			||||||
            'x-content': t('Allows configuration of the whole system, including UI, collections, permissions, etc.'),
 | 
					 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          'strategy.actions': {
 | 
					          'strategy.actions': {
 | 
				
			||||||
            title: t('Global action permissions'),
 | 
					            title: t('Global action permissions'),
 | 
				
			||||||
            description: t('All collections use general action permissions by default; permission configured individually will override the default one.'),
 | 
					            description: t(
 | 
				
			||||||
 | 
					              'All collections use general action permissions by default; permission configured individually will override the default one.',
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
            'x-component': 'StrategyActions',
 | 
					            'x-component': 'StrategyActions',
 | 
				
			||||||
            'x-decorator': 'FormItem',
 | 
					            'x-decorator': 'FormItem',
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
				
			|||||||
@ -2,10 +2,11 @@ import { Spin } from 'antd';
 | 
				
			|||||||
import React, { createContext, useContext } from 'react';
 | 
					import React, { createContext, useContext } from 'react';
 | 
				
			||||||
import { useRequest } from '../../api-client';
 | 
					import { useRequest } from '../../api-client';
 | 
				
			||||||
import { SchemaComponent } from '../../schema-component';
 | 
					import { SchemaComponent } from '../../schema-component';
 | 
				
			||||||
import { roleSchema } from './schemas/roles';
 | 
					 | 
				
			||||||
import { MenuItemsProvider } from '../Configuration/MenuItemsProvider';
 | 
					import { MenuItemsProvider } from '../Configuration/MenuItemsProvider';
 | 
				
			||||||
 | 
					import { PermissionProvider, SettingCenterPermissionProvider } from '../Configuration/PermisionProvider';
 | 
				
			||||||
 | 
					import { roleSchema } from './schemas/roles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const AvailableActionsContext = createContext(null);
 | 
					const AvailableActionsContext = createContext([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const AvailableActionsProver: React.FC = (props) => {
 | 
					const AvailableActionsProver: React.FC = (props) => {
 | 
				
			||||||
  const { data, loading } = useRequest({
 | 
					  const { data, loading } = useRequest({
 | 
				
			||||||
@ -26,7 +27,7 @@ export const RoleTable = () => {
 | 
				
			|||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
      <AvailableActionsProver>
 | 
					      <AvailableActionsProver>
 | 
				
			||||||
        <SchemaComponent schema={roleSchema} components={{ MenuItemsProvider }} />
 | 
					        <SchemaComponent schema={roleSchema} components={{ MenuItemsProvider, SettingCenterPermissionProvider,PermissionProvider }} />
 | 
				
			||||||
      </AvailableActionsProver>
 | 
					      </AvailableActionsProver>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
				
			|||||||
@ -37,8 +37,8 @@ export const RolesResourcesActions = connect((props) => {
 | 
				
			|||||||
  const roleCollection = useRecord();
 | 
					  const roleCollection = useRecord();
 | 
				
			||||||
  const availableActions = useAvailableActions();
 | 
					  const availableActions = useAvailableActions();
 | 
				
			||||||
  const { getCollection, getCollectionFields } = useCollectionManager();
 | 
					  const { getCollection, getCollectionFields } = useCollectionManager();
 | 
				
			||||||
  const collection = getCollection(roleCollection.name);
 | 
					  const collection = getCollection(roleCollection.collectionName);
 | 
				
			||||||
  const collectionFields = getCollectionFields(roleCollection.name);
 | 
					  const collectionFields = getCollectionFields(roleCollection.collectionName);
 | 
				
			||||||
  const compile = useCompile();
 | 
					  const compile = useCompile();
 | 
				
			||||||
  const { t } = useTranslation();
 | 
					  const { t } = useTranslation();
 | 
				
			||||||
  const field = useField<ArrayField>();
 | 
					  const field = useField<ArrayField>();
 | 
				
			||||||
 | 
				
			|||||||
@ -3,4 +3,4 @@ export { RoleConfigure } from './RoleConfigure';
 | 
				
			|||||||
export { RolesResourcesActions } from './RolesResourcesActions';
 | 
					export { RolesResourcesActions } from './RolesResourcesActions';
 | 
				
			||||||
export { RoleTable } from './RoleTable';
 | 
					export { RoleTable } from './RoleTable';
 | 
				
			||||||
export { StrategyActions } from './StrategyActions';
 | 
					export { StrategyActions } from './StrategyActions';
 | 
				
			||||||
 | 
					export { SettingsCenterConfigure,SettingCenterProvider } from './ConfigureCenter';
 | 
				
			||||||
 | 
				
			|||||||
@ -18,17 +18,30 @@ const collection = {
 | 
				
			|||||||
        required: true,
 | 
					        required: true,
 | 
				
			||||||
      } as ISchema,
 | 
					      } as ISchema,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    // {
 | 
					    {
 | 
				
			||||||
    //   type: 'string',
 | 
					      type: 'string',
 | 
				
			||||||
    //   name: 'name',
 | 
					      name: 'name',
 | 
				
			||||||
    //   interface: 'input',
 | 
					      interface: 'input',
 | 
				
			||||||
    //   uiSchema: {
 | 
					      uiSchema: {
 | 
				
			||||||
    //     title: '数据表标识',
 | 
					        title: '{{t("Collection name")}}',
 | 
				
			||||||
    //     type: 'string',
 | 
					        type: 'string',
 | 
				
			||||||
    //     'x-component': 'Input',
 | 
					        'x-component': 'Input',
 | 
				
			||||||
    //     description: '使用英文',
 | 
					      } as ISchema,
 | 
				
			||||||
    //   } as ISchema,
 | 
					    },
 | 
				
			||||||
    // },
 | 
					    {
 | 
				
			||||||
 | 
					      type: 'string',
 | 
				
			||||||
 | 
					      name: 'type',
 | 
				
			||||||
 | 
					      interface: 'input',
 | 
				
			||||||
 | 
					      uiSchema: {
 | 
				
			||||||
 | 
					        title: '{{t("Resource type")}}',
 | 
				
			||||||
 | 
					        type: 'string',
 | 
				
			||||||
 | 
					        'x-component': 'Select',
 | 
				
			||||||
 | 
					        enum: [
 | 
				
			||||||
 | 
					          { label: '{{t("Collection")}}', value: 'collection', color: 'green' },
 | 
				
			||||||
 | 
					          { label: '{{t("Association")}}', value: 'association', color: 'blue' },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      } as ISchema,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      type: 'string',
 | 
					      type: 'string',
 | 
				
			||||||
      name: 'usingConfig',
 | 
					      name: 'usingConfig',
 | 
				
			||||||
@ -89,7 +102,7 @@ export const roleCollectionsSchema: ISchema = {
 | 
				
			|||||||
        useDataSource: '{{ cm.useDataSourceFromRAC }}',
 | 
					        useDataSource: '{{ cm.useDataSourceFromRAC }}',
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      properties: {
 | 
					      properties: {
 | 
				
			||||||
        column1: {
 | 
					        column0: {
 | 
				
			||||||
          type: 'void',
 | 
					          type: 'void',
 | 
				
			||||||
          'x-decorator': 'Table.Column.Decorator',
 | 
					          'x-decorator': 'Table.Column.Decorator',
 | 
				
			||||||
          'x-component': 'Table.Column',
 | 
					          'x-component': 'Table.Column',
 | 
				
			||||||
@ -101,6 +114,18 @@ export const roleCollectionsSchema: ISchema = {
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        // column1: {
 | 
				
			||||||
 | 
					        //   type: 'void',
 | 
				
			||||||
 | 
					        //   'x-decorator': 'Table.Column.Decorator',
 | 
				
			||||||
 | 
					        //   'x-component': 'Table.Column',
 | 
				
			||||||
 | 
					        //   properties: {
 | 
				
			||||||
 | 
					        //     type: {
 | 
				
			||||||
 | 
					        //       type: 'string',
 | 
				
			||||||
 | 
					        //       'x-component': 'CollectionField',
 | 
				
			||||||
 | 
					        //       'x-read-pretty': true,
 | 
				
			||||||
 | 
					        //     },
 | 
				
			||||||
 | 
					        //   },
 | 
				
			||||||
 | 
					        // },
 | 
				
			||||||
        column2: {
 | 
					        column2: {
 | 
				
			||||||
          type: 'void',
 | 
					          type: 'void',
 | 
				
			||||||
          'x-decorator': 'Table.Column.Decorator',
 | 
					          'x-decorator': 'Table.Column.Decorator',
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,9 @@
 | 
				
			|||||||
import { ISchema } from '@formily/react';
 | 
					import { ISchema } from '@formily/react';
 | 
				
			||||||
import { uid } from '@formily/shared';
 | 
					import { uid } from '@formily/shared';
 | 
				
			||||||
 | 
					import pick from 'lodash/pick';
 | 
				
			||||||
 | 
					import { useEffect } from 'react';
 | 
				
			||||||
import { useRequest } from '../../../api-client';
 | 
					import { useRequest } from '../../../api-client';
 | 
				
			||||||
 | 
					import { useRecord } from '../../../record-provider';
 | 
				
			||||||
import { useActionContext } from '../../../schema-component';
 | 
					import { useActionContext } from '../../../schema-component';
 | 
				
			||||||
import { roleCollectionsSchema } from './roleCollections';
 | 
					import { roleCollectionsSchema } from './roleCollections';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -112,6 +115,7 @@ export const roleSchema: ISchema = {
 | 
				
			|||||||
                          Promise.resolve({
 | 
					                          Promise.resolve({
 | 
				
			||||||
                            data: {
 | 
					                            data: {
 | 
				
			||||||
                              name: `r_${uid()}`,
 | 
					                              name: `r_${uid()}`,
 | 
				
			||||||
 | 
					                              snippets: ['!ui.*', '!pm', '!pm.*'],
 | 
				
			||||||
                            },
 | 
					                            },
 | 
				
			||||||
                          }),
 | 
					                          }),
 | 
				
			||||||
                        { ...options, refreshDeps: [ctx.visible] },
 | 
					                        { ...options, refreshDeps: [ctx.visible] },
 | 
				
			||||||
@ -227,11 +231,14 @@ export const roleSchema: ISchema = {
 | 
				
			|||||||
                      type: 'void',
 | 
					                      type: 'void',
 | 
				
			||||||
                      title: '{{t("Configure")}}',
 | 
					                      title: '{{t("Configure")}}',
 | 
				
			||||||
                      'x-component': 'Action.Link',
 | 
					                      'x-component': 'Action.Link',
 | 
				
			||||||
 | 
					                      'x-decorator': 'ACLActionProvider',
 | 
				
			||||||
 | 
					                      'x-acl-action': 'roles:update',
 | 
				
			||||||
                      'x-component-props': {},
 | 
					                      'x-component-props': {},
 | 
				
			||||||
                      properties: {
 | 
					                      properties: {
 | 
				
			||||||
                        drawer: {
 | 
					                        drawer: {
 | 
				
			||||||
                          type: 'void',
 | 
					                          type: 'void',
 | 
				
			||||||
                          'x-component': 'Action.Drawer',
 | 
					                          'x-component': 'Action.Drawer',
 | 
				
			||||||
 | 
					                          'x-decorator': 'PermissionProvider',
 | 
				
			||||||
                          title: '{{t("Configure permissions")}}',
 | 
					                          title: '{{t("Configure permissions")}}',
 | 
				
			||||||
                          properties: {
 | 
					                          properties: {
 | 
				
			||||||
                            tabs1: {
 | 
					                            tabs1: {
 | 
				
			||||||
@ -271,6 +278,19 @@ export const roleSchema: ISchema = {
 | 
				
			|||||||
                                    },
 | 
					                                    },
 | 
				
			||||||
                                  },
 | 
					                                  },
 | 
				
			||||||
                                },
 | 
					                                },
 | 
				
			||||||
 | 
					                                tab4: {
 | 
				
			||||||
 | 
					                                  type: 'void',
 | 
				
			||||||
 | 
					                                  title: '{{t("Settings center permissions")}}',
 | 
				
			||||||
 | 
					                                  'x-decorator': 'SettingCenterPermissionProvider',
 | 
				
			||||||
 | 
					                                  'x-component': 'Tabs.TabPane',
 | 
				
			||||||
 | 
					                                  'x-component-props': {},
 | 
				
			||||||
 | 
					                                  properties: {
 | 
				
			||||||
 | 
					                                    menu: {
 | 
				
			||||||
 | 
					                                      'x-decorator': 'SettingCenterProvider',
 | 
				
			||||||
 | 
					                                      'x-component': 'SettingsCenterConfigure',
 | 
				
			||||||
 | 
					                                    },
 | 
				
			||||||
 | 
					                                  },
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
                              },
 | 
					                              },
 | 
				
			||||||
                            },
 | 
					                            },
 | 
				
			||||||
                          },
 | 
					                          },
 | 
				
			||||||
@ -280,6 +300,8 @@ export const roleSchema: ISchema = {
 | 
				
			|||||||
                    update: {
 | 
					                    update: {
 | 
				
			||||||
                      type: 'void',
 | 
					                      type: 'void',
 | 
				
			||||||
                      title: '{{t("Edit")}}',
 | 
					                      title: '{{t("Edit")}}',
 | 
				
			||||||
 | 
					                      'x-decorator': 'ACLActionProvider',
 | 
				
			||||||
 | 
					                      'x-acl-action': 'roles:update',
 | 
				
			||||||
                      'x-component': 'Action.Link',
 | 
					                      'x-component': 'Action.Link',
 | 
				
			||||||
                      'x-component-props': {
 | 
					                      'x-component-props': {
 | 
				
			||||||
                        type: 'primary',
 | 
					                        type: 'primary',
 | 
				
			||||||
@ -290,7 +312,23 @@ export const roleSchema: ISchema = {
 | 
				
			|||||||
                          'x-component': 'Action.Drawer',
 | 
					                          'x-component': 'Action.Drawer',
 | 
				
			||||||
                          'x-decorator': 'Form',
 | 
					                          'x-decorator': 'Form',
 | 
				
			||||||
                          'x-decorator-props': {
 | 
					                          'x-decorator-props': {
 | 
				
			||||||
                            useValues: '{{ cm.useValuesFromRecord }}',
 | 
					                            useValues: (options) => {
 | 
				
			||||||
 | 
					                              const record = useRecord();
 | 
				
			||||||
 | 
					                              const result = useRequest(
 | 
				
			||||||
 | 
					                                () => Promise.resolve({ data: pick(record, ['title', 'name', 'default']) }),
 | 
				
			||||||
 | 
					                                {
 | 
				
			||||||
 | 
					                                  ...options,
 | 
				
			||||||
 | 
					                                  manual: true,
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                              );
 | 
				
			||||||
 | 
					                              const ctx = useActionContext();
 | 
				
			||||||
 | 
					                              useEffect(() => {
 | 
				
			||||||
 | 
					                                if (ctx.visible) {
 | 
				
			||||||
 | 
					                                  result.run();
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                              }, [ctx.visible]);
 | 
				
			||||||
 | 
					                              return result;
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
                          },
 | 
					                          },
 | 
				
			||||||
                          title: '{{t("Edit role")}}',
 | 
					                          title: '{{t("Edit role")}}',
 | 
				
			||||||
                          properties: {
 | 
					                          properties: {
 | 
				
			||||||
@ -337,6 +375,8 @@ export const roleSchema: ISchema = {
 | 
				
			|||||||
                    delete: {
 | 
					                    delete: {
 | 
				
			||||||
                      type: 'void',
 | 
					                      type: 'void',
 | 
				
			||||||
                      title: '{{ t("Delete") }}',
 | 
					                      title: '{{ t("Delete") }}',
 | 
				
			||||||
 | 
					                      'x-acl-action': 'roles:destroy',
 | 
				
			||||||
 | 
					                      'x-decorator': 'ACLActionProvider',
 | 
				
			||||||
                      'x-component': 'Action.Link',
 | 
					                      'x-component': 'Action.Link',
 | 
				
			||||||
                      'x-component-props': {
 | 
					                      'x-component-props': {
 | 
				
			||||||
                        confirm: {
 | 
					                        confirm: {
 | 
				
			||||||
 | 
				
			|||||||
@ -222,6 +222,7 @@ export const scopesSchema: ISchema = {
 | 
				
			|||||||
                              type: 'void',
 | 
					                              type: 'void',
 | 
				
			||||||
                              title: '{{ t("Edit") }}',
 | 
					                              title: '{{ t("Edit") }}',
 | 
				
			||||||
                              'x-action': 'update',
 | 
					                              'x-action': 'update',
 | 
				
			||||||
 | 
					                              'x-decorator': 'ACLActionProvider',
 | 
				
			||||||
                              'x-component': 'Action.Link',
 | 
					                              'x-component': 'Action.Link',
 | 
				
			||||||
                              'x-component-props': {
 | 
					                              'x-component-props': {
 | 
				
			||||||
                                openMode: 'drawer',
 | 
					                                openMode: 'drawer',
 | 
				
			||||||
@ -308,6 +309,7 @@ export const scopesSchema: ISchema = {
 | 
				
			|||||||
                            destroy: {
 | 
					                            destroy: {
 | 
				
			||||||
                              title: '{{ t("Delete") }}',
 | 
					                              title: '{{ t("Delete") }}',
 | 
				
			||||||
                              'x-action': 'destroy',
 | 
					                              'x-action': 'destroy',
 | 
				
			||||||
 | 
					                              'x-decorator': 'ACLActionProvider',
 | 
				
			||||||
                              'x-component': 'Action.Link',
 | 
					                              'x-component': 'Action.Link',
 | 
				
			||||||
                              'x-designer': 'Action.Designer',
 | 
					                              'x-designer': 'Action.Designer',
 | 
				
			||||||
                              'x-component-props': {
 | 
					                              'x-component-props': {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,4 @@
 | 
				
			|||||||
export * from './ACLProvider';
 | 
					export * from './ACLProvider';
 | 
				
			||||||
export * from './ACLShortcut';
 | 
					export * from './ACLShortcut';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import './style.less';
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										10
									
								
								packages/core/client/src/acl/style.less
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								packages/core/client/src/acl/style.less
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					.ant-table-cell {
 | 
				
			||||||
 | 
					  > .ant-space-horizontal {
 | 
				
			||||||
 | 
					    .ant-space-item:empty:not(:last-child) + .ant-space-item-split {
 | 
				
			||||||
 | 
					      display: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .ant-space-item-split:has(+ .ant-space-item:empty) {
 | 
				
			||||||
 | 
					      display: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -22,6 +22,10 @@ export class APIClient extends APIClientSDK {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interceptors() {
 | 
					  interceptors() {
 | 
				
			||||||
 | 
					    this.axios.interceptors.request.use((config) => {
 | 
				
			||||||
 | 
					      config.headers['X-With-ACL-Meta'] = true;
 | 
				
			||||||
 | 
					      return config;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
    super.interceptors();
 | 
					    super.interceptors();
 | 
				
			||||||
    this.notification();
 | 
					    this.notification();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -37,11 +41,11 @@ export class APIClient extends APIClientSDK {
 | 
				
			|||||||
        if (error.response.data.type === 'application/json') {
 | 
					        if (error.response.data.type === 'application/json') {
 | 
				
			||||||
          handleErrorMessage(error);
 | 
					          handleErrorMessage(error);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            notification.error({
 | 
					          notification.error({
 | 
				
			||||||
              message: error?.response?.data?.errors?.map?.((error: any) => {
 | 
					            message: error?.response?.data?.errors?.map?.((error: any) => {
 | 
				
			||||||
                return React.createElement('div', { children: error.message });
 | 
					              return React.createElement('div', { children: error.message });
 | 
				
			||||||
              }),
 | 
					            }),
 | 
				
			||||||
            });
 | 
					          });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        throw error;
 | 
					        throw error;
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
				
			|||||||
@ -120,11 +120,11 @@ export const useKanbanBlockContext = () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const useDisableCardDrag = () => {
 | 
					const useDisableCardDrag = () => {
 | 
				
			||||||
  const ctx = useKanbanBlockContext();
 | 
					  const ctx = useKanbanBlockContext();
 | 
				
			||||||
  const { allowAll, allowConfigure, getActionParams } = useACLRoleContext();
 | 
					  const { allowAll, allowConfigure, parseAction } = useACLRoleContext();
 | 
				
			||||||
  if (allowAll || allowConfigure) {
 | 
					  if (allowAll || allowConfigure) {
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  const result = getActionParams(`${ctx?.props?.resource}:update`, { skipOwnCheck: true });
 | 
					  const result = parseAction(`${ctx?.props?.resource}:update`, { ignoreScope: true });
 | 
				
			||||||
  return !result;
 | 
					  return !result;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -21,8 +21,8 @@ export const useCollection = () => {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    [],
 | 
					    [],
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  const totalFields = unionBy(currentFields?.concat(inheritedFields),'name').filter((v)=>{
 | 
					  const totalFields = unionBy(currentFields?.concat(inheritedFields), 'name').filter((v) => {
 | 
				
			||||||
    return !v.isForeignKey
 | 
					    return !v.isForeignKey;
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    ...collection,
 | 
					    ...collection,
 | 
				
			||||||
@ -32,7 +32,14 @@ export const useCollection = () => {
 | 
				
			|||||||
      return fields?.find((field) => field.name === name);
 | 
					      return fields?.find((field) => field.name === name);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    fields: totalFields,
 | 
					    fields: totalFields,
 | 
				
			||||||
 | 
					    getPrimaryKey: () => {
 | 
				
			||||||
 | 
					      if (collection.targetKey || collection.filterTargetKey) {
 | 
				
			||||||
 | 
					        return collection.targetKey || collection.filterTargetKey;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const field = currentFields.find((field) => field.primaryKey);
 | 
				
			||||||
 | 
					      return field ? field.name : 'id';
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    currentFields,
 | 
					    currentFields,
 | 
				
			||||||
    inheritedFields
 | 
					    inheritedFields,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,7 @@ import {
 | 
				
			|||||||
  recordPickerSelector,
 | 
					  recordPickerSelector,
 | 
				
			||||||
  recordPickerViewer,
 | 
					  recordPickerViewer,
 | 
				
			||||||
  relationshipType,
 | 
					  relationshipType,
 | 
				
			||||||
  reverseFieldProperties,
 | 
					  reverseFieldProperties
 | 
				
			||||||
} from './properties';
 | 
					} from './properties';
 | 
				
			||||||
import { IField } from './types';
 | 
					import { IField } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -59,6 +59,7 @@ export const o2m: IField = {
 | 
				
			|||||||
          block: {
 | 
					          block: {
 | 
				
			||||||
            type: 'void',
 | 
					            type: 'void',
 | 
				
			||||||
            'x-decorator': 'TableFieldProvider',
 | 
					            'x-decorator': 'TableFieldProvider',
 | 
				
			||||||
 | 
					            'x-acl-action': `${association}:list`,
 | 
				
			||||||
            'x-decorator-props': {
 | 
					            'x-decorator-props': {
 | 
				
			||||||
              collection: field.target,
 | 
					              collection: field.target,
 | 
				
			||||||
              association: association,
 | 
					              association: association,
 | 
				
			||||||
 | 
				
			|||||||
@ -28,6 +28,7 @@ export const subTable: IField = {
 | 
				
			|||||||
      block: {
 | 
					      block: {
 | 
				
			||||||
        type: 'void',
 | 
					        type: 'void',
 | 
				
			||||||
        'x-decorator': 'TableFieldProvider',
 | 
					        'x-decorator': 'TableFieldProvider',
 | 
				
			||||||
 | 
					        'x-acl-action': `${field.target}:list`,
 | 
				
			||||||
        'x-decorator-props': {
 | 
					        'x-decorator-props': {
 | 
				
			||||||
          collection: field.target,
 | 
					          collection: field.target,
 | 
				
			||||||
          association: association,
 | 
					          association: association,
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,11 @@ export const calendar: ICollectionTemplate = {
 | 
				
			|||||||
  order: 2,
 | 
					  order: 2,
 | 
				
			||||||
  color: 'orange',
 | 
					  color: 'orange',
 | 
				
			||||||
  default: {
 | 
					  default: {
 | 
				
			||||||
 | 
					    createdBy: true,
 | 
				
			||||||
 | 
					    updatedBy: true,
 | 
				
			||||||
 | 
					    createdAt: true,
 | 
				
			||||||
 | 
					    updatedAt: true,
 | 
				
			||||||
 | 
					    sortable: true,
 | 
				
			||||||
    fields: [
 | 
					    fields: [
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        name: 'cron',
 | 
					        name: 'cron',
 | 
				
			||||||
 | 
				
			|||||||
@ -419,6 +419,10 @@ export default {
 | 
				
			|||||||
  "General permissions": "General permissions",
 | 
					  "General permissions": "General permissions",
 | 
				
			||||||
  "Global action permissions": "Global action permissions",
 | 
					  "Global action permissions": "Global action permissions",
 | 
				
			||||||
  "General action permissions": "General action permissions",
 | 
					  "General action permissions": "General action permissions",
 | 
				
			||||||
 | 
					  "Settings center permissions":"Settings center permissions",
 | 
				
			||||||
 | 
					  "Allow to desgin pages":"Allow to desgin pages",
 | 
				
			||||||
 | 
					  "Allow to manage plugins":"Allow to manage plugins",
 | 
				
			||||||
 | 
					  "Allow to configure plugins":"Allow to configure plugins",
 | 
				
			||||||
  "Action display name": "Action display name",
 | 
					  "Action display name": "Action display name",
 | 
				
			||||||
  "Allow": "Allow",
 | 
					  "Allow": "Allow",
 | 
				
			||||||
  "Data scope": "Data scope",
 | 
					  "Data scope": "Data scope",
 | 
				
			||||||
 | 
				
			|||||||
@ -402,6 +402,10 @@ export default {
 | 
				
			|||||||
  "General permissions": "一般設定",
 | 
					  "General permissions": "一般設定",
 | 
				
			||||||
  "Global action permissions": "グローバル操作権限",
 | 
					  "Global action permissions": "グローバル操作権限",
 | 
				
			||||||
  "General action permissions": "一般操作権限",
 | 
					  "General action permissions": "一般操作権限",
 | 
				
			||||||
 | 
					  "Settings center permissions":"中央権限の設定",
 | 
				
			||||||
 | 
					  'Allow to desgin pages':"インタフェース構成の許可",
 | 
				
			||||||
 | 
					  "Allow to manage plugins":"管理プラグインの許可",
 | 
				
			||||||
 | 
					  "Allow to configure plugins":"管理構成センターの許可",
 | 
				
			||||||
  "Action display name": "操作名",
 | 
					  "Action display name": "操作名",
 | 
				
			||||||
  "Allow": "許可する",
 | 
					  "Allow": "許可する",
 | 
				
			||||||
  "Data scope": "レコードスコープ",
 | 
					  "Data scope": "レコードスコープ",
 | 
				
			||||||
 | 
				
			|||||||
@ -496,6 +496,13 @@ export default {
 | 
				
			|||||||
  'General permissions': '通用配置',
 | 
					  'General permissions': '通用配置',
 | 
				
			||||||
  'Global action permissions': '全局操作权限',
 | 
					  'Global action permissions': '全局操作权限',
 | 
				
			||||||
  'General action permissions': '通用操作权限',
 | 
					  'General action permissions': '通用操作权限',
 | 
				
			||||||
 | 
					  "Settings center permissions":"配置中心权限",
 | 
				
			||||||
 | 
					  'Allow to desgin pages':"允许界面配置",
 | 
				
			||||||
 | 
					  "Allow to manage plugins":"允许管理插件",
 | 
				
			||||||
 | 
					  "Allow to configure plugins":"允许管理配置中心",
 | 
				
			||||||
 | 
					  'Allows to configure interface': '允许配置界面',
 | 
				
			||||||
 | 
					  'Allows to install, activate, disable plugins': '允许安装、激活、禁用插件',
 | 
				
			||||||
 | 
					  'Allows to configure plugins': '允许配置插件',
 | 
				
			||||||
  'Action display name': '操作名称',
 | 
					  'Action display name': '操作名称',
 | 
				
			||||||
  'Allow': '允许',
 | 
					  'Allow': '允许',
 | 
				
			||||||
  'Data scope': '数据范围',
 | 
					  'Data scope': '数据范围',
 | 
				
			||||||
@ -614,5 +621,7 @@ export default {
 | 
				
			|||||||
  "Search and select collection": "搜索并选择数据表",
 | 
					  "Search and select collection": "搜索并选择数据表",
 | 
				
			||||||
  'Please fill in the iframe URL': '请填写嵌入的地址',
 | 
					  'Please fill in the iframe URL': '请填写嵌入的地址',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  'Fix block': '固定区块'
 | 
					  'Fix block': '固定区块',
 | 
				
			||||||
 | 
					  'Plugin name': '插件',
 | 
				
			||||||
 | 
					  'Plugin tab name': '插件标签页',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@ import { get } from 'lodash';
 | 
				
			|||||||
import React, { createContext, useContext } from 'react';
 | 
					import React, { createContext, useContext } from 'react';
 | 
				
			||||||
import { useTranslation } from 'react-i18next';
 | 
					import { useTranslation } from 'react-i18next';
 | 
				
			||||||
import { useHistory } from 'react-router-dom';
 | 
					import { useHistory } from 'react-router-dom';
 | 
				
			||||||
 | 
					import { useACLRoleContext } from '../acl/ACLProvider';
 | 
				
			||||||
import { PluginManagerContext } from './context';
 | 
					import { PluginManagerContext } from './context';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const usePrefixCls = (
 | 
					export const usePrefixCls = (
 | 
				
			||||||
@ -142,19 +143,15 @@ PluginManager.Toolbar.Item = (props) => {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const RemotePluginManagerToolbar = () => {
 | 
					export const RemotePluginManagerToolbar = () => {
 | 
				
			||||||
  // const api = useAPIClient();
 | 
					  const { allowAll, snippets } = useACLRoleContext();
 | 
				
			||||||
  // const { data, loading } = useRequest({
 | 
					  const getSnippetsAllow = (aclKey) => {
 | 
				
			||||||
  //   resource: 'plugins',
 | 
					    return allowAll || snippets?.includes(aclKey);
 | 
				
			||||||
  //   action: 'getPinned',
 | 
					  };
 | 
				
			||||||
  // });
 | 
					 | 
				
			||||||
  // if (loading) {
 | 
					 | 
				
			||||||
  //   return <Spin />;
 | 
					 | 
				
			||||||
  // }
 | 
					 | 
				
			||||||
  const items = [
 | 
					  const items = [
 | 
				
			||||||
    { component: 'DesignableSwitch', pin: true },
 | 
					    { component: 'DesignableSwitch', pin: true, isAllow: getSnippetsAllow('ui.*') },
 | 
				
			||||||
    { component: 'PluginManagerLink', pin: true },
 | 
					    { component: 'PluginManagerLink', pin: true, isAllow: getSnippetsAllow('pm') },
 | 
				
			||||||
    { component: 'SettingsCenterDropdown', pin: true },
 | 
					    { component: 'SettingsCenterDropdown', pin: true, isAllow: getSnippetsAllow('pm.*') },
 | 
				
			||||||
    // ...data?.data,
 | 
					    // ...data?.data,
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
  return <PluginManager.Toolbar items={items} />;
 | 
					  return <PluginManager.Toolbar items={items.filter((v) => v.isAllow)} />;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -2,27 +2,14 @@ import { AppstoreAddOutlined, SettingOutlined } from '@ant-design/icons';
 | 
				
			|||||||
import { ISchema } from '@formily/react';
 | 
					import { ISchema } from '@formily/react';
 | 
				
			||||||
import { uid } from '@formily/shared';
 | 
					import { uid } from '@formily/shared';
 | 
				
			||||||
import { Dropdown, Menu } from 'antd';
 | 
					import { Dropdown, Menu } from 'antd';
 | 
				
			||||||
import React, { useState } from 'react';
 | 
					import React, { useState, useContext } from 'react';
 | 
				
			||||||
import { useTranslation } from 'react-i18next';
 | 
					import { useTranslation } from 'react-i18next';
 | 
				
			||||||
import { useHistory } from 'react-router-dom';
 | 
					import { useHistory } from 'react-router-dom';
 | 
				
			||||||
import { PluginManager } from '../plugin-manager';
 | 
					import { PluginManager } from '../plugin-manager';
 | 
				
			||||||
 | 
					import { useCompile } from '../schema-component';
 | 
				
			||||||
import { ActionContext } from '../schema-component';
 | 
					import { ActionContext } from '../schema-component';
 | 
				
			||||||
 | 
					import { useACLRoleContext } from '../acl/ACLProvider';
 | 
				
			||||||
const schema: ISchema = {
 | 
					import { getPluginsTabs, SettingsCenterContext } from './index';
 | 
				
			||||||
  type: 'object',
 | 
					 | 
				
			||||||
  properties: {
 | 
					 | 
				
			||||||
    [uid()]: {
 | 
					 | 
				
			||||||
      'x-component': 'Action.Drawer',
 | 
					 | 
				
			||||||
      type: 'void',
 | 
					 | 
				
			||||||
      title: '{{t("Collections & Fields")}}',
 | 
					 | 
				
			||||||
      properties: {
 | 
					 | 
				
			||||||
        configuration: {
 | 
					 | 
				
			||||||
          'x-component': 'ConfigurationTable',
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const PluginManagerLink = () => {
 | 
					export const PluginManagerLink = () => {
 | 
				
			||||||
  const [visible, setVisible] = useState(false);
 | 
					  const [visible, setVisible] = useState(false);
 | 
				
			||||||
@ -41,51 +28,41 @@ export const PluginManagerLink = () => {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getBookmarkTabs = (data) => {
 | 
				
			||||||
 | 
					  const bookmarkTabs = [];
 | 
				
			||||||
 | 
					  data.forEach((plugin) => {
 | 
				
			||||||
 | 
					    const tabs = plugin.tabs;
 | 
				
			||||||
 | 
					    tabs.forEach((tab) => {
 | 
				
			||||||
 | 
					      tab.isBookmark && tab.isAllow && bookmarkTabs.push({ ...tab, path: `${plugin.key}/${tab.key}` });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  return bookmarkTabs;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
export const SettingsCenterDropdown = () => {
 | 
					export const SettingsCenterDropdown = () => {
 | 
				
			||||||
 | 
					  const { snippets = [] } = useACLRoleContext();
 | 
				
			||||||
  const [visible, setVisible] = useState(false);
 | 
					  const [visible, setVisible] = useState(false);
 | 
				
			||||||
  const { t } = useTranslation();
 | 
					  const { t } = useTranslation();
 | 
				
			||||||
 | 
					  const compile = useCompile();
 | 
				
			||||||
  const history = useHistory();
 | 
					  const history = useHistory();
 | 
				
			||||||
  const items = [
 | 
					  const itemData = useContext(SettingsCenterContext);
 | 
				
			||||||
    {
 | 
					  const pluginsTabs = getPluginsTabs(itemData, snippets);
 | 
				
			||||||
      title: t('Collections & Fields'),
 | 
					  const bookmarkTabs = getBookmarkTabs(pluginsTabs);
 | 
				
			||||||
      path: 'collection-manager/collections',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      title: t('Roles & Permissions'),
 | 
					 | 
				
			||||||
      path: 'acl/roles',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      title: t('File storages'),
 | 
					 | 
				
			||||||
      path: 'file-manager/storages',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      title: t('System settings'),
 | 
					 | 
				
			||||||
      path: 'system-settings/system-settings',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      title: t('workflow:Workflow'),
 | 
					 | 
				
			||||||
      path: 'workflow/workflows',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    // {
 | 
					 | 
				
			||||||
    //   title: t('Graph Collections'),
 | 
					 | 
				
			||||||
    //   path: 'graph/collections',
 | 
					 | 
				
			||||||
    // },
 | 
					 | 
				
			||||||
  ];
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <ActionContext.Provider value={{ visible, setVisible }}>
 | 
					    <ActionContext.Provider value={{ visible, setVisible }}>
 | 
				
			||||||
      <Dropdown
 | 
					      <Dropdown
 | 
				
			||||||
 | 
					        placement="bottom"
 | 
				
			||||||
        overlay={
 | 
					        overlay={
 | 
				
			||||||
          <Menu>
 | 
					          <Menu>
 | 
				
			||||||
            <Menu.ItemGroup title={t('Bookmark')}>
 | 
					            <Menu.ItemGroup title={t('Bookmark')}>
 | 
				
			||||||
              {items.map((item) => {
 | 
					              {bookmarkTabs.map((tab) => {
 | 
				
			||||||
                return (
 | 
					                return (
 | 
				
			||||||
                  <Menu.Item
 | 
					                  <Menu.Item
 | 
				
			||||||
                    onClick={() => {
 | 
					                    onClick={() => {
 | 
				
			||||||
                      history.push('/admin/settings/' + item.path);
 | 
					                      history.push('/admin/settings/' + tab.path);
 | 
				
			||||||
                    }}
 | 
					                    }}
 | 
				
			||||||
                    key={item.path}
 | 
					                    key={tab.path}
 | 
				
			||||||
                  >
 | 
					                  >
 | 
				
			||||||
                    {item.title}
 | 
					                    {compile(tab.title)}
 | 
				
			||||||
                  </Menu.Item>
 | 
					                  </Menu.Item>
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
              })}
 | 
					              })}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,12 @@
 | 
				
			|||||||
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons';
 | 
					import { DeleteOutlined, SettingOutlined } from '@ant-design/icons';
 | 
				
			||||||
import { css } from '@emotion/css';
 | 
					import { css } from '@emotion/css';
 | 
				
			||||||
import { Avatar, Card, Layout, Menu, message, PageHeader, Popconfirm, Spin, Switch, Tabs } from 'antd';
 | 
					import { Avatar, Card, Layout, Menu, message, PageHeader, Popconfirm, Result, Spin, Switch, Tabs } from 'antd';
 | 
				
			||||||
 | 
					import { sortBy } from 'lodash';
 | 
				
			||||||
import React, { createContext, useContext, useMemo } from 'react';
 | 
					import React, { createContext, useContext, useMemo } from 'react';
 | 
				
			||||||
import { useTranslation } from 'react-i18next';
 | 
					import { useTranslation } from 'react-i18next';
 | 
				
			||||||
import { Redirect, useHistory, useRouteMatch } from 'react-router-dom';
 | 
					import { Redirect, useHistory, useRouteMatch } from 'react-router-dom';
 | 
				
			||||||
import { ACLPane } from '../acl';
 | 
					import { ACLPane } from '../acl';
 | 
				
			||||||
 | 
					import { useACLRoleContext } from '../acl/ACLProvider';
 | 
				
			||||||
import { useAPIClient, useRequest } from '../api-client';
 | 
					import { useAPIClient, useRequest } from '../api-client';
 | 
				
			||||||
import { CollectionManagerPane } from '../collection-manager';
 | 
					import { CollectionManagerPane } from '../collection-manager';
 | 
				
			||||||
import { useDocumentTitle } from '../document-title';
 | 
					import { useDocumentTitle } from '../document-title';
 | 
				
			||||||
@ -149,7 +151,7 @@ const LocalPlugins = () => {
 | 
				
			|||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      {data?.data?.map((item) => {
 | 
					      {data?.data?.map((item) => {
 | 
				
			||||||
        return <PluginCard key={item.id} data={item} />;
 | 
					        return <PluginCard data={item} />;
 | 
				
			||||||
      })}
 | 
					      })}
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
@ -188,8 +190,9 @@ const PluginList = (props) => {
 | 
				
			|||||||
  const { tabName = 'local' } = match.params || {};
 | 
					  const { tabName = 'local' } = match.params || {};
 | 
				
			||||||
  const { setTitle } = useDocumentTitle();
 | 
					  const { setTitle } = useDocumentTitle();
 | 
				
			||||||
  const { t } = useTranslation();
 | 
					  const { t } = useTranslation();
 | 
				
			||||||
 | 
					  const { snippets = [] } = useACLRoleContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return snippets.includes('pm') ? (
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
      <PageHeader
 | 
					      <PageHeader
 | 
				
			||||||
        ghost={false}
 | 
					        ghost={false}
 | 
				
			||||||
@ -217,6 +220,8 @@ const PluginList = (props) => {
 | 
				
			|||||||
        )}
 | 
					        )}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					  ) : (
 | 
				
			||||||
 | 
					    <Result status="404" title="404" subTitle="Sorry, the page you visited does not exist." />
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -226,16 +231,17 @@ const settings = {
 | 
				
			|||||||
    icon: 'LockOutlined',
 | 
					    icon: 'LockOutlined',
 | 
				
			||||||
    tabs: {
 | 
					    tabs: {
 | 
				
			||||||
      roles: {
 | 
					      roles: {
 | 
				
			||||||
 | 
					        isBookmark: true,
 | 
				
			||||||
        title: '{{t("Roles & Permissions")}}',
 | 
					        title: '{{t("Roles & Permissions")}}',
 | 
				
			||||||
        component: ACLPane,
 | 
					        component: ACLPane,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  'block-templates': {
 | 
					  'ui-schema-storage': {
 | 
				
			||||||
    title: '{{t("Block templates")}}',
 | 
					    title: '{{t("Block templates")}}',
 | 
				
			||||||
    icon: 'LayoutOutlined',
 | 
					    icon: 'LayoutOutlined',
 | 
				
			||||||
    tabs: {
 | 
					    tabs: {
 | 
				
			||||||
      list: {
 | 
					      'block-templates': {
 | 
				
			||||||
        title: '{{t("Block templates")}}',
 | 
					        title: '{{t("Block templates")}}',
 | 
				
			||||||
        component: BlockTemplatesPane,
 | 
					        component: BlockTemplatesPane,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@ -246,6 +252,7 @@ const settings = {
 | 
				
			|||||||
    title: '{{t("Collection manager")}}',
 | 
					    title: '{{t("Collection manager")}}',
 | 
				
			||||||
    tabs: {
 | 
					    tabs: {
 | 
				
			||||||
      collections: {
 | 
					      collections: {
 | 
				
			||||||
 | 
					        isBookmark: true,
 | 
				
			||||||
        title: '{{t("Collections & Fields")}}',
 | 
					        title: '{{t("Collections & Fields")}}',
 | 
				
			||||||
        component: CollectionManagerPane,
 | 
					        component: CollectionManagerPane,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@ -256,6 +263,7 @@ const settings = {
 | 
				
			|||||||
    title: '{{t("System settings")}}',
 | 
					    title: '{{t("System settings")}}',
 | 
				
			||||||
    tabs: {
 | 
					    tabs: {
 | 
				
			||||||
      'system-settings': {
 | 
					      'system-settings': {
 | 
				
			||||||
 | 
					        isBookmark: true,
 | 
				
			||||||
        title: '{{t("System settings")}}',
 | 
					        title: '{{t("System settings")}}',
 | 
				
			||||||
        component: SystemSettingsPane,
 | 
					        component: SystemSettingsPane,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@ -263,18 +271,44 @@ const settings = {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getPluginsTabs = (items, snippets) => {
 | 
				
			||||||
 | 
					  const pluginsTabs = Object.keys(items).map((plugin) => {
 | 
				
			||||||
 | 
					    const tabsObj = items[plugin].tabs;
 | 
				
			||||||
 | 
					    const tabs = sortBy(
 | 
				
			||||||
 | 
					      Object.keys(tabsObj).map((tab) => {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          key: tab,
 | 
				
			||||||
 | 
					          ...tabsObj[tab],
 | 
				
			||||||
 | 
					          isAllow: snippets.includes('pm.*') && !snippets?.includes(`!pm.${plugin}.${tab}`),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      (o) => !o.isAllow,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      ...items[plugin],
 | 
				
			||||||
 | 
					      key: plugin,
 | 
				
			||||||
 | 
					      tabs,
 | 
				
			||||||
 | 
					      isAllow: !tabs.every((v) => !v.isAllow),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  return sortBy(pluginsTabs, (o) => !o.isAllow);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const SettingsCenter = (props) => {
 | 
					const SettingsCenter = (props) => {
 | 
				
			||||||
 | 
					  const { snippets = [] } = useACLRoleContext();
 | 
				
			||||||
  const match = useRouteMatch<any>();
 | 
					  const match = useRouteMatch<any>();
 | 
				
			||||||
  const history = useHistory<any>();
 | 
					  const history = useHistory<any>();
 | 
				
			||||||
  const items = useContext(SettingsCenterContext);
 | 
					  const items = useContext(SettingsCenterContext);
 | 
				
			||||||
 | 
					  const pluginsTabs = getPluginsTabs(items, snippets);
 | 
				
			||||||
  const compile = useCompile();
 | 
					  const compile = useCompile();
 | 
				
			||||||
  const firstUri = useMemo(() => {
 | 
					  const firstUri = useMemo(() => {
 | 
				
			||||||
    const keys = Object.keys(items).sort();
 | 
					    const pluginName = pluginsTabs[0].key;
 | 
				
			||||||
    const pluginName = keys.shift();
 | 
					    const tabName = pluginsTabs[0].tabs[0].key;
 | 
				
			||||||
    const tabName = Object.keys(items?.[pluginName]?.tabs || {}).shift();
 | 
					 | 
				
			||||||
    return `/admin/settings/${pluginName}/${tabName}`;
 | 
					    return `/admin/settings/${pluginName}/${tabName}`;
 | 
				
			||||||
  }, [items]);
 | 
					  }, [pluginsTabs]);
 | 
				
			||||||
  const { pluginName, tabName } = match.params || {};
 | 
					  const { pluginName, tabName } = match.params || {};
 | 
				
			||||||
 | 
					  const activePlugin = pluginsTabs.find((v) => v.key === pluginName);
 | 
				
			||||||
 | 
					  const aclPluginTabCheck = activePlugin?.isAllow && activePlugin.tabs.find((v) => v.key === tabName)?.isAllow;
 | 
				
			||||||
  if (!pluginName) {
 | 
					  if (!pluginName) {
 | 
				
			||||||
    return <Redirect to={firstUri} />;
 | 
					    return <Redirect to={firstUri} />;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -286,78 +320,86 @@ const SettingsCenter = (props) => {
 | 
				
			|||||||
    return <Redirect to={`/admin/settings/${pluginName}/${firstTabName}`} />;
 | 
					    return <Redirect to={`/admin/settings/${pluginName}/${firstTabName}`} />;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  const component = items[pluginName]?.tabs?.[tabName]?.component;
 | 
					  const component = items[pluginName]?.tabs?.[tabName]?.component;
 | 
				
			||||||
  const menuItems: any = Object.keys(items)
 | 
					  const plugin: any = pluginsTabs.find((v) => v.key === pluginName);
 | 
				
			||||||
    .sort()
 | 
					  const menuItems: any = pluginsTabs
 | 
				
			||||||
    .map((key) => {
 | 
					    .filter((plugin) => plugin.isAllow)
 | 
				
			||||||
      const item = items[key];
 | 
					    .map((plugin) => {
 | 
				
			||||||
      const tabKey = Object.keys(item.tabs).shift();
 | 
					 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
        label: compile(item.title),
 | 
					        label: compile(plugin.title),
 | 
				
			||||||
        key: key,
 | 
					        key: plugin.key,
 | 
				
			||||||
        icon: item.icon ? <Icon type={item.icon} /> : null,
 | 
					        icon: plugin.icon ? <Icon type={plugin.icon} /> : null,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Layout>
 | 
					    <div>
 | 
				
			||||||
      <div
 | 
					      <Layout>
 | 
				
			||||||
        style={
 | 
					        <div
 | 
				
			||||||
          {
 | 
					          style={
 | 
				
			||||||
            '--side-menu-width': '200px',
 | 
					            {
 | 
				
			||||||
          } as Record<string, string>
 | 
					              '--side-menu-width': '200px',
 | 
				
			||||||
        }
 | 
					            } as Record<string, string>
 | 
				
			||||||
        className={css`
 | 
					 | 
				
			||||||
          width: var(--side-menu-width);
 | 
					 | 
				
			||||||
          overflow: hidden;
 | 
					 | 
				
			||||||
          flex: 0 0 var(--side-menu-width);
 | 
					 | 
				
			||||||
          max-width: var(--side-menu-width);
 | 
					 | 
				
			||||||
          min-width: var(--side-menu-width);
 | 
					 | 
				
			||||||
          pointer-events: none;
 | 
					 | 
				
			||||||
        `}
 | 
					 | 
				
			||||||
      ></div>
 | 
					 | 
				
			||||||
      <Layout.Sider
 | 
					 | 
				
			||||||
        className={css`
 | 
					 | 
				
			||||||
          height: 100%;
 | 
					 | 
				
			||||||
          position: fixed;
 | 
					 | 
				
			||||||
          padding-top: 46px;
 | 
					 | 
				
			||||||
          left: 0;
 | 
					 | 
				
			||||||
          top: 0;
 | 
					 | 
				
			||||||
          background: rgba(0, 0, 0, 0);
 | 
					 | 
				
			||||||
          z-index: 100;
 | 
					 | 
				
			||||||
        `}
 | 
					 | 
				
			||||||
        theme={'light'}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <Menu
 | 
					 | 
				
			||||||
          selectedKeys={[pluginName]}
 | 
					 | 
				
			||||||
          style={{ height: 'calc(100vh - 46px)', overflowY: 'auto', overflowX: 'hidden' }}
 | 
					 | 
				
			||||||
          onClick={(e) => {
 | 
					 | 
				
			||||||
            const item = items[e.key];
 | 
					 | 
				
			||||||
            const tabKey = Object.keys(item.tabs).shift();
 | 
					 | 
				
			||||||
            history.push(`/admin/settings/${e.key}/${tabKey}`);
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
          items={menuItems as any}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </Layout.Sider>
 | 
					 | 
				
			||||||
      <Layout.Content>
 | 
					 | 
				
			||||||
        <PageHeader
 | 
					 | 
				
			||||||
          ghost={false}
 | 
					 | 
				
			||||||
          title={compile(items[pluginName]?.title)}
 | 
					 | 
				
			||||||
          footer={
 | 
					 | 
				
			||||||
            <Tabs
 | 
					 | 
				
			||||||
              activeKey={tabName}
 | 
					 | 
				
			||||||
              onChange={(activeKey) => {
 | 
					 | 
				
			||||||
                history.push(`/admin/settings/${pluginName}/${activeKey}`);
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              {Object.keys(items[pluginName]?.tabs).map((tabKey) => {
 | 
					 | 
				
			||||||
                const tab = items[pluginName].tabs?.[tabKey];
 | 
					 | 
				
			||||||
                return <Tabs.TabPane tab={compile(tab?.title)} key={tabKey} />;
 | 
					 | 
				
			||||||
              })}
 | 
					 | 
				
			||||||
            </Tabs>
 | 
					 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        />
 | 
					          className={css`
 | 
				
			||||||
        <div style={{ margin: 24 }}>{component && React.createElement(component)}</div>
 | 
					            width: var(--side-menu-width);
 | 
				
			||||||
      </Layout.Content>
 | 
					            overflow: hidden;
 | 
				
			||||||
    </Layout>
 | 
					            flex: 0 0 var(--side-menu-width);
 | 
				
			||||||
 | 
					            max-width: var(--side-menu-width);
 | 
				
			||||||
 | 
					            min-width: var(--side-menu-width);
 | 
				
			||||||
 | 
					            pointer-events: none;
 | 
				
			||||||
 | 
					          `}
 | 
				
			||||||
 | 
					        ></div>
 | 
				
			||||||
 | 
					        <Layout.Sider
 | 
				
			||||||
 | 
					          className={css`
 | 
				
			||||||
 | 
					            height: 100%;
 | 
				
			||||||
 | 
					            position: fixed;
 | 
				
			||||||
 | 
					            padding-top: 46px;
 | 
				
			||||||
 | 
					            left: 0;
 | 
				
			||||||
 | 
					            top: 0;
 | 
				
			||||||
 | 
					            background: rgba(0, 0, 0, 0);
 | 
				
			||||||
 | 
					            z-index: 100;
 | 
				
			||||||
 | 
					          `}
 | 
				
			||||||
 | 
					          theme={'light'}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <Menu
 | 
				
			||||||
 | 
					            selectedKeys={[pluginName]}
 | 
				
			||||||
 | 
					            style={{ height: 'calc(100vh - 46px)', overflowY: 'auto', overflowX: 'hidden' }}
 | 
				
			||||||
 | 
					            onClick={(e) => {
 | 
				
			||||||
 | 
					              const item = items[e.key];
 | 
				
			||||||
 | 
					              const tabKey = Object.keys(item.tabs).shift();
 | 
				
			||||||
 | 
					              history.push(`/admin/settings/${e.key}/${tabKey}`);
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					            items={menuItems as any}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </Layout.Sider>
 | 
				
			||||||
 | 
					        <Layout.Content>
 | 
				
			||||||
 | 
					          {aclPluginTabCheck && (
 | 
				
			||||||
 | 
					            <PageHeader
 | 
				
			||||||
 | 
					              ghost={false}
 | 
				
			||||||
 | 
					              title={compile(items[pluginName]?.title)}
 | 
				
			||||||
 | 
					              footer={
 | 
				
			||||||
 | 
					                <Tabs
 | 
				
			||||||
 | 
					                  activeKey={tabName}
 | 
				
			||||||
 | 
					                  onChange={(activeKey) => {
 | 
				
			||||||
 | 
					                    history.push(`/admin/settings/${pluginName}/${activeKey}`);
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  {plugin.tabs?.map((tab) => {
 | 
				
			||||||
 | 
					                    return tab.isAllow && <Tabs.TabPane tab={compile(tab?.title)} key={tab.key} />;
 | 
				
			||||||
 | 
					                  })}
 | 
				
			||||||
 | 
					                </Tabs>
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					          <div style={{ margin: 24 }}>
 | 
				
			||||||
 | 
					            {aclPluginTabCheck ? (
 | 
				
			||||||
 | 
					              component && React.createElement(component)
 | 
				
			||||||
 | 
					            ) : (
 | 
				
			||||||
 | 
					              <Result status="404" title="404" subTitle="Sorry, the page you visited does not exist." />
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </Layout.Content>
 | 
				
			||||||
 | 
					      </Layout>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,6 @@ import { Layout, Spin } from 'antd';
 | 
				
			|||||||
import React, { createContext, useContext, useMemo, useRef, useState } from 'react';
 | 
					import React, { createContext, useContext, useMemo, useRef, useState } from 'react';
 | 
				
			||||||
import { useHistory, useRouteMatch } from 'react-router-dom';
 | 
					import { useHistory, useRouteMatch } from 'react-router-dom';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ACLAllowConfigure,
 | 
					 | 
				
			||||||
  ACLRolesCheckProvider,
 | 
					  ACLRolesCheckProvider,
 | 
				
			||||||
  CurrentUser,
 | 
					  CurrentUser,
 | 
				
			||||||
  CurrentUserProvider,
 | 
					  CurrentUserProvider,
 | 
				
			||||||
@ -25,8 +24,8 @@ import { PoweredBy } from '../../../powered-by';
 | 
				
			|||||||
import { useMutationObserver } from 'ahooks';
 | 
					import { useMutationObserver } from 'ahooks';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const filterByACL = (schema, options) => {
 | 
					const filterByACL = (schema, options) => {
 | 
				
			||||||
  const { allowAll, allowConfigure, allowMenuItemIds = [] } = options;
 | 
					  const { allowAll, allowMenuItemIds = [] } = options;
 | 
				
			||||||
  if (allowAll || allowConfigure) {
 | 
					  if (allowAll) {
 | 
				
			||||||
    return schema;
 | 
					    return schema;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  const filterSchema = (s) => {
 | 
					  const filterSchema = (s) => {
 | 
				
			||||||
@ -123,7 +122,7 @@ const MenuEditor = (props) => {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const InternalAdminLayout = (props: any) => {
 | 
					export const InternalAdminLayout = (props: any) => {
 | 
				
			||||||
  const sideMenuRef = useRef<HTMLDivElement>();
 | 
					  const sideMenuRef = useRef<HTMLDivElement>();
 | 
				
			||||||
  const [sideMenuWidth, setSideMenuWidth] = useState(0);
 | 
					  const [sideMenuWidth, setSideMenuWidth] = useState(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -192,9 +191,7 @@ const InternalAdminLayout = (props: any) => {
 | 
				
			|||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div style={{ position: 'absolute', height: '100%', zIndex: 10, top: 0, right: 0 }}>
 | 
					          <div style={{ position: 'absolute', height: '100%', zIndex: 10, top: 0, right: 0 }}>
 | 
				
			||||||
            <ACLAllowConfigure>
 | 
					            <RemotePluginManagerToolbar />
 | 
				
			||||||
              <RemotePluginManagerToolbar />
 | 
					 | 
				
			||||||
            </ACLAllowConfigure>
 | 
					 | 
				
			||||||
            <CurrentUser />
 | 
					            <CurrentUser />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -89,28 +89,31 @@ export const Action: ComposedAction = observer((props: any) => {
 | 
				
			|||||||
  const form = useForm();
 | 
					  const form = useForm();
 | 
				
			||||||
  const designerProps = fieldSchema['x-designer-props'];
 | 
					  const designerProps = fieldSchema['x-designer-props'];
 | 
				
			||||||
  const openMode = fieldSchema?.['x-component-props']?.['openMode'];
 | 
					  const openMode = fieldSchema?.['x-component-props']?.['openMode'];
 | 
				
			||||||
 | 
					  const disabled = form.disabled || field.disabled;
 | 
				
			||||||
  const openSize = fieldSchema?.['x-component-props']?.['openSize'];
 | 
					  const openSize = fieldSchema?.['x-component-props']?.['openSize'];
 | 
				
			||||||
  const renderButton = () => (
 | 
					  const renderButton = () => (
 | 
				
			||||||
    <SortableItem
 | 
					    <SortableItem
 | 
				
			||||||
      {...others}
 | 
					      {...others}
 | 
				
			||||||
      loading={field?.data?.loading}
 | 
					      loading={field?.data?.loading}
 | 
				
			||||||
      icon={<Icon type={icon} />}
 | 
					      icon={<Icon type={icon} />}
 | 
				
			||||||
      disabled={form.disabled}
 | 
					      disabled={disabled}
 | 
				
			||||||
      onClick={(e: React.MouseEvent) => {
 | 
					      onClick={(e: React.MouseEvent) => {
 | 
				
			||||||
        e.preventDefault();
 | 
					        if (!disabled) {
 | 
				
			||||||
        e.stopPropagation();
 | 
					          e.preventDefault();
 | 
				
			||||||
        const onOk = () => {
 | 
					          e.stopPropagation();
 | 
				
			||||||
          onClick?.(e);
 | 
					          const onOk = () => {
 | 
				
			||||||
          setVisible(true);
 | 
					            onClick?.(e);
 | 
				
			||||||
          run();
 | 
					            setVisible(true);
 | 
				
			||||||
        };
 | 
					            run();
 | 
				
			||||||
        if (confirm) {
 | 
					          };
 | 
				
			||||||
          Modal.confirm({
 | 
					          if (confirm) {
 | 
				
			||||||
            ...confirm,
 | 
					            Modal.confirm({
 | 
				
			||||||
            onOk,
 | 
					              ...confirm,
 | 
				
			||||||
          });
 | 
					              onOk,
 | 
				
			||||||
        } else {
 | 
					            });
 | 
				
			||||||
          onOk();
 | 
					          } else {
 | 
				
			||||||
 | 
					            onOk();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }}
 | 
					      }}
 | 
				
			||||||
      component={component || Button}
 | 
					      component={component || Button}
 | 
				
			||||||
 | 
				
			|||||||
@ -6,10 +6,11 @@ import { uid } from '@formily/shared';
 | 
				
			|||||||
import _ from 'lodash';
 | 
					import _ from 'lodash';
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import { useTranslation } from 'react-i18next';
 | 
					import { useTranslation } from 'react-i18next';
 | 
				
			||||||
import { useCompile, useDesignable, useFieldComponentOptions } from '../../hooks';
 | 
					import { ACLCollectionFieldProvider } from '../../../acl/ACLProvider';
 | 
				
			||||||
import { useFilterByTk, useFormBlockContext } from '../../../block-provider';
 | 
					import { useFilterByTk, useFormBlockContext } from '../../../block-provider';
 | 
				
			||||||
import { useCollection, useCollectionManager } from '../../../collection-manager';
 | 
					import { useCollection, useCollectionManager } from '../../../collection-manager';
 | 
				
			||||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
 | 
					import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
 | 
				
			||||||
 | 
					import { useCompile, useDesignable, useFieldComponentOptions } from '../../hooks';
 | 
				
			||||||
import { BlockItem } from '../block-item';
 | 
					import { BlockItem } from '../block-item';
 | 
				
			||||||
import { HTMLEncode } from '../input/shared';
 | 
					import { HTMLEncode } from '../input/shared';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -26,27 +27,29 @@ const divWrap = (schema: ISchema) => {
 | 
				
			|||||||
export const FormItem: any = (props) => {
 | 
					export const FormItem: any = (props) => {
 | 
				
			||||||
  const field = useField();
 | 
					  const field = useField();
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <BlockItem className={'nb-form-item'}>
 | 
					    <ACLCollectionFieldProvider>
 | 
				
			||||||
      <Item
 | 
					      <BlockItem className={'nb-form-item'}>
 | 
				
			||||||
        className={`${css`
 | 
					        <Item
 | 
				
			||||||
          & .ant-space {
 | 
					          className={`${css`
 | 
				
			||||||
            flex-wrap: wrap;
 | 
					            & .ant-space {
 | 
				
			||||||
 | 
					              flex-wrap: wrap;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          `}`}
 | 
				
			||||||
 | 
					          {...props}
 | 
				
			||||||
 | 
					          extra={
 | 
				
			||||||
 | 
					            typeof field.description === 'string' ? (
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                dangerouslySetInnerHTML={{
 | 
				
			||||||
 | 
					                  __html: HTMLEncode(field.description).split('\n').join('<br/>'),
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            ) : (
 | 
				
			||||||
 | 
					              field.description
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        `}`}
 | 
					        />
 | 
				
			||||||
        {...props}
 | 
					      </BlockItem>
 | 
				
			||||||
        extra={
 | 
					    </ACLCollectionFieldProvider>
 | 
				
			||||||
          typeof field.description === 'string' ? (
 | 
					 | 
				
			||||||
            <div
 | 
					 | 
				
			||||||
              dangerouslySetInnerHTML={{
 | 
					 | 
				
			||||||
                __html: HTMLEncode(field.description).split('\n').join('<br/>'),
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          ) : (
 | 
					 | 
				
			||||||
            field.description
 | 
					 | 
				
			||||||
          )
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </BlockItem>
 | 
					 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ import { MenuOutlined } from '@ant-design/icons';
 | 
				
			|||||||
import { SortableContext, useSortable } from '@dnd-kit/sortable';
 | 
					import { SortableContext, useSortable } from '@dnd-kit/sortable';
 | 
				
			||||||
import { css } from '@emotion/css';
 | 
					import { css } from '@emotion/css';
 | 
				
			||||||
import { ArrayField, Field } from '@formily/core';
 | 
					import { ArrayField, Field } from '@formily/core';
 | 
				
			||||||
import { ISchema, observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
 | 
					import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
 | 
				
			||||||
import { reaction } from '@formily/reactive';
 | 
					import { reaction } from '@formily/reactive';
 | 
				
			||||||
import { useEventListener, useMemoizedFn } from 'ahooks';
 | 
					import { useEventListener, useMemoizedFn } from 'ahooks';
 | 
				
			||||||
import { Table as AntdTable, TableColumnProps } from 'antd';
 | 
					import { Table as AntdTable, TableColumnProps } from 'antd';
 | 
				
			||||||
@ -11,21 +11,23 @@ import React, { RefCallback, useCallback, useEffect, useMemo, useRef, useState }
 | 
				
			|||||||
import { useTranslation } from 'react-i18next';
 | 
					import { useTranslation } from 'react-i18next';
 | 
				
			||||||
import { DndContext, useDesignable } from '../..';
 | 
					import { DndContext, useDesignable } from '../..';
 | 
				
			||||||
import { RecordIndexProvider, RecordProvider, useSchemaInitializer } from '../../../';
 | 
					import { RecordIndexProvider, RecordProvider, useSchemaInitializer } from '../../../';
 | 
				
			||||||
 | 
					import { useACLFieldWhitelist } from '../../../acl/ACLProvider';
 | 
				
			||||||
import { isCollectionFieldComponent, isColumnComponent } from './utils';
 | 
					import { isCollectionFieldComponent, isColumnComponent } from './utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const useTableColumns = () => {
 | 
					const useTableColumns = () => {
 | 
				
			||||||
  const start = Date.now();
 | 
					 | 
				
			||||||
  const field = useField<ArrayField>();
 | 
					  const field = useField<ArrayField>();
 | 
				
			||||||
  const schema = useFieldSchema();
 | 
					  const schema = useFieldSchema();
 | 
				
			||||||
 | 
					  const { schemaInWhitelist } = useACLFieldWhitelist();
 | 
				
			||||||
  const { designable } = useDesignable();
 | 
					  const { designable } = useDesignable();
 | 
				
			||||||
  const { exists, render } = useSchemaInitializer(schema['x-initializer']);
 | 
					  const { exists, render } = useSchemaInitializer(schema['x-initializer']);
 | 
				
			||||||
  const columns = schema
 | 
					  const columns = schema
 | 
				
			||||||
    .reduceProperties((buf, s) => {
 | 
					    .reduceProperties((buf, s) => {
 | 
				
			||||||
      if (isColumnComponent(s)) {
 | 
					      if (isColumnComponent(s) && schemaInWhitelist(Object.values(s.properties || {}).pop())) {
 | 
				
			||||||
        return buf.concat([s]);
 | 
					        return buf.concat([s]);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      return buf;
 | 
				
			||||||
    }, [])
 | 
					    }, [])
 | 
				
			||||||
    .map((s: Schema) => {
 | 
					    ?.map((s: Schema) => {
 | 
				
			||||||
      const collectionFields = s.reduceProperties((buf, s) => {
 | 
					      const collectionFields = s.reduceProperties((buf, s) => {
 | 
				
			||||||
        if (isCollectionFieldComponent(s)) {
 | 
					        if (isCollectionFieldComponent(s)) {
 | 
				
			||||||
          return buf.concat([s]);
 | 
					          return buf.concat([s]);
 | 
				
			||||||
@ -42,9 +44,9 @@ const useTableColumns = () => {
 | 
				
			|||||||
        render: (v, record) => {
 | 
					        render: (v, record) => {
 | 
				
			||||||
          const index = field.value?.indexOf(record);
 | 
					          const index = field.value?.indexOf(record);
 | 
				
			||||||
          return (
 | 
					          return (
 | 
				
			||||||
            <RecordIndexProvider index={index}>
 | 
					            <RecordIndexProvider index={record.__index || index}>
 | 
				
			||||||
              <RecordProvider record={record}>
 | 
					              <RecordProvider record={record}>
 | 
				
			||||||
                <RecursionField schema={s} name={index} onlyRenderProperties />
 | 
					                <RecursionField schema={s} name={record.__index || index} onlyRenderProperties />
 | 
				
			||||||
              </RecordProvider>
 | 
					              </RecordProvider>
 | 
				
			||||||
            </RecordIndexProvider>
 | 
					            </RecordIndexProvider>
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
 | 
				
			|||||||
@ -35,7 +35,7 @@ const useTableColumns = () => {
 | 
				
			|||||||
          return (
 | 
					          return (
 | 
				
			||||||
            <RecordIndexProvider index={index}>
 | 
					            <RecordIndexProvider index={index}>
 | 
				
			||||||
              <RecordProvider record={record}>
 | 
					              <RecordProvider record={record}>
 | 
				
			||||||
                <RecursionField schema={s} name={index} onlyRenderProperties />
 | 
					                <RecursionField schema={s} name={record.__index || index} onlyRenderProperties />
 | 
				
			||||||
              </RecordProvider>
 | 
					              </RecordProvider>
 | 
				
			||||||
            </RecordIndexProvider>
 | 
					            </RecordIndexProvider>
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
 | 
				
			|||||||
@ -108,6 +108,7 @@ export const ReadPrettyFormActionInitializers = {
 | 
				
			|||||||
            'x-component': 'Action',
 | 
					            'x-component': 'Action',
 | 
				
			||||||
            'x-designer': 'Action.Designer',
 | 
					            'x-designer': 'Action.Designer',
 | 
				
			||||||
            'x-action': 'customize:update',
 | 
					            'x-action': 'customize:update',
 | 
				
			||||||
 | 
					            'x-decorator': 'ACLActionProvider',
 | 
				
			||||||
            'x-acl-action': 'update',
 | 
					            'x-acl-action': 'update',
 | 
				
			||||||
            'x-action-settings': {
 | 
					            'x-action-settings': {
 | 
				
			||||||
              assignedValues: {},
 | 
					              assignedValues: {},
 | 
				
			||||||
 | 
				
			|||||||
@ -173,6 +173,7 @@ export const TableActionColumnInitializers = (props: any) => {
 | 
				
			|||||||
                title: '{{ t("Update record") }}',
 | 
					                title: '{{ t("Update record") }}',
 | 
				
			||||||
                'x-component': 'Action.Link',
 | 
					                'x-component': 'Action.Link',
 | 
				
			||||||
                'x-action': 'customize:update',
 | 
					                'x-action': 'customize:update',
 | 
				
			||||||
 | 
					                'x-decorator': 'ACLActionProvider',
 | 
				
			||||||
                'x-acl-action': 'update',
 | 
					                'x-acl-action': 'update',
 | 
				
			||||||
                'x-designer': 'Action.Designer',
 | 
					                'x-designer': 'Action.Designer',
 | 
				
			||||||
                'x-action-settings': {
 | 
					                'x-action-settings': {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { ISchema, Schema } from '@formily/react';
 | 
					import { Schema } from '@formily/react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 表格操作配置
 | 
					// 表格操作配置
 | 
				
			||||||
export const TableActionInitializers = {
 | 
					export const TableActionInitializers = {
 | 
				
			||||||
 | 
				
			|||||||
@ -201,11 +201,9 @@ export const useFormItemInitializerFields = (options?: any) => {
 | 
				
			|||||||
    ?.filter((field) => field?.interface && !field?.isForeignKey)
 | 
					    ?.filter((field) => field?.interface && !field?.isForeignKey)
 | 
				
			||||||
    ?.map((field) => {
 | 
					    ?.map((field) => {
 | 
				
			||||||
      const interfaceConfig = getInterface(field.interface);
 | 
					      const interfaceConfig = getInterface(field.interface);
 | 
				
			||||||
 | 
					 | 
				
			||||||
      const schema = {
 | 
					      const schema = {
 | 
				
			||||||
        type: 'string',
 | 
					        type: 'string',
 | 
				
			||||||
        name: field.name,
 | 
					        name: field.name,
 | 
				
			||||||
        // title: field?.uiSchema?.title || field.name,
 | 
					 | 
				
			||||||
        'x-designer': 'FormItem.Designer',
 | 
					        'x-designer': 'FormItem.Designer',
 | 
				
			||||||
        'x-component': field.interface === 'o2m' && !snapshot ? 'TableField' : 'CollectionField',
 | 
					        'x-component': field.interface === 'o2m' && !snapshot ? 'TableField' : 'CollectionField',
 | 
				
			||||||
        'x-decorator': 'FormItem',
 | 
					        'x-decorator': 'FormItem',
 | 
				
			||||||
@ -241,7 +239,6 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => {
 | 
				
			|||||||
  const form = useForm();
 | 
					  const form = useForm();
 | 
				
			||||||
  const { readPretty = form.readPretty, block = 'Form' } = options || {};
 | 
					  const { readPretty = form.readPretty, block = 'Form' } = options || {};
 | 
				
			||||||
  const interfaces = block === 'Form' ? ['m2o'] : ['o2o', 'oho', 'obo', 'm2o'];
 | 
					  const interfaces = block === 'Form' ? ['m2o'] : ['o2o', 'oho', 'obo', 'm2o'];
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const groups = fields
 | 
					  const groups = fields
 | 
				
			||||||
    ?.filter((field) => {
 | 
					    ?.filter((field) => {
 | 
				
			||||||
      return interfaces.includes(field.interface);
 | 
					      return interfaces.includes(field.interface);
 | 
				
			||||||
@ -255,7 +252,6 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => {
 | 
				
			|||||||
          const schema = {
 | 
					          const schema = {
 | 
				
			||||||
            type: 'string',
 | 
					            type: 'string',
 | 
				
			||||||
            name: `${field.name}.${subField.name}`,
 | 
					            name: `${field.name}.${subField.name}`,
 | 
				
			||||||
            // title: subField?.uiSchema?.title || subField.name,
 | 
					 | 
				
			||||||
            'x-designer': 'FormItem.Designer',
 | 
					            'x-designer': 'FormItem.Designer',
 | 
				
			||||||
            'x-component': 'CollectionField',
 | 
					            'x-component': 'CollectionField',
 | 
				
			||||||
            'x-read-pretty': readPretty,
 | 
					            'x-read-pretty': readPretty,
 | 
				
			||||||
@ -265,7 +261,6 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => {
 | 
				
			|||||||
            'x-decorator': 'FormItem',
 | 
					            'x-decorator': 'FormItem',
 | 
				
			||||||
            'x-collection-field': `${name}.${field.name}.${subField.name}`,
 | 
					            'x-collection-field': `${name}.${field.name}.${subField.name}`,
 | 
				
			||||||
          };
 | 
					          };
 | 
				
			||||||
          // interfaceConfig?.schemaInitialize?.(schema, { field, block: 'Form', readPretty: form.readPretty });
 | 
					 | 
				
			||||||
          return {
 | 
					          return {
 | 
				
			||||||
            type: 'item',
 | 
					            type: 'item',
 | 
				
			||||||
            title: subField?.uiSchema?.title || subField.name,
 | 
					            title: subField?.uiSchema?.title || subField.name,
 | 
				
			||||||
@ -630,7 +625,7 @@ export const createDetailsBlockSchema = (options) => {
 | 
				
			|||||||
  const resourceName = resource || association || collection;
 | 
					  const resourceName = resource || association || collection;
 | 
				
			||||||
  const schema: ISchema = {
 | 
					  const schema: ISchema = {
 | 
				
			||||||
    type: 'void',
 | 
					    type: 'void',
 | 
				
			||||||
    'x-acl-action': `${resourceName}:get`,
 | 
					    'x-acl-action': `${resourceName}:view`,
 | 
				
			||||||
    'x-decorator': 'DetailsBlockProvider',
 | 
					    'x-decorator': 'DetailsBlockProvider',
 | 
				
			||||||
    'x-decorator-props': {
 | 
					    'x-decorator-props': {
 | 
				
			||||||
      resource: resourceName,
 | 
					      resource: resourceName,
 | 
				
			||||||
@ -807,7 +802,7 @@ export const createReadPrettyFormBlockSchema = (options) => {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  console.log(JSON.stringify(schema, null, 2));
 | 
					  // console.log(JSON.stringify(schema, null, 2));
 | 
				
			||||||
  return schema;
 | 
					  return schema;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -879,7 +874,7 @@ export const createTableBlockSchema = (options) => {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  console.log(JSON.stringify(schema, null, 2));
 | 
					  // console.log(JSON.stringify(schema, null, 2));
 | 
				
			||||||
  return schema;
 | 
					  return schema;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ import {
 | 
				
			|||||||
  Sequelize,
 | 
					  Sequelize,
 | 
				
			||||||
  SyncOptions,
 | 
					  SyncOptions,
 | 
				
			||||||
  Transactionable,
 | 
					  Transactionable,
 | 
				
			||||||
  Utils,
 | 
					  Utils
 | 
				
			||||||
} from 'sequelize';
 | 
					} from 'sequelize';
 | 
				
			||||||
import { SequelizeStorage, Umzug } from 'umzug';
 | 
					import { SequelizeStorage, Umzug } from 'umzug';
 | 
				
			||||||
import { Collection, CollectionOptions, RepositoryType } from './collection';
 | 
					import { Collection, CollectionOptions, RepositoryType } from './collection';
 | 
				
			||||||
@ -58,7 +58,7 @@ import {
 | 
				
			|||||||
  SyncListener,
 | 
					  SyncListener,
 | 
				
			||||||
  UpdateListener,
 | 
					  UpdateListener,
 | 
				
			||||||
  UpdateWithAssociationsListener,
 | 
					  UpdateWithAssociationsListener,
 | 
				
			||||||
  ValidateListener,
 | 
					  ValidateListener
 | 
				
			||||||
} from './types';
 | 
					} from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface MergeOptions extends merge.Options {}
 | 
					export interface MergeOptions extends merge.Options {}
 | 
				
			||||||
@ -356,7 +356,19 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
				
			|||||||
   * @param name
 | 
					   * @param name
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  getCollection(name: string): Collection {
 | 
					  getCollection(name: string): Collection {
 | 
				
			||||||
    return this.collections.get(name);
 | 
					    if (!name) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [collectionName, associationName] = name.split('.');
 | 
				
			||||||
 | 
					    let collection = this.collections.get(collectionName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (associationName) {
 | 
				
			||||||
 | 
					      const target = collection.getField(associationName)?.target;
 | 
				
			||||||
 | 
					      return target ? this.collections.get(target) : null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return collection;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  hasCollection(name: string): boolean {
 | 
					  hasCollection(name: string): boolean {
 | 
				
			||||||
@ -389,11 +401,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
				
			|||||||
  getRepository<R extends ArrayFieldRepository>(name: string, relationId: string | number): R;
 | 
					  getRepository<R extends ArrayFieldRepository>(name: string, relationId: string | number): R;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getRepository<R extends RelationRepository>(name: string, relationId?: string | number): Repository | R {
 | 
					  getRepository<R extends RelationRepository>(name: string, relationId?: string | number): Repository | R {
 | 
				
			||||||
    if (relationId) {
 | 
					    const [collection, relation] = name.split('.');
 | 
				
			||||||
      const [collection, relation] = name.split('.');
 | 
					    if (relation) {
 | 
				
			||||||
      return this.getRepository(collection)?.relation(relation)?.of(relationId) as R;
 | 
					      return this.getRepository(collection)?.relation(relation)?.of(relationId) as R;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    return this.getCollection(name)?.repository;
 | 
					    return this.getCollection(name)?.repository;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -11,7 +11,7 @@ import {
 | 
				
			|||||||
  Op,
 | 
					  Op,
 | 
				
			||||||
  Transactionable,
 | 
					  Transactionable,
 | 
				
			||||||
  UpdateOptions as SequelizeUpdateOptions,
 | 
					  UpdateOptions as SequelizeUpdateOptions,
 | 
				
			||||||
  WhereOperators,
 | 
					  WhereOperators
 | 
				
			||||||
} from 'sequelize';
 | 
					} from 'sequelize';
 | 
				
			||||||
import { Collection } from './collection';
 | 
					import { Collection } from './collection';
 | 
				
			||||||
import { Database } from './database';
 | 
					import { Database } from './database';
 | 
				
			||||||
@ -193,6 +193,9 @@ class RelationRepositoryBuilder<R extends RelationRepository> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  of(id: string | number): R {
 | 
					  of(id: string | number): R {
 | 
				
			||||||
 | 
					    if (!this.association) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    const klass = this.builder()[this.association.associationType];
 | 
					    const klass = this.builder()[this.association.associationType];
 | 
				
			||||||
    return new klass(this.collection, this.associationName, id);
 | 
					    return new klass(this.collection, this.associationName, id);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,10 @@ import { availableActions } from './available-action';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const configureResources = [
 | 
					const configureResources = [
 | 
				
			||||||
  'roles',
 | 
					  'roles',
 | 
				
			||||||
 | 
					  'users',
 | 
				
			||||||
  'collections',
 | 
					  'collections',
 | 
				
			||||||
 | 
					  'fields',
 | 
				
			||||||
 | 
					  'collections.fields',
 | 
				
			||||||
  'roles.collections',
 | 
					  'roles.collections',
 | 
				
			||||||
  'roles.resources',
 | 
					  'roles.resources',
 | 
				
			||||||
  'rolesResourcesScopes',
 | 
					  'rolesResourcesScopes',
 | 
				
			||||||
 | 
				
			|||||||
@ -45,6 +45,7 @@ export interface ApplicationOptions {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export interface DefaultState extends KoaDefaultState {
 | 
					export interface DefaultState extends KoaDefaultState {
 | 
				
			||||||
  currentUser?: any;
 | 
					  currentUser?: any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  [key: string]: any;
 | 
					  [key: string]: any;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -53,6 +54,7 @@ export interface DefaultContext extends KoaDefaultContext {
 | 
				
			|||||||
  cache: Cache;
 | 
					  cache: Cache;
 | 
				
			||||||
  resourcer: Resourcer;
 | 
					  resourcer: Resourcer;
 | 
				
			||||||
  i18n: any;
 | 
					  i18n: any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  [key: string]: any;
 | 
					  [key: string]: any;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -46,6 +46,10 @@ export function dataWrapping() {
 | 
				
			|||||||
        ctx.body = {
 | 
					        ctx.body = {
 | 
				
			||||||
          data: ctx.body,
 | 
					          data: ctx.body,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (ctx.bodyMeta) {
 | 
				
			||||||
 | 
					          ctx.body.meta = ctx.bodyMeta;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else if (ctx.action) {
 | 
					    } else if (ctx.action) {
 | 
				
			||||||
      ctx.body = {
 | 
					      ctx.body = {
 | 
				
			||||||
 | 
				
			|||||||
@ -17,6 +17,7 @@ export function db2resource(ctx: ResourcerContext & { db: Database }, next: () =
 | 
				
			|||||||
  if (!params) {
 | 
					  if (!params) {
 | 
				
			||||||
    return next();
 | 
					    return next();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const resourceName = getNameByParams(params);
 | 
					  const resourceName = getNameByParams(params);
 | 
				
			||||||
  // 如果资源名称未被定义
 | 
					  // 如果资源名称未被定义
 | 
				
			||||||
  if (resourcer.isDefined(resourceName)) {
 | 
					  if (resourcer.isDefined(resourceName)) {
 | 
				
			||||||
 | 
				
			|||||||
@ -43,8 +43,11 @@ export class PluginManager {
 | 
				
			|||||||
    this.repository.setPluginManager(this);
 | 
					    this.repository.setPluginManager(this);
 | 
				
			||||||
    this.app.resourcer.define(resourceOptions);
 | 
					    this.app.resourcer.define(resourceOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.app.acl.allow('pm', ['enable', 'disable', 'remove'], 'allowConfigure');
 | 
					    this.app.acl.registerSnippet({
 | 
				
			||||||
    this.app.acl.allow('applicationPlugins', 'list', 'allowConfigure');
 | 
					      name: 'pm',
 | 
				
			||||||
 | 
					      actions: ['pm:*', 'applicationPlugins:list'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.server = net.createServer((socket) => {
 | 
					    this.server = net.createServer((socket) => {
 | 
				
			||||||
      socket.on('data', async (data) => {
 | 
					      socket.on('data', async (data) => {
 | 
				
			||||||
        const { method, plugins } = JSON.parse(data.toString());
 | 
					        const { method, plugins } = JSON.parse(data.toString());
 | 
				
			||||||
@ -253,21 +256,6 @@ export class PluginManager {
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const file = resolve(
 | 
					 | 
				
			||||||
      process.cwd(),
 | 
					 | 
				
			||||||
      'packages',
 | 
					 | 
				
			||||||
      process.env.APP_PACKAGE_ROOT || 'app',
 | 
					 | 
				
			||||||
      'client/src/plugins',
 | 
					 | 
				
			||||||
      `${plugin}.ts`,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    if (!fs.existsSync(file)) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        require.resolve(`${packageName}/client`);
 | 
					 | 
				
			||||||
        await fs.promises.writeFile(file, `export { default } from '${packageName}/client';`);
 | 
					 | 
				
			||||||
        const { run } = require('@nocobase/cli/src/util');
 | 
					 | 
				
			||||||
        await run('yarn', ['nocobase', 'postinstall']);
 | 
					 | 
				
			||||||
      } catch (error) {}
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return instance;
 | 
					    return instance;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -36,6 +36,10 @@ export abstract class Plugin<O = any> implements PluginInterface {
 | 
				
			|||||||
    this.afterAdd();
 | 
					    this.afterAdd();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get name() {
 | 
				
			||||||
 | 
					    return this.options.name as string;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get db() {
 | 
					  get db() {
 | 
				
			||||||
    return this.app.db;
 | 
					    return this.app.db;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -42,6 +42,154 @@ describe('acl', () => {
 | 
				
			|||||||
    uiSchemaRepository = db.getRepository('uiSchemas');
 | 
					    uiSchemaRepository = db.getRepository('uiSchemas');
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should not have permission to list comments', async () => {
 | 
				
			||||||
 | 
					    await db.getCollection('collections').repository.create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'comments',
 | 
				
			||||||
 | 
					        fields: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name: 'content',
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      context: {},
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.getCollection('collections').repository.create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'posts',
 | 
				
			||||||
 | 
					        fields: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name: 'title',
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name: 'comments',
 | 
				
			||||||
 | 
					            type: 'hasMany',
 | 
				
			||||||
 | 
					            target: 'comments',
 | 
				
			||||||
 | 
					            interface: 'linkTo',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      context: {},
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.getRepository('roles').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'test-role',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await adminAgent.resource('roles.resources', 'test-role').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'posts',
 | 
				
			||||||
 | 
					        usingActionsConfig: true,
 | 
				
			||||||
 | 
					        actions: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name: 'view',
 | 
				
			||||||
 | 
					            fields: ['comments'],
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const acl = app.acl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(
 | 
				
			||||||
 | 
					      acl.can({
 | 
				
			||||||
 | 
					        role: 'test-role',
 | 
				
			||||||
 | 
					        resource: 'posts.comments',
 | 
				
			||||||
 | 
					        action: 'list',
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					    ).not.toBeNull();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(
 | 
				
			||||||
 | 
					      acl.can({
 | 
				
			||||||
 | 
					        role: 'test-role',
 | 
				
			||||||
 | 
					        resource: 'comments',
 | 
				
			||||||
 | 
					        action: 'list',
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					    ).toBeNull();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should not destroy default roles when user is root user', async () => {
 | 
				
			||||||
 | 
					    const rootUser = await db.getRepository('users').findOne({
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        email: process.env.INIT_ROOT_EMAIL,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const userPlugin = app.getPlugin('users') as UsersPlugin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const adminAgent = app.agent().auth(
 | 
				
			||||||
 | 
					      userPlugin.jwtService.sign({
 | 
				
			||||||
 | 
					        userId: rootUser.get('id'),
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      { type: 'bearer' },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(await db.getCollection('roles').repository.count()).toBe(3);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //@ts-ignore
 | 
				
			||||||
 | 
					    await adminAgent.resource('roles').destroy({
 | 
				
			||||||
 | 
					      filterByTk: 'root',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(await db.getCollection('roles').repository.count()).toBe(3);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should not destroy default roles', async () => {
 | 
				
			||||||
 | 
					    expect(await db.getCollection('roles').repository.count()).toBe(3);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await adminAgent.resource('roles').destroy({
 | 
				
			||||||
 | 
					      filterByTk: 'root',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(await db.getCollection('roles').repository.count()).toBe(3);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should not destroy all scope', async () => {
 | 
				
			||||||
 | 
					    let allScope = await adminAgent.resource('rolesResourcesScopes').get({
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        key: 'all',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(allScope.body.data).toBeDefined();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await adminAgent.resource('rolesResourcesScopes').destroy({
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        key: 'all',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    allScope = await adminAgent.resource('rolesResourcesScopes').get({
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        key: 'all',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(allScope.body.data).toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should not destroy roles collections', async () => {
 | 
				
			||||||
 | 
					    let rolesCollection = await adminAgent.resource('collections').get({
 | 
				
			||||||
 | 
					      filterByTk: 'roles',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(rolesCollection.body.data).toBeDefined();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await adminAgent.resource('collections').destroy({
 | 
				
			||||||
 | 
					      filterByTk: 'roles',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rolesCollection = await adminAgent.resource('collections').get({
 | 
				
			||||||
 | 
					      filterByTk: 'roles',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(rolesCollection.body.data).toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should works with universal actions', async () => {
 | 
					  it('should works with universal actions', async () => {
 | 
				
			||||||
    await db.getRepository('roles').create({
 | 
					    await db.getRepository('roles').create({
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
@ -474,6 +622,7 @@ describe('acl', () => {
 | 
				
			|||||||
        strategy: {
 | 
					        strategy: {
 | 
				
			||||||
          actions: ['*'],
 | 
					          actions: ['*'],
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        snippets: ['pm.*'],
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    const UserRepo = db.getCollection('users').repository;
 | 
					    const UserRepo = db.getCollection('users').repository;
 | 
				
			||||||
@ -545,4 +694,44 @@ describe('acl', () => {
 | 
				
			|||||||
      action: 'view',
 | 
					      action: 'view',
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should destroy new role when user are root user', async () => {
 | 
				
			||||||
 | 
					    const roles = await db.getRepository('roles').find();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const users = await db.getRepository('users').find();
 | 
				
			||||||
 | 
					    const rootUser = await db.getRepository('users').findOne({
 | 
				
			||||||
 | 
					      filterByTk: 1,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const userPlugin = app.getPlugin('users') as UsersPlugin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const rootAgent = app.agent().auth(
 | 
				
			||||||
 | 
					      userPlugin.jwtService.sign({
 | 
				
			||||||
 | 
					        userId: rootUser.get('id'),
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      { type: 'bearer' },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const response = await rootAgent
 | 
				
			||||||
 | 
					      // @ts-ignore
 | 
				
			||||||
 | 
					      .resource('roles')
 | 
				
			||||||
 | 
					      .create({
 | 
				
			||||||
 | 
					        values: {
 | 
				
			||||||
 | 
					          name: 'testRole',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(response.statusCode).toEqual(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(await db.getRepository('roles').findOne({ filterByTk: 'testRole' })).toBeDefined();
 | 
				
			||||||
 | 
					    const destroyResponse = await rootAgent
 | 
				
			||||||
 | 
					      // @ts-ignore
 | 
				
			||||||
 | 
					      .resource('roles')
 | 
				
			||||||
 | 
					      .destroy({
 | 
				
			||||||
 | 
					        filterByTk: 'testRole',
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(destroyResponse.statusCode).toEqual(200);
 | 
				
			||||||
 | 
					    expect(await db.getRepository('roles').findOne({ filterByTk: 'testRole' })).toBeNull();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										141
									
								
								packages/plugins/acl/src/__tests__/actions.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								packages/plugins/acl/src/__tests__/actions.test.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,141 @@
 | 
				
			|||||||
 | 
					import { MockServer } from '@nocobase/test';
 | 
				
			||||||
 | 
					import { prepareApp } from './prepare';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('destroy action with acl', () => {
 | 
				
			||||||
 | 
					  let app: MockServer;
 | 
				
			||||||
 | 
					  let Post;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    app = await prepareApp();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Post = app.collection({
 | 
				
			||||||
 | 
					      name: 'posts',
 | 
				
			||||||
 | 
					      fields: [
 | 
				
			||||||
 | 
					        { type: 'string', name: 'title' },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'bigInt',
 | 
				
			||||||
 | 
					          name: 'createdById',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await app.db.sync();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  afterEach(async () => {
 | 
				
			||||||
 | 
					    await app.destroy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should throw error when user has no permission to destroy record', async () => {
 | 
				
			||||||
 | 
					    const userRole = app.acl.define({
 | 
				
			||||||
 | 
					      role: 'user',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // user can destroy post which created by himself
 | 
				
			||||||
 | 
					    userRole.grantAction('posts:destroy', {
 | 
				
			||||||
 | 
					      own: true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const p1 = await Post.repository.create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        title: 'p1',
 | 
				
			||||||
 | 
					        createById: 2,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    app.resourcer.use(
 | 
				
			||||||
 | 
					      (ctx, next) => {
 | 
				
			||||||
 | 
					        ctx.state.currentRole = 'user';
 | 
				
			||||||
 | 
					        ctx.state.currentUser = {
 | 
				
			||||||
 | 
					          id: 1,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        return next();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        before: 'acl',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const response = await app
 | 
				
			||||||
 | 
					      .agent()
 | 
				
			||||||
 | 
					      .resource('posts')
 | 
				
			||||||
 | 
					      .destroy({
 | 
				
			||||||
 | 
					        filterByTk: p1.get('id'),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // should throw errors
 | 
				
			||||||
 | 
					    expect(response.statusCode).toEqual(403);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should throw error when user has no permissions with array query', async () => {
 | 
				
			||||||
 | 
					    const userRole = app.acl.define({
 | 
				
			||||||
 | 
					      role: 'user',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    userRole.grantAction('posts:destroy', {
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        'title.$in': ['p1', 'p2', 'p3'],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Post.repository.create({
 | 
				
			||||||
 | 
					      values: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          title: 'p1',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          title: 'p2',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          title: 'p3',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          title: 'p4',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          title: 'p5',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          title: 'p6',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    app.resourcer.use(
 | 
				
			||||||
 | 
					      (ctx, next) => {
 | 
				
			||||||
 | 
					        ctx.state.currentRole = 'user';
 | 
				
			||||||
 | 
					        ctx.state.currentUser = {
 | 
				
			||||||
 | 
					          id: 1,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        return next();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        before: 'acl',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const response = await app
 | 
				
			||||||
 | 
					      .agent()
 | 
				
			||||||
 | 
					      .resource('posts')
 | 
				
			||||||
 | 
					      .destroy({
 | 
				
			||||||
 | 
					        filter: {
 | 
				
			||||||
 | 
					          'title.$in': ['p4', 'p5', 'p6'],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // should throw error
 | 
				
			||||||
 | 
					    expect(response.statusCode).toEqual(403);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const response2 = await app
 | 
				
			||||||
 | 
					      .agent()
 | 
				
			||||||
 | 
					      .resource('posts')
 | 
				
			||||||
 | 
					      .destroy({
 | 
				
			||||||
 | 
					        filter: {
 | 
				
			||||||
 | 
					          'title.$in': ['p1'],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // should throw error
 | 
				
			||||||
 | 
					    expect(response2.statusCode).toEqual(200);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -4,6 +4,108 @@ import UsersPlugin from '@nocobase/plugin-users';
 | 
				
			|||||||
import { MockServer } from '@nocobase/test';
 | 
					import { MockServer } from '@nocobase/test';
 | 
				
			||||||
import { prepareApp } from './prepare';
 | 
					import { prepareApp } from './prepare';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('association test', () => {
 | 
				
			||||||
 | 
					  let app: MockServer;
 | 
				
			||||||
 | 
					  let db: Database;
 | 
				
			||||||
 | 
					  let acl: ACL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let user;
 | 
				
			||||||
 | 
					  let userAgent;
 | 
				
			||||||
 | 
					  let admin;
 | 
				
			||||||
 | 
					  let adminAgent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  afterEach(async () => {
 | 
				
			||||||
 | 
					    await app.destroy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    app = await prepareApp();
 | 
				
			||||||
 | 
					    db = app.db;
 | 
				
			||||||
 | 
					    acl = app.acl;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should set association actions', async () => {
 | 
				
			||||||
 | 
					    await db.getRepository('collections').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'posts',
 | 
				
			||||||
 | 
					        fields: [
 | 
				
			||||||
 | 
					          { name: 'title', type: 'string' },
 | 
				
			||||||
 | 
					          { name: 'userComments', type: 'hasMany', target: 'comments', interface: 'linkTo' },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      context: {},
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.getRepository('collections').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'comments',
 | 
				
			||||||
 | 
					        fields: [{ name: 'content', type: 'string' }],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      context: {},
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.getRepository('roles').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'test-role',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      context: {},
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.getRepository('roles.resources', 'test-role').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'posts',
 | 
				
			||||||
 | 
					        usingActionsConfig: true,
 | 
				
			||||||
 | 
					        actions: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name: 'view',
 | 
				
			||||||
 | 
					            fields: ['userComments'],
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      context: {},
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const role = acl.getRole('test-role');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(
 | 
				
			||||||
 | 
					      acl.can({
 | 
				
			||||||
 | 
					        role: 'test-role',
 | 
				
			||||||
 | 
					        action: 'list',
 | 
				
			||||||
 | 
					        resource: 'posts.userComments',
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					    ).not.toBeNull();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const post = await db.getRepository('posts').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        title: 'hello world',
 | 
				
			||||||
 | 
					        userComments: [{ content: 'comment 1' }],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const UserRepo = db.getCollection('users').repository;
 | 
				
			||||||
 | 
					    const user = await UserRepo.create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        roles: ['test-role'],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const userPlugin = app.getPlugin('users') as UsersPlugin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const userAgent = app.agent().auth(
 | 
				
			||||||
 | 
					      userPlugin.jwtService.sign({
 | 
				
			||||||
 | 
					        userId: user.get('id'),
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      { type: 'bearer' },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //@ts-ignore
 | 
				
			||||||
 | 
					    const response = await userAgent.resource('posts').list({});
 | 
				
			||||||
 | 
					    expect(response.statusCode).toEqual(200);
 | 
				
			||||||
 | 
					    const post1 = response.body.data[0];
 | 
				
			||||||
 | 
					    expect(post1.userComments).not.toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('association field acl', () => {
 | 
					describe('association field acl', () => {
 | 
				
			||||||
  let app: MockServer;
 | 
					  let app: MockServer;
 | 
				
			||||||
  let db: Database;
 | 
					  let db: Database;
 | 
				
			||||||
@ -26,22 +128,24 @@ describe('association field acl', () => {
 | 
				
			|||||||
    await db.getRepository('roles').create({
 | 
					    await db.getRepository('roles').create({
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
        name: 'new',
 | 
					        name: 'new',
 | 
				
			||||||
        allowConfigure: true,
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await db.getRepository('roles').create({
 | 
					    await db.getRepository('roles').create({
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
        name: 'testAdmin',
 | 
					        name: 'testAdmin',
 | 
				
			||||||
        allowConfigure: true,
 | 
					        snippets: ['pm.*'],
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const UserRepo = db.getCollection('users').repository;
 | 
					    const UserRepo = db.getCollection('users').repository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    user = await UserRepo.create({
 | 
					    user = await UserRepo.create({
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
        roles: ['new'],
 | 
					        roles: ['new'],
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    admin = await UserRepo.create({
 | 
					    admin = await UserRepo.create({
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
        roles: ['testAdmin'],
 | 
					        roles: ['testAdmin'],
 | 
				
			||||||
@ -55,6 +159,7 @@ describe('association field acl', () => {
 | 
				
			|||||||
      }),
 | 
					      }),
 | 
				
			||||||
      { type: 'bearer' },
 | 
					      { type: 'bearer' },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    adminAgent = app.agent().auth(
 | 
					    adminAgent = app.agent().auth(
 | 
				
			||||||
      userPlugin.jwtService.sign({
 | 
					      userPlugin.jwtService.sign({
 | 
				
			||||||
        userId: admin.get('id'),
 | 
					        userId: admin.get('id'),
 | 
				
			||||||
@ -119,9 +224,22 @@ describe('association field acl', () => {
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await adminAgent.resource('roles.resources', 'new').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'orders',
 | 
				
			||||||
 | 
					        usingActionsConfig: true,
 | 
				
			||||||
 | 
					        actions: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name: 'view',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should revoke target action on association action revoke', async () => {
 | 
					  // skip because of disable grant associations target action
 | 
				
			||||||
 | 
					  it.skip('should revoke target action on association action revoke', async () => {
 | 
				
			||||||
    expect(
 | 
					    expect(
 | 
				
			||||||
      acl.can({
 | 
					      acl.can({
 | 
				
			||||||
        role: 'new',
 | 
					        role: 'new',
 | 
				
			||||||
@ -173,6 +291,7 @@ describe('association field acl', () => {
 | 
				
			|||||||
    const actionId = viewAction.get('id') as number;
 | 
					    const actionId = viewAction.get('id') as number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const response = await adminAgent.resource('roles.resources', 'new').update({
 | 
					    const response = await adminAgent.resource('roles.resources', 'new').update({
 | 
				
			||||||
 | 
					      filterByTk: 'users',
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
        name: 'users',
 | 
					        name: 'users',
 | 
				
			||||||
        usingActionsConfig: true,
 | 
					        usingActionsConfig: true,
 | 
				
			||||||
@ -197,6 +316,7 @@ describe('association field acl', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  it('should revoke association action on field deleted', async () => {
 | 
					  it('should revoke association action on field deleted', async () => {
 | 
				
			||||||
    await adminAgent.resource('roles.resources', 'new').update({
 | 
					    await adminAgent.resource('roles.resources', 'new').update({
 | 
				
			||||||
 | 
					      filterByTk: 'users',
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
        name: 'users',
 | 
					        name: 'users',
 | 
				
			||||||
        usingActionsConfig: true,
 | 
					        usingActionsConfig: true,
 | 
				
			||||||
@ -208,6 +328,7 @@ describe('association field acl', () => {
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expect(
 | 
					    expect(
 | 
				
			||||||
      acl.can({
 | 
					      acl.can({
 | 
				
			||||||
        role: 'new',
 | 
					        role: 'new',
 | 
				
			||||||
@ -222,6 +343,7 @@ describe('association field acl', () => {
 | 
				
			|||||||
        whitelist: ['age', 'name'],
 | 
					        whitelist: ['age', 'name'],
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const roleResource = await db.getRepository('rolesResources').findOne({
 | 
					    const roleResource = await db.getRepository('rolesResources').findOne({
 | 
				
			||||||
      filter: {
 | 
					      filter: {
 | 
				
			||||||
        name: 'users',
 | 
					        name: 'users',
 | 
				
			||||||
 | 
				
			|||||||
@ -23,7 +23,7 @@ describe('configuration', () => {
 | 
				
			|||||||
    await db.getRepository('roles').create({
 | 
					    await db.getRepository('roles').create({
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
        name: 'test1',
 | 
					        name: 'test1',
 | 
				
			||||||
        allowConfigure: true,
 | 
					        snippets: ['pm.*'],
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -36,23 +36,29 @@ describe('configuration', () => {
 | 
				
			|||||||
    const UserRepo = db.getCollection('users').repository;
 | 
					    const UserRepo = db.getCollection('users').repository;
 | 
				
			||||||
    admin = await UserRepo.create({
 | 
					    admin = await UserRepo.create({
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
        roles: ['test1']
 | 
					        roles: ['test1'],
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    user = await UserRepo.create({
 | 
					    user = await UserRepo.create({
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
        roles: ['test2']
 | 
					        roles: ['test2'],
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const userPlugin = app.getPlugin('users') as UsersPlugin;
 | 
					    const userPlugin = app.getPlugin('users') as UsersPlugin;
 | 
				
			||||||
    adminAgent = app.agent().auth(userPlugin.jwtService.sign({
 | 
					    adminAgent = app.agent().auth(
 | 
				
			||||||
      userId: admin.get('id'),
 | 
					      userPlugin.jwtService.sign({
 | 
				
			||||||
    }), { type: 'bearer' });
 | 
					        userId: admin.get('id'),
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      { type: 'bearer' },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    userAgent = app.agent().auth(userPlugin.jwtService.sign({
 | 
					    userAgent = app.agent().auth(
 | 
				
			||||||
      userId: user.get('id'),
 | 
					      userPlugin.jwtService.sign({
 | 
				
			||||||
    }), { type: 'bearer' });
 | 
					        userId: user.get('id'),
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      { type: 'bearer' },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    guestAgent = app.agent();
 | 
					    guestAgent = app.agent();
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										273
									
								
								packages/plugins/acl/src/__tests__/list-action.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								packages/plugins/acl/src/__tests__/list-action.test.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,273 @@
 | 
				
			|||||||
 | 
					import { Database } from '@nocobase/database';
 | 
				
			||||||
 | 
					import { prepareApp } from './prepare';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('list action with acl', () => {
 | 
				
			||||||
 | 
					  let app;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let Post;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    app = await prepareApp();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Post = app.db.collection({
 | 
				
			||||||
 | 
					      name: 'posts',
 | 
				
			||||||
 | 
					      fields: [
 | 
				
			||||||
 | 
					        { type: 'string', name: 'title' },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'bigInt',
 | 
				
			||||||
 | 
					          name: 'createdById',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await app.db.sync();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  afterEach(async () => {
 | 
				
			||||||
 | 
					    await app.destroy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should list with meta permission that has difference primary key', async () => {
 | 
				
			||||||
 | 
					    const userRole = app.acl.define({
 | 
				
			||||||
 | 
					      role: 'user',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    userRole.grantAction('tests:view', {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    userRole.grantAction('tests:update', {
 | 
				
			||||||
 | 
					      own: true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const Test = app.db.collection({
 | 
				
			||||||
 | 
					      name: 'tests',
 | 
				
			||||||
 | 
					      fields: [
 | 
				
			||||||
 | 
					        { type: 'string', name: 'name', primaryKey: true },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'bigInt',
 | 
				
			||||||
 | 
					          name: 'createdById',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      autoGenId: false,
 | 
				
			||||||
 | 
					      filterTargetKey: 'name',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await app.db.sync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Test.repository.create({
 | 
				
			||||||
 | 
					      values: [
 | 
				
			||||||
 | 
					        { name: 't1', createdById: 1 },
 | 
				
			||||||
 | 
					        { name: 't2', createdById: 1 },
 | 
				
			||||||
 | 
					        { name: 't3', createdById: 2 },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    app.resourcer.use(
 | 
				
			||||||
 | 
					      (ctx, next) => {
 | 
				
			||||||
 | 
					        ctx.state.currentRole = 'user';
 | 
				
			||||||
 | 
					        ctx.state.currentUser = {
 | 
				
			||||||
 | 
					          id: 1,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return next();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        before: 'acl',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    const response = await app.agent().set('X-With-ACL-Meta', true).resource('tests').list({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const data = response.body;
 | 
				
			||||||
 | 
					    expect(data.meta.allowedActions.view).toEqual(['t1', 't2', 't3']);
 | 
				
			||||||
 | 
					    expect(data.meta.allowedActions.update).toEqual(['t1', 't2']);
 | 
				
			||||||
 | 
					    expect(data.meta.allowedActions.destroy).toEqual([]);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should list items with meta permission', async () => {
 | 
				
			||||||
 | 
					    const userRole = app.acl.define({
 | 
				
			||||||
 | 
					      role: 'user',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    userRole.grantAction('posts:view', {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    userRole.grantAction('posts:update', {
 | 
				
			||||||
 | 
					      own: true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Post.repository.create({
 | 
				
			||||||
 | 
					      values: [
 | 
				
			||||||
 | 
					        { title: 'p1', createdById: 1 },
 | 
				
			||||||
 | 
					        { title: 'p2', createdById: 1 },
 | 
				
			||||||
 | 
					        { title: 'p3', createdById: 2 },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    app.resourcer.use(
 | 
				
			||||||
 | 
					      (ctx, next) => {
 | 
				
			||||||
 | 
					        ctx.state.currentRole = 'user';
 | 
				
			||||||
 | 
					        ctx.state.currentUser = {
 | 
				
			||||||
 | 
					          id: 1,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return next();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        before: 'acl',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const response = await app.agent().set('X-With-ACL-Meta', true).resource('posts').list({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const data = response.body;
 | 
				
			||||||
 | 
					    expect(data.meta.allowedActions.view).toEqual([1, 2, 3]);
 | 
				
			||||||
 | 
					    expect(data.meta.allowedActions.update).toEqual([1, 2]);
 | 
				
			||||||
 | 
					    expect(data.meta.allowedActions.destroy).toEqual([]);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should response item permission when request get action', async () => {
 | 
				
			||||||
 | 
					    const userRole = app.acl.define({
 | 
				
			||||||
 | 
					      role: 'user',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    userRole.grantAction('posts:view', {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    userRole.grantAction('posts:update', {
 | 
				
			||||||
 | 
					      own: true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Post.repository.create({
 | 
				
			||||||
 | 
					      values: [
 | 
				
			||||||
 | 
					        { title: 'p1', createdById: 1 },
 | 
				
			||||||
 | 
					        { title: 'p2', createdById: 1 },
 | 
				
			||||||
 | 
					        { title: 'p3', createdById: 2 },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    app.resourcer.use(
 | 
				
			||||||
 | 
					      (ctx, next) => {
 | 
				
			||||||
 | 
					        ctx.state.currentRole = 'user';
 | 
				
			||||||
 | 
					        ctx.state.currentUser = {
 | 
				
			||||||
 | 
					          id: 1,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return next();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        before: 'acl',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getResponse = await app.agent().set('X-With-ACL-Meta', true).resource('posts').get({
 | 
				
			||||||
 | 
					      filterByTk: 1,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getBody = getResponse.body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(getBody.meta.allowedActions).toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('list association action with acl', () => {
 | 
				
			||||||
 | 
					  let app;
 | 
				
			||||||
 | 
					  let db: Database;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  afterEach(async () => {
 | 
				
			||||||
 | 
					    await app.destroy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    app = await prepareApp();
 | 
				
			||||||
 | 
					    db = app.db;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    app.db.collection({
 | 
				
			||||||
 | 
					      name: 'posts',
 | 
				
			||||||
 | 
					      fields: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'string',
 | 
				
			||||||
 | 
					          name: 'title',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'hasMany',
 | 
				
			||||||
 | 
					          name: 'comments',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    app.db.collection({
 | 
				
			||||||
 | 
					      name: 'comments',
 | 
				
			||||||
 | 
					      fields: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'string',
 | 
				
			||||||
 | 
					          name: 'content',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: 'belongsTo',
 | 
				
			||||||
 | 
					          name: 'post',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await app.db.sync();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should list allowedActions', async () => {
 | 
				
			||||||
 | 
					    await db.getRepository('roles').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'newRole',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const user = await db.getRepository('users').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        roles: ['newRole'],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.getRepository('roles.resources', 'newRole').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'posts',
 | 
				
			||||||
 | 
					        usingActionConfig: true,
 | 
				
			||||||
 | 
					        actions: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name: 'view',
 | 
				
			||||||
 | 
					            fields: ['title', 'comments'],
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name: 'create',
 | 
				
			||||||
 | 
					            fields: ['title', 'comments'],
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const userPlugin = app.getPlugin('users');
 | 
				
			||||||
 | 
					    const userAgent = app.agent().set('X-With-ACL-Meta', true).auth(
 | 
				
			||||||
 | 
					      userPlugin.jwtService.sign({
 | 
				
			||||||
 | 
					        userId: user.get('id'),
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      { type: 'bearer' },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await userAgent.resource('posts').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        title: 'post1',
 | 
				
			||||||
 | 
					        comments: [{ content: 'comment1' }, { content: 'comment2' }],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const response = await userAgent.resource('posts').list({});
 | 
				
			||||||
 | 
					    expect(response.statusCode).toEqual(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const commentsResponse = await userAgent.resource('posts.comments', 1).list({});
 | 
				
			||||||
 | 
					    const data = commentsResponse.body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * allowedActions.view == [1]
 | 
				
			||||||
 | 
					     * allowedActions.update = []
 | 
				
			||||||
 | 
					     * allowedActions.destroy = []
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    expect(data['meta']['allowedActions']).toBeDefined();
 | 
				
			||||||
 | 
					    expect(data['meta']['allowedActions'].view).toContain(1);
 | 
				
			||||||
 | 
					    expect(data['meta']['allowedActions'].view).toContain(2);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { Database, Model } from '@nocobase/database';
 | 
					import { ArrayFieldRepository, Database, Model } from '@nocobase/database';
 | 
				
			||||||
import UsersPlugin from '@nocobase/plugin-users';
 | 
					import UsersPlugin from '@nocobase/plugin-users';
 | 
				
			||||||
import { MockServer } from '@nocobase/test';
 | 
					import { MockServer } from '@nocobase/test';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -96,4 +96,29 @@ describe('role api', () => {
 | 
				
			|||||||
    expect(defaultRole.length).toEqual(1);
 | 
					    expect(defaultRole.length).toEqual(1);
 | 
				
			||||||
    expect(defaultRole[0].get('name')).toEqual('role2');
 | 
					    expect(defaultRole[0].get('name')).toEqual('role2');
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should sync snippet patterns', async () => {
 | 
				
			||||||
 | 
					    app.acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: 'collections',
 | 
				
			||||||
 | 
					      actions: ['collection:*'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.getRepository('roles').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'role1',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.getRepository<ArrayFieldRepository>('roles.snippets', 'role1').set({
 | 
				
			||||||
 | 
					      values: ['collections'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const role1Instance = await db.getRepository('roles').findOne({
 | 
				
			||||||
 | 
					      filterByTk: 'role1',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const role1 = app.acl.getRole('role1');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(role1.toJSON()['snippets']).toEqual(['collections']);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										41
									
								
								packages/plugins/acl/src/__tests__/snippets.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								packages/plugins/acl/src/__tests__/snippets.test.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					import { MockServer } from '@nocobase/test';
 | 
				
			||||||
 | 
					import { prepareApp } from './prepare';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('snippet', () => {
 | 
				
			||||||
 | 
					  let app: MockServer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    app = await prepareApp();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  afterEach(async () => {
 | 
				
			||||||
 | 
					    await app.destroy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should not allow to create collections when global allow create', async () => {
 | 
				
			||||||
 | 
					    await app.db.getRepository('roles').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'testRole',
 | 
				
			||||||
 | 
					        strategy: { actions: ['view', 'update:own', 'destroy:own', 'create'] },
 | 
				
			||||||
 | 
					        snippets: ['!ui.*', '!pm', '!pm.*'],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const user = await app.db.getRepository('users').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        roles: ['testRole'],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const userPlugin: any = app.getPlugin('users');
 | 
				
			||||||
 | 
					    const userAgent: any = app.agent().auth(
 | 
				
			||||||
 | 
					      userPlugin.jwtService.sign({
 | 
				
			||||||
 | 
					        userId: user.get('id'),
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      { type: 'bearer' },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const createCollectionResponse = await userAgent.resource('collections').create({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(createCollectionResponse.statusCode).toEqual(403);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -21,9 +21,9 @@ describe('actions', () => {
 | 
				
			|||||||
    pluginUser = app.getPlugin('users');
 | 
					    pluginUser = app.getPlugin('users');
 | 
				
			||||||
    adminUser = await db.getRepository('users').findOne({
 | 
					    adminUser = await db.getRepository('users').findOne({
 | 
				
			||||||
      filter: {
 | 
					      filter: {
 | 
				
			||||||
        email: process.env.INIT_ROOT_EMAIL
 | 
					        email: process.env.INIT_ROOT_EMAIL,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      appends: ['roles']
 | 
					      appends: ['roles'],
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    agent = app.agent();
 | 
					    agent = app.agent();
 | 
				
			||||||
@ -44,9 +44,98 @@ describe('actions', () => {
 | 
				
			|||||||
      filterByTk: adminUser.id,
 | 
					      filterByTk: adminUser.id,
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
        nickname: 'a',
 | 
					        nickname: 'a',
 | 
				
			||||||
        roles: adminUser.roles
 | 
					        roles: adminUser.roles,
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    expect(res2.status).toBe(200);
 | 
					    expect(res2.status).toBe(200);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('can destroy users role', async () => {
 | 
				
			||||||
 | 
					    const role2 = await db.getRepository('roles').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'test',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const users2 = await db.getRepository('users').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        email: 'test2@nocobase.com',
 | 
				
			||||||
 | 
					        name: 'test2',
 | 
				
			||||||
 | 
					        password: '123456',
 | 
				
			||||||
 | 
					        roles: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name: 'test',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let response = await agent.post('/users:signin').send({
 | 
				
			||||||
 | 
					      email: 'test2@nocobase.com',
 | 
				
			||||||
 | 
					      password: '123456',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(response.statusCode).toEqual(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const token = response.body.data.token;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const loggedAgent = app.agent().auth(token, { type: 'bearer' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const rolesCheckResponse = (await loggedAgent.set('Accept', 'application/json').get('/roles:check')) as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(rolesCheckResponse.statusCode).toEqual(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.getRepository('roles').destroy({
 | 
				
			||||||
 | 
					      filterByTk: 'test',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    response = await agent.post('/users:signin').send({
 | 
				
			||||||
 | 
					      email: 'test2@nocobase.com',
 | 
				
			||||||
 | 
					      password: '123456',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(response.statusCode).toEqual(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const rolesCheckResponse2 = (await loggedAgent.set('Accept', 'application/json').get('/roles:check')) as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(rolesCheckResponse2.status).toEqual(500);
 | 
				
			||||||
 | 
					    expect(rolesCheckResponse2.body.errors[0].message).toEqual('Role not found');
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should destroy through table record when destroy role', async () => {
 | 
				
			||||||
 | 
					    await db.getRepository('roles').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'test',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const users2 = await db.getRepository('users').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        email: 'test2@nocobase.com',
 | 
				
			||||||
 | 
					        name: 'test2',
 | 
				
			||||||
 | 
					        password: '123456',
 | 
				
			||||||
 | 
					        roles: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name: 'test',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(await users2.countRoles()).toEqual(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.getRepository('roles').destroy({
 | 
				
			||||||
 | 
					      filterByTk: 'test',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(await users2.countRoles()).toEqual(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.getRepository('roles').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'test',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(await users2.countRoles()).toEqual(0);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,10 @@
 | 
				
			|||||||
const map2obj = (map: Map<string, string>) => {
 | 
					const map2obj = (map: Map<string, string>) => {
 | 
				
			||||||
  const obj = {};
 | 
					  const obj = {};
 | 
				
			||||||
  for(let [key, value] of map){
 | 
					  for (let [key, value] of map) {
 | 
				
			||||||
    obj[key] = value;
 | 
					    obj[key] = value;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 return obj;
 | 
					  return obj;
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function checkAction(ctx, next) {
 | 
					export async function checkAction(ctx, next) {
 | 
				
			||||||
  const currentRole = ctx.state.currentRole;
 | 
					  const currentRole = ctx.state.currentRole;
 | 
				
			||||||
@ -23,9 +23,11 @@ export async function checkAction(ctx, next) {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const role = ctx.app.acl.getRole(currentRole);
 | 
					    const role = ctx.app.acl.getRole(currentRole);
 | 
				
			||||||
 | 
					    const availableActions = ctx.app.acl.getAvailableActions();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ctx.body = {
 | 
					    ctx.body = {
 | 
				
			||||||
      ...role.toJSON(),
 | 
					      ...role.toJSON(),
 | 
				
			||||||
 | 
					      availableActions: [...availableActions.keys()],
 | 
				
			||||||
      resources: [...role.resources.keys()],
 | 
					      resources: [...role.resources.keys()],
 | 
				
			||||||
      actionAlias: map2obj(ctx.app.acl.actionAlias),
 | 
					      actionAlias: map2obj(ctx.app.acl.actionAlias),
 | 
				
			||||||
      allowAll: currentRole === 'root',
 | 
					      allowAll: currentRole === 'root',
 | 
				
			||||||
 | 
				
			|||||||
@ -15,6 +15,7 @@ const roleCollectionsResource = {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      const db: Database = ctx.db;
 | 
					      const db: Database = ctx.db;
 | 
				
			||||||
      const collectionRepository = db.getRepository('collections');
 | 
					      const collectionRepository = db.getRepository('collections');
 | 
				
			||||||
 | 
					      const fieldRepository = db.getRepository('fields');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // all collections
 | 
					      // all collections
 | 
				
			||||||
      const [collections, count] = await collectionRepository.findAndCount({
 | 
					      const [collections, count] = await collectionRepository.findAndCount({
 | 
				
			||||||
@ -35,23 +36,52 @@ const roleCollectionsResource = {
 | 
				
			|||||||
        .filter((roleResources) => roleResources.get('usingActionsConfig'))
 | 
					        .filter((roleResources) => roleResources.get('usingActionsConfig'))
 | 
				
			||||||
        .map((roleResources) => roleResources.get('name'));
 | 
					        .map((roleResources) => roleResources.get('name'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const items = collections.map((collection, i) => {
 | 
				
			||||||
 | 
					        const exists = roleResourcesNames.includes(collection.get('name'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const usingConfig: UsingConfigType = roleResourceActionResourceNames.includes(collection.get('name'))
 | 
				
			||||||
 | 
					          ? 'resourceAction'
 | 
				
			||||||
 | 
					          : 'strategy';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const c = db.getCollection(collection.get('name'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // const children = [...c.fields.values()]
 | 
				
			||||||
 | 
					        //   .filter(
 | 
				
			||||||
 | 
					        //     (f) => f.options.interface && ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(f.options.type),
 | 
				
			||||||
 | 
					        //   )
 | 
				
			||||||
 | 
					        //   .map((f, j) => {
 | 
				
			||||||
 | 
					        //     const name = `${collection.get('name')}.${f.options.name}`;
 | 
				
			||||||
 | 
					        //     const usingConfig: UsingConfigType = roleResourceActionResourceNames.includes(name)
 | 
				
			||||||
 | 
					        //       ? 'resourceAction'
 | 
				
			||||||
 | 
					        //       : 'strategy';
 | 
				
			||||||
 | 
					        //     const exists = roleResourcesNames.includes(name);
 | 
				
			||||||
 | 
					        //     return {
 | 
				
			||||||
 | 
					        //       type: 'association',
 | 
				
			||||||
 | 
					        //       __index: `${i}.children.${j}`,
 | 
				
			||||||
 | 
					        //       name,
 | 
				
			||||||
 | 
					        //       collectionName: f.options.target,
 | 
				
			||||||
 | 
					        //       title: f.options?.uiSchema?.title,
 | 
				
			||||||
 | 
					        //       roleName: role,
 | 
				
			||||||
 | 
					        //       usingConfig,
 | 
				
			||||||
 | 
					        //       exists,
 | 
				
			||||||
 | 
					        //     };
 | 
				
			||||||
 | 
					        //   });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          type: 'collection',
 | 
				
			||||||
 | 
					          name: collection.get('name') as string,
 | 
				
			||||||
 | 
					          collectionName: collection.get('name'),
 | 
				
			||||||
 | 
					          title: collection.get('title') as string,
 | 
				
			||||||
 | 
					          roleName: role,
 | 
				
			||||||
 | 
					          usingConfig,
 | 
				
			||||||
 | 
					          exists,
 | 
				
			||||||
 | 
					          // children: children.length > 0 ? children : null,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      ctx.body = {
 | 
					      ctx.body = {
 | 
				
			||||||
        count,
 | 
					        count,
 | 
				
			||||||
        rows: collections.map((collection) => {
 | 
					        rows: items,
 | 
				
			||||||
          const exists = roleResourcesNames.includes(collection.get('name'));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          const usingConfig: UsingConfigType = roleResourceActionResourceNames.includes(collection.get('name'))
 | 
					 | 
				
			||||||
            ? 'resourceAction'
 | 
					 | 
				
			||||||
            : 'strategy';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          return {
 | 
					 | 
				
			||||||
            name: collection.get('name') as string,
 | 
					 | 
				
			||||||
            title: collection.get('title') as string,
 | 
					 | 
				
			||||||
            roleName: role,
 | 
					 | 
				
			||||||
            usingConfig,
 | 
					 | 
				
			||||||
            exists,
 | 
					 | 
				
			||||||
          };
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
        page: Number(page),
 | 
					        page: Number(page),
 | 
				
			||||||
        pageSize: Number(pageSize),
 | 
					        pageSize: Number(pageSize),
 | 
				
			||||||
        totalPage: totalPage(count, pageSize),
 | 
					        totalPage: totalPage(count, pageSize),
 | 
				
			||||||
 | 
				
			|||||||
@ -75,5 +75,10 @@ export default {
 | 
				
			|||||||
      sourceKey: 'name',
 | 
					      sourceKey: 'name',
 | 
				
			||||||
      targetKey: 'name',
 | 
					      targetKey: 'name',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      type: 'set',
 | 
				
			||||||
 | 
					      name: 'snippets',
 | 
				
			||||||
 | 
					      defaultValue: ['!ui.*', '!pm', '!pm.*'],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
} as CollectionOptions;
 | 
					} as CollectionOptions;
 | 
				
			||||||
 | 
				
			|||||||
@ -23,6 +23,7 @@ export default {
 | 
				
			|||||||
      type: 'belongsTo',
 | 
					      type: 'belongsTo',
 | 
				
			||||||
      name: 'scope',
 | 
					      name: 'scope',
 | 
				
			||||||
      target: 'rolesResourcesScopes',
 | 
					      target: 'rolesResourcesScopes',
 | 
				
			||||||
 | 
					      onDelete: 'RESTRICT',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
} as CollectionOptions;
 | 
					} as CollectionOptions;
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,7 @@ export default extend({
 | 
				
			|||||||
      target: 'roles',
 | 
					      target: 'roles',
 | 
				
			||||||
      foreignKey: 'userId',
 | 
					      foreignKey: 'userId',
 | 
				
			||||||
      otherKey: 'roleName',
 | 
					      otherKey: 'roleName',
 | 
				
			||||||
 | 
					      onDelete: 'CASCADE',
 | 
				
			||||||
      sourceKey: 'id',
 | 
					      sourceKey: 'id',
 | 
				
			||||||
      targetKey: 'name',
 | 
					      targetKey: 'name',
 | 
				
			||||||
      through: 'rolesUsers',
 | 
					      through: 'rolesUsers',
 | 
				
			||||||
@ -25,6 +26,6 @@ export default extend({
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					import { Migration } from '@nocobase/server';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class extends Migration {
 | 
				
			||||||
 | 
					  async up() {
 | 
				
			||||||
 | 
					    await this.app.db.getRepository('roles').update({
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        $or: [{ allowConfigure: true }, { name: 'root' }],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        snippets: ['ui.*', 'pm', 'pm.*'],
 | 
				
			||||||
 | 
					        allowConfigure: false,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async down() {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -17,5 +17,7 @@ export class RoleModel extends Model {
 | 
				
			|||||||
      ...((this.get('strategy') as object) || {}),
 | 
					      ...((this.get('strategy') as object) || {}),
 | 
				
			||||||
      allowConfigure: this.get('allowConfigure') as boolean,
 | 
					      allowConfigure: this.get('allowConfigure') as boolean,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    role.snippets = new Set(this.get('snippets'));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -50,7 +50,7 @@ export class RoleResourceActionModel extends Model {
 | 
				
			|||||||
        continue;
 | 
					        continue;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const fieldType = collectionField.get('interface') as string;
 | 
					      const fieldType = collectionField.get('type') as string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const fieldActions: AssociationFieldAction = associationFieldsActions?.[fieldType]?.[availableAction];
 | 
					      const fieldActions: AssociationFieldAction = associationFieldsActions?.[fieldType]?.[availableAction];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -59,8 +59,9 @@ export class RoleResourceActionModel extends Model {
 | 
				
			|||||||
      if (fieldActions) {
 | 
					      if (fieldActions) {
 | 
				
			||||||
        // grant association actions to role
 | 
					        // grant association actions to role
 | 
				
			||||||
        const associationActions = fieldActions.associationActions || [];
 | 
					        const associationActions = fieldActions.associationActions || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        associationActions.forEach((associationAction) => {
 | 
					        associationActions.forEach((associationAction) => {
 | 
				
			||||||
          const actionName = `${resourceName}.${fieldTarget}:${associationAction}`;
 | 
					          const actionName = `${resourceName}.${collectionField.get('name')}:${associationAction}`;
 | 
				
			||||||
          role.grantAction(actionName);
 | 
					          role.grantAction(actionName);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -69,6 +70,12 @@ export class RoleResourceActionModel extends Model {
 | 
				
			|||||||
        targetActions.forEach((targetAction) => {
 | 
					        targetActions.forEach((targetAction) => {
 | 
				
			||||||
          const targetActionPath = `${fieldTarget}:${targetAction}`;
 | 
					          const targetActionPath = `${fieldTarget}:${targetAction}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const existsAction = role.getActionParams(targetActionPath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (existsAction) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          // set resource target action with current resourceName
 | 
					          // set resource target action with current resourceName
 | 
				
			||||||
          grantHelper.resourceTargetActionMap.set(`${role.name}.${resourceName}`, [
 | 
					          grantHelper.resourceTargetActionMap.set(`${role.name}.${resourceName}`, [
 | 
				
			||||||
            ...(grantHelper.resourceTargetActionMap.get(resourceName) || []),
 | 
					            ...(grantHelper.resourceTargetActionMap.get(resourceName) || []),
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,8 @@
 | 
				
			|||||||
import { Context } from '@nocobase/actions';
 | 
					import { NoPermissionError } from '@nocobase/acl';
 | 
				
			||||||
import { Collection } from '@nocobase/database';
 | 
					import { Context, utils as actionUtils } from '@nocobase/actions';
 | 
				
			||||||
 | 
					import { Collection, RelationField } from '@nocobase/database';
 | 
				
			||||||
import { Plugin } from '@nocobase/server';
 | 
					import { Plugin } from '@nocobase/server';
 | 
				
			||||||
 | 
					import lodash from 'lodash';
 | 
				
			||||||
import { resolve } from 'path';
 | 
					import { resolve } from 'path';
 | 
				
			||||||
import { availableActionResource } from './actions/available-actions';
 | 
					import { availableActionResource } from './actions/available-actions';
 | 
				
			||||||
import { checkAction } from './actions/role-check';
 | 
					import { checkAction } from './actions/role-check';
 | 
				
			||||||
@ -49,41 +51,52 @@ export class PluginACL extends Plugin {
 | 
				
			|||||||
  registerAssociationFieldsActions() {
 | 
					  registerAssociationFieldsActions() {
 | 
				
			||||||
    // if grant create action to role, it should
 | 
					    // if grant create action to role, it should
 | 
				
			||||||
    // also grant add action and association target's view action
 | 
					    // also grant add action and association target's view action
 | 
				
			||||||
    this.registerAssociationFieldAction('linkTo', {
 | 
					
 | 
				
			||||||
 | 
					    this.registerAssociationFieldAction('hasOne', {
 | 
				
			||||||
      view: {
 | 
					      view: {
 | 
				
			||||||
        associationActions: ['list', 'get'],
 | 
					        associationActions: ['list', 'get', 'view'],
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      create: {
 | 
					      create: {
 | 
				
			||||||
        associationActions: ['add'],
 | 
					        associationActions: ['create', 'set'],
 | 
				
			||||||
        targetActions: ['view'],
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      update: {
 | 
					      update: {
 | 
				
			||||||
        associationActions: ['add', 'remove', 'toggle'],
 | 
					        associationActions: ['update', 'remove', 'set'],
 | 
				
			||||||
        targetActions: ['view'],
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.registerAssociationFieldAction('attachments', {
 | 
					    this.registerAssociationFieldAction('hasMany', {
 | 
				
			||||||
      view: {
 | 
					      view: {
 | 
				
			||||||
        associationActions: ['list', 'get'],
 | 
					        associationActions: ['list', 'get', 'view'],
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      add: {
 | 
					      create: {
 | 
				
			||||||
        associationActions: ['upload', 'add'],
 | 
					        associationActions: ['create', 'set', 'add'],
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      update: {
 | 
					      update: {
 | 
				
			||||||
        associationActions: ['update', 'add', 'remove', 'toggle'],
 | 
					        associationActions: ['update', 'remove', 'set'],
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.registerAssociationFieldAction('subTable', {
 | 
					    this.registerAssociationFieldAction('belongsTo', {
 | 
				
			||||||
      view: {
 | 
					      view: {
 | 
				
			||||||
        associationActions: ['list', 'get'],
 | 
					        associationActions: ['list', 'get', 'view'],
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      create: {
 | 
					      create: {
 | 
				
			||||||
        associationActions: ['create'],
 | 
					        associationActions: ['create', 'set'],
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      update: {
 | 
					      update: {
 | 
				
			||||||
        associationActions: ['update', 'destroy'],
 | 
					        associationActions: ['update', 'remove', 'set'],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.registerAssociationFieldAction('belongsToMany', {
 | 
				
			||||||
 | 
					      view: {
 | 
				
			||||||
 | 
					        associationActions: ['list', 'get', 'view'],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      create: {
 | 
				
			||||||
 | 
					        associationActions: ['create', 'set', 'add'],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      update: {
 | 
				
			||||||
 | 
					        associationActions: ['update', 'remove', 'set', 'toggle'],
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -123,12 +136,62 @@ export class PluginACL extends Plugin {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async beforeLoad() {
 | 
					  async beforeLoad() {
 | 
				
			||||||
 | 
					    this.db.addMigrations({
 | 
				
			||||||
 | 
					      namespace: this.name,
 | 
				
			||||||
 | 
					      directory: resolve(__dirname, './migrations'),
 | 
				
			||||||
 | 
					      context: {
 | 
				
			||||||
 | 
					        plugin: this,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.app.db.registerModels({
 | 
					    this.app.db.registerModels({
 | 
				
			||||||
      RoleResourceActionModel,
 | 
					      RoleResourceActionModel,
 | 
				
			||||||
      RoleResourceModel,
 | 
					      RoleResourceModel,
 | 
				
			||||||
      RoleModel,
 | 
					      RoleModel,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: `pm.${this.name}.roles`,
 | 
				
			||||||
 | 
					      actions: [
 | 
				
			||||||
 | 
					        'roles:*',
 | 
				
			||||||
 | 
					        'roles.snippets:*',
 | 
				
			||||||
 | 
					        'availableActions:list',
 | 
				
			||||||
 | 
					        'roles.collections:list',
 | 
				
			||||||
 | 
					        'roles.resources:*',
 | 
				
			||||||
 | 
					        'uiSchemas:getProperties',
 | 
				
			||||||
 | 
					        'roles.menuUiSchemas:*',
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // change resource fields to association fields
 | 
				
			||||||
 | 
					    this.app.acl.beforeGrantAction((ctx) => {
 | 
				
			||||||
 | 
					      const actionName = this.app.acl.resolveActionAlias(ctx.actionName);
 | 
				
			||||||
 | 
					      const collection = this.app.db.getCollection(ctx.resourceName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!collection) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const fieldsParams = ctx.params.fields;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!fieldsParams) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (actionName == 'view' || actionName == 'export') {
 | 
				
			||||||
 | 
					        const associationsFields = fieldsParams.filter((fieldName) => {
 | 
				
			||||||
 | 
					          const field = collection.getField(fieldName);
 | 
				
			||||||
 | 
					          return field instanceof RelationField;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ctx.params = {
 | 
				
			||||||
 | 
					          ...ctx.params,
 | 
				
			||||||
 | 
					          fields: lodash.difference(fieldsParams, associationsFields),
 | 
				
			||||||
 | 
					          appends: associationsFields,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.registerAssociationFieldsActions();
 | 
					    this.registerAssociationFieldsActions();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.app.resourcer.define(availableActionResource);
 | 
					    this.app.resourcer.define(availableActionResource);
 | 
				
			||||||
@ -147,6 +210,7 @@ export class PluginACL extends Plugin {
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        transaction,
 | 
					        transaction,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (defaultRole && (await model.countRoles({ transaction })) == 0) {
 | 
					      if (defaultRole && (await model.countRoles({ transaction })) == 0) {
 | 
				
			||||||
        await model.addRoles(defaultRole, { transaction });
 | 
					        await model.addRoles(defaultRole, { transaction });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -269,7 +333,7 @@ export class PluginACL extends Plugin {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // sync database role data to acl
 | 
					    // sync database role data to acl
 | 
				
			||||||
    this.app.on('afterLoad', async (app, options) => {
 | 
					    this.app.on('afterLoad', async (app, options) => {
 | 
				
			||||||
      if (options?.method === 'install') {
 | 
					      if (options?.method === 'install' || options?.method === 'upgrade') {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const exists = await this.app.db.collectionExistsInDb('roles');
 | 
					      const exists = await this.app.db.collectionExistsInDb('roles');
 | 
				
			||||||
@ -320,6 +384,7 @@ export class PluginACL extends Plugin {
 | 
				
			|||||||
            name: 'root',
 | 
					            name: 'root',
 | 
				
			||||||
            title: '{{t("Root")}}',
 | 
					            title: '{{t("Root")}}',
 | 
				
			||||||
            hidden: true,
 | 
					            hidden: true,
 | 
				
			||||||
 | 
					            snippets: ['ui.*', 'pm', 'pm.*'],
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            name: 'admin',
 | 
					            name: 'admin',
 | 
				
			||||||
@ -327,6 +392,7 @@ export class PluginACL extends Plugin {
 | 
				
			|||||||
            allowConfigure: true,
 | 
					            allowConfigure: true,
 | 
				
			||||||
            allowNewMenu: true,
 | 
					            allowNewMenu: true,
 | 
				
			||||||
            strategy: { actions: ['create', 'view', 'update', 'destroy'] },
 | 
					            strategy: { actions: ['create', 'view', 'update', 'destroy'] },
 | 
				
			||||||
 | 
					            snippets: ['ui.*', 'pm', 'pm.*'],
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            name: 'member',
 | 
					            name: 'member',
 | 
				
			||||||
@ -334,6 +400,7 @@ export class PluginACL extends Plugin {
 | 
				
			|||||||
            allowNewMenu: true,
 | 
					            allowNewMenu: true,
 | 
				
			||||||
            strategy: { actions: ['view', 'update:own', 'destroy:own', 'create'] },
 | 
					            strategy: { actions: ['view', 'update:own', 'destroy:own', 'create'] },
 | 
				
			||||||
            default: true,
 | 
					            default: true,
 | 
				
			||||||
 | 
					            snippets: ['!ui.*', '!pm', '!pm.*'],
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
@ -359,16 +426,44 @@ export class PluginACL extends Plugin {
 | 
				
			|||||||
    this.app.resourcer.use(setCurrentRole, { tag: 'setCurrentRole', before: 'acl', after: 'parseToken' });
 | 
					    this.app.resourcer.use(setCurrentRole, { tag: 'setCurrentRole', before: 'acl', after: 'parseToken' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.app.acl.allow('users', 'setDefaultRole', 'loggedIn');
 | 
					    this.app.acl.allow('users', 'setDefaultRole', 'loggedIn');
 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.app.acl.allow('roles', 'check', 'loggedIn');
 | 
					    this.app.acl.allow('roles', 'check', 'loggedIn');
 | 
				
			||||||
    this.app.acl.allow('roles', ['create', 'update', 'destroy'], 'allowConfigure');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.app.acl.allow('roles.menuUiSchemas', ['set', 'toggle', 'list'], 'allowConfigure');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.app.acl.allow('*', '*', (ctx) => {
 | 
					    this.app.acl.allow('*', '*', (ctx) => {
 | 
				
			||||||
      return ctx.state.currentRole === 'root';
 | 
					      return ctx.state.currentRole === 'root';
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.addFixedParams('collections', 'destroy', () => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        filter: {
 | 
				
			||||||
 | 
					          $and: [{ 'name.$ne': 'roles' }, { 'name.$ne': 'rolesUsers' }],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.addFixedParams('rolesResourcesScopes', 'destroy', () => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        filter: {
 | 
				
			||||||
 | 
					          $and: [{ 'key.$ne': 'all' }, { 'key.$ne': 'own' }],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.addFixedParams('rolesResourcesScopes', 'update', () => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        filter: {
 | 
				
			||||||
 | 
					          $and: [{ 'key.$ne': 'all' }, { 'key.$ne': 'own' }],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.addFixedParams('roles', 'destroy', () => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        filter: {
 | 
				
			||||||
 | 
					          $and: [{ 'name.$ne': 'root' }, { 'name.$ne': 'admin' }, { 'name.$ne': 'member' }],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.app.resourcer.use(async (ctx, next) => {
 | 
					    this.app.resourcer.use(async (ctx, next) => {
 | 
				
			||||||
      const { actionName, resourceName, params } = ctx.action;
 | 
					      const { actionName, resourceName, params } = ctx.action;
 | 
				
			||||||
      const { showAnonymous } = params || {};
 | 
					      const { showAnonymous } = params || {};
 | 
				
			||||||
@ -381,11 +476,13 @@ export class PluginACL extends Plugin {
 | 
				
			|||||||
          });
 | 
					          });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (actionName === 'update' && resourceName === 'roles.resources') {
 | 
					      if (actionName === 'update' && resourceName === 'roles.resources') {
 | 
				
			||||||
        ctx.action.mergeParams({
 | 
					        ctx.action.mergeParams({
 | 
				
			||||||
          updateAssociationValues: ['actions'],
 | 
					          updateAssociationValues: ['actions'],
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await next();
 | 
					      await next();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -405,6 +502,7 @@ export class PluginACL extends Plugin {
 | 
				
			|||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          collection = ctx.db.getCollection(resourceName);
 | 
					          collection = ctx.db.getCollection(resourceName);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (collection && collection.hasField('createdById')) {
 | 
					        if (collection && collection.hasField('createdById')) {
 | 
				
			||||||
          ctx.permission.can.params.fields.push('createdById');
 | 
					          ctx.permission.can.params.fields.push('createdById');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -413,37 +511,252 @@ export class PluginACL extends Plugin {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const parseJsonTemplate = this.app.acl.parseJsonTemplate;
 | 
					    const parseJsonTemplate = this.app.acl.parseJsonTemplate;
 | 
				
			||||||
    this.app.acl.use(async (ctx: Context, next) => {
 | 
					
 | 
				
			||||||
      const { actionName, resourceName, resourceOf } = ctx.action;
 | 
					    this.app.acl.use(
 | 
				
			||||||
      if (resourceName.includes('.') && resourceOf) {
 | 
					      async (ctx: Context, next) => {
 | 
				
			||||||
        if (!ctx?.permission?.can?.params) {
 | 
					        const { actionName, resourceName, resourceOf } = ctx.action;
 | 
				
			||||||
          return next();
 | 
					        // is association request
 | 
				
			||||||
        }
 | 
					        if (resourceName.includes('.') && resourceOf) {
 | 
				
			||||||
        // 关联数据去掉 filter
 | 
					          if (!ctx?.permission?.can?.params) {
 | 
				
			||||||
        delete ctx.permission.can.params.filter;
 | 
					            return next();
 | 
				
			||||||
        // 关联数据能不能处理取决于 source 是否有权限
 | 
					          }
 | 
				
			||||||
        const [collectionName] = resourceName.split('.');
 | 
					          // 关联数据去掉 filter
 | 
				
			||||||
        const action = ctx.can({ resource: collectionName, action: actionName });
 | 
					          delete ctx.permission.can.params.filter;
 | 
				
			||||||
        const availableAction = this.app.acl.getAvailableAction(actionName);
 | 
					          // 关联数据能不能处理取决于 source 是否有权限
 | 
				
			||||||
        if (availableAction?.options?.onNewRecord) {
 | 
					          const [collectionName] = resourceName.split('.');
 | 
				
			||||||
          if (action) {
 | 
					          const action = ctx.can({ resource: collectionName, action: actionName });
 | 
				
			||||||
            ctx.permission.skip = true;
 | 
					
 | 
				
			||||||
 | 
					          const availableAction = this.app.acl.getAvailableAction(actionName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (availableAction?.options?.onNewRecord) {
 | 
				
			||||||
 | 
					            if (action) {
 | 
				
			||||||
 | 
					              ctx.permission.skip = true;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              ctx.permission.can = false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            ctx.permission.can = false;
 | 
					            const filter = parseJsonTemplate(action?.params?.filter || {}, ctx);
 | 
				
			||||||
          }
 | 
					            const sourceInstance = await ctx.db.getRepository(collectionName).findOne({
 | 
				
			||||||
        } else {
 | 
					              filterByTk: resourceOf,
 | 
				
			||||||
          const filter = parseJsonTemplate(action?.params?.filter || {}, ctx);
 | 
					              filter,
 | 
				
			||||||
          const sourceInstance = await ctx.db.getRepository(collectionName).findOne({
 | 
					            });
 | 
				
			||||||
            filterByTk: resourceOf,
 | 
					            if (!sourceInstance) {
 | 
				
			||||||
            filter,
 | 
					              ctx.permission.can = false;
 | 
				
			||||||
          });
 | 
					            }
 | 
				
			||||||
          if (!sourceInstance) {
 | 
					 | 
				
			||||||
            ctx.permission.can = false;
 | 
					 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					        await next();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        before: 'core',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // throw error when user has no fixed params permissions
 | 
				
			||||||
 | 
					    this.app.acl.use(
 | 
				
			||||||
 | 
					      async (ctx: any, next) => {
 | 
				
			||||||
 | 
					        const action = ctx.permission?.can?.action;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (action == 'destroy' && !ctx.action.resourceName.includes('.')) {
 | 
				
			||||||
 | 
					          const repository = actionUtils.getRepositoryFromParams(ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // params after merge with fixed params
 | 
				
			||||||
 | 
					          const filteredCount = await repository.count(ctx.permission.mergedParams);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // params user requested
 | 
				
			||||||
 | 
					          const queryCount = await repository.count(ctx.permission.rawParams);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (queryCount > filteredCount) {
 | 
				
			||||||
 | 
					            ctx.throw(403, 'No permissions');
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await next();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        after: 'core',
 | 
				
			||||||
 | 
					        group: 'after',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const withACLMeta = async (ctx: any, next) => {
 | 
				
			||||||
      await next();
 | 
					      await next();
 | 
				
			||||||
    });
 | 
					
 | 
				
			||||||
 | 
					      if (!ctx.action) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const { resourceName, actionName } = ctx.action;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!ctx.get('X-With-ACL-Meta')) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const collection = ctx.db.getCollection(resourceName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!collection) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (ctx.status !== 200) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!['list', 'get'].includes(actionName)) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const Model = collection.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const primaryKeyField = Model.primaryKeyField || Model.primaryKeyAttribute;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const dataPath = ctx.body?.rows ? 'body.rows' : 'body';
 | 
				
			||||||
 | 
					      let listData = lodash.get(ctx, dataPath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (actionName == 'get') {
 | 
				
			||||||
 | 
					        listData = lodash.castArray(listData);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const actions = ['view', 'update', 'destroy'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const actionsParams = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      for (const action of actions) {
 | 
				
			||||||
 | 
					        const actionCtx: any = {
 | 
				
			||||||
 | 
					          db: ctx.db,
 | 
				
			||||||
 | 
					          action: {
 | 
				
			||||||
 | 
					            actionName: action,
 | 
				
			||||||
 | 
					            name: action,
 | 
				
			||||||
 | 
					            params: {},
 | 
				
			||||||
 | 
					            resourceName: ctx.action.resourceName,
 | 
				
			||||||
 | 
					            resourceOf: ctx.action.resourceOf,
 | 
				
			||||||
 | 
					            mergeParams() {},
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          state: {
 | 
				
			||||||
 | 
					            currentRole: ctx.state.currentRole,
 | 
				
			||||||
 | 
					            currentUser: (() => {
 | 
				
			||||||
 | 
					              if (!ctx.state.currentUser) {
 | 
				
			||||||
 | 
					                return null;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              if (ctx.state.currentUser.toJSON) {
 | 
				
			||||||
 | 
					                return ctx.state.currentUser?.toJSON();
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              return ctx.state.currentUser;
 | 
				
			||||||
 | 
					            })(),
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          permission: {},
 | 
				
			||||||
 | 
					          throw(...args) {
 | 
				
			||||||
 | 
					            throw new NoPermissionError(...args);
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          await this.app.acl.getActionParams(actionCtx);
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					          if (e instanceof NoPermissionError) {
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          throw e;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        actionsParams.push([
 | 
				
			||||||
 | 
					          action,
 | 
				
			||||||
 | 
					          actionCtx.permission?.can === null && !actionCtx.permission.skip
 | 
				
			||||||
 | 
					            ? null
 | 
				
			||||||
 | 
					            : actionCtx.permission?.parsedParams || {},
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const ids = listData.map((item) => item[primaryKeyField]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const conditions = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const allAllowed = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      for (const [action, params] of actionsParams) {
 | 
				
			||||||
 | 
					        if (!params) {
 | 
				
			||||||
 | 
					          continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (lodash.isEmpty(params) || lodash.isEmpty(params.filter)) {
 | 
				
			||||||
 | 
					          allAllowed.push(action);
 | 
				
			||||||
 | 
					          continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const queryParams = collection.repository.buildQueryOptions(params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const actionSql = ctx.db.sequelize.queryInterface.queryGenerator.selectQuery(
 | 
				
			||||||
 | 
					          Model.getTableName(),
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            // ...queryParams,
 | 
				
			||||||
 | 
					            where: queryParams.where,
 | 
				
			||||||
 | 
					            attributes: [primaryKeyField],
 | 
				
			||||||
 | 
					            includeIgnoreAttributes: false,
 | 
				
			||||||
 | 
					            // include: queryParams.include,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          Model,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const whereCase = actionSql.match(/WHERE (.*?);/)[1];
 | 
				
			||||||
 | 
					        conditions.push({
 | 
				
			||||||
 | 
					          whereCase,
 | 
				
			||||||
 | 
					          action,
 | 
				
			||||||
 | 
					          include: queryParams.include,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const results = await collection.model.findAll({
 | 
				
			||||||
 | 
					        where: {
 | 
				
			||||||
 | 
					          [primaryKeyField]: ids,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        attributes: [
 | 
				
			||||||
 | 
					          primaryKeyField,
 | 
				
			||||||
 | 
					          ...conditions.map((condition) => {
 | 
				
			||||||
 | 
					            return [ctx.db.sequelize.literal(`CASE WHEN ${condition.whereCase} THEN 1 ELSE 0 END`), condition.action];
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        include: conditions.map((condition) => condition.include).flat(),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const allowedActions = actions
 | 
				
			||||||
 | 
					        .map((action) => {
 | 
				
			||||||
 | 
					          if (allAllowed.includes(action)) {
 | 
				
			||||||
 | 
					            return [action, ids];
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return [action, results.filter((item) => Boolean(item.get(action))).map((item) => item.get(primaryKeyField))];
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .reduce((acc, [action, ids]) => {
 | 
				
			||||||
 | 
					          acc[action] = ids;
 | 
				
			||||||
 | 
					          return acc;
 | 
				
			||||||
 | 
					        }, {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (actionName == 'get') {
 | 
				
			||||||
 | 
					        ctx.bodyMeta = {
 | 
				
			||||||
 | 
					          ...(ctx.bodyMeta || {}),
 | 
				
			||||||
 | 
					          allowedActions: allowedActions,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (actionName == 'list') {
 | 
				
			||||||
 | 
					        ctx.body.allowedActions = allowedActions;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // append allowedActions to list & get response
 | 
				
			||||||
 | 
					    this.app.use(
 | 
				
			||||||
 | 
					      async (ctx, next) => {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          await withACLMeta(ctx, next);
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					          ctx.logger.error(error);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      { after: 'restApi', group: 'after' },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async install() {
 | 
					  async install() {
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					import { Database } from '@nocobase/database';
 | 
				
			||||||
 | 
					import { MockServer, mockServer } from '@nocobase/test';
 | 
				
			||||||
 | 
					import Plugin from '../index';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('actions test', () => {
 | 
				
			||||||
 | 
					  let app: MockServer;
 | 
				
			||||||
 | 
					  let db: Database;
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    app = mockServer({
 | 
				
			||||||
 | 
					      registerActions: true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await app.cleanDb();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    app.plugin(Plugin);
 | 
				
			||||||
 | 
					    await app.load();
 | 
				
			||||||
 | 
					    await app.db.sync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    db = app.db;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  afterEach(async () => {
 | 
				
			||||||
 | 
					    await app.destroy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should only call list action on chinaRegions resource', async () => {
 | 
				
			||||||
 | 
					    const listResponse = await app.agent().resource('chinaRegions').list();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(listResponse.statusCode).toEqual(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const createResponse = await app.agent().resource('chinaRegions').create();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(createResponse.statusCode).toEqual(404);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -11,7 +11,18 @@ export class PluginChinaRegion extends Plugin {
 | 
				
			|||||||
    await this.db.import({
 | 
					    await this.db.import({
 | 
				
			||||||
      directory: resolve(__dirname, 'collections'),
 | 
					      directory: resolve(__dirname, 'collections'),
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.app.acl.allow('chinaRegions', 'list', 'loggedIn');
 | 
					    this.app.acl.allow('chinaRegions', 'list', 'loggedIn');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.resourcer.use(async (ctx, next) => {
 | 
				
			||||||
 | 
					      const { resourceName, actionName } = ctx.action.params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (resourceName == 'chinaRegions' && actionName !== 'list') {
 | 
				
			||||||
 | 
					        ctx.throw(404, 'Not Found');
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        await next();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async importData() {
 | 
					  async importData() {
 | 
				
			||||||
@ -63,7 +74,6 @@ export class PluginChinaRegion extends Plugin {
 | 
				
			|||||||
    const count = await ChinaRegion.count();
 | 
					    const count = await ChinaRegion.count();
 | 
				
			||||||
    // console.log(`${count} rows of region data imported in ${(Date.now() - timer) / 1000}s`);
 | 
					    // console.log(`${count} rows of region data imported in ${(Date.now() - timer) / 1000}s`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default PluginChinaRegion;
 | 
					export default PluginChinaRegion;
 | 
				
			||||||
 | 
				
			|||||||
@ -38,6 +38,15 @@ export class CollectionManagerPlugin extends Plugin {
 | 
				
			|||||||
      CollectionRepository,
 | 
					      CollectionRepository,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: `pm.${this.name}.collections`,
 | 
				
			||||||
 | 
					      actions: [
 | 
				
			||||||
 | 
					        'collections:*',
 | 
				
			||||||
 | 
					        // 'fields:*',
 | 
				
			||||||
 | 
					        'collections.fields:*',
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.app.db.on('fields.beforeUpdate', async (model, options) => {
 | 
					    this.app.db.on('fields.beforeUpdate', async (model, options) => {
 | 
				
			||||||
      const newValue = options.values;
 | 
					      const newValue = options.values;
 | 
				
			||||||
      if (
 | 
					      if (
 | 
				
			||||||
@ -213,7 +222,6 @@ export class CollectionManagerPlugin extends Plugin {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.app.acl.allow('collections', 'list', 'loggedIn');
 | 
					    this.app.acl.allow('collections', 'list', 'loggedIn');
 | 
				
			||||||
    this.app.acl.allow('collections', ['create', 'update', 'destroy'], 'allowConfigure');
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async load() {
 | 
					  async load() {
 | 
				
			||||||
 | 
				
			|||||||
@ -26,6 +26,13 @@ export default class PluginFileManager extends Plugin {
 | 
				
			|||||||
  async load() {
 | 
					  async load() {
 | 
				
			||||||
    await this.importCollections(resolve(__dirname, './collections'));
 | 
					    await this.importCollections(resolve(__dirname, './collections'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: `pm.${this.name}.storages`,
 | 
				
			||||||
 | 
					      actions: ['storages:*'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.allow('attachments', 'upload', 'loggedIn');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 暂时中间件只能通过 use 加进来
 | 
					    // 暂时中间件只能通过 use 加进来
 | 
				
			||||||
    this.app.resourcer.use(uploadMiddleware);
 | 
					    this.app.resourcer.use(uploadMiddleware);
 | 
				
			||||||
    this.app.resourcer.registerActionHandler('upload', uploadAction);
 | 
					    this.app.resourcer.registerActionHandler('upload', uploadAction);
 | 
				
			||||||
@ -34,6 +41,24 @@ export default class PluginFileManager extends Plugin {
 | 
				
			|||||||
      await getStorageConfig(STORAGE_TYPE_LOCAL).middleware(this.app);
 | 
					      await getStorageConfig(STORAGE_TYPE_LOCAL).middleware(this.app);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.app.acl.allow('attachments', 'upload', 'loggedIn');
 | 
					    const defaultStorageName = getStorageConfig(this.storageType()).defaults().name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.addFixedParams('storages', 'destroy', () => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        filter: { 'name.$ne': defaultStorageName },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const ownMerger = () => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        filter: {
 | 
				
			||||||
 | 
					          createdById: '{{ctx.state.currentUser.id}}',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.addFixedParams('attachments', 'update', ownMerger);
 | 
				
			||||||
 | 
					    this.app.acl.addFixedParams('attachments', 'create', ownMerger);
 | 
				
			||||||
 | 
					    this.app.acl.addFixedParams('attachments', 'destroy', ownMerger);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -11,11 +11,17 @@ export class IframeBlockPlugin extends Plugin {
 | 
				
			|||||||
    await this.db.import({
 | 
					    await this.db.import({
 | 
				
			||||||
      directory: path.resolve(__dirname, 'collections'),
 | 
					      directory: path.resolve(__dirname, 'collections'),
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    this.app.acl.allow('iframeHtml', ['get', 'create', 'update', 'destroy'], 'allowConfigure');
 | 
					 | 
				
			||||||
    this.app.acl.allow('iframeHtml', 'getHtml', 'loggedIn');
 | 
					 | 
				
			||||||
    this.app.actions({
 | 
					    this.app.actions({
 | 
				
			||||||
      'iframeHtml:getHtml': getHtml,
 | 
					      'iframeHtml:getHtml': getHtml,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.allow('iframeHtml', 'getHtml', 'loggedIn');
 | 
				
			||||||
 | 
					    this.app.acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: 'ui.iframeHtml',
 | 
				
			||||||
 | 
					      actions: [
 | 
				
			||||||
 | 
					        'iframeHtml:*',
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async install(options?: InstallOptions) {}
 | 
					  async install(options?: InstallOptions) {}
 | 
				
			||||||
 | 
				
			|||||||
@ -42,14 +42,14 @@ const initImportSettings = (fields) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const ImportActionInitializer = (props) => {
 | 
					export const ImportActionInitializer = (props) => {
 | 
				
			||||||
  const { item, insert } = props;
 | 
					  const { item, insert } = props;
 | 
				
			||||||
  const { exists, remove } = useCurrentSchema('import', 'x-action', item.find, item.remove);
 | 
					  const { exists, remove } = useCurrentSchema('importXlsx', 'x-action', item.find, item.remove);
 | 
				
			||||||
  const { name } = useCollection();
 | 
					  const { name } = useCollection();
 | 
				
			||||||
  const fields = useFields(name);
 | 
					  const fields = useFields(name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const schema: ISchema = {
 | 
					  const schema: ISchema = {
 | 
				
			||||||
    type: 'void',
 | 
					    type: 'void',
 | 
				
			||||||
    title: '{{ t("Import") }}',
 | 
					    title: '{{ t("Import") }}',
 | 
				
			||||||
    'x-action': 'import',
 | 
					    'x-action': 'importXlsx',
 | 
				
			||||||
    'x-action-settings': {
 | 
					    'x-action-settings': {
 | 
				
			||||||
      importSettings: { importColumns: [], explain: '' },
 | 
					      importSettings: { importColumns: [], explain: '' },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
				
			|||||||
@ -18,7 +18,7 @@ import { ImportStatus } from './ImportModal';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const useImportSchema = (s: Schema) => {
 | 
					const useImportSchema = (s: Schema) => {
 | 
				
			||||||
  let schema = s;
 | 
					  let schema = s;
 | 
				
			||||||
  while (schema && schema['x-action'] !== 'import') {
 | 
					  while (schema && schema['x-action'] !== 'importXlsx') {
 | 
				
			||||||
    schema = schema.parent;
 | 
					    schema = schema.parent;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return { schema };
 | 
					  return { schema };
 | 
				
			||||||
 | 
				
			|||||||
@ -11,31 +11,18 @@ export class ImportPlugin extends Plugin {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async load() {
 | 
					  async load() {
 | 
				
			||||||
    // Visit: http://localhost:13000/api/import:importXlsx
 | 
					 | 
				
			||||||
    this.app.resourcer.use(importMiddleware);
 | 
					    this.app.resourcer.use(importMiddleware);
 | 
				
			||||||
    this.app.resourcer.registerActionHandler('downloadXlsxTemplate', downloadXlsxTemplate);
 | 
					    this.app.resourcer.registerActionHandler('downloadXlsxTemplate', downloadXlsxTemplate);
 | 
				
			||||||
    this.app.resourcer.registerActionHandler('importXlsx', importXlsx);
 | 
					    this.app.resourcer.registerActionHandler('importXlsx', importXlsx);
 | 
				
			||||||
    // this.app.resource({
 | 
					
 | 
				
			||||||
    //   name: 'import',
 | 
					 | 
				
			||||||
    //   actions: {
 | 
					 | 
				
			||||||
    //     importXlsx,
 | 
					 | 
				
			||||||
    //   },
 | 
					 | 
				
			||||||
    // });
 | 
					 | 
				
			||||||
    this.app.acl.setAvailableAction('importXlsx', {
 | 
					    this.app.acl.setAvailableAction('importXlsx', {
 | 
				
			||||||
      displayName: '{{t("Import")}}',
 | 
					      displayName: '{{t("Import")}}',
 | 
				
			||||||
      allowConfigureFields: true,
 | 
					      allowConfigureFields: true,
 | 
				
			||||||
      type: 'new-data',
 | 
					      type: 'new-data',
 | 
				
			||||||
      onNewRecord: true,
 | 
					      onNewRecord: true,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    this.app.acl.use(async (ctx, next) => {
 | 
					
 | 
				
			||||||
      const { actionName } = ctx.action;
 | 
					    this.app.acl.allow('*', 'downloadXlsxTemplate', 'loggedIn');
 | 
				
			||||||
      if (['downloadXlsxTemplate', 'importXlsx'].includes(actionName)) {
 | 
					 | 
				
			||||||
        ctx.permission = {
 | 
					 | 
				
			||||||
          skip: true,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      await next();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async install(options: InstallOptions) {
 | 
					  async install(options: InstallOptions) {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,46 +1,49 @@
 | 
				
			|||||||
import { InstallOptions, Plugin } from '@nocobase/server';
 | 
					import { InstallOptions, Plugin } from '@nocobase/server';
 | 
				
			||||||
import { CircleField, LineStringField, PointField, PolygonField } from './fields';
 | 
					 | 
				
			||||||
import { resolve } from 'path';
 | 
					import { resolve } from 'path';
 | 
				
			||||||
import { getConfiguration, setConfiguration } from './actions';
 | 
					import { getConfiguration, setConfiguration } from './actions';
 | 
				
			||||||
 | 
					import { CircleField, LineStringField, PointField, PolygonField } from './fields';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class MapPlugin extends Plugin {
 | 
					export class MapPlugin extends Plugin {
 | 
				
			||||||
  afterAdd() { }
 | 
					  afterAdd() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeLoad() {
 | 
					  beforeLoad() {
 | 
				
			||||||
    const fields = {
 | 
					    const fields = {
 | 
				
			||||||
      point: PointField,
 | 
					      point: PointField,
 | 
				
			||||||
      polygon: PolygonField,
 | 
					      polygon: PolygonField,
 | 
				
			||||||
      lineString: LineStringField,
 | 
					      lineString: LineStringField,
 | 
				
			||||||
      circle: CircleField
 | 
					      circle: CircleField,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.db.registerFieldTypes(fields);
 | 
					    this.db.registerFieldTypes(fields);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
  async load() {
 | 
					  async load() {
 | 
				
			||||||
    await this.db.import({
 | 
					    await this.db.import({
 | 
				
			||||||
      directory: resolve(__dirname, 'collections'),
 | 
					      directory: resolve(__dirname, 'collections'),
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.app.resource(({
 | 
					    this.app.resource({
 | 
				
			||||||
      name: 'map-configuration',
 | 
					      name: 'map-configuration',
 | 
				
			||||||
      actions: {
 | 
					      actions: {
 | 
				
			||||||
        get: getConfiguration,
 | 
					        get: getConfiguration,
 | 
				
			||||||
        set: setConfiguration
 | 
					        set: setConfiguration,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      only: ['get', 'set']
 | 
					      only: ['get', 'set'],
 | 
				
			||||||
    }))
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: `pm.${this.name}.configuration`,
 | 
				
			||||||
 | 
					      actions: ['map-configuration:*'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async install(options?: InstallOptions) { }
 | 
					  async install(options?: InstallOptions) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async afterEnable() { }
 | 
					  async afterEnable() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async afterDisable() { }
 | 
					  async afterDisable() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async remove() { }
 | 
					  async remove() {}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default MapPlugin;
 | 
					export default MapPlugin;
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,6 @@ import { resolve } from 'path';
 | 
				
			|||||||
import { ApplicationModel } from './models/application';
 | 
					import { ApplicationModel } from './models/application';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class PluginMultiAppManager extends Plugin {
 | 
					export class PluginMultiAppManager extends Plugin {
 | 
				
			||||||
 | 
					 | 
				
			||||||
  async install(options?: InstallOptions) {
 | 
					  async install(options?: InstallOptions) {
 | 
				
			||||||
    const repo = this.db.getRepository<any>('collections');
 | 
					    const repo = this.db.getRepository<any>('collections');
 | 
				
			||||||
    if (repo) {
 | 
					    if (repo) {
 | 
				
			||||||
@ -46,5 +45,12 @@ export class PluginMultiAppManager extends Plugin {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: `pm.${this.name}.applications`,
 | 
				
			||||||
 | 
					      actions: [
 | 
				
			||||||
 | 
					        'applications:*',
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ export default function (props) {
 | 
				
			|||||||
            title: t('OIDC manager'),
 | 
					            title: t('OIDC manager'),
 | 
				
			||||||
            icon: 'FileOutlined',
 | 
					            icon: 'FileOutlined',
 | 
				
			||||||
            tabs: {
 | 
					            tabs: {
 | 
				
			||||||
              storages: {
 | 
					              providers: {
 | 
				
			||||||
                title: t('OIDC Providers'),
 | 
					                title: t('OIDC Providers'),
 | 
				
			||||||
                component: OIDCPanel,
 | 
					                component: OIDCPanel,
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
 | 
				
			|||||||
@ -44,9 +44,12 @@ export class OidcPlugin extends Plugin {
 | 
				
			|||||||
      await next();
 | 
					      await next();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 开放访问权限
 | 
					    this.app.acl.allow('oidc', '*', 'public');
 | 
				
			||||||
    this.app.acl.allow('oidcProviders', '*', 'allowConfigure');
 | 
					
 | 
				
			||||||
    this.app.acl.allow('oidc', '*');
 | 
					    this.app.acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: `pm.${this.name}.providers`,
 | 
				
			||||||
 | 
					      actions: ['oidcProviders:*'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async install(options?: InstallOptions) {}
 | 
					  async install(options?: InstallOptions) {}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,5 @@
 | 
				
			|||||||
 | 
					import { PluginManagerContext, SettingsCenterProvider, SigninPageExtensionProvider } from '@nocobase/client';
 | 
				
			||||||
import React, { useContext } from 'react';
 | 
					import React, { useContext } from 'react';
 | 
				
			||||||
import { PluginManagerContext, SettingsCenterProvider } from '@nocobase/client';
 | 
					 | 
				
			||||||
import { SigninPageExtensionProvider } from '@nocobase/client';
 | 
					 | 
				
			||||||
import { useSamlTranslation } from './locale';
 | 
					import { useSamlTranslation } from './locale';
 | 
				
			||||||
import { SAMLList } from './SAMLList';
 | 
					import { SAMLList } from './SAMLList';
 | 
				
			||||||
import { SAMLPanel } from './SAMLPanel';
 | 
					import { SAMLPanel } from './SAMLPanel';
 | 
				
			||||||
@ -17,7 +16,7 @@ export default function (props) {
 | 
				
			|||||||
            title: t('SAML manager'),
 | 
					            title: t('SAML manager'),
 | 
				
			||||||
            icon: 'FileOutlined',
 | 
					            icon: 'FileOutlined',
 | 
				
			||||||
            tabs: {
 | 
					            tabs: {
 | 
				
			||||||
              storages: {
 | 
					              providers: {
 | 
				
			||||||
                title: t('SAML Providers'),
 | 
					                title: t('SAML Providers'),
 | 
				
			||||||
                component: SAMLPanel,
 | 
					                component: SAMLPanel,
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
 | 
				
			|||||||
@ -33,8 +33,12 @@ export class SAMLPlugin extends Plugin {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 开放访问权限
 | 
					    // 开放访问权限
 | 
				
			||||||
    this.app.acl.allow('samlProviders', '*', 'allowConfigure');
 | 
					    this.app.acl.allow('saml', '*', 'public');
 | 
				
			||||||
    this.app.acl.allow('saml', '*');
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: `pm.${this.name}.providers`,
 | 
				
			||||||
 | 
					      actions: ['samlProviders:*'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async install(options?: InstallOptions) {}
 | 
					  async install(options?: InstallOptions) {}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,3 @@
 | 
				
			|||||||
import { skip } from '@nocobase/acl';
 | 
					 | 
				
			||||||
import { InstallOptions, Plugin } from '@nocobase/server';
 | 
					import { InstallOptions, Plugin } from '@nocobase/server';
 | 
				
			||||||
import { resolve } from 'path';
 | 
					import { resolve } from 'path';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -29,17 +28,25 @@ export class SystemSettingsPlugin extends Plugin {
 | 
				
			|||||||
    if (cmd) {
 | 
					    if (cmd) {
 | 
				
			||||||
      cmd.option('-l, --lang [lang]');
 | 
					      cmd.option('-l, --lang [lang]');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: `pm.${this.name}.system-settings`,
 | 
				
			||||||
 | 
					      actions: ['systemSettings:update'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async load() {
 | 
					  async load() {
 | 
				
			||||||
    await this.importCollections(resolve(__dirname, './collections'));
 | 
					    await this.app.db.import({
 | 
				
			||||||
 | 
					      directory: resolve(__dirname, 'collections'),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.app.acl.use(
 | 
					    this.app.acl.addFixedParams('systemSettings', 'destroy', () => {
 | 
				
			||||||
      skip({
 | 
					      return {
 | 
				
			||||||
        resourceName: 'systemSettings',
 | 
					        'id.$ne': 1,
 | 
				
			||||||
        actionName: 'get',
 | 
					      };
 | 
				
			||||||
      }),
 | 
					    });
 | 
				
			||||||
    );
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.allow('systemSettings', 'get', 'public');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,3 @@
 | 
				
			|||||||
import { skip } from '@nocobase/acl';
 | 
					 | 
				
			||||||
import { MagicAttributeModel } from '@nocobase/database';
 | 
					import { MagicAttributeModel } from '@nocobase/database';
 | 
				
			||||||
import { Plugin } from '@nocobase/server';
 | 
					import { Plugin } from '@nocobase/server';
 | 
				
			||||||
import { resolve } from 'path';
 | 
					import { resolve } from 'path';
 | 
				
			||||||
@ -87,12 +86,7 @@ export class UiRoutesStoragePlugin extends Plugin {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    await this.importCollections(resolve(__dirname, './collections'));
 | 
					    await this.importCollections(resolve(__dirname, './collections'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.app.acl.use(
 | 
					    this.app.acl.allow('uiRoutes', 'getAccessible');
 | 
				
			||||||
      skip({
 | 
					 | 
				
			||||||
        resourceName: 'uiRoutes',
 | 
					 | 
				
			||||||
        actionName: 'getAccessible',
 | 
					 | 
				
			||||||
      }),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -26,6 +26,29 @@ export class UiSchemaStoragePlugin extends Plugin {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    this.registerRepository();
 | 
					    this.registerRepository();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: `pm.${this.name}.block-templates`,
 | 
				
			||||||
 | 
					      actions: ['uiSchemaTemplates:*'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: 'ui.uiSchemas',
 | 
				
			||||||
 | 
					      actions: [
 | 
				
			||||||
 | 
					        'uiSchemas:insert',
 | 
				
			||||||
 | 
					        'uiSchemas:insertNewSchema',
 | 
				
			||||||
 | 
					        'uiSchemas:remove',
 | 
				
			||||||
 | 
					        'uiSchemas:patch',
 | 
				
			||||||
 | 
					        'uiSchemas:batchPatch',
 | 
				
			||||||
 | 
					        'uiSchemas:clearAncestor',
 | 
				
			||||||
 | 
					        'uiSchemas:insertBeforeBegin',
 | 
				
			||||||
 | 
					        'uiSchemas:insertAfterBegin',
 | 
				
			||||||
 | 
					        'uiSchemas:insertBeforeEnd',
 | 
				
			||||||
 | 
					        'uiSchemas:insertAfterEnd',
 | 
				
			||||||
 | 
					        'uiSchemas:insertAdjacent',
 | 
				
			||||||
 | 
					        'uiSchemas:saveAsTemplate',
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db.on('uiSchemas.beforeCreate', function setUid(model) {
 | 
					    db.on('uiSchemas.beforeCreate', function setUid(model) {
 | 
				
			||||||
      if (!model.get('name')) {
 | 
					      if (!model.get('name')) {
 | 
				
			||||||
        model.set('name', uid());
 | 
					        model.set('name', uid());
 | 
				
			||||||
@ -61,8 +84,8 @@ export class UiSchemaStoragePlugin extends Plugin {
 | 
				
			|||||||
      actions: uiSchemaActions,
 | 
					      actions: uiSchemaActions,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.app.acl.allow('uiSchemas', '*', 'loggedIn');
 | 
					    this.app.acl.allow('uiSchemas', ['getProperties', 'getJsonSchema'], 'loggedIn');
 | 
				
			||||||
    this.app.acl.allow('uiSchemaTemplates', '*', 'loggedIn');
 | 
					    this.app.acl.allow('uiSchemaTemplates', ['get', 'list'], 'loggedIn');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async load() {
 | 
					  async load() {
 | 
				
			||||||
 | 
				
			|||||||
@ -25,8 +25,8 @@ describe('actions', () => {
 | 
				
			|||||||
    pluginUser = app.getPlugin('users');
 | 
					    pluginUser = app.getPlugin('users');
 | 
				
			||||||
    adminUser = await db.getRepository('users').findOne({
 | 
					    adminUser = await db.getRepository('users').findOne({
 | 
				
			||||||
      filter: {
 | 
					      filter: {
 | 
				
			||||||
        email: process.env.INIT_ROOT_EMAIL
 | 
					        email: process.env.INIT_ROOT_EMAIL,
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    agent = app.agent();
 | 
					    agent = app.agent();
 | 
				
			||||||
@ -67,16 +67,16 @@ describe('actions', () => {
 | 
				
			|||||||
    const res1 = await agent.resource('users').updateProfile({
 | 
					    const res1 = await agent.resource('users').updateProfile({
 | 
				
			||||||
      filterByTk: adminUser.id,
 | 
					      filterByTk: adminUser.id,
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
        nickname: 'a'
 | 
					        nickname: 'a',
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    expect(res1.status).toBe(401);
 | 
					    expect(res1.status).toBe(401);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const res2 = await adminAgent.resource('users').updateProfile({
 | 
					    const res2 = await adminAgent.resource('users').updateProfile({
 | 
				
			||||||
      filterByTk: adminUser.id,
 | 
					      filterByTk: adminUser.id,
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
        nickname: 'a'
 | 
					        nickname: 'a',
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    expect(res2.status).toBe(200);
 | 
					    expect(res2.status).toBe(200);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
				
			|||||||
@ -92,6 +92,22 @@ export default class UsersPlugin extends Plugin<UserPluginConfig> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    this.app.resourcer.use(parseToken, { tag: 'parseToken' });
 | 
					    this.app.resourcer.use(parseToken, { tag: 'parseToken' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.addFixedParams('users', 'destroy', () => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        filter: {
 | 
				
			||||||
 | 
					          'id.$ne': 1,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.addFixedParams('collections', 'destroy', () => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        filter: {
 | 
				
			||||||
 | 
					          'name.$ne': 'users',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const publicActions = ['check', 'signin', 'signup', 'lostpassword', 'resetpassword', 'getUserByResetToken'];
 | 
					    const publicActions = ['check', 'signin', 'signup', 'lostpassword', 'resetpassword', 'getUserByResetToken'];
 | 
				
			||||||
    const loggedInActions = ['signout', 'updateProfile', 'changePassword'];
 | 
					    const loggedInActions = ['signout', 'updateProfile', 'changePassword'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -137,8 +137,11 @@ export default class VerificationPlugin extends Plugin {
 | 
				
			|||||||
      return this.intercept(context, next);
 | 
					      return this.intercept(context, next);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    app.acl.allow('verifications', 'create');
 | 
					    app.acl.allow('verifications', 'create', 'public');
 | 
				
			||||||
    app.acl.allow('verifications_providers', '*', 'allowConfigure');
 | 
					    this.app.acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: `pm.${this.name}.providers`,
 | 
				
			||||||
 | 
					      actions: ['verifications_providers:*'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getDefault() {
 | 
					  async getDefault() {
 | 
				
			||||||
 | 
				
			|||||||
@ -37,15 +37,18 @@ function WorkflowPane() {
 | 
				
			|||||||
export const WorkflowProvider = (props) => {
 | 
					export const WorkflowProvider = (props) => {
 | 
				
			||||||
  const ctx = useContext(PluginManagerContext);
 | 
					  const ctx = useContext(PluginManagerContext);
 | 
				
			||||||
  const { routes, components, ...others } = useContext(RouteSwitchContext);
 | 
					  const { routes, components, ...others } = useContext(RouteSwitchContext);
 | 
				
			||||||
  routes[1].routes.unshift({
 | 
					  routes[1].routes.unshift(
 | 
				
			||||||
    type: 'route',
 | 
					    {
 | 
				
			||||||
    path: '/admin/settings/workflow/workflows/:id',
 | 
					      type: 'route',
 | 
				
			||||||
    component: 'WorkflowPage',
 | 
					      path: '/admin/settings/workflow/workflows/:id',
 | 
				
			||||||
  }, {
 | 
					      component: 'WorkflowPage',
 | 
				
			||||||
    type: 'route',
 | 
					    },
 | 
				
			||||||
    path: '/admin/settings/workflow/executions/:id',
 | 
					    {
 | 
				
			||||||
    component: 'ExecutionPage',
 | 
					      type: 'route',
 | 
				
			||||||
  });
 | 
					      path: '/admin/settings/workflow/executions/:id',
 | 
				
			||||||
 | 
					      component: 'ExecutionPage',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <SettingsCenterProvider
 | 
					    <SettingsCenterProvider
 | 
				
			||||||
      settings={{
 | 
					      settings={{
 | 
				
			||||||
@ -55,6 +58,7 @@ export const WorkflowProvider = (props) => {
 | 
				
			|||||||
          title: lang('Workflow'),
 | 
					          title: lang('Workflow'),
 | 
				
			||||||
          tabs: {
 | 
					          tabs: {
 | 
				
			||||||
            workflows: {
 | 
					            workflows: {
 | 
				
			||||||
 | 
					              isBookmark: true,
 | 
				
			||||||
              title: lang('Workflow'),
 | 
					              title: lang('Workflow'),
 | 
				
			||||||
              component: WorkflowPane,
 | 
					              component: WorkflowPane,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
@ -70,10 +74,10 @@ export const WorkflowProvider = (props) => {
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <RouteSwitchContext.Provider value={{ components: { ...components, WorkflowPage, ExecutionPage }, ...others, routes }}>
 | 
					        <RouteSwitchContext.Provider
 | 
				
			||||||
          <WorkflowContext.Provider value={{ triggers, instructions }}>
 | 
					          value={{ components: { ...components, WorkflowPage, ExecutionPage }, ...others, routes }}
 | 
				
			||||||
            {props.children}
 | 
					        >
 | 
				
			||||||
          </WorkflowContext.Provider>
 | 
					          <WorkflowContext.Provider value={{ triggers, instructions }}>{props.children}</WorkflowContext.Provider>
 | 
				
			||||||
        </RouteSwitchContext.Provider>
 | 
					        </RouteSwitchContext.Provider>
 | 
				
			||||||
      </PluginManagerContext.Provider>
 | 
					      </PluginManagerContext.Provider>
 | 
				
			||||||
    </SettingsCenterProvider>
 | 
					    </SettingsCenterProvider>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import path from 'path';
 | 
					import path from 'path';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Op, Transactionable } from '@nocobase/database';
 | 
					import { Op } from '@nocobase/database';
 | 
				
			||||||
import { Plugin } from '@nocobase/server';
 | 
					import { Plugin } from '@nocobase/server';
 | 
				
			||||||
import { Registry } from '@nocobase/utils';
 | 
					import { Registry } from '@nocobase/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -10,10 +10,10 @@ import { EXECUTION_STATUS } from './constants';
 | 
				
			|||||||
import extensions from './extensions';
 | 
					import extensions from './extensions';
 | 
				
			||||||
import initInstructions, { Instruction } from './instructions';
 | 
					import initInstructions, { Instruction } from './instructions';
 | 
				
			||||||
import ExecutionModel from './models/Execution';
 | 
					import ExecutionModel from './models/Execution';
 | 
				
			||||||
 | 
					import JobModel from './models/Job';
 | 
				
			||||||
import WorkflowModel from './models/Workflow';
 | 
					import WorkflowModel from './models/Workflow';
 | 
				
			||||||
import Processor from './Processor';
 | 
					import Processor from './Processor';
 | 
				
			||||||
import initTriggers, { Trigger } from './triggers';
 | 
					import initTriggers, { Trigger } from './triggers';
 | 
				
			||||||
import JobModel from './models/Job';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Pending = [ExecutionModel, JobModel?];
 | 
					type Pending = [ExecutionModel, JobModel?];
 | 
				
			||||||
@ -72,6 +72,20 @@ export default class WorkflowPlugin extends Plugin {
 | 
				
			|||||||
  async load() {
 | 
					  async load() {
 | 
				
			||||||
    const { db, options } = this;
 | 
					    const { db, options } = this;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.registerSnippet({
 | 
				
			||||||
 | 
					      name: `pm.${this.name}.workflows`,
 | 
				
			||||||
 | 
					      actions: [
 | 
				
			||||||
 | 
					        'workflows:*',
 | 
				
			||||||
 | 
					        'workflows.nodes:*',
 | 
				
			||||||
 | 
					        'executions:list',
 | 
				
			||||||
 | 
					        'executions:get',
 | 
				
			||||||
 | 
					        'flow_nodes:update',
 | 
				
			||||||
 | 
					        'flow_nodes:destroy',
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.app.acl.allow('users_jobs', ['list', 'get', 'submit'], 'loggedIn');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await db.import({
 | 
					    await db.import({
 | 
				
			||||||
      directory: path.resolve(__dirname, 'collections'),
 | 
					      directory: path.resolve(__dirname, 'collections'),
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
				
			|||||||
@ -34,7 +34,7 @@ export class PresetNocoBase extends Plugin {
 | 
				
			|||||||
    return localPlugins;
 | 
					    return localPlugins;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async addBuiltInPlugins() {
 | 
					  async addBuiltInPlugins(options?: any) {
 | 
				
			||||||
    const builtInPlugins = this.getBuiltInPlugins();
 | 
					    const builtInPlugins = this.getBuiltInPlugins();
 | 
				
			||||||
    await this.app.pm.add(builtInPlugins, {
 | 
					    await this.app.pm.add(builtInPlugins, {
 | 
				
			||||||
      enabled: true,
 | 
					      enabled: true,
 | 
				
			||||||
@ -43,7 +43,7 @@ export class PresetNocoBase extends Plugin {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
    const localPlugins = this.getLocalPlugins();
 | 
					    const localPlugins = this.getLocalPlugins();
 | 
				
			||||||
    await this.app.pm.add(localPlugins, {});
 | 
					    await this.app.pm.add(localPlugins, {});
 | 
				
			||||||
    await this.app.reload();
 | 
					    await this.app.reload({ method: options.method });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  afterAdd() {
 | 
					  afterAdd() {
 | 
				
			||||||
@ -59,15 +59,15 @@ export class PresetNocoBase extends Plugin {
 | 
				
			|||||||
        if (r) {
 | 
					        if (r) {
 | 
				
			||||||
          console.log(`Clear the installed application plugins`);
 | 
					          console.log(`Clear the installed application plugins`);
 | 
				
			||||||
          await this.db.getRepository('applicationPlugins').destroy({ truncate: true });
 | 
					          await this.db.getRepository('applicationPlugins').destroy({ truncate: true });
 | 
				
			||||||
          await this.app.reload();
 | 
					          await this.app.reload({ method: options.method });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    this.app.on('beforeUpgrade', async () => {
 | 
					    this.app.on('beforeUpgrade', async (options) => {
 | 
				
			||||||
      const result = await this.app.version.satisfies('<0.8.0-alpha.1');
 | 
					      const result = await this.app.version.satisfies('<0.8.0-alpha.1');
 | 
				
			||||||
      if (result) {
 | 
					      if (result) {
 | 
				
			||||||
        console.log(`Initialize all built-in plugins`);
 | 
					        console.log(`Initialize all built-in plugins`);
 | 
				
			||||||
        await this.addBuiltInPlugins();
 | 
					        await this.addBuiltInPlugins({ method: 'upgrade' });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const builtInPlugins = this.getBuiltInPlugins();
 | 
					      const builtInPlugins = this.getBuiltInPlugins();
 | 
				
			||||||
      const plugins = await this.app.db.getRepository('applicationPlugins').find();
 | 
					      const plugins = await this.app.db.getRepository('applicationPlugins').find();
 | 
				
			||||||
@ -85,14 +85,15 @@ export class PresetNocoBase extends Plugin {
 | 
				
			|||||||
        localPlugins.filter((plugin) => !pluginNames.includes(plugin)),
 | 
					        localPlugins.filter((plugin) => !pluginNames.includes(plugin)),
 | 
				
			||||||
        {},
 | 
					        {},
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      await this.app.reload();
 | 
					      await this.app.reload({ method: 'upgrade' });
 | 
				
			||||||
      await this.app.db.sync();
 | 
					      await this.app.db.sync();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    this.app.on('beforeInstall', async () => {
 | 
					    this.app.on('beforeInstall', async (options) => {
 | 
				
			||||||
      console.log(`Initialize all built-in plugins`);
 | 
					      console.log(`Initialize all built-in plugins`);
 | 
				
			||||||
      await this.addBuiltInPlugins();
 | 
					      await this.addBuiltInPlugins({ method: 'install' });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeLoad() {
 | 
					  beforeLoad() {
 | 
				
			||||||
    this.db.addMigrations({
 | 
					    this.db.addMigrations({
 | 
				
			||||||
      namespace: this.getName(),
 | 
					      namespace: this.getName(),
 | 
				
			||||||
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user