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",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@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": {
 | 
			
		||||
    "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',
 | 
			
		||||
        actionName: 'login',
 | 
			
		||||
      },
 | 
			
		||||
      log: {
 | 
			
		||||
        info() {},
 | 
			
		||||
      },
 | 
			
		||||
      app: {
 | 
			
		||||
        acl,
 | 
			
		||||
      },
 | 
			
		||||
@ -49,6 +52,7 @@ describe('skip', () => {
 | 
			
		||||
    const nextFunc = jest.fn();
 | 
			
		||||
 | 
			
		||||
    let skip = false;
 | 
			
		||||
 | 
			
		||||
    acl.allow('users', 'login', (ctx) => {
 | 
			
		||||
      return skip;
 | 
			
		||||
    });
 | 
			
		||||
@ -76,6 +80,9 @@ describe('skip', () => {
 | 
			
		||||
        resourceName: 'users',
 | 
			
		||||
        actionName: 'login',
 | 
			
		||||
      },
 | 
			
		||||
      log: {
 | 
			
		||||
        info() {},
 | 
			
		||||
      },
 | 
			
		||||
      app: {
 | 
			
		||||
        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)) {
 | 
			
		||||
      const predicateName = this.actionsAsObject[actionName];
 | 
			
		||||
      if (predicateName) {
 | 
			
		||||
        return predicate[predicateName];
 | 
			
		||||
        return lodash.cloneDeep(predicate[predicateName]);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return true;
 | 
			
		||||
@ -62,10 +62,6 @@ export class ACLAvailableStrategy {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  allow(resourceName: string, actionName: string) {
 | 
			
		||||
    if (this.acl.isConfigResource(resourceName) && this.allowConfigure) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return this.matchAction(this.acl.resolveActionAlias(actionName));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,8 @@
 | 
			
		||||
import { ACL, DefineOptions } from './acl';
 | 
			
		||||
import { AvailableStrategyOptions } from './acl-available-strategy';
 | 
			
		||||
import { ACLAvailableStrategy, AvailableStrategyOptions } from './acl-available-strategy';
 | 
			
		||||
import { ACLResource } from './acl-resource';
 | 
			
		||||
import lodash from 'lodash';
 | 
			
		||||
import minimatch from 'minimatch';
 | 
			
		||||
 | 
			
		||||
export interface RoleActionParams {
 | 
			
		||||
  fields?: string[];
 | 
			
		||||
@ -18,6 +20,7 @@ export interface ResourceActionsOptions {
 | 
			
		||||
export class ACLRole {
 | 
			
		||||
  strategy: string | AvailableStrategyOptions;
 | 
			
		||||
  resources = new Map<string, ACLResource>();
 | 
			
		||||
  snippets: Set<string> = new Set();
 | 
			
		||||
 | 
			
		||||
  constructor(public acl: ACL, public name: string) {}
 | 
			
		||||
 | 
			
		||||
@ -29,6 +32,16 @@ export class ACLRole {
 | 
			
		||||
    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) {
 | 
			
		||||
    const resource = this.getResource(resourceName);
 | 
			
		||||
    return resource.getActions();
 | 
			
		||||
@ -67,6 +80,64 @@ export class ACLRole {
 | 
			
		||||
    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 {
 | 
			
		||||
    const actions = {};
 | 
			
		||||
 | 
			
		||||
@ -81,6 +152,7 @@ export class ACLRole {
 | 
			
		||||
      role: this.name,
 | 
			
		||||
      strategy: this.strategy,
 | 
			
		||||
      actions,
 | 
			
		||||
      snippets: Array.from(this.snippets),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { Action } from '@nocobase/resourcer';
 | 
			
		||||
import { Toposort, ToposortOptions } from '@nocobase/utils';
 | 
			
		||||
import { assign, Toposort, ToposortOptions } from '@nocobase/utils';
 | 
			
		||||
import EventEmitter from 'events';
 | 
			
		||||
import parse from 'json-templates';
 | 
			
		||||
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 { ACLRole, ResourceActionsOptions, RoleActionParams } from './acl-role';
 | 
			
		||||
import { AllowManager, ConditionFunc } from './allow-manager';
 | 
			
		||||
import FixedParamsManager, { Merger } from './fixed-params-manager';
 | 
			
		||||
import SnippetManager, { SnippetOptions } from './snippet-manager';
 | 
			
		||||
 | 
			
		||||
interface CanResult {
 | 
			
		||||
  role: string;
 | 
			
		||||
@ -22,6 +24,7 @@ export interface DefineOptions {
 | 
			
		||||
  strategy?: string | AvailableStrategyOptions;
 | 
			
		||||
  actions?: ResourceActionsOptions;
 | 
			
		||||
  routes?: any;
 | 
			
		||||
  snippets?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ListenerContext {
 | 
			
		||||
@ -39,14 +42,18 @@ interface CanArgs {
 | 
			
		||||
  role: string;
 | 
			
		||||
  resource: string;
 | 
			
		||||
  action: string;
 | 
			
		||||
  ctx?: any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ACL extends EventEmitter {
 | 
			
		||||
  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>;
 | 
			
		||||
 | 
			
		||||
  public allowManager = new AllowManager(this);
 | 
			
		||||
  public snippetManager = new SnippetManager();
 | 
			
		||||
 | 
			
		||||
  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 {
 | 
			
		||||
@ -164,6 +230,28 @@ export class ACL extends EventEmitter {
 | 
			
		||||
      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);
 | 
			
		||||
 | 
			
		||||
    if (aclResource) {
 | 
			
		||||
@ -171,39 +259,37 @@ export class ACL extends EventEmitter {
 | 
			
		||||
 | 
			
		||||
      if (actionParams) {
 | 
			
		||||
        // handle single action config
 | 
			
		||||
        return {
 | 
			
		||||
        return mergeParams({
 | 
			
		||||
          role,
 | 
			
		||||
          resource,
 | 
			
		||||
          action,
 | 
			
		||||
          params: actionParams,
 | 
			
		||||
        };
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!aclRole.strategy) {
 | 
			
		||||
    const roleStrategy = aclRole.getStrategy();
 | 
			
		||||
 | 
			
		||||
    if (!roleStrategy && !snippetAllowed) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const roleStrategy = lodash.isString(aclRole.strategy)
 | 
			
		||||
      ? this.availableStrategy.get(aclRole.strategy)
 | 
			
		||||
      : new ACLAvailableStrategy(this, aclRole.strategy);
 | 
			
		||||
    let roleStrategyParams = roleStrategy?.allow(resource, this.resolveActionAlias(action));
 | 
			
		||||
 | 
			
		||||
    if (!roleStrategy) {
 | 
			
		||||
      return null;
 | 
			
		||||
    if (!roleStrategyParams && snippetAllowed) {
 | 
			
		||||
      roleStrategyParams = {};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const roleStrategyParams = roleStrategy.allow(resource, this.resolveActionAlias(action));
 | 
			
		||||
 | 
			
		||||
    if (roleStrategyParams) {
 | 
			
		||||
      const result = { role, resource, action };
 | 
			
		||||
      const result = { role, resource, action, params: {} };
 | 
			
		||||
 | 
			
		||||
      if (lodash.isPlainObject(roleStrategyParams)) {
 | 
			
		||||
        result['params'] = roleStrategyParams;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return result;
 | 
			
		||||
      return mergeParams(result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return null;
 | 
			
		||||
@ -218,10 +304,17 @@ export class ACL extends EventEmitter {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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) {
 | 
			
		||||
    return this.skip(resourceName, actionNames, condition);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  skip(resourceName: string, actionNames: string[] | string, condition?: string | ConditionFunc) {
 | 
			
		||||
    if (!Array.isArray(actionNames)) {
 | 
			
		||||
      actionNames = [actionNames];
 | 
			
		||||
    }
 | 
			
		||||
@ -242,23 +335,10 @@ export class ACL extends EventEmitter {
 | 
			
		||||
  middleware() {
 | 
			
		||||
    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) {
 | 
			
		||||
      const roleName = ctx.state.currentRole || 'anonymous';
 | 
			
		||||
      const { resourceName, actionName } = ctx.action;
 | 
			
		||||
 | 
			
		||||
      const resourcerAction: Action = ctx.action;
 | 
			
		||||
 | 
			
		||||
      ctx.can = (options: Omit<CanArgs, 'role'>) => {
 | 
			
		||||
        return acl.can({ role: roleName, ...options });
 | 
			
		||||
      };
 | 
			
		||||
@ -267,28 +347,30 @@ export class ACL extends EventEmitter {
 | 
			
		||||
        can: ctx.can({ resource: resourceName, action: actionName }),
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      return compose(acl.middlewares.nodes)(ctx, async () => {
 | 
			
		||||
        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();
 | 
			
		||||
      });
 | 
			
		||||
      return compose(acl.middlewares.nodes)(ctx, 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;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.registerAllowCondition('public', (ctx) => {
 | 
			
		||||
      return true;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.registerAllowCondition('allowConfigure', async (ctx) => {
 | 
			
		||||
      const roleName = ctx.state.currentRole;
 | 
			
		||||
      if (!roleName) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const roleInstance = await ctx.db.getRepository('roles').findOne({
 | 
			
		||||
        filter: {
 | 
			
		||||
          name: roleName,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
      const role = acl.getRole(roleName);
 | 
			
		||||
      if (!role) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return roleInstance?.get('allowConfigure');
 | 
			
		||||
      return role.getStrategy()?.allowConfigure;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -43,12 +46,14 @@ export class AllowManager {
 | 
			
		||||
    for (const fetchActionStep of fetchActionSteps) {
 | 
			
		||||
      const resource = this.skipActions.get(fetchActionStep);
 | 
			
		||||
      if (resource) {
 | 
			
		||||
        const condition = resource.get('*') || resource.get(actionName);
 | 
			
		||||
        for (const fetchActionStep of ['*', actionName]) {
 | 
			
		||||
          const condition = resource.get(fetchActionStep);
 | 
			
		||||
          if (condition) {
 | 
			
		||||
            results.push(typeof condition === 'string' ? this.registeredCondition.get(condition) : condition);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return results;
 | 
			
		||||
  }
 | 
			
		||||
@ -57,11 +62,8 @@ export class AllowManager {
 | 
			
		||||
    this.registeredCondition.set(name, condition);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  aclMiddleware() {
 | 
			
		||||
    return async (ctx, next) => {
 | 
			
		||||
      const { resourceName, actionName } = ctx.action;
 | 
			
		||||
      const skippedConditions = ctx.app.acl.allowManager.getAllowedConditions(resourceName, actionName);
 | 
			
		||||
      let skip = false;
 | 
			
		||||
  async isAllowed(resourceName: string, actionName: string, ctx: any) {
 | 
			
		||||
    const skippedConditions = this.getAllowedConditions(resourceName, actionName);
 | 
			
		||||
 | 
			
		||||
    for (const skippedCondition of skippedConditions) {
 | 
			
		||||
      if (skippedCondition) {
 | 
			
		||||
@ -74,17 +76,26 @@ export class AllowManager {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (skipResult) {
 | 
			
		||||
            skip = true;
 | 
			
		||||
            break;
 | 
			
		||||
          return true;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  aclMiddleware() {
 | 
			
		||||
    return async (ctx, next) => {
 | 
			
		||||
      const { resourceName, actionName } = ctx.action;
 | 
			
		||||
      let skip = await this.acl.allowManager.isAllowed(resourceName, actionName, ctx);
 | 
			
		||||
 | 
			
		||||
      if (skip) {
 | 
			
		||||
        ctx.permission = {
 | 
			
		||||
          ...(ctx.permission || {}),
 | 
			
		||||
          skip: true,
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      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-resource';
 | 
			
		||||
export * from './acl-role';
 | 
			
		||||
export * from './no-permission-error';
 | 
			
		||||
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 { mockServer } from './index';
 | 
			
		||||
 | 
			
		||||
describe('destroy action', () => {
 | 
			
		||||
  let app: MockServer;
 | 
			
		||||
  let app;
 | 
			
		||||
  let Post;
 | 
			
		||||
  let Comment;
 | 
			
		||||
  let Tag;
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,10 @@
 | 
			
		||||
import { registerActions } from '@nocobase/actions';
 | 
			
		||||
import { mockServer } from './index';
 | 
			
		||||
import { mockServer as actionMockServer } from './index';
 | 
			
		||||
 | 
			
		||||
describe('list action', () => {
 | 
			
		||||
  let app;
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    app = mockServer();
 | 
			
		||||
    app = actionMockServer();
 | 
			
		||||
    registerActions(app);
 | 
			
		||||
 | 
			
		||||
    const Post = app.collection({
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,8 @@
 | 
			
		||||
import { Context } from '..';
 | 
			
		||||
import { getRepositoryFromParams } from '../utils';
 | 
			
		||||
import { Context } from '../index';
 | 
			
		||||
 | 
			
		||||
export async function destroy(ctx: Context, next) {
 | 
			
		||||
  const repository = getRepositoryFromParams(ctx);
 | 
			
		||||
 | 
			
		||||
  const { filterByTk, filter } = ctx.action.params;
 | 
			
		||||
 | 
			
		||||
  const instance = await repository.destroy({
 | 
			
		||||
 | 
			
		||||
@ -57,10 +57,13 @@ async function listWithNonPaged(ctx: Context) {
 | 
			
		||||
 | 
			
		||||
export async function list(ctx: Context, next) {
 | 
			
		||||
  const { paginate } = ctx.action.params;
 | 
			
		||||
 | 
			
		||||
  if (paginate === false || paginate === 'false') {
 | 
			
		||||
    await listWithNonPaged(ctx);
 | 
			
		||||
    ctx.paginate = false;
 | 
			
		||||
  } else {
 | 
			
		||||
    await listWithPagination(ctx);
 | 
			
		||||
    ctx.paginate = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  await next();
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,15 @@
 | 
			
		||||
import { useFieldSchema } from '@formily/react';
 | 
			
		||||
import { Schema, useFieldSchema } from '@formily/react';
 | 
			
		||||
import { Spin } from 'antd';
 | 
			
		||||
import React, { createContext, useContext } from 'react';
 | 
			
		||||
import { Redirect } from 'react-router-dom';
 | 
			
		||||
import { useAPIClient, useRequest } from '../api-client';
 | 
			
		||||
import { useBlockRequestContext } from '../block-provider/BlockProvider';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
export const ACLContext = createContext(null);
 | 
			
		||||
export const ACLContext = createContext<any>({});
 | 
			
		||||
 | 
			
		||||
export const ACLProvider = (props) => {
 | 
			
		||||
  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) => {
 | 
			
		||||
  const route = getRouteUrl(props.children.props);
 | 
			
		||||
  const { setDesignable } = useDesignable();
 | 
			
		||||
  const api = useAPIClient();
 | 
			
		||||
  const result = useRequest(
 | 
			
		||||
@ -28,7 +52,7 @@ export const ACLRolesCheckProvider = (props) => {
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      onSuccess(data) {
 | 
			
		||||
        if (!data?.data?.allowConfigure && !data?.data?.allowAll) {
 | 
			
		||||
        if (!data?.data?.snippets.includes('ui.*')) {
 | 
			
		||||
          setDesignable(false);
 | 
			
		||||
        }
 | 
			
		||||
        if (data?.data?.role !== api.auth.role) {
 | 
			
		||||
@ -48,9 +72,9 @@ export const ACLRolesCheckProvider = (props) => {
 | 
			
		||||
 | 
			
		||||
export const useRoleRecheck = () => {
 | 
			
		||||
  const ctx = useContext(ACLContext);
 | 
			
		||||
  const { allowAll, allowConfigure } = useACLRoleContext();
 | 
			
		||||
  const { allowAll } = useACLRoleContext();
 | 
			
		||||
  return () => {
 | 
			
		||||
    if (allowAll || allowConfigure) {
 | 
			
		||||
    if (allowAll) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    ctx.refresh();
 | 
			
		||||
@ -61,98 +85,191 @@ export const useACLContext = () => {
 | 
			
		||||
  return useContext(ACLContext);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useACLRoleContext = () => {
 | 
			
		||||
  const ctx = useContext(ACLContext);
 | 
			
		||||
  const data = ctx.data?.data;
 | 
			
		||||
export const ACLActionParamsContext = createContext<any>({});
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
    ...data,
 | 
			
		||||
    getActionParams(path: string, { skipOwnCheck, isOwn }) {
 | 
			
		||||
      const [resourceName, act] = path.split(':');
 | 
			
		||||
      const currentAction = data?.actionAlias?.[act] || act;
 | 
			
		||||
      const hasResource = data?.resources?.includes(resourceName);
 | 
			
		||||
      const params = data?.actions?.[`${resourceName}:${currentAction}`];
 | 
			
		||||
      if (hasResource) {
 | 
			
		||||
        if (!skipOwnCheck && params?.own) {
 | 
			
		||||
          return isOwn ? params : null;
 | 
			
		||||
        }
 | 
			
		||||
        return params;
 | 
			
		||||
      }
 | 
			
		||||
      const strategyActions = data?.strategy?.actions || [];
 | 
			
		||||
      const strategyAction = strategyActions?.find((action) => {
 | 
			
		||||
    data,
 | 
			
		||||
    getActionAlias,
 | 
			
		||||
    inResources: (resourceName: string) => {
 | 
			
		||||
      return data?.resources?.includes?.(resourceName);
 | 
			
		||||
    },
 | 
			
		||||
    getResourceActionParams: (actionPath: string) => {
 | 
			
		||||
      const [resourceName] = actionPath.split(':');
 | 
			
		||||
      const actionAlias = getActionAlias(actionPath);
 | 
			
		||||
      return data?.actions?.[`${resourceName}:${actionAlias}`] || data?.actions?.[actionPath];
 | 
			
		||||
    },
 | 
			
		||||
    getStrategyActionParams: (actionPath: string) => {
 | 
			
		||||
      const actionAlias = getActionAlias(actionPath);
 | 
			
		||||
      const strategyAction = data?.strategy?.actions.find((action) => {
 | 
			
		||||
        const [value] = action.split(':');
 | 
			
		||||
        return value === currentAction;
 | 
			
		||||
        return value === actionAlias;
 | 
			
		||||
      });
 | 
			
		||||
      if (!strategyAction) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (skipOwnCheck) {
 | 
			
		||||
        return {};
 | 
			
		||||
      }
 | 
			
		||||
      const [, actionScope] = strategyAction.split(':');
 | 
			
		||||
      if (actionScope === 'own') {
 | 
			
		||||
        return isOwn;
 | 
			
		||||
      }
 | 
			
		||||
      return {};
 | 
			
		||||
      return strategyAction ? {} : null;
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ACLAllowConfigure = (props) => {
 | 
			
		||||
  const { allowAll, allowConfigure } = useACLRoleContext();
 | 
			
		||||
  if (allowAll || allowConfigure) {
 | 
			
		||||
    return <>{props.children}</>;
 | 
			
		||||
const getIgnoreScope = (options: any = {}) => {
 | 
			
		||||
  const { schema, recordPkValue } = options;
 | 
			
		||||
  let ignoreScope = false;
 | 
			
		||||
  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) => {
 | 
			
		||||
  const { allowAll, allowConfigure, getActionParams } = useACLRoleContext();
 | 
			
		||||
  const fieldSchema = useFieldSchema();
 | 
			
		||||
  const isOwn = useRecordIsOwn();
 | 
			
		||||
  if (allowAll || allowConfigure) {
 | 
			
		||||
  const { allowAll, parseAction } = useACLRoleContext();
 | 
			
		||||
  const schema = useFieldSchema();
 | 
			
		||||
  if (allowAll) {
 | 
			
		||||
    return <>{props.children}</>;
 | 
			
		||||
  }
 | 
			
		||||
  const path = fieldSchema['x-acl-action'];
 | 
			
		||||
  const skipScopeCheck = fieldSchema['x-acl-action-props']?.skipScopeCheck;
 | 
			
		||||
  if (!path) {
 | 
			
		||||
  const actionPath = schema['x-acl-action'];
 | 
			
		||||
  if (!actionPath) {
 | 
			
		||||
    return <>{props.children}</>;
 | 
			
		||||
  }
 | 
			
		||||
  const params = getActionParams(path, { isOwn, skipOwnCheck: skipScopeCheck === false ? false : true });
 | 
			
		||||
  const params = parseAction(actionPath, { schema });
 | 
			
		||||
  if (!params) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  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) => {
 | 
			
		||||
  const { name } = useCollection();
 | 
			
		||||
  const fieldSchema = useFieldSchema();
 | 
			
		||||
  const isOwn = useRecordIsOwn();
 | 
			
		||||
  const { allowAll, allowConfigure, getActionParams } = useACLRoleContext();
 | 
			
		||||
  if (!name || allowAll || allowConfigure) {
 | 
			
		||||
  const recordPkValue = useRecordPkValue();
 | 
			
		||||
  const resource = useResourceName();
 | 
			
		||||
  const { parseAction } = useACLRoleContext();
 | 
			
		||||
  const schema = useFieldSchema();
 | 
			
		||||
  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}</>;
 | 
			
		||||
  }
 | 
			
		||||
  const actionName = fieldSchema['x-action'];
 | 
			
		||||
  const path = fieldSchema['x-acl-action'] || `${name}:${actionName}`;
 | 
			
		||||
  const skipScopeCheck = fieldSchema['x-acl-action-props']?.skipScopeCheck;
 | 
			
		||||
  const params = getActionParams(path, { skipOwnCheck: skipScopeCheck, isOwn });
 | 
			
		||||
  const params = parseAction(actionPath, { schema, recordPkValue });
 | 
			
		||||
  if (!params) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  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) => {
 | 
			
		||||
  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}</>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ACLMenuItemProvider = (props) => {
 | 
			
		||||
  const { allowAll, allowConfigure, allowMenuItemIds = [] } = useACLRoleContext();
 | 
			
		||||
  const { allowAll, allowMenuItemIds = [], snippets } = useACLRoleContext();
 | 
			
		||||
  const fieldSchema = useFieldSchema();
 | 
			
		||||
  if (allowAll || allowConfigure) {
 | 
			
		||||
  if (allowAll || snippets.includes('ui.*')) {
 | 
			
		||||
    return <>{props.children}</>;
 | 
			
		||||
  }
 | 
			
		||||
  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 { uniq } from 'lodash';
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useAPIClient, useRequest } from '../../api-client';
 | 
			
		||||
import { useRecord } from '../../record-provider';
 | 
			
		||||
import { uniq } from 'lodash';
 | 
			
		||||
import { useMenuItems } from './MenuItemsProvider';
 | 
			
		||||
 | 
			
		||||
const findUids = (items) => {
 | 
			
		||||
@ -117,7 +117,7 @@ export const MenuConfigure = () => {
 | 
			
		||||
                  message.success(t('Saved successfully'));
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
              {t('Accessible')}
 | 
			
		||||
              {' '}{t('Accessible')}
 | 
			
		||||
            </>
 | 
			
		||||
          ),
 | 
			
		||||
          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 { message } from 'antd';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { connect } from '@formily/react';
 | 
			
		||||
import { Checkbox } from 'antd';
 | 
			
		||||
import uniq from 'lodash/uniq';
 | 
			
		||||
import React, { useContext } from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useAPIClient, useRequest } from '../../api-client';
 | 
			
		||||
import { useRecord } from '../../record-provider';
 | 
			
		||||
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 = () => {
 | 
			
		||||
  const api = useAPIClient();
 | 
			
		||||
  const record = useRecord();
 | 
			
		||||
  const { update, currentRecord } = useContext(PermissionContext);
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  return (
 | 
			
		||||
    <SchemaComponent
 | 
			
		||||
      components={{ SnippetCheckboxGroup }}
 | 
			
		||||
      schema={{
 | 
			
		||||
        type: 'void',
 | 
			
		||||
        name: 'form',
 | 
			
		||||
        'x-component': 'Form',
 | 
			
		||||
        'x-component-props': {
 | 
			
		||||
          useValues: (options) => {
 | 
			
		||||
            const api = useAPIClient();
 | 
			
		||||
            return useRequest(
 | 
			
		||||
              {
 | 
			
		||||
                resource: 'roles',
 | 
			
		||||
                action: 'get',
 | 
			
		||||
                params: {
 | 
			
		||||
                  filterByTk: record.name,
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
              () =>
 | 
			
		||||
                api
 | 
			
		||||
                  .resource('roles')
 | 
			
		||||
                  .get({
 | 
			
		||||
                    filterByTk: currentRecord.name,
 | 
			
		||||
                  })
 | 
			
		||||
                  .then((res) => {
 | 
			
		||||
                    const record = res?.data?.data;
 | 
			
		||||
                    record.snippets?.forEach((key) => {
 | 
			
		||||
                      record[key] = true;
 | 
			
		||||
                    });
 | 
			
		||||
                    return { data: record };
 | 
			
		||||
                  }),
 | 
			
		||||
              options,
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
@ -34,24 +84,22 @@ export const RoleConfigure = () => {
 | 
			
		||||
              if (!form.modified) {
 | 
			
		||||
                return;
 | 
			
		||||
              }
 | 
			
		||||
              await api.resource('roles').update({
 | 
			
		||||
                filterByTk: record.name,
 | 
			
		||||
                values: form.values,
 | 
			
		||||
              });
 | 
			
		||||
              message.success(t('Saved successfully'));
 | 
			
		||||
              await update(field, form);
 | 
			
		||||
            });
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        properties: {
 | 
			
		||||
          allowConfigure: {
 | 
			
		||||
          snippets: {
 | 
			
		||||
            title: t('Configure permissions'),
 | 
			
		||||
            type: 'boolean',
 | 
			
		||||
            'x-decorator': 'FormItem',
 | 
			
		||||
            'x-component': 'Checkbox',
 | 
			
		||||
            'x-content': t('Allows configuration of the whole system, including UI, collections, permissions, etc.'),
 | 
			
		||||
            'x-component': 'SnippetCheckboxGroup',
 | 
			
		||||
          },
 | 
			
		||||
          'strategy.actions': {
 | 
			
		||||
            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-decorator': 'FormItem',
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
@ -2,10 +2,11 @@ import { Spin } from 'antd';
 | 
			
		||||
import React, { createContext, useContext } from 'react';
 | 
			
		||||
import { useRequest } from '../../api-client';
 | 
			
		||||
import { SchemaComponent } from '../../schema-component';
 | 
			
		||||
import { roleSchema } from './schemas/roles';
 | 
			
		||||
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 { data, loading } = useRequest({
 | 
			
		||||
@ -26,7 +27,7 @@ export const RoleTable = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <AvailableActionsProver>
 | 
			
		||||
        <SchemaComponent schema={roleSchema} components={{ MenuItemsProvider }} />
 | 
			
		||||
        <SchemaComponent schema={roleSchema} components={{ MenuItemsProvider, SettingCenterPermissionProvider,PermissionProvider }} />
 | 
			
		||||
      </AvailableActionsProver>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
@ -37,8 +37,8 @@ export const RolesResourcesActions = connect((props) => {
 | 
			
		||||
  const roleCollection = useRecord();
 | 
			
		||||
  const availableActions = useAvailableActions();
 | 
			
		||||
  const { getCollection, getCollectionFields } = useCollectionManager();
 | 
			
		||||
  const collection = getCollection(roleCollection.name);
 | 
			
		||||
  const collectionFields = getCollectionFields(roleCollection.name);
 | 
			
		||||
  const collection = getCollection(roleCollection.collectionName);
 | 
			
		||||
  const collectionFields = getCollectionFields(roleCollection.collectionName);
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const field = useField<ArrayField>();
 | 
			
		||||
 | 
			
		||||
@ -3,4 +3,4 @@ export { RoleConfigure } from './RoleConfigure';
 | 
			
		||||
export { RolesResourcesActions } from './RolesResourcesActions';
 | 
			
		||||
export { RoleTable } from './RoleTable';
 | 
			
		||||
export { StrategyActions } from './StrategyActions';
 | 
			
		||||
 | 
			
		||||
export { SettingsCenterConfigure,SettingCenterProvider } from './ConfigureCenter';
 | 
			
		||||
 | 
			
		||||
@ -18,17 +18,30 @@ const collection = {
 | 
			
		||||
        required: true,
 | 
			
		||||
      } as ISchema,
 | 
			
		||||
    },
 | 
			
		||||
    // {
 | 
			
		||||
    //   type: 'string',
 | 
			
		||||
    //   name: 'name',
 | 
			
		||||
    //   interface: 'input',
 | 
			
		||||
    //   uiSchema: {
 | 
			
		||||
    //     title: '数据表标识',
 | 
			
		||||
    //     type: 'string',
 | 
			
		||||
    //     'x-component': 'Input',
 | 
			
		||||
    //     description: '使用英文',
 | 
			
		||||
    //   } as ISchema,
 | 
			
		||||
    // },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      name: 'name',
 | 
			
		||||
      interface: 'input',
 | 
			
		||||
      uiSchema: {
 | 
			
		||||
        title: '{{t("Collection name")}}',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        'x-component': 'Input',
 | 
			
		||||
      } 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',
 | 
			
		||||
      name: 'usingConfig',
 | 
			
		||||
@ -89,7 +102,7 @@ export const roleCollectionsSchema: ISchema = {
 | 
			
		||||
        useDataSource: '{{ cm.useDataSourceFromRAC }}',
 | 
			
		||||
      },
 | 
			
		||||
      properties: {
 | 
			
		||||
        column1: {
 | 
			
		||||
        column0: {
 | 
			
		||||
          type: 'void',
 | 
			
		||||
          'x-decorator': 'Table.Column.Decorator',
 | 
			
		||||
          '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: {
 | 
			
		||||
          type: 'void',
 | 
			
		||||
          'x-decorator': 'Table.Column.Decorator',
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,9 @@
 | 
			
		||||
import { ISchema } from '@formily/react';
 | 
			
		||||
import { uid } from '@formily/shared';
 | 
			
		||||
import pick from 'lodash/pick';
 | 
			
		||||
import { useEffect } from 'react';
 | 
			
		||||
import { useRequest } from '../../../api-client';
 | 
			
		||||
import { useRecord } from '../../../record-provider';
 | 
			
		||||
import { useActionContext } from '../../../schema-component';
 | 
			
		||||
import { roleCollectionsSchema } from './roleCollections';
 | 
			
		||||
 | 
			
		||||
@ -112,6 +115,7 @@ export const roleSchema: ISchema = {
 | 
			
		||||
                          Promise.resolve({
 | 
			
		||||
                            data: {
 | 
			
		||||
                              name: `r_${uid()}`,
 | 
			
		||||
                              snippets: ['!ui.*', '!pm', '!pm.*'],
 | 
			
		||||
                            },
 | 
			
		||||
                          }),
 | 
			
		||||
                        { ...options, refreshDeps: [ctx.visible] },
 | 
			
		||||
@ -227,11 +231,14 @@ export const roleSchema: ISchema = {
 | 
			
		||||
                      type: 'void',
 | 
			
		||||
                      title: '{{t("Configure")}}',
 | 
			
		||||
                      'x-component': 'Action.Link',
 | 
			
		||||
                      'x-decorator': 'ACLActionProvider',
 | 
			
		||||
                      'x-acl-action': 'roles:update',
 | 
			
		||||
                      'x-component-props': {},
 | 
			
		||||
                      properties: {
 | 
			
		||||
                        drawer: {
 | 
			
		||||
                          type: 'void',
 | 
			
		||||
                          'x-component': 'Action.Drawer',
 | 
			
		||||
                          'x-decorator': 'PermissionProvider',
 | 
			
		||||
                          title: '{{t("Configure permissions")}}',
 | 
			
		||||
                          properties: {
 | 
			
		||||
                            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: {
 | 
			
		||||
                      type: 'void',
 | 
			
		||||
                      title: '{{t("Edit")}}',
 | 
			
		||||
                      'x-decorator': 'ACLActionProvider',
 | 
			
		||||
                      'x-acl-action': 'roles:update',
 | 
			
		||||
                      'x-component': 'Action.Link',
 | 
			
		||||
                      'x-component-props': {
 | 
			
		||||
                        type: 'primary',
 | 
			
		||||
@ -290,7 +312,23 @@ export const roleSchema: ISchema = {
 | 
			
		||||
                          'x-component': 'Action.Drawer',
 | 
			
		||||
                          'x-decorator': 'Form',
 | 
			
		||||
                          '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")}}',
 | 
			
		||||
                          properties: {
 | 
			
		||||
@ -337,6 +375,8 @@ export const roleSchema: ISchema = {
 | 
			
		||||
                    delete: {
 | 
			
		||||
                      type: 'void',
 | 
			
		||||
                      title: '{{ t("Delete") }}',
 | 
			
		||||
                      'x-acl-action': 'roles:destroy',
 | 
			
		||||
                      'x-decorator': 'ACLActionProvider',
 | 
			
		||||
                      'x-component': 'Action.Link',
 | 
			
		||||
                      'x-component-props': {
 | 
			
		||||
                        confirm: {
 | 
			
		||||
 | 
			
		||||
@ -222,6 +222,7 @@ export const scopesSchema: ISchema = {
 | 
			
		||||
                              type: 'void',
 | 
			
		||||
                              title: '{{ t("Edit") }}',
 | 
			
		||||
                              'x-action': 'update',
 | 
			
		||||
                              'x-decorator': 'ACLActionProvider',
 | 
			
		||||
                              'x-component': 'Action.Link',
 | 
			
		||||
                              'x-component-props': {
 | 
			
		||||
                                openMode: 'drawer',
 | 
			
		||||
@ -308,6 +309,7 @@ export const scopesSchema: ISchema = {
 | 
			
		||||
                            destroy: {
 | 
			
		||||
                              title: '{{ t("Delete") }}',
 | 
			
		||||
                              'x-action': 'destroy',
 | 
			
		||||
                              'x-decorator': 'ACLActionProvider',
 | 
			
		||||
                              'x-component': 'Action.Link',
 | 
			
		||||
                              'x-designer': 'Action.Designer',
 | 
			
		||||
                              'x-component-props': {
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
export * from './ACLProvider';
 | 
			
		||||
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() {
 | 
			
		||||
    this.axios.interceptors.request.use((config) => {
 | 
			
		||||
      config.headers['X-With-ACL-Meta'] = true;
 | 
			
		||||
      return config;
 | 
			
		||||
    });
 | 
			
		||||
    super.interceptors();
 | 
			
		||||
    this.notification();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -120,11 +120,11 @@ export const useKanbanBlockContext = () => {
 | 
			
		||||
 | 
			
		||||
const useDisableCardDrag = () => {
 | 
			
		||||
  const ctx = useKanbanBlockContext();
 | 
			
		||||
  const { allowAll, allowConfigure, getActionParams } = useACLRoleContext();
 | 
			
		||||
  const { allowAll, allowConfigure, parseAction } = useACLRoleContext();
 | 
			
		||||
  if (allowAll || allowConfigure) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  const result = getActionParams(`${ctx?.props?.resource}:update`, { skipOwnCheck: true });
 | 
			
		||||
  const result = parseAction(`${ctx?.props?.resource}:update`, { ignoreScope: true });
 | 
			
		||||
  return !result;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@ export const useCollection = () => {
 | 
			
		||||
    [],
 | 
			
		||||
  );
 | 
			
		||||
  const totalFields = unionBy(currentFields?.concat(inheritedFields), 'name').filter((v) => {
 | 
			
		||||
    return !v.isForeignKey
 | 
			
		||||
    return !v.isForeignKey;
 | 
			
		||||
  });
 | 
			
		||||
  return {
 | 
			
		||||
    ...collection,
 | 
			
		||||
@ -32,7 +32,14 @@ export const useCollection = () => {
 | 
			
		||||
      return fields?.find((field) => field.name === name);
 | 
			
		||||
    },
 | 
			
		||||
    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,
 | 
			
		||||
    inheritedFields
 | 
			
		||||
    inheritedFields,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import {
 | 
			
		||||
  recordPickerSelector,
 | 
			
		||||
  recordPickerViewer,
 | 
			
		||||
  relationshipType,
 | 
			
		||||
  reverseFieldProperties,
 | 
			
		||||
  reverseFieldProperties
 | 
			
		||||
} from './properties';
 | 
			
		||||
import { IField } from './types';
 | 
			
		||||
 | 
			
		||||
@ -59,6 +59,7 @@ export const o2m: IField = {
 | 
			
		||||
          block: {
 | 
			
		||||
            type: 'void',
 | 
			
		||||
            'x-decorator': 'TableFieldProvider',
 | 
			
		||||
            'x-acl-action': `${association}:list`,
 | 
			
		||||
            'x-decorator-props': {
 | 
			
		||||
              collection: field.target,
 | 
			
		||||
              association: association,
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,7 @@ export const subTable: IField = {
 | 
			
		||||
      block: {
 | 
			
		||||
        type: 'void',
 | 
			
		||||
        'x-decorator': 'TableFieldProvider',
 | 
			
		||||
        'x-acl-action': `${field.target}:list`,
 | 
			
		||||
        'x-decorator-props': {
 | 
			
		||||
          collection: field.target,
 | 
			
		||||
          association: association,
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,11 @@ export const calendar: ICollectionTemplate = {
 | 
			
		||||
  order: 2,
 | 
			
		||||
  color: 'orange',
 | 
			
		||||
  default: {
 | 
			
		||||
    createdBy: true,
 | 
			
		||||
    updatedBy: true,
 | 
			
		||||
    createdAt: true,
 | 
			
		||||
    updatedAt: true,
 | 
			
		||||
    sortable: true,
 | 
			
		||||
    fields: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'cron',
 | 
			
		||||
 | 
			
		||||
@ -419,6 +419,10 @@ export default {
 | 
			
		||||
  "General permissions": "General permissions",
 | 
			
		||||
  "Global action permissions": "Global 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",
 | 
			
		||||
  "Allow": "Allow",
 | 
			
		||||
  "Data scope": "Data scope",
 | 
			
		||||
 | 
			
		||||
@ -402,6 +402,10 @@ export default {
 | 
			
		||||
  "General permissions": "一般設定",
 | 
			
		||||
  "Global action permissions": "グローバル操作権限",
 | 
			
		||||
  "General action permissions": "一般操作権限",
 | 
			
		||||
  "Settings center permissions":"中央権限の設定",
 | 
			
		||||
  'Allow to desgin pages':"インタフェース構成の許可",
 | 
			
		||||
  "Allow to manage plugins":"管理プラグインの許可",
 | 
			
		||||
  "Allow to configure plugins":"管理構成センターの許可",
 | 
			
		||||
  "Action display name": "操作名",
 | 
			
		||||
  "Allow": "許可する",
 | 
			
		||||
  "Data scope": "レコードスコープ",
 | 
			
		||||
 | 
			
		||||
@ -496,6 +496,13 @@ export default {
 | 
			
		||||
  'General permissions': '通用配置',
 | 
			
		||||
  'Global 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': '操作名称',
 | 
			
		||||
  'Allow': '允许',
 | 
			
		||||
  'Data scope': '数据范围',
 | 
			
		||||
@ -614,5 +621,7 @@ export default {
 | 
			
		||||
  "Search and select collection": "搜索并选择数据表",
 | 
			
		||||
  '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 { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useHistory } from 'react-router-dom';
 | 
			
		||||
import { useACLRoleContext } from '../acl/ACLProvider';
 | 
			
		||||
import { PluginManagerContext } from './context';
 | 
			
		||||
 | 
			
		||||
export const usePrefixCls = (
 | 
			
		||||
@ -142,19 +143,15 @@ PluginManager.Toolbar.Item = (props) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const RemotePluginManagerToolbar = () => {
 | 
			
		||||
  // const api = useAPIClient();
 | 
			
		||||
  // const { data, loading } = useRequest({
 | 
			
		||||
  //   resource: 'plugins',
 | 
			
		||||
  //   action: 'getPinned',
 | 
			
		||||
  // });
 | 
			
		||||
  // if (loading) {
 | 
			
		||||
  //   return <Spin />;
 | 
			
		||||
  // }
 | 
			
		||||
  const { allowAll, snippets } = useACLRoleContext();
 | 
			
		||||
  const getSnippetsAllow = (aclKey) => {
 | 
			
		||||
    return allowAll || snippets?.includes(aclKey);
 | 
			
		||||
  };
 | 
			
		||||
  const items = [
 | 
			
		||||
    { component: 'DesignableSwitch', pin: true },
 | 
			
		||||
    { component: 'PluginManagerLink', pin: true },
 | 
			
		||||
    { component: 'SettingsCenterDropdown', pin: true },
 | 
			
		||||
    { component: 'DesignableSwitch', pin: true, isAllow: getSnippetsAllow('ui.*') },
 | 
			
		||||
    { component: 'PluginManagerLink', pin: true, isAllow: getSnippetsAllow('pm') },
 | 
			
		||||
    { component: 'SettingsCenterDropdown', pin: true, isAllow: getSnippetsAllow('pm.*') },
 | 
			
		||||
    // ...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 { uid } from '@formily/shared';
 | 
			
		||||
import { Dropdown, Menu } from 'antd';
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import React, { useState, useContext } from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useHistory } from 'react-router-dom';
 | 
			
		||||
import { PluginManager } from '../plugin-manager';
 | 
			
		||||
import { useCompile } from '../schema-component';
 | 
			
		||||
import { ActionContext } from '../schema-component';
 | 
			
		||||
 | 
			
		||||
const schema: ISchema = {
 | 
			
		||||
  type: 'object',
 | 
			
		||||
  properties: {
 | 
			
		||||
    [uid()]: {
 | 
			
		||||
      'x-component': 'Action.Drawer',
 | 
			
		||||
      type: 'void',
 | 
			
		||||
      title: '{{t("Collections & Fields")}}',
 | 
			
		||||
      properties: {
 | 
			
		||||
        configuration: {
 | 
			
		||||
          'x-component': 'ConfigurationTable',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
import { useACLRoleContext } from '../acl/ACLProvider';
 | 
			
		||||
import { getPluginsTabs, SettingsCenterContext } from './index';
 | 
			
		||||
 | 
			
		||||
export const PluginManagerLink = () => {
 | 
			
		||||
  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 = () => {
 | 
			
		||||
  const { snippets = [] } = useACLRoleContext();
 | 
			
		||||
  const [visible, setVisible] = useState(false);
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  const items = [
 | 
			
		||||
    {
 | 
			
		||||
      title: t('Collections & Fields'),
 | 
			
		||||
      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',
 | 
			
		||||
    // },
 | 
			
		||||
  ];
 | 
			
		||||
  const itemData = useContext(SettingsCenterContext);
 | 
			
		||||
  const pluginsTabs = getPluginsTabs(itemData, snippets);
 | 
			
		||||
  const bookmarkTabs = getBookmarkTabs(pluginsTabs);
 | 
			
		||||
  return (
 | 
			
		||||
    <ActionContext.Provider value={{ visible, setVisible }}>
 | 
			
		||||
      <Dropdown
 | 
			
		||||
        placement="bottom"
 | 
			
		||||
        overlay={
 | 
			
		||||
          <Menu>
 | 
			
		||||
            <Menu.ItemGroup title={t('Bookmark')}>
 | 
			
		||||
              {items.map((item) => {
 | 
			
		||||
              {bookmarkTabs.map((tab) => {
 | 
			
		||||
                return (
 | 
			
		||||
                  <Menu.Item
 | 
			
		||||
                    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>
 | 
			
		||||
                );
 | 
			
		||||
              })}
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,12 @@
 | 
			
		||||
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons';
 | 
			
		||||
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 { useTranslation } from 'react-i18next';
 | 
			
		||||
import { Redirect, useHistory, useRouteMatch } from 'react-router-dom';
 | 
			
		||||
import { ACLPane } from '../acl';
 | 
			
		||||
import { useACLRoleContext } from '../acl/ACLProvider';
 | 
			
		||||
import { useAPIClient, useRequest } from '../api-client';
 | 
			
		||||
import { CollectionManagerPane } from '../collection-manager';
 | 
			
		||||
import { useDocumentTitle } from '../document-title';
 | 
			
		||||
@ -149,7 +151,7 @@ const LocalPlugins = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {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 { setTitle } = useDocumentTitle();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const { snippets = [] } = useACLRoleContext();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
  return snippets.includes('pm') ? (
 | 
			
		||||
    <div>
 | 
			
		||||
      <PageHeader
 | 
			
		||||
        ghost={false}
 | 
			
		||||
@ -217,6 +220,8 @@ const PluginList = (props) => {
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  ) : (
 | 
			
		||||
    <Result status="404" title="404" subTitle="Sorry, the page you visited does not exist." />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -226,16 +231,17 @@ const settings = {
 | 
			
		||||
    icon: 'LockOutlined',
 | 
			
		||||
    tabs: {
 | 
			
		||||
      roles: {
 | 
			
		||||
        isBookmark: true,
 | 
			
		||||
        title: '{{t("Roles & Permissions")}}',
 | 
			
		||||
        component: ACLPane,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'block-templates': {
 | 
			
		||||
  'ui-schema-storage': {
 | 
			
		||||
    title: '{{t("Block templates")}}',
 | 
			
		||||
    icon: 'LayoutOutlined',
 | 
			
		||||
    tabs: {
 | 
			
		||||
      list: {
 | 
			
		||||
      'block-templates': {
 | 
			
		||||
        title: '{{t("Block templates")}}',
 | 
			
		||||
        component: BlockTemplatesPane,
 | 
			
		||||
      },
 | 
			
		||||
@ -246,6 +252,7 @@ const settings = {
 | 
			
		||||
    title: '{{t("Collection manager")}}',
 | 
			
		||||
    tabs: {
 | 
			
		||||
      collections: {
 | 
			
		||||
        isBookmark: true,
 | 
			
		||||
        title: '{{t("Collections & Fields")}}',
 | 
			
		||||
        component: CollectionManagerPane,
 | 
			
		||||
      },
 | 
			
		||||
@ -256,6 +263,7 @@ const settings = {
 | 
			
		||||
    title: '{{t("System settings")}}',
 | 
			
		||||
    tabs: {
 | 
			
		||||
      'system-settings': {
 | 
			
		||||
        isBookmark: true,
 | 
			
		||||
        title: '{{t("System settings")}}',
 | 
			
		||||
        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 { snippets = [] } = useACLRoleContext();
 | 
			
		||||
  const match = useRouteMatch<any>();
 | 
			
		||||
  const history = useHistory<any>();
 | 
			
		||||
  const items = useContext(SettingsCenterContext);
 | 
			
		||||
  const pluginsTabs = getPluginsTabs(items, snippets);
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const firstUri = useMemo(() => {
 | 
			
		||||
    const keys = Object.keys(items).sort();
 | 
			
		||||
    const pluginName = keys.shift();
 | 
			
		||||
    const tabName = Object.keys(items?.[pluginName]?.tabs || {}).shift();
 | 
			
		||||
    const pluginName = pluginsTabs[0].key;
 | 
			
		||||
    const tabName = pluginsTabs[0].tabs[0].key;
 | 
			
		||||
    return `/admin/settings/${pluginName}/${tabName}`;
 | 
			
		||||
  }, [items]);
 | 
			
		||||
  }, [pluginsTabs]);
 | 
			
		||||
  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) {
 | 
			
		||||
    return <Redirect to={firstUri} />;
 | 
			
		||||
  }
 | 
			
		||||
@ -286,18 +320,18 @@ const SettingsCenter = (props) => {
 | 
			
		||||
    return <Redirect to={`/admin/settings/${pluginName}/${firstTabName}`} />;
 | 
			
		||||
  }
 | 
			
		||||
  const component = items[pluginName]?.tabs?.[tabName]?.component;
 | 
			
		||||
  const menuItems: any = Object.keys(items)
 | 
			
		||||
    .sort()
 | 
			
		||||
    .map((key) => {
 | 
			
		||||
      const item = items[key];
 | 
			
		||||
      const tabKey = Object.keys(item.tabs).shift();
 | 
			
		||||
  const plugin: any = pluginsTabs.find((v) => v.key === pluginName);
 | 
			
		||||
  const menuItems: any = pluginsTabs
 | 
			
		||||
    .filter((plugin) => plugin.isAllow)
 | 
			
		||||
    .map((plugin) => {
 | 
			
		||||
      return {
 | 
			
		||||
        label: compile(item.title),
 | 
			
		||||
        key: key,
 | 
			
		||||
        icon: item.icon ? <Icon type={item.icon} /> : null,
 | 
			
		||||
        label: compile(plugin.title),
 | 
			
		||||
        key: plugin.key,
 | 
			
		||||
        icon: plugin.icon ? <Icon type={plugin.icon} /> : null,
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <Layout>
 | 
			
		||||
        <div
 | 
			
		||||
          style={
 | 
			
		||||
@ -338,6 +372,7 @@ const SettingsCenter = (props) => {
 | 
			
		||||
          />
 | 
			
		||||
        </Layout.Sider>
 | 
			
		||||
        <Layout.Content>
 | 
			
		||||
          {aclPluginTabCheck && (
 | 
			
		||||
            <PageHeader
 | 
			
		||||
              ghost={false}
 | 
			
		||||
              title={compile(items[pluginName]?.title)}
 | 
			
		||||
@ -348,16 +383,23 @@ const SettingsCenter = (props) => {
 | 
			
		||||
                    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} />;
 | 
			
		||||
                  {plugin.tabs?.map((tab) => {
 | 
			
		||||
                    return tab.isAllow && <Tabs.TabPane tab={compile(tab?.title)} key={tab.key} />;
 | 
			
		||||
                  })}
 | 
			
		||||
                </Tabs>
 | 
			
		||||
              }
 | 
			
		||||
            />
 | 
			
		||||
        <div style={{ margin: 24 }}>{component && React.createElement(component)}</div>
 | 
			
		||||
          )}
 | 
			
		||||
          <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 { useHistory, useRouteMatch } from 'react-router-dom';
 | 
			
		||||
import {
 | 
			
		||||
  ACLAllowConfigure,
 | 
			
		||||
  ACLRolesCheckProvider,
 | 
			
		||||
  CurrentUser,
 | 
			
		||||
  CurrentUserProvider,
 | 
			
		||||
@ -25,8 +24,8 @@ import { PoweredBy } from '../../../powered-by';
 | 
			
		||||
import { useMutationObserver } from 'ahooks';
 | 
			
		||||
 | 
			
		||||
const filterByACL = (schema, options) => {
 | 
			
		||||
  const { allowAll, allowConfigure, allowMenuItemIds = [] } = options;
 | 
			
		||||
  if (allowAll || allowConfigure) {
 | 
			
		||||
  const { allowAll, allowMenuItemIds = [] } = options;
 | 
			
		||||
  if (allowAll) {
 | 
			
		||||
    return schema;
 | 
			
		||||
  }
 | 
			
		||||
  const filterSchema = (s) => {
 | 
			
		||||
@ -123,7 +122,7 @@ const MenuEditor = (props) => {
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const InternalAdminLayout = (props: any) => {
 | 
			
		||||
export const InternalAdminLayout = (props: any) => {
 | 
			
		||||
  const sideMenuRef = useRef<HTMLDivElement>();
 | 
			
		||||
  const [sideMenuWidth, setSideMenuWidth] = useState(0);
 | 
			
		||||
 | 
			
		||||
@ -192,9 +191,7 @@ const InternalAdminLayout = (props: any) => {
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div style={{ position: 'absolute', height: '100%', zIndex: 10, top: 0, right: 0 }}>
 | 
			
		||||
            <ACLAllowConfigure>
 | 
			
		||||
            <RemotePluginManagerToolbar />
 | 
			
		||||
            </ACLAllowConfigure>
 | 
			
		||||
            <CurrentUser />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
@ -89,14 +89,16 @@ export const Action: ComposedAction = observer((props: any) => {
 | 
			
		||||
  const form = useForm();
 | 
			
		||||
  const designerProps = fieldSchema['x-designer-props'];
 | 
			
		||||
  const openMode = fieldSchema?.['x-component-props']?.['openMode'];
 | 
			
		||||
  const disabled = form.disabled || field.disabled;
 | 
			
		||||
  const openSize = fieldSchema?.['x-component-props']?.['openSize'];
 | 
			
		||||
  const renderButton = () => (
 | 
			
		||||
    <SortableItem
 | 
			
		||||
      {...others}
 | 
			
		||||
      loading={field?.data?.loading}
 | 
			
		||||
      icon={<Icon type={icon} />}
 | 
			
		||||
      disabled={form.disabled}
 | 
			
		||||
      disabled={disabled}
 | 
			
		||||
      onClick={(e: React.MouseEvent) => {
 | 
			
		||||
        if (!disabled) {
 | 
			
		||||
          e.preventDefault();
 | 
			
		||||
          e.stopPropagation();
 | 
			
		||||
          const onOk = () => {
 | 
			
		||||
@ -112,6 +114,7 @@ export const Action: ComposedAction = observer((props: any) => {
 | 
			
		||||
          } else {
 | 
			
		||||
            onOk();
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }}
 | 
			
		||||
      component={component || Button}
 | 
			
		||||
      className={classnames(className, actionDesignerCss)}
 | 
			
		||||
 | 
			
		||||
@ -6,10 +6,11 @@ import { uid } from '@formily/shared';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useCompile, useDesignable, useFieldComponentOptions } from '../../hooks';
 | 
			
		||||
import { ACLCollectionFieldProvider } from '../../../acl/ACLProvider';
 | 
			
		||||
import { useFilterByTk, useFormBlockContext } from '../../../block-provider';
 | 
			
		||||
import { useCollection, useCollectionManager } from '../../../collection-manager';
 | 
			
		||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
 | 
			
		||||
import { useCompile, useDesignable, useFieldComponentOptions } from '../../hooks';
 | 
			
		||||
import { BlockItem } from '../block-item';
 | 
			
		||||
import { HTMLEncode } from '../input/shared';
 | 
			
		||||
 | 
			
		||||
@ -26,6 +27,7 @@ const divWrap = (schema: ISchema) => {
 | 
			
		||||
export const FormItem: any = (props) => {
 | 
			
		||||
  const field = useField();
 | 
			
		||||
  return (
 | 
			
		||||
    <ACLCollectionFieldProvider>
 | 
			
		||||
      <BlockItem className={'nb-form-item'}>
 | 
			
		||||
        <Item
 | 
			
		||||
          className={`${css`
 | 
			
		||||
@ -47,6 +49,7 @@ export const FormItem: any = (props) => {
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
      </BlockItem>
 | 
			
		||||
    </ACLCollectionFieldProvider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import { MenuOutlined } from '@ant-design/icons';
 | 
			
		||||
import { SortableContext, useSortable } from '@dnd-kit/sortable';
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
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 { useEventListener, useMemoizedFn } from 'ahooks';
 | 
			
		||||
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 { DndContext, useDesignable } from '../..';
 | 
			
		||||
import { RecordIndexProvider, RecordProvider, useSchemaInitializer } from '../../../';
 | 
			
		||||
import { useACLFieldWhitelist } from '../../../acl/ACLProvider';
 | 
			
		||||
import { isCollectionFieldComponent, isColumnComponent } from './utils';
 | 
			
		||||
 | 
			
		||||
const useTableColumns = () => {
 | 
			
		||||
  const start = Date.now();
 | 
			
		||||
  const field = useField<ArrayField>();
 | 
			
		||||
  const schema = useFieldSchema();
 | 
			
		||||
  const { schemaInWhitelist } = useACLFieldWhitelist();
 | 
			
		||||
  const { designable } = useDesignable();
 | 
			
		||||
  const { exists, render } = useSchemaInitializer(schema['x-initializer']);
 | 
			
		||||
  const columns = schema
 | 
			
		||||
    .reduceProperties((buf, s) => {
 | 
			
		||||
      if (isColumnComponent(s)) {
 | 
			
		||||
      if (isColumnComponent(s) && schemaInWhitelist(Object.values(s.properties || {}).pop())) {
 | 
			
		||||
        return buf.concat([s]);
 | 
			
		||||
      }
 | 
			
		||||
      return buf;
 | 
			
		||||
    }, [])
 | 
			
		||||
    .map((s: Schema) => {
 | 
			
		||||
    ?.map((s: Schema) => {
 | 
			
		||||
      const collectionFields = s.reduceProperties((buf, s) => {
 | 
			
		||||
        if (isCollectionFieldComponent(s)) {
 | 
			
		||||
          return buf.concat([s]);
 | 
			
		||||
@ -42,9 +44,9 @@ const useTableColumns = () => {
 | 
			
		||||
        render: (v, record) => {
 | 
			
		||||
          const index = field.value?.indexOf(record);
 | 
			
		||||
          return (
 | 
			
		||||
            <RecordIndexProvider index={index}>
 | 
			
		||||
            <RecordIndexProvider index={record.__index || index}>
 | 
			
		||||
              <RecordProvider record={record}>
 | 
			
		||||
                <RecursionField schema={s} name={index} onlyRenderProperties />
 | 
			
		||||
                <RecursionField schema={s} name={record.__index || index} onlyRenderProperties />
 | 
			
		||||
              </RecordProvider>
 | 
			
		||||
            </RecordIndexProvider>
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
@ -35,7 +35,7 @@ const useTableColumns = () => {
 | 
			
		||||
          return (
 | 
			
		||||
            <RecordIndexProvider index={index}>
 | 
			
		||||
              <RecordProvider record={record}>
 | 
			
		||||
                <RecursionField schema={s} name={index} onlyRenderProperties />
 | 
			
		||||
                <RecursionField schema={s} name={record.__index || index} onlyRenderProperties />
 | 
			
		||||
              </RecordProvider>
 | 
			
		||||
            </RecordIndexProvider>
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
@ -108,6 +108,7 @@ export const ReadPrettyFormActionInitializers = {
 | 
			
		||||
            'x-component': 'Action',
 | 
			
		||||
            'x-designer': 'Action.Designer',
 | 
			
		||||
            'x-action': 'customize:update',
 | 
			
		||||
            'x-decorator': 'ACLActionProvider',
 | 
			
		||||
            'x-acl-action': 'update',
 | 
			
		||||
            'x-action-settings': {
 | 
			
		||||
              assignedValues: {},
 | 
			
		||||
 | 
			
		||||
@ -173,6 +173,7 @@ export const TableActionColumnInitializers = (props: any) => {
 | 
			
		||||
                title: '{{ t("Update record") }}',
 | 
			
		||||
                'x-component': 'Action.Link',
 | 
			
		||||
                'x-action': 'customize:update',
 | 
			
		||||
                'x-decorator': 'ACLActionProvider',
 | 
			
		||||
                'x-acl-action': 'update',
 | 
			
		||||
                'x-designer': 'Action.Designer',
 | 
			
		||||
                'x-action-settings': {
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { ISchema, Schema } from '@formily/react';
 | 
			
		||||
import { Schema } from '@formily/react';
 | 
			
		||||
 | 
			
		||||
// 表格操作配置
 | 
			
		||||
export const TableActionInitializers = {
 | 
			
		||||
 | 
			
		||||
@ -201,11 +201,9 @@ export const useFormItemInitializerFields = (options?: any) => {
 | 
			
		||||
    ?.filter((field) => field?.interface && !field?.isForeignKey)
 | 
			
		||||
    ?.map((field) => {
 | 
			
		||||
      const interfaceConfig = getInterface(field.interface);
 | 
			
		||||
 | 
			
		||||
      const schema = {
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        name: field.name,
 | 
			
		||||
        // title: field?.uiSchema?.title || field.name,
 | 
			
		||||
        'x-designer': 'FormItem.Designer',
 | 
			
		||||
        'x-component': field.interface === 'o2m' && !snapshot ? 'TableField' : 'CollectionField',
 | 
			
		||||
        'x-decorator': 'FormItem',
 | 
			
		||||
@ -241,7 +239,6 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => {
 | 
			
		||||
  const form = useForm();
 | 
			
		||||
  const { readPretty = form.readPretty, block = 'Form' } = options || {};
 | 
			
		||||
  const interfaces = block === 'Form' ? ['m2o'] : ['o2o', 'oho', 'obo', 'm2o'];
 | 
			
		||||
 | 
			
		||||
  const groups = fields
 | 
			
		||||
    ?.filter((field) => {
 | 
			
		||||
      return interfaces.includes(field.interface);
 | 
			
		||||
@ -255,7 +252,6 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => {
 | 
			
		||||
          const schema = {
 | 
			
		||||
            type: 'string',
 | 
			
		||||
            name: `${field.name}.${subField.name}`,
 | 
			
		||||
            // title: subField?.uiSchema?.title || subField.name,
 | 
			
		||||
            'x-designer': 'FormItem.Designer',
 | 
			
		||||
            'x-component': 'CollectionField',
 | 
			
		||||
            'x-read-pretty': readPretty,
 | 
			
		||||
@ -265,7 +261,6 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => {
 | 
			
		||||
            'x-decorator': 'FormItem',
 | 
			
		||||
            'x-collection-field': `${name}.${field.name}.${subField.name}`,
 | 
			
		||||
          };
 | 
			
		||||
          // interfaceConfig?.schemaInitialize?.(schema, { field, block: 'Form', readPretty: form.readPretty });
 | 
			
		||||
          return {
 | 
			
		||||
            type: 'item',
 | 
			
		||||
            title: subField?.uiSchema?.title || subField.name,
 | 
			
		||||
@ -630,7 +625,7 @@ export const createDetailsBlockSchema = (options) => {
 | 
			
		||||
  const resourceName = resource || association || collection;
 | 
			
		||||
  const schema: ISchema = {
 | 
			
		||||
    type: 'void',
 | 
			
		||||
    'x-acl-action': `${resourceName}:get`,
 | 
			
		||||
    'x-acl-action': `${resourceName}:view`,
 | 
			
		||||
    'x-decorator': 'DetailsBlockProvider',
 | 
			
		||||
    'x-decorator-props': {
 | 
			
		||||
      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;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -879,7 +874,7 @@ export const createTableBlockSchema = (options) => {
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
  console.log(JSON.stringify(schema, null, 2));
 | 
			
		||||
  // console.log(JSON.stringify(schema, null, 2));
 | 
			
		||||
  return schema;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@ import {
 | 
			
		||||
  Sequelize,
 | 
			
		||||
  SyncOptions,
 | 
			
		||||
  Transactionable,
 | 
			
		||||
  Utils,
 | 
			
		||||
  Utils
 | 
			
		||||
} from 'sequelize';
 | 
			
		||||
import { SequelizeStorage, Umzug } from 'umzug';
 | 
			
		||||
import { Collection, CollectionOptions, RepositoryType } from './collection';
 | 
			
		||||
@ -58,7 +58,7 @@ import {
 | 
			
		||||
  SyncListener,
 | 
			
		||||
  UpdateListener,
 | 
			
		||||
  UpdateWithAssociationsListener,
 | 
			
		||||
  ValidateListener,
 | 
			
		||||
  ValidateListener
 | 
			
		||||
} from './types';
 | 
			
		||||
 | 
			
		||||
export interface MergeOptions extends merge.Options {}
 | 
			
		||||
@ -356,7 +356,19 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
			
		||||
   * @param name
 | 
			
		||||
   */
 | 
			
		||||
  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 {
 | 
			
		||||
@ -389,11 +401,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
			
		||||
  getRepository<R extends ArrayFieldRepository>(name: string, relationId: string | number): R;
 | 
			
		||||
 | 
			
		||||
  getRepository<R extends RelationRepository>(name: string, relationId?: string | number): Repository | R {
 | 
			
		||||
    if (relationId) {
 | 
			
		||||
    const [collection, relation] = name.split('.');
 | 
			
		||||
    if (relation) {
 | 
			
		||||
      return this.getRepository(collection)?.relation(relation)?.of(relationId) as R;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return this.getCollection(name)?.repository;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ import {
 | 
			
		||||
  Op,
 | 
			
		||||
  Transactionable,
 | 
			
		||||
  UpdateOptions as SequelizeUpdateOptions,
 | 
			
		||||
  WhereOperators,
 | 
			
		||||
  WhereOperators
 | 
			
		||||
} from 'sequelize';
 | 
			
		||||
import { Collection } from './collection';
 | 
			
		||||
import { Database } from './database';
 | 
			
		||||
@ -193,6 +193,9 @@ class RelationRepositoryBuilder<R extends RelationRepository> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  of(id: string | number): R {
 | 
			
		||||
    if (!this.association) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const klass = this.builder()[this.association.associationType];
 | 
			
		||||
    return new klass(this.collection, this.associationName, id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,10 @@ import { availableActions } from './available-action';
 | 
			
		||||
 | 
			
		||||
const configureResources = [
 | 
			
		||||
  'roles',
 | 
			
		||||
  'users',
 | 
			
		||||
  'collections',
 | 
			
		||||
  'fields',
 | 
			
		||||
  'collections.fields',
 | 
			
		||||
  'roles.collections',
 | 
			
		||||
  'roles.resources',
 | 
			
		||||
  'rolesResourcesScopes',
 | 
			
		||||
 | 
			
		||||
@ -45,6 +45,7 @@ export interface ApplicationOptions {
 | 
			
		||||
 | 
			
		||||
export interface DefaultState extends KoaDefaultState {
 | 
			
		||||
  currentUser?: any;
 | 
			
		||||
 | 
			
		||||
  [key: string]: any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -53,6 +54,7 @@ export interface DefaultContext extends KoaDefaultContext {
 | 
			
		||||
  cache: Cache;
 | 
			
		||||
  resourcer: Resourcer;
 | 
			
		||||
  i18n: any;
 | 
			
		||||
 | 
			
		||||
  [key: string]: any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -46,6 +46,10 @@ export function dataWrapping() {
 | 
			
		||||
        ctx.body = {
 | 
			
		||||
          data: ctx.body,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (ctx.bodyMeta) {
 | 
			
		||||
          ctx.body.meta = ctx.bodyMeta;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } else if (ctx.action) {
 | 
			
		||||
      ctx.body = {
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ export function db2resource(ctx: ResourcerContext & { db: Database }, next: () =
 | 
			
		||||
  if (!params) {
 | 
			
		||||
    return next();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const resourceName = getNameByParams(params);
 | 
			
		||||
  // 如果资源名称未被定义
 | 
			
		||||
  if (resourcer.isDefined(resourceName)) {
 | 
			
		||||
 | 
			
		||||
@ -43,8 +43,11 @@ export class PluginManager {
 | 
			
		||||
    this.repository.setPluginManager(this);
 | 
			
		||||
    this.app.resourcer.define(resourceOptions);
 | 
			
		||||
 | 
			
		||||
    this.app.acl.allow('pm', ['enable', 'disable', 'remove'], 'allowConfigure');
 | 
			
		||||
    this.app.acl.allow('applicationPlugins', 'list', 'allowConfigure');
 | 
			
		||||
    this.app.acl.registerSnippet({
 | 
			
		||||
      name: 'pm',
 | 
			
		||||
      actions: ['pm:*', 'applicationPlugins:list'],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.server = net.createServer((socket) => {
 | 
			
		||||
      socket.on('data', async (data) => {
 | 
			
		||||
        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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,10 @@ export abstract class Plugin<O = any> implements PluginInterface {
 | 
			
		||||
    this.afterAdd();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get name() {
 | 
			
		||||
    return this.options.name as string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get db() {
 | 
			
		||||
    return this.app.db;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -42,6 +42,154 @@ describe('acl', () => {
 | 
			
		||||
    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 () => {
 | 
			
		||||
    await db.getRepository('roles').create({
 | 
			
		||||
      values: {
 | 
			
		||||
@ -474,6 +622,7 @@ describe('acl', () => {
 | 
			
		||||
        strategy: {
 | 
			
		||||
          actions: ['*'],
 | 
			
		||||
        },
 | 
			
		||||
        snippets: ['pm.*'],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    const UserRepo = db.getCollection('users').repository;
 | 
			
		||||
@ -545,4 +694,44 @@ describe('acl', () => {
 | 
			
		||||
      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 { 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', () => {
 | 
			
		||||
  let app: MockServer;
 | 
			
		||||
  let db: Database;
 | 
			
		||||
@ -26,22 +128,24 @@ describe('association field acl', () => {
 | 
			
		||||
    await db.getRepository('roles').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'new',
 | 
			
		||||
        allowConfigure: true,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.getRepository('roles').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'testAdmin',
 | 
			
		||||
        allowConfigure: true,
 | 
			
		||||
        snippets: ['pm.*'],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const UserRepo = db.getCollection('users').repository;
 | 
			
		||||
 | 
			
		||||
    user = await UserRepo.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        roles: ['new'],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    admin = await UserRepo.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        roles: ['testAdmin'],
 | 
			
		||||
@ -55,6 +159,7 @@ describe('association field acl', () => {
 | 
			
		||||
      }),
 | 
			
		||||
      { type: 'bearer' },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    adminAgent = app.agent().auth(
 | 
			
		||||
      userPlugin.jwtService.sign({
 | 
			
		||||
        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(
 | 
			
		||||
      acl.can({
 | 
			
		||||
        role: 'new',
 | 
			
		||||
@ -173,6 +291,7 @@ describe('association field acl', () => {
 | 
			
		||||
    const actionId = viewAction.get('id') as number;
 | 
			
		||||
 | 
			
		||||
    const response = await adminAgent.resource('roles.resources', 'new').update({
 | 
			
		||||
      filterByTk: 'users',
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'users',
 | 
			
		||||
        usingActionsConfig: true,
 | 
			
		||||
@ -197,6 +316,7 @@ describe('association field acl', () => {
 | 
			
		||||
 | 
			
		||||
  it('should revoke association action on field deleted', async () => {
 | 
			
		||||
    await adminAgent.resource('roles.resources', 'new').update({
 | 
			
		||||
      filterByTk: 'users',
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'users',
 | 
			
		||||
        usingActionsConfig: true,
 | 
			
		||||
@ -208,6 +328,7 @@ describe('association field acl', () => {
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      acl.can({
 | 
			
		||||
        role: 'new',
 | 
			
		||||
@ -222,6 +343,7 @@ describe('association field acl', () => {
 | 
			
		||||
        whitelist: ['age', 'name'],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const roleResource = await db.getRepository('rolesResources').findOne({
 | 
			
		||||
      filter: {
 | 
			
		||||
        name: 'users',
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,7 @@ describe('configuration', () => {
 | 
			
		||||
    await db.getRepository('roles').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'test1',
 | 
			
		||||
        allowConfigure: true,
 | 
			
		||||
        snippets: ['pm.*'],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -36,23 +36,29 @@ describe('configuration', () => {
 | 
			
		||||
    const UserRepo = db.getCollection('users').repository;
 | 
			
		||||
    admin = await UserRepo.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        roles: ['test1']
 | 
			
		||||
      }
 | 
			
		||||
        roles: ['test1'],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    user = await UserRepo.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        roles: ['test2']
 | 
			
		||||
      }
 | 
			
		||||
        roles: ['test2'],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const userPlugin = app.getPlugin('users') as UsersPlugin;
 | 
			
		||||
    adminAgent = app.agent().auth(userPlugin.jwtService.sign({
 | 
			
		||||
    adminAgent = app.agent().auth(
 | 
			
		||||
      userPlugin.jwtService.sign({
 | 
			
		||||
        userId: admin.get('id'),
 | 
			
		||||
    }), { type: 'bearer' });
 | 
			
		||||
      }),
 | 
			
		||||
      { type: 'bearer' },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    userAgent = app.agent().auth(userPlugin.jwtService.sign({
 | 
			
		||||
    userAgent = app.agent().auth(
 | 
			
		||||
      userPlugin.jwtService.sign({
 | 
			
		||||
        userId: user.get('id'),
 | 
			
		||||
    }), { type: 'bearer' });
 | 
			
		||||
      }),
 | 
			
		||||
      { type: 'bearer' },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    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 { MockServer } from '@nocobase/test';
 | 
			
		||||
 | 
			
		||||
@ -96,4 +96,29 @@ describe('role api', () => {
 | 
			
		||||
    expect(defaultRole.length).toEqual(1);
 | 
			
		||||
    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');
 | 
			
		||||
    adminUser = await db.getRepository('users').findOne({
 | 
			
		||||
      filter: {
 | 
			
		||||
        email: process.env.INIT_ROOT_EMAIL
 | 
			
		||||
        email: process.env.INIT_ROOT_EMAIL,
 | 
			
		||||
      },
 | 
			
		||||
      appends: ['roles']
 | 
			
		||||
      appends: ['roles'],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    agent = app.agent();
 | 
			
		||||
@ -44,9 +44,98 @@ describe('actions', () => {
 | 
			
		||||
      filterByTk: adminUser.id,
 | 
			
		||||
      values: {
 | 
			
		||||
        nickname: 'a',
 | 
			
		||||
        roles: adminUser.roles
 | 
			
		||||
      }
 | 
			
		||||
        roles: adminUser.roles,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    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);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ const map2obj = (map: Map<string, string>) => {
 | 
			
		||||
    obj[key] = value;
 | 
			
		||||
  }
 | 
			
		||||
  return obj;
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export async function checkAction(ctx, next) {
 | 
			
		||||
  const currentRole = ctx.state.currentRole;
 | 
			
		||||
@ -23,9 +23,11 @@ export async function checkAction(ctx, next) {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const role = ctx.app.acl.getRole(currentRole);
 | 
			
		||||
    const availableActions = ctx.app.acl.getAvailableActions();
 | 
			
		||||
 | 
			
		||||
    ctx.body = {
 | 
			
		||||
      ...role.toJSON(),
 | 
			
		||||
      availableActions: [...availableActions.keys()],
 | 
			
		||||
      resources: [...role.resources.keys()],
 | 
			
		||||
      actionAlias: map2obj(ctx.app.acl.actionAlias),
 | 
			
		||||
      allowAll: currentRole === 'root',
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@ const roleCollectionsResource = {
 | 
			
		||||
 | 
			
		||||
      const db: Database = ctx.db;
 | 
			
		||||
      const collectionRepository = db.getRepository('collections');
 | 
			
		||||
      const fieldRepository = db.getRepository('fields');
 | 
			
		||||
 | 
			
		||||
      // all collections
 | 
			
		||||
      const [collections, count] = await collectionRepository.findAndCount({
 | 
			
		||||
@ -35,23 +36,52 @@ const roleCollectionsResource = {
 | 
			
		||||
        .filter((roleResources) => roleResources.get('usingActionsConfig'))
 | 
			
		||||
        .map((roleResources) => roleResources.get('name'));
 | 
			
		||||
 | 
			
		||||
      ctx.body = {
 | 
			
		||||
        count,
 | 
			
		||||
        rows: collections.map((collection) => {
 | 
			
		||||
      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 = {
 | 
			
		||||
        count,
 | 
			
		||||
        rows: items,
 | 
			
		||||
        page: Number(page),
 | 
			
		||||
        pageSize: Number(pageSize),
 | 
			
		||||
        totalPage: totalPage(count, pageSize),
 | 
			
		||||
 | 
			
		||||
@ -75,5 +75,10 @@ export default {
 | 
			
		||||
      sourceKey: 'name',
 | 
			
		||||
      targetKey: 'name',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'set',
 | 
			
		||||
      name: 'snippets',
 | 
			
		||||
      defaultValue: ['!ui.*', '!pm', '!pm.*'],
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
} as CollectionOptions;
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,7 @@ export default {
 | 
			
		||||
      type: 'belongsTo',
 | 
			
		||||
      name: 'scope',
 | 
			
		||||
      target: 'rolesResourcesScopes',
 | 
			
		||||
      onDelete: 'RESTRICT',
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
} as CollectionOptions;
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ export default extend({
 | 
			
		||||
      target: 'roles',
 | 
			
		||||
      foreignKey: 'userId',
 | 
			
		||||
      otherKey: 'roleName',
 | 
			
		||||
      onDelete: 'CASCADE',
 | 
			
		||||
      sourceKey: 'id',
 | 
			
		||||
      targetKey: 'name',
 | 
			
		||||
      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) || {}),
 | 
			
		||||
      allowConfigure: this.get('allowConfigure') as boolean,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    role.snippets = new Set(this.get('snippets'));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -50,7 +50,7 @@ export class RoleResourceActionModel extends Model {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const fieldType = collectionField.get('interface') as string;
 | 
			
		||||
      const fieldType = collectionField.get('type') as string;
 | 
			
		||||
 | 
			
		||||
      const fieldActions: AssociationFieldAction = associationFieldsActions?.[fieldType]?.[availableAction];
 | 
			
		||||
 | 
			
		||||
@ -59,8 +59,9 @@ export class RoleResourceActionModel extends Model {
 | 
			
		||||
      if (fieldActions) {
 | 
			
		||||
        // grant association actions to role
 | 
			
		||||
        const associationActions = fieldActions.associationActions || [];
 | 
			
		||||
 | 
			
		||||
        associationActions.forEach((associationAction) => {
 | 
			
		||||
          const actionName = `${resourceName}.${fieldTarget}:${associationAction}`;
 | 
			
		||||
          const actionName = `${resourceName}.${collectionField.get('name')}:${associationAction}`;
 | 
			
		||||
          role.grantAction(actionName);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -69,6 +70,12 @@ export class RoleResourceActionModel extends Model {
 | 
			
		||||
        targetActions.forEach((targetAction) => {
 | 
			
		||||
          const targetActionPath = `${fieldTarget}:${targetAction}`;
 | 
			
		||||
 | 
			
		||||
          const existsAction = role.getActionParams(targetActionPath);
 | 
			
		||||
 | 
			
		||||
          if (existsAction) {
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // set resource target action with current resourceName
 | 
			
		||||
          grantHelper.resourceTargetActionMap.set(`${role.name}.${resourceName}`, [
 | 
			
		||||
            ...(grantHelper.resourceTargetActionMap.get(resourceName) || []),
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,8 @@
 | 
			
		||||
import { Context } from '@nocobase/actions';
 | 
			
		||||
import { Collection } from '@nocobase/database';
 | 
			
		||||
import { NoPermissionError } from '@nocobase/acl';
 | 
			
		||||
import { Context, utils as actionUtils } from '@nocobase/actions';
 | 
			
		||||
import { Collection, RelationField } from '@nocobase/database';
 | 
			
		||||
import { Plugin } from '@nocobase/server';
 | 
			
		||||
import lodash from 'lodash';
 | 
			
		||||
import { resolve } from 'path';
 | 
			
		||||
import { availableActionResource } from './actions/available-actions';
 | 
			
		||||
import { checkAction } from './actions/role-check';
 | 
			
		||||
@ -49,41 +51,52 @@ export class PluginACL extends Plugin {
 | 
			
		||||
  registerAssociationFieldsActions() {
 | 
			
		||||
    // if grant create action to role, it should
 | 
			
		||||
    // also grant add action and association target's view action
 | 
			
		||||
    this.registerAssociationFieldAction('linkTo', {
 | 
			
		||||
 | 
			
		||||
    this.registerAssociationFieldAction('hasOne', {
 | 
			
		||||
      view: {
 | 
			
		||||
        associationActions: ['list', 'get'],
 | 
			
		||||
        associationActions: ['list', 'get', 'view'],
 | 
			
		||||
      },
 | 
			
		||||
      create: {
 | 
			
		||||
        associationActions: ['add'],
 | 
			
		||||
        targetActions: ['view'],
 | 
			
		||||
        associationActions: ['create', 'set'],
 | 
			
		||||
      },
 | 
			
		||||
      update: {
 | 
			
		||||
        associationActions: ['add', 'remove', 'toggle'],
 | 
			
		||||
        targetActions: ['view'],
 | 
			
		||||
        associationActions: ['update', 'remove', 'set'],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.registerAssociationFieldAction('attachments', {
 | 
			
		||||
    this.registerAssociationFieldAction('hasMany', {
 | 
			
		||||
      view: {
 | 
			
		||||
        associationActions: ['list', 'get'],
 | 
			
		||||
        associationActions: ['list', 'get', 'view'],
 | 
			
		||||
      },
 | 
			
		||||
      add: {
 | 
			
		||||
        associationActions: ['upload', 'add'],
 | 
			
		||||
      create: {
 | 
			
		||||
        associationActions: ['create', 'set', 'add'],
 | 
			
		||||
      },
 | 
			
		||||
      update: {
 | 
			
		||||
        associationActions: ['update', 'add', 'remove', 'toggle'],
 | 
			
		||||
        associationActions: ['update', 'remove', 'set'],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.registerAssociationFieldAction('subTable', {
 | 
			
		||||
    this.registerAssociationFieldAction('belongsTo', {
 | 
			
		||||
      view: {
 | 
			
		||||
        associationActions: ['list', 'get'],
 | 
			
		||||
        associationActions: ['list', 'get', 'view'],
 | 
			
		||||
      },
 | 
			
		||||
      create: {
 | 
			
		||||
        associationActions: ['create'],
 | 
			
		||||
        associationActions: ['create', 'set'],
 | 
			
		||||
      },
 | 
			
		||||
      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() {
 | 
			
		||||
    this.db.addMigrations({
 | 
			
		||||
      namespace: this.name,
 | 
			
		||||
      directory: resolve(__dirname, './migrations'),
 | 
			
		||||
      context: {
 | 
			
		||||
        plugin: this,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.app.db.registerModels({
 | 
			
		||||
      RoleResourceActionModel,
 | 
			
		||||
      RoleResourceModel,
 | 
			
		||||
      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.app.resourcer.define(availableActionResource);
 | 
			
		||||
@ -147,6 +210,7 @@ export class PluginACL extends Plugin {
 | 
			
		||||
        },
 | 
			
		||||
        transaction,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (defaultRole && (await model.countRoles({ transaction })) == 0) {
 | 
			
		||||
        await model.addRoles(defaultRole, { transaction });
 | 
			
		||||
      }
 | 
			
		||||
@ -269,7 +333,7 @@ export class PluginACL extends Plugin {
 | 
			
		||||
 | 
			
		||||
    // sync database role data to acl
 | 
			
		||||
    this.app.on('afterLoad', async (app, options) => {
 | 
			
		||||
      if (options?.method === 'install') {
 | 
			
		||||
      if (options?.method === 'install' || options?.method === 'upgrade') {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const exists = await this.app.db.collectionExistsInDb('roles');
 | 
			
		||||
@ -320,6 +384,7 @@ export class PluginACL extends Plugin {
 | 
			
		||||
            name: 'root',
 | 
			
		||||
            title: '{{t("Root")}}',
 | 
			
		||||
            hidden: true,
 | 
			
		||||
            snippets: ['ui.*', 'pm', 'pm.*'],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'admin',
 | 
			
		||||
@ -327,6 +392,7 @@ export class PluginACL extends Plugin {
 | 
			
		||||
            allowConfigure: true,
 | 
			
		||||
            allowNewMenu: true,
 | 
			
		||||
            strategy: { actions: ['create', 'view', 'update', 'destroy'] },
 | 
			
		||||
            snippets: ['ui.*', 'pm', 'pm.*'],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'member',
 | 
			
		||||
@ -334,6 +400,7 @@ export class PluginACL extends Plugin {
 | 
			
		||||
            allowNewMenu: true,
 | 
			
		||||
            strategy: { actions: ['view', 'update:own', 'destroy:own', 'create'] },
 | 
			
		||||
            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.acl.allow('users', 'setDefaultRole', '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) => {
 | 
			
		||||
      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) => {
 | 
			
		||||
      const { actionName, resourceName, params } = ctx.action;
 | 
			
		||||
      const { showAnonymous } = params || {};
 | 
			
		||||
@ -381,11 +476,13 @@ export class PluginACL extends Plugin {
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (actionName === 'update' && resourceName === 'roles.resources') {
 | 
			
		||||
        ctx.action.mergeParams({
 | 
			
		||||
          updateAssociationValues: ['actions'],
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await next();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -405,6 +502,7 @@ export class PluginACL extends Plugin {
 | 
			
		||||
        } else {
 | 
			
		||||
          collection = ctx.db.getCollection(resourceName);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (collection && collection.hasField('createdById')) {
 | 
			
		||||
          ctx.permission.can.params.fields.push('createdById');
 | 
			
		||||
        }
 | 
			
		||||
@ -413,8 +511,11 @@ export class PluginACL extends Plugin {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const parseJsonTemplate = this.app.acl.parseJsonTemplate;
 | 
			
		||||
    this.app.acl.use(async (ctx: Context, next) => {
 | 
			
		||||
 | 
			
		||||
    this.app.acl.use(
 | 
			
		||||
      async (ctx: Context, next) => {
 | 
			
		||||
        const { actionName, resourceName, resourceOf } = ctx.action;
 | 
			
		||||
        // is association request
 | 
			
		||||
        if (resourceName.includes('.') && resourceOf) {
 | 
			
		||||
          if (!ctx?.permission?.can?.params) {
 | 
			
		||||
            return next();
 | 
			
		||||
@ -424,7 +525,9 @@ export class PluginACL extends Plugin {
 | 
			
		||||
          // 关联数据能不能处理取决于 source 是否有权限
 | 
			
		||||
          const [collectionName] = resourceName.split('.');
 | 
			
		||||
          const action = ctx.can({ resource: collectionName, action: actionName });
 | 
			
		||||
 | 
			
		||||
          const availableAction = this.app.acl.getAvailableAction(actionName);
 | 
			
		||||
 | 
			
		||||
          if (availableAction?.options?.onNewRecord) {
 | 
			
		||||
            if (action) {
 | 
			
		||||
              ctx.permission.skip = true;
 | 
			
		||||
@ -443,9 +546,219 @@ export class PluginACL extends Plugin {
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        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();
 | 
			
		||||
 | 
			
		||||
      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() {
 | 
			
		||||
    const repo = this.db.getRepository<any>('collections');
 | 
			
		||||
    if (repo) {
 | 
			
		||||
 | 
			
		||||
@ -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({
 | 
			
		||||
      directory: resolve(__dirname, 'collections'),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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() {
 | 
			
		||||
@ -63,7 +74,6 @@ export class PluginChinaRegion extends Plugin {
 | 
			
		||||
    const count = await ChinaRegion.count();
 | 
			
		||||
    // console.log(`${count} rows of region data imported in ${(Date.now() - timer) / 1000}s`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default PluginChinaRegion;
 | 
			
		||||
 | 
			
		||||
@ -38,6 +38,15 @@ export class CollectionManagerPlugin extends Plugin {
 | 
			
		||||
      CollectionRepository,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.app.acl.registerSnippet({
 | 
			
		||||
      name: `pm.${this.name}.collections`,
 | 
			
		||||
      actions: [
 | 
			
		||||
        'collections:*',
 | 
			
		||||
        // 'fields:*',
 | 
			
		||||
        'collections.fields:*',
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.app.db.on('fields.beforeUpdate', async (model, options) => {
 | 
			
		||||
      const newValue = options.values;
 | 
			
		||||
      if (
 | 
			
		||||
@ -213,7 +222,6 @@ export class CollectionManagerPlugin extends Plugin {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.app.acl.allow('collections', 'list', 'loggedIn');
 | 
			
		||||
    this.app.acl.allow('collections', ['create', 'update', 'destroy'], 'allowConfigure');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async load() {
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,13 @@ export default class PluginFileManager extends Plugin {
 | 
			
		||||
  async load() {
 | 
			
		||||
    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 加进来
 | 
			
		||||
    this.app.resourcer.use(uploadMiddleware);
 | 
			
		||||
    this.app.resourcer.registerActionHandler('upload', uploadAction);
 | 
			
		||||
@ -34,6 +41,24 @@ export default class PluginFileManager extends Plugin {
 | 
			
		||||
      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({
 | 
			
		||||
      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({
 | 
			
		||||
      'iframeHtml:getHtml': getHtml,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.app.acl.allow('iframeHtml', 'getHtml', 'loggedIn');
 | 
			
		||||
    this.app.acl.registerSnippet({
 | 
			
		||||
      name: 'ui.iframeHtml',
 | 
			
		||||
      actions: [
 | 
			
		||||
        'iframeHtml:*',
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async install(options?: InstallOptions) {}
 | 
			
		||||
 | 
			
		||||
@ -42,14 +42,14 @@ const initImportSettings = (fields) => {
 | 
			
		||||
 | 
			
		||||
export const ImportActionInitializer = (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 fields = useFields(name);
 | 
			
		||||
 | 
			
		||||
  const schema: ISchema = {
 | 
			
		||||
    type: 'void',
 | 
			
		||||
    title: '{{ t("Import") }}',
 | 
			
		||||
    'x-action': 'import',
 | 
			
		||||
    'x-action': 'importXlsx',
 | 
			
		||||
    'x-action-settings': {
 | 
			
		||||
      importSettings: { importColumns: [], explain: '' },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ import { ImportStatus } from './ImportModal';
 | 
			
		||||
 | 
			
		||||
const useImportSchema = (s: Schema) => {
 | 
			
		||||
  let schema = s;
 | 
			
		||||
  while (schema && schema['x-action'] !== 'import') {
 | 
			
		||||
  while (schema && schema['x-action'] !== 'importXlsx') {
 | 
			
		||||
    schema = schema.parent;
 | 
			
		||||
  }
 | 
			
		||||
  return { schema };
 | 
			
		||||
 | 
			
		||||
@ -11,31 +11,18 @@ export class ImportPlugin extends Plugin {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async load() {
 | 
			
		||||
    // Visit: http://localhost:13000/api/import:importXlsx
 | 
			
		||||
    this.app.resourcer.use(importMiddleware);
 | 
			
		||||
    this.app.resourcer.registerActionHandler('downloadXlsxTemplate', downloadXlsxTemplate);
 | 
			
		||||
    this.app.resourcer.registerActionHandler('importXlsx', importXlsx);
 | 
			
		||||
    // this.app.resource({
 | 
			
		||||
    //   name: 'import',
 | 
			
		||||
    //   actions: {
 | 
			
		||||
    //     importXlsx,
 | 
			
		||||
    //   },
 | 
			
		||||
    // });
 | 
			
		||||
 | 
			
		||||
    this.app.acl.setAvailableAction('importXlsx', {
 | 
			
		||||
      displayName: '{{t("Import")}}',
 | 
			
		||||
      allowConfigureFields: true,
 | 
			
		||||
      type: 'new-data',
 | 
			
		||||
      onNewRecord: true,
 | 
			
		||||
    });
 | 
			
		||||
    this.app.acl.use(async (ctx, next) => {
 | 
			
		||||
      const { actionName } = ctx.action;
 | 
			
		||||
      if (['downloadXlsxTemplate', 'importXlsx'].includes(actionName)) {
 | 
			
		||||
        ctx.permission = {
 | 
			
		||||
          skip: true,
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
      await next();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.app.acl.allow('*', 'downloadXlsxTemplate', 'loggedIn');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async install(options: InstallOptions) {
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { InstallOptions, Plugin } from '@nocobase/server';
 | 
			
		||||
import { CircleField, LineStringField, PointField, PolygonField } from './fields';
 | 
			
		||||
import { resolve } from 'path';
 | 
			
		||||
import { getConfiguration, setConfiguration } from './actions';
 | 
			
		||||
import { CircleField, LineStringField, PointField, PolygonField } from './fields';
 | 
			
		||||
 | 
			
		||||
export class MapPlugin extends Plugin {
 | 
			
		||||
  afterAdd() {}
 | 
			
		||||
@ -11,27 +11,30 @@ export class MapPlugin extends Plugin {
 | 
			
		||||
      point: PointField,
 | 
			
		||||
      polygon: PolygonField,
 | 
			
		||||
      lineString: LineStringField,
 | 
			
		||||
      circle: CircleField
 | 
			
		||||
      circle: CircleField,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.db.registerFieldTypes(fields);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  async load() {
 | 
			
		||||
    await this.db.import({
 | 
			
		||||
      directory: resolve(__dirname, 'collections'),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.app.resource(({
 | 
			
		||||
    this.app.resource({
 | 
			
		||||
      name: 'map-configuration',
 | 
			
		||||
      actions: {
 | 
			
		||||
        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) {}
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@ import { resolve } from 'path';
 | 
			
		||||
import { ApplicationModel } from './models/application';
 | 
			
		||||
 | 
			
		||||
export class PluginMultiAppManager extends Plugin {
 | 
			
		||||
 | 
			
		||||
  async install(options?: InstallOptions) {
 | 
			
		||||
    const repo = this.db.getRepository<any>('collections');
 | 
			
		||||
    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'),
 | 
			
		||||
            icon: 'FileOutlined',
 | 
			
		||||
            tabs: {
 | 
			
		||||
              storages: {
 | 
			
		||||
              providers: {
 | 
			
		||||
                title: t('OIDC Providers'),
 | 
			
		||||
                component: OIDCPanel,
 | 
			
		||||
              },
 | 
			
		||||
 | 
			
		||||
@ -44,9 +44,12 @@ export class OidcPlugin extends Plugin {
 | 
			
		||||
      await next();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 开放访问权限
 | 
			
		||||
    this.app.acl.allow('oidcProviders', '*', 'allowConfigure');
 | 
			
		||||
    this.app.acl.allow('oidc', '*');
 | 
			
		||||
    this.app.acl.allow('oidc', '*', 'public');
 | 
			
		||||
 | 
			
		||||
    this.app.acl.registerSnippet({
 | 
			
		||||
      name: `pm.${this.name}.providers`,
 | 
			
		||||
      actions: ['oidcProviders:*'],
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async install(options?: InstallOptions) {}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,5 @@
 | 
			
		||||
import { PluginManagerContext, SettingsCenterProvider, SigninPageExtensionProvider } from '@nocobase/client';
 | 
			
		||||
import React, { useContext } from 'react';
 | 
			
		||||
import { PluginManagerContext, SettingsCenterProvider } from '@nocobase/client';
 | 
			
		||||
import { SigninPageExtensionProvider } from '@nocobase/client';
 | 
			
		||||
import { useSamlTranslation } from './locale';
 | 
			
		||||
import { SAMLList } from './SAMLList';
 | 
			
		||||
import { SAMLPanel } from './SAMLPanel';
 | 
			
		||||
@ -17,7 +16,7 @@ export default function (props) {
 | 
			
		||||
            title: t('SAML manager'),
 | 
			
		||||
            icon: 'FileOutlined',
 | 
			
		||||
            tabs: {
 | 
			
		||||
              storages: {
 | 
			
		||||
              providers: {
 | 
			
		||||
                title: t('SAML Providers'),
 | 
			
		||||
                component: SAMLPanel,
 | 
			
		||||
              },
 | 
			
		||||
 | 
			
		||||
@ -33,8 +33,12 @@ export class SAMLPlugin extends Plugin {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 开放访问权限
 | 
			
		||||
    this.app.acl.allow('samlProviders', '*', 'allowConfigure');
 | 
			
		||||
    this.app.acl.allow('saml', '*');
 | 
			
		||||
    this.app.acl.allow('saml', '*', 'public');
 | 
			
		||||
 | 
			
		||||
    this.app.acl.registerSnippet({
 | 
			
		||||
      name: `pm.${this.name}.providers`,
 | 
			
		||||
      actions: ['samlProviders:*'],
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async install(options?: InstallOptions) {}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,3 @@
 | 
			
		||||
import { skip } from '@nocobase/acl';
 | 
			
		||||
import { InstallOptions, Plugin } from '@nocobase/server';
 | 
			
		||||
import { resolve } from 'path';
 | 
			
		||||
 | 
			
		||||
@ -29,17 +28,25 @@ export class SystemSettingsPlugin extends Plugin {
 | 
			
		||||
    if (cmd) {
 | 
			
		||||
      cmd.option('-l, --lang [lang]');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.app.acl.registerSnippet({
 | 
			
		||||
      name: `pm.${this.name}.system-settings`,
 | 
			
		||||
      actions: ['systemSettings:update'],
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async load() {
 | 
			
		||||
    await this.importCollections(resolve(__dirname, './collections'));
 | 
			
		||||
    await this.app.db.import({
 | 
			
		||||
      directory: resolve(__dirname, 'collections'),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.app.acl.use(
 | 
			
		||||
      skip({
 | 
			
		||||
        resourceName: 'systemSettings',
 | 
			
		||||
        actionName: 'get',
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
    this.app.acl.addFixedParams('systemSettings', 'destroy', () => {
 | 
			
		||||
      return {
 | 
			
		||||
        'id.$ne': 1,
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.app.acl.allow('systemSettings', 'get', 'public');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,3 @@
 | 
			
		||||
import { skip } from '@nocobase/acl';
 | 
			
		||||
import { MagicAttributeModel } from '@nocobase/database';
 | 
			
		||||
import { Plugin } from '@nocobase/server';
 | 
			
		||||
import { resolve } from 'path';
 | 
			
		||||
@ -87,12 +86,7 @@ export class UiRoutesStoragePlugin extends Plugin {
 | 
			
		||||
 | 
			
		||||
    await this.importCollections(resolve(__dirname, './collections'));
 | 
			
		||||
 | 
			
		||||
    this.app.acl.use(
 | 
			
		||||
      skip({
 | 
			
		||||
        resourceName: 'uiRoutes',
 | 
			
		||||
        actionName: 'getAccessible',
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
    this.app.acl.allow('uiRoutes', 'getAccessible');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,29 @@ export class UiSchemaStoragePlugin extends Plugin {
 | 
			
		||||
 | 
			
		||||
    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) {
 | 
			
		||||
      if (!model.get('name')) {
 | 
			
		||||
        model.set('name', uid());
 | 
			
		||||
@ -61,8 +84,8 @@ export class UiSchemaStoragePlugin extends Plugin {
 | 
			
		||||
      actions: uiSchemaActions,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.app.acl.allow('uiSchemas', '*', 'loggedIn');
 | 
			
		||||
    this.app.acl.allow('uiSchemaTemplates', '*', 'loggedIn');
 | 
			
		||||
    this.app.acl.allow('uiSchemas', ['getProperties', 'getJsonSchema'], 'loggedIn');
 | 
			
		||||
    this.app.acl.allow('uiSchemaTemplates', ['get', 'list'], 'loggedIn');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async load() {
 | 
			
		||||
 | 
			
		||||
@ -25,8 +25,8 @@ describe('actions', () => {
 | 
			
		||||
    pluginUser = app.getPlugin('users');
 | 
			
		||||
    adminUser = await db.getRepository('users').findOne({
 | 
			
		||||
      filter: {
 | 
			
		||||
        email: process.env.INIT_ROOT_EMAIL
 | 
			
		||||
      }
 | 
			
		||||
        email: process.env.INIT_ROOT_EMAIL,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    agent = app.agent();
 | 
			
		||||
@ -67,16 +67,16 @@ describe('actions', () => {
 | 
			
		||||
    const res1 = await agent.resource('users').updateProfile({
 | 
			
		||||
      filterByTk: adminUser.id,
 | 
			
		||||
      values: {
 | 
			
		||||
        nickname: 'a'
 | 
			
		||||
      }
 | 
			
		||||
        nickname: 'a',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    expect(res1.status).toBe(401);
 | 
			
		||||
 | 
			
		||||
    const res2 = await adminAgent.resource('users').updateProfile({
 | 
			
		||||
      filterByTk: adminUser.id,
 | 
			
		||||
      values: {
 | 
			
		||||
        nickname: 'a'
 | 
			
		||||
      }
 | 
			
		||||
        nickname: 'a',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    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.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 loggedInActions = ['signout', 'updateProfile', 'changePassword'];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -137,8 +137,11 @@ export default class VerificationPlugin extends Plugin {
 | 
			
		||||
      return this.intercept(context, next);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    app.acl.allow('verifications', 'create');
 | 
			
		||||
    app.acl.allow('verifications_providers', '*', 'allowConfigure');
 | 
			
		||||
    app.acl.allow('verifications', 'create', 'public');
 | 
			
		||||
    this.app.acl.registerSnippet({
 | 
			
		||||
      name: `pm.${this.name}.providers`,
 | 
			
		||||
      actions: ['verifications_providers:*'],
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getDefault() {
 | 
			
		||||
 | 
			
		||||
@ -37,15 +37,18 @@ function WorkflowPane() {
 | 
			
		||||
export const WorkflowProvider = (props) => {
 | 
			
		||||
  const ctx = useContext(PluginManagerContext);
 | 
			
		||||
  const { routes, components, ...others } = useContext(RouteSwitchContext);
 | 
			
		||||
  routes[1].routes.unshift({
 | 
			
		||||
  routes[1].routes.unshift(
 | 
			
		||||
    {
 | 
			
		||||
      type: 'route',
 | 
			
		||||
      path: '/admin/settings/workflow/workflows/:id',
 | 
			
		||||
      component: 'WorkflowPage',
 | 
			
		||||
  }, {
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'route',
 | 
			
		||||
      path: '/admin/settings/workflow/executions/:id',
 | 
			
		||||
      component: 'ExecutionPage',
 | 
			
		||||
  });
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
  return (
 | 
			
		||||
    <SettingsCenterProvider
 | 
			
		||||
      settings={{
 | 
			
		||||
@ -55,6 +58,7 @@ export const WorkflowProvider = (props) => {
 | 
			
		||||
          title: lang('Workflow'),
 | 
			
		||||
          tabs: {
 | 
			
		||||
            workflows: {
 | 
			
		||||
              isBookmark: true,
 | 
			
		||||
              title: lang('Workflow'),
 | 
			
		||||
              component: WorkflowPane,
 | 
			
		||||
            },
 | 
			
		||||
@ -70,10 +74,10 @@ export const WorkflowProvider = (props) => {
 | 
			
		||||
          },
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <RouteSwitchContext.Provider value={{ components: { ...components, WorkflowPage, ExecutionPage }, ...others, routes }}>
 | 
			
		||||
          <WorkflowContext.Provider value={{ triggers, instructions }}>
 | 
			
		||||
            {props.children}
 | 
			
		||||
          </WorkflowContext.Provider>
 | 
			
		||||
        <RouteSwitchContext.Provider
 | 
			
		||||
          value={{ components: { ...components, WorkflowPage, ExecutionPage }, ...others, routes }}
 | 
			
		||||
        >
 | 
			
		||||
          <WorkflowContext.Provider value={{ triggers, instructions }}>{props.children}</WorkflowContext.Provider>
 | 
			
		||||
        </RouteSwitchContext.Provider>
 | 
			
		||||
      </PluginManagerContext.Provider>
 | 
			
		||||
    </SettingsCenterProvider>
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import path from 'path';
 | 
			
		||||
 | 
			
		||||
import { Op, Transactionable } from '@nocobase/database';
 | 
			
		||||
import { Op } from '@nocobase/database';
 | 
			
		||||
import { Plugin } from '@nocobase/server';
 | 
			
		||||
import { Registry } from '@nocobase/utils';
 | 
			
		||||
 | 
			
		||||
@ -10,10 +10,10 @@ import { EXECUTION_STATUS } from './constants';
 | 
			
		||||
import extensions from './extensions';
 | 
			
		||||
import initInstructions, { Instruction } from './instructions';
 | 
			
		||||
import ExecutionModel from './models/Execution';
 | 
			
		||||
import JobModel from './models/Job';
 | 
			
		||||
import WorkflowModel from './models/Workflow';
 | 
			
		||||
import Processor from './Processor';
 | 
			
		||||
import initTriggers, { Trigger } from './triggers';
 | 
			
		||||
import JobModel from './models/Job';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type Pending = [ExecutionModel, JobModel?];
 | 
			
		||||
@ -72,6 +72,20 @@ export default class WorkflowPlugin extends Plugin {
 | 
			
		||||
  async load() {
 | 
			
		||||
    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({
 | 
			
		||||
      directory: path.resolve(__dirname, 'collections'),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,7 @@ export class PresetNocoBase extends Plugin {
 | 
			
		||||
    return localPlugins;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async addBuiltInPlugins() {
 | 
			
		||||
  async addBuiltInPlugins(options?: any) {
 | 
			
		||||
    const builtInPlugins = this.getBuiltInPlugins();
 | 
			
		||||
    await this.app.pm.add(builtInPlugins, {
 | 
			
		||||
      enabled: true,
 | 
			
		||||
@ -43,7 +43,7 @@ export class PresetNocoBase extends Plugin {
 | 
			
		||||
    });
 | 
			
		||||
    const localPlugins = this.getLocalPlugins();
 | 
			
		||||
    await this.app.pm.add(localPlugins, {});
 | 
			
		||||
    await this.app.reload();
 | 
			
		||||
    await this.app.reload({ method: options.method });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  afterAdd() {
 | 
			
		||||
@ -59,15 +59,15 @@ export class PresetNocoBase extends Plugin {
 | 
			
		||||
        if (r) {
 | 
			
		||||
          console.log(`Clear the installed application plugins`);
 | 
			
		||||
          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');
 | 
			
		||||
      if (result) {
 | 
			
		||||
        console.log(`Initialize all built-in plugins`);
 | 
			
		||||
        await this.addBuiltInPlugins();
 | 
			
		||||
        await this.addBuiltInPlugins({ method: 'upgrade' });
 | 
			
		||||
      }
 | 
			
		||||
      const builtInPlugins = this.getBuiltInPlugins();
 | 
			
		||||
      const plugins = await this.app.db.getRepository('applicationPlugins').find();
 | 
			
		||||
@ -85,14 +85,15 @@ export class PresetNocoBase extends Plugin {
 | 
			
		||||
        localPlugins.filter((plugin) => !pluginNames.includes(plugin)),
 | 
			
		||||
        {},
 | 
			
		||||
      );
 | 
			
		||||
      await this.app.reload();
 | 
			
		||||
      await this.app.reload({ method: 'upgrade' });
 | 
			
		||||
      await this.app.db.sync();
 | 
			
		||||
    });
 | 
			
		||||
    this.app.on('beforeInstall', async () => {
 | 
			
		||||
    this.app.on('beforeInstall', async (options) => {
 | 
			
		||||
      console.log(`Initialize all built-in plugins`);
 | 
			
		||||
      await this.addBuiltInPlugins();
 | 
			
		||||
      await this.addBuiltInPlugins({ method: 'install' });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  beforeLoad() {
 | 
			
		||||
    this.db.addMigrations({
 | 
			
		||||
      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