Fix acl target action error (#311)
* fix: field association resource name * chore: resourceCollection fields unique index * fix: test * feat: allowConfigure permission skip * feat: skip with array type actionNames * chore: rename acl skip to allow * fix: type * chore: rename SkipManager to AllowManager
This commit is contained in:
parent
2fd27ea9f3
commit
b511ef3d8f
@ -26,7 +26,7 @@ describe('skip', () => {
|
|||||||
await middlewareFunc(ctx, nextFunc);
|
await middlewareFunc(ctx, nextFunc);
|
||||||
expect(nextFunc).toHaveBeenCalledTimes(0);
|
expect(nextFunc).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
acl.skip('users', 'login');
|
acl.allow('users', 'login');
|
||||||
|
|
||||||
await middlewareFunc(ctx, nextFunc);
|
await middlewareFunc(ctx, nextFunc);
|
||||||
expect(nextFunc).toHaveBeenCalledTimes(1);
|
expect(nextFunc).toHaveBeenCalledTimes(1);
|
||||||
@ -49,7 +49,7 @@ describe('skip', () => {
|
|||||||
const nextFunc = jest.fn();
|
const nextFunc = jest.fn();
|
||||||
|
|
||||||
let skip = false;
|
let skip = false;
|
||||||
acl.skip('users', 'login', (ctx) => {
|
acl.allow('users', 'login', (ctx) => {
|
||||||
return skip;
|
return skip;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ describe('skip', () => {
|
|||||||
const middlewareFunc = acl.middleware();
|
const middlewareFunc = acl.middleware();
|
||||||
|
|
||||||
const conditionFn = jest.fn();
|
const conditionFn = jest.fn();
|
||||||
acl.skipManager.registerSkipCondition('superUser', () => {
|
acl.allowManager.registerAllowCondition('superUser', async () => {
|
||||||
conditionFn();
|
conditionFn();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@ -84,7 +84,7 @@ describe('skip', () => {
|
|||||||
|
|
||||||
const nextFunc = jest.fn();
|
const nextFunc = jest.fn();
|
||||||
|
|
||||||
acl.skip('users', 'login', 'superUser');
|
acl.allow('users', 'login', 'superUser');
|
||||||
|
|
||||||
await middlewareFunc(ctx, nextFunc);
|
await middlewareFunc(ctx, nextFunc);
|
||||||
expect(nextFunc).toHaveBeenCalledTimes(1);
|
expect(nextFunc).toHaveBeenCalledTimes(1);
|
@ -5,7 +5,7 @@ import lodash from 'lodash';
|
|||||||
import { AclAvailableAction, AvailableActionOptions } from './acl-available-action';
|
import { AclAvailableAction, AvailableActionOptions } from './acl-available-action';
|
||||||
import { ACLAvailableStrategy, AvailableStrategyOptions, predicate } from './acl-available-strategy';
|
import { ACLAvailableStrategy, AvailableStrategyOptions, predicate } from './acl-available-strategy';
|
||||||
import { ACLRole, RoleActionParams } from './acl-role';
|
import { ACLRole, RoleActionParams } from './acl-role';
|
||||||
import { SkipManager } from './skip-manager';
|
import { AllowManager } from './allow-manager';
|
||||||
const parse = require('json-templates');
|
const parse = require('json-templates');
|
||||||
|
|
||||||
interface CanResult {
|
interface CanResult {
|
||||||
@ -47,7 +47,7 @@ export class ACL extends EventEmitter {
|
|||||||
protected availableStrategy = new Map<string, ACLAvailableStrategy>();
|
protected availableStrategy = new Map<string, ACLAvailableStrategy>();
|
||||||
protected middlewares = [];
|
protected middlewares = [];
|
||||||
|
|
||||||
public skipManager = new SkipManager(this);
|
public allowManager = new AllowManager(this);
|
||||||
|
|
||||||
roles = new Map<string, ACLRole>();
|
roles = new Map<string, ACLRole>();
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ export class ACL extends EventEmitter {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.middlewares.push(this.skipManager.aclMiddleware());
|
this.middlewares.push(this.allowManager.aclMiddleware());
|
||||||
}
|
}
|
||||||
|
|
||||||
define(options: DefineOptions): ACLRole {
|
define(options: DefineOptions): ACLRole {
|
||||||
@ -217,8 +217,14 @@ export class ACL extends EventEmitter {
|
|||||||
this.middlewares.push(fn);
|
this.middlewares.push(fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
skip(resourceName: string, actionName: string, condition?: any) {
|
allow(resourceName: string, actionNames: string[] | string, condition?: any) {
|
||||||
this.skipManager.skip(resourceName, actionName, condition);
|
if (!Array.isArray(actionNames)) {
|
||||||
|
actionNames = [actionNames];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const actionName of actionNames) {
|
||||||
|
this.allowManager.allow(resourceName, actionName, condition);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parseJsonTemplate(json: any, ctx: any) {
|
parseJsonTemplate(json: any, ctx: any) {
|
||||||
@ -243,12 +249,6 @@ export class ACL extends EventEmitter {
|
|||||||
return params;
|
return params;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ctxToObject = (ctx) => {
|
|
||||||
return {
|
|
||||||
state: JSON.parse(JSON.stringify(ctx.state)),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
||||||
|
@ -1,26 +1,39 @@
|
|||||||
import { ACL } from './acl';
|
import { ACL } from './acl';
|
||||||
|
|
||||||
type ConditionFunc = (ctx: any) => boolean;
|
type ConditionFunc = (ctx: any) => Promise<boolean>;
|
||||||
|
|
||||||
export class SkipManager {
|
export class AllowManager {
|
||||||
protected skipActions = new Map<string, Map<string, string | ConditionFunc | true>>();
|
protected skipActions = new Map<string, Map<string, string | ConditionFunc | true>>();
|
||||||
|
|
||||||
protected registeredCondition = new Map<string, ConditionFunc>();
|
protected registeredCondition = new Map<string, ConditionFunc>();
|
||||||
|
|
||||||
constructor(public acl: ACL) {
|
constructor(public acl: ACL) {
|
||||||
this.registerSkipCondition('logged-in', (ctx) => {
|
this.registerAllowCondition('loggedIn', (ctx) => {
|
||||||
return ctx.state.currentUser;
|
return ctx.state.currentUser;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.registerAllowCondition('allowConfigure', async (ctx) => {
|
||||||
|
const roleName = ctx.state.currentRole;
|
||||||
|
if (!roleName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleInstance = await ctx.db.getRepository('roles').findOne({
|
||||||
|
name: roleName,
|
||||||
|
});
|
||||||
|
|
||||||
|
return roleInstance?.get('allowConfigure');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
skip(resourceName: string, actionName: string, condition?: string | ConditionFunc) {
|
allow(resourceName: string, actionName: string, condition?: string | ConditionFunc) {
|
||||||
const actionMap = this.skipActions.get(resourceName) || new Map<string, string | ConditionFunc>();
|
const actionMap = this.skipActions.get(resourceName) || new Map<string, string | ConditionFunc>();
|
||||||
actionMap.set(actionName, condition || true);
|
actionMap.set(actionName, condition || true);
|
||||||
|
|
||||||
this.skipActions.set(resourceName, actionMap);
|
this.skipActions.set(resourceName, actionMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSkippedConditions(resourceName: string, actionName: string): Array<ConditionFunc | true> {
|
getAllowedConditions(resourceName: string, actionName: string): Array<ConditionFunc | true> {
|
||||||
const fetchActionSteps: string[] = ['*', resourceName];
|
const fetchActionSteps: string[] = ['*', resourceName];
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
@ -38,14 +51,14 @@ export class SkipManager {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerSkipCondition(name: string, condition: ConditionFunc) {
|
registerAllowCondition(name: string, condition: ConditionFunc) {
|
||||||
this.registeredCondition.set(name, condition);
|
this.registeredCondition.set(name, condition);
|
||||||
}
|
}
|
||||||
|
|
||||||
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.skipManager.getSkippedConditions(resourceName, actionName);
|
const skippedConditions = ctx.app.acl.allowManager.getAllowedConditions(resourceName, actionName);
|
||||||
let skip = false;
|
let skip = false;
|
||||||
|
|
||||||
for (const skippedCondition of skippedConditions) {
|
for (const skippedCondition of skippedConditions) {
|
||||||
@ -53,7 +66,7 @@ export class SkipManager {
|
|||||||
let skipResult = false;
|
let skipResult = false;
|
||||||
|
|
||||||
if (typeof skippedCondition === 'function') {
|
if (typeof skippedCondition === 'function') {
|
||||||
skipResult = skippedCondition(ctx);
|
skipResult = await skippedCondition(ctx);
|
||||||
} else if (skippedCondition) {
|
} else if (skippedCondition) {
|
||||||
skipResult = true;
|
skipResult = true;
|
||||||
}
|
}
|
45
packages/plugins/acl/src/__tests__/configuration.test.ts
Normal file
45
packages/plugins/acl/src/__tests__/configuration.test.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { MockServer } from '@nocobase/test';
|
||||||
|
import { Database } from '@nocobase/database';
|
||||||
|
import { ACL } from '@nocobase/acl';
|
||||||
|
import { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage';
|
||||||
|
import { changeMockRole, prepareApp } from './prepare';
|
||||||
|
|
||||||
|
describe('configuration', () => {
|
||||||
|
let app: MockServer;
|
||||||
|
let db: Database;
|
||||||
|
let acl: ACL;
|
||||||
|
|
||||||
|
let uiSchemaRepository: UiSchemaRepository;
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await prepareApp();
|
||||||
|
db = app.db;
|
||||||
|
acl = app.acl;
|
||||||
|
|
||||||
|
uiSchemaRepository = db.getRepository('uiSchemas');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should list collections', async () => {
|
||||||
|
expect((await app.agent().resource('collections').create()).statusCode).toEqual(403);
|
||||||
|
expect((await app.agent().resource('collections').list()).statusCode).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow when role has allowConfigure with true value', async () => {
|
||||||
|
await db.getRepository('roles').create({
|
||||||
|
values: {
|
||||||
|
name: 'admin1',
|
||||||
|
title: 'admin allowConfigure',
|
||||||
|
allowConfigure: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
changeMockRole('admin1');
|
||||||
|
|
||||||
|
expect((await app.agent().resource('collections').create()).statusCode).toEqual(200);
|
||||||
|
expect((await app.agent().resource('collections').list()).statusCode).toEqual(200);
|
||||||
|
});
|
||||||
|
});
|
@ -3,6 +3,12 @@ import { CollectionOptions } from '@nocobase/database';
|
|||||||
export default {
|
export default {
|
||||||
name: 'rolesResources',
|
name: 'rolesResources',
|
||||||
model: 'RoleResourceModel',
|
model: 'RoleResourceModel',
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
unique: true,
|
||||||
|
fields: ['roleName', 'name'],
|
||||||
|
},
|
||||||
|
],
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
|
@ -14,6 +14,7 @@ export class RoleResourceActionModel extends Model {
|
|||||||
const db: Database = this.constructor.database;
|
const db: Database = this.constructor.database;
|
||||||
|
|
||||||
const { resourceName, role, acl, associationFieldsActions, grantHelper } = options;
|
const { resourceName, role, acl, associationFieldsActions, grantHelper } = options;
|
||||||
|
|
||||||
const actionName = this.get('name') as string;
|
const actionName = this.get('name') as string;
|
||||||
|
|
||||||
const fields = this.get('fields') as any;
|
const fields = this.get('fields') as any;
|
||||||
@ -47,17 +48,19 @@ export class RoleResourceActionModel extends Model {
|
|||||||
|
|
||||||
const fieldActions: AssociationFieldAction = associationFieldsActions?.[fieldType]?.[availableAction];
|
const fieldActions: AssociationFieldAction = associationFieldsActions?.[fieldType]?.[availableAction];
|
||||||
|
|
||||||
|
const fieldTarget = collectionField.get('target');
|
||||||
|
|
||||||
if (fieldActions) {
|
if (fieldActions) {
|
||||||
const associationActions = fieldActions.associationActions || [];
|
const associationActions = fieldActions.associationActions || [];
|
||||||
associationActions.forEach((associationAction) => {
|
associationActions.forEach((associationAction) => {
|
||||||
const actionName = `${resourceName}.${field}:${associationAction}`;
|
const actionName = `${resourceName}.${fieldTarget}:${associationAction}`;
|
||||||
role.grantAction(actionName);
|
role.grantAction(actionName);
|
||||||
});
|
});
|
||||||
|
|
||||||
const targetActions = fieldActions.targetActions || [];
|
const targetActions = fieldActions.targetActions || [];
|
||||||
|
|
||||||
targetActions.forEach((targetAction) => {
|
targetActions.forEach((targetAction) => {
|
||||||
const targetActionPath = `${field}:${targetAction}`;
|
const targetActionPath = `${fieldTarget}:${targetAction}`;
|
||||||
|
|
||||||
grantHelper.resourceTargetActionMap.set(resourceName, [
|
grantHelper.resourceTargetActionMap.set(resourceName, [
|
||||||
...(grantHelper.resourceTargetActionMap.get(resourceName) || []),
|
...(grantHelper.resourceTargetActionMap.get(resourceName) || []),
|
||||||
|
@ -35,6 +35,7 @@ export class RoleResourceModel extends Model {
|
|||||||
const roleName = this.get('roleName') as string;
|
const roleName = this.get('roleName') as string;
|
||||||
const role = acl.getRole(roleName);
|
const role = acl.getRole(roleName);
|
||||||
|
|
||||||
|
// revoke resource of role
|
||||||
await this.revoke({ role, resourceName, grantHelper });
|
await this.revoke({ role, resourceName, grantHelper });
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -293,11 +293,13 @@ export class PluginACL extends Plugin {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.app.acl.skip('roles.menuUiSchemas', 'set', 'logged-in');
|
|
||||||
this.app.acl.skip('roles.menuUiSchemas', 'toggle', 'logged-in');
|
this.app.acl.allow('roles', 'check', 'loggedIn');
|
||||||
this.app.acl.skip('roles.menuUiSchemas', 'list', 'logged-in');
|
this.app.acl.allow('roles', ['create', 'update', 'destroy'], 'allowConfigure');
|
||||||
this.app.acl.skip('roles', 'check', 'logged-in');
|
|
||||||
this.app.acl.skip('*', '*', (ctx) => {
|
this.app.acl.allow('roles.menuUiSchemas', ['set', 'toggle', 'list'], 'allowConfigure');
|
||||||
|
|
||||||
|
this.app.acl.allow('*', '*', (ctx) => {
|
||||||
return ctx.state.currentRole === 'root';
|
return ctx.state.currentRole === 'root';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ export class ChinaRegionPlugin extends Plugin {
|
|||||||
await this.db.import({
|
await this.db.import({
|
||||||
directory: resolve(__dirname, 'collections'),
|
directory: resolve(__dirname, 'collections'),
|
||||||
});
|
});
|
||||||
this.app.acl.skip('chinaRegions', 'list', 'logged-in');
|
this.app.acl.allow('chinaRegions', 'list', 'loggedIn');
|
||||||
}
|
}
|
||||||
|
|
||||||
async importData() {
|
async importData() {
|
||||||
|
@ -235,7 +235,8 @@ export class CollectionManagerPlugin extends Plugin {
|
|||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.app.acl.skip('collections', 'list', 'logged-in');
|
this.app.acl.allow('collections', 'list', 'loggedIn');
|
||||||
|
this.app.acl.allow('collections', ['create', 'update', 'destroy'], 'allowConfigure');
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
|
@ -36,7 +36,7 @@ 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.skip('attachments', 'upload', 'logged-in');
|
this.app.acl.allow('attachments', 'upload', 'loggedIn');
|
||||||
}
|
}
|
||||||
|
|
||||||
getName(): string {
|
getName(): string {
|
||||||
|
@ -61,8 +61,8 @@ export class UiSchemaStoragePlugin extends Plugin {
|
|||||||
actions: uiSchemaActions,
|
actions: uiSchemaActions,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.app.acl.skip('uiSchemas', '*', 'logged-in');
|
this.app.acl.allow('uiSchemas', '*', 'loggedIn');
|
||||||
this.app.acl.skip('uiSchemaTemplates', '*', 'logged-in');
|
this.app.acl.allow('uiSchemaTemplates', '*', 'loggedIn');
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
|
@ -11,6 +11,7 @@ describe('createdBy/updatedBy', () => {
|
|||||||
api = mockServer();
|
api = mockServer();
|
||||||
api.plugin(UsersPlugin, userPluginConfig);
|
api.plugin(UsersPlugin, userPluginConfig);
|
||||||
api.plugin(PluginACL);
|
api.plugin(PluginACL);
|
||||||
|
await api.cleanDb();
|
||||||
await api.loadAndInstall();
|
await api.loadAndInstall();
|
||||||
db = api.db;
|
db = api.db;
|
||||||
});
|
});
|
||||||
|
@ -96,8 +96,8 @@ export default class UsersPlugin extends Plugin<UserPluginConfig> {
|
|||||||
const publicActions = ['check', 'signin', 'signup', 'lostpassword', 'resetpassword', 'getUserByResetToken'];
|
const publicActions = ['check', 'signin', 'signup', 'lostpassword', 'resetpassword', 'getUserByResetToken'];
|
||||||
const loggedInActions = ['signout', 'updateProfile', 'changePassword', 'setDefaultRole'];
|
const loggedInActions = ['signout', 'updateProfile', 'changePassword', 'setDefaultRole'];
|
||||||
|
|
||||||
publicActions.forEach((action) => this.app.acl.skip('users', action));
|
publicActions.forEach((action) => this.app.acl.allow('users', action));
|
||||||
loggedInActions.forEach((action) => this.app.acl.skip('users', action, 'logged-in'));
|
loggedInActions.forEach((action) => this.app.acl.allow('users', action, 'loggedIn'));
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
|
Loading…
Reference in New Issue
Block a user