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