featEnable permission (#229)
* feat: acl skip * feat: skip-manager * feat: root user permission skip * fix: test * feat: set user role * fix: code review * feat: setDefaultRole for users
This commit is contained in:
parent
accb2a59b9
commit
d98714d9fd
93
packages/acl/src/__tests__/skip.test.ts
Normal file
93
packages/acl/src/__tests__/skip.test.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { ACL } from '..';
|
||||||
|
|
||||||
|
describe('skip', () => {
|
||||||
|
let acl: ACL;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
acl = new ACL();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip action', async () => {
|
||||||
|
const middlewareFunc = acl.middleware();
|
||||||
|
const ctx: any = {
|
||||||
|
state: {},
|
||||||
|
action: {
|
||||||
|
resourceName: 'users',
|
||||||
|
actionName: 'login',
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
acl,
|
||||||
|
},
|
||||||
|
throw() {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextFunc = jest.fn();
|
||||||
|
|
||||||
|
await middlewareFunc(ctx, nextFunc);
|
||||||
|
expect(nextFunc).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
acl.skip('users', 'login');
|
||||||
|
|
||||||
|
await middlewareFunc(ctx, nextFunc);
|
||||||
|
expect(nextFunc).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip action with condition', async () => {
|
||||||
|
const middlewareFunc = acl.middleware();
|
||||||
|
const ctx: any = {
|
||||||
|
state: {},
|
||||||
|
action: {
|
||||||
|
resourceName: 'users',
|
||||||
|
actionName: 'login',
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
acl,
|
||||||
|
},
|
||||||
|
throw() {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextFunc = jest.fn();
|
||||||
|
|
||||||
|
let skip = false;
|
||||||
|
acl.skip('users', 'login', (ctx) => {
|
||||||
|
return skip;
|
||||||
|
});
|
||||||
|
|
||||||
|
await middlewareFunc(ctx, nextFunc);
|
||||||
|
expect(nextFunc).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
skip = true;
|
||||||
|
await middlewareFunc(ctx, nextFunc);
|
||||||
|
expect(nextFunc).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip action with registered condition', async () => {
|
||||||
|
const middlewareFunc = acl.middleware();
|
||||||
|
|
||||||
|
const conditionFn = jest.fn();
|
||||||
|
acl.skipManager.registerSkipCondition('superUser', () => {
|
||||||
|
conditionFn();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ctx: any = {
|
||||||
|
state: {},
|
||||||
|
action: {
|
||||||
|
resourceName: 'users',
|
||||||
|
actionName: 'login',
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
acl,
|
||||||
|
},
|
||||||
|
throw() {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextFunc = jest.fn();
|
||||||
|
|
||||||
|
acl.skip('users', 'login', 'superUser');
|
||||||
|
|
||||||
|
await middlewareFunc(ctx, nextFunc);
|
||||||
|
expect(nextFunc).toHaveBeenCalledTimes(1);
|
||||||
|
expect(conditionFn).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
@ -5,6 +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';
|
||||||
const parse = require('json-templates');
|
const parse = require('json-templates');
|
||||||
|
|
||||||
interface CanResult {
|
interface CanResult {
|
||||||
@ -46,6 +47,8 @@ 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);
|
||||||
|
|
||||||
roles = new Map<string, ACLRole>();
|
roles = new Map<string, ACLRole>();
|
||||||
|
|
||||||
actionAlias = new Map<string, string>();
|
actionAlias = new Map<string, string>();
|
||||||
@ -81,6 +84,8 @@ export class ACL extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.middlewares.push(this.skipManager.aclMiddleware());
|
||||||
}
|
}
|
||||||
|
|
||||||
define(options: DefineOptions): ACLRole {
|
define(options: DefineOptions): ACLRole {
|
||||||
@ -207,6 +212,10 @@ export class ACL extends EventEmitter {
|
|||||||
this.middlewares.push(fn);
|
this.middlewares.push(fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
skip(resourceName: string, actionName: string, condition?: any) {
|
||||||
|
this.skipManager.skip(resourceName, actionName, condition);
|
||||||
|
}
|
||||||
|
|
||||||
middleware() {
|
middleware() {
|
||||||
const acl = this;
|
const acl = this;
|
||||||
|
|
||||||
|
76
packages/acl/src/skip-manager.ts
Normal file
76
packages/acl/src/skip-manager.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { ACL } from './acl';
|
||||||
|
|
||||||
|
type ConditionFunc = (ctx: any) => boolean;
|
||||||
|
|
||||||
|
export class SkipManager {
|
||||||
|
protected skipActions = new Map<string, Map<string, string | ConditionFunc | true>>();
|
||||||
|
|
||||||
|
protected registeredCondition = new Map<string, ConditionFunc>();
|
||||||
|
|
||||||
|
constructor(public acl: ACL) {
|
||||||
|
this.registerSkipCondition('logged-in', (ctx) => {
|
||||||
|
return ctx.state.currentUser;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
skip(resourceName: string, actionName: string, condition?: string | ConditionFunc) {
|
||||||
|
const actionMap = this.skipActions.get(resourceName) || new Map<string, string | ConditionFunc>();
|
||||||
|
actionMap.set(actionName, condition || true);
|
||||||
|
|
||||||
|
this.skipActions.set(resourceName, actionMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSkippedConditions(resourceName: string, actionName: string): Array<ConditionFunc | true> {
|
||||||
|
const fetchActionSteps: string[] = ['*', resourceName];
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const fetchActionStep of fetchActionSteps) {
|
||||||
|
const resource = this.skipActions.get(fetchActionStep);
|
||||||
|
if (resource) {
|
||||||
|
const condition = resource.get('*') || resource.get(actionName);
|
||||||
|
if (condition) {
|
||||||
|
results.push(typeof condition === 'string' ? this.registeredCondition.get(condition) : condition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerSkipCondition(name: string, condition: ConditionFunc) {
|
||||||
|
this.registeredCondition.set(name, condition);
|
||||||
|
}
|
||||||
|
|
||||||
|
aclMiddleware() {
|
||||||
|
return async (ctx, next) => {
|
||||||
|
const { resourceName, actionName } = ctx.action;
|
||||||
|
const skippedConditions = ctx.app.acl.skipManager.getSkippedConditions(resourceName, actionName);
|
||||||
|
let skip = false;
|
||||||
|
|
||||||
|
for (const skippedCondition of skippedConditions) {
|
||||||
|
if (skippedCondition) {
|
||||||
|
let skipResult = false;
|
||||||
|
|
||||||
|
if (typeof skippedCondition === 'function') {
|
||||||
|
skipResult = skippedCondition(ctx);
|
||||||
|
} else if (skippedCondition) {
|
||||||
|
skipResult = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipResult) {
|
||||||
|
skip = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skip) {
|
||||||
|
ctx.permission = {
|
||||||
|
skip: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -53,13 +53,6 @@ for (const plugin of plugins) {
|
|||||||
api.plugin(require(plugin).default);
|
api.plugin(require(plugin).default);
|
||||||
}
|
}
|
||||||
|
|
||||||
api.acl.use(async (ctx, next) => {
|
|
||||||
ctx.permission = {
|
|
||||||
skip: true,
|
|
||||||
};
|
|
||||||
await next();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (process.argv.length < 3) {
|
if (process.argv.length < 3) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
process.argv.push('start', '--port', process.env.API_PORT || 12302);
|
process.argv.push('start', '--port', process.env.API_PORT || 12302);
|
||||||
|
@ -77,6 +77,8 @@ export class CollectionManagerPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.app.acl.skip('collections', 'list', 'logged-in');
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
|
@ -60,6 +60,8 @@ export class UiSchemaStoragePlugin extends Plugin {
|
|||||||
name: 'uiSchemas',
|
name: 'uiSchemas',
|
||||||
actions: uiSchemaActions,
|
actions: uiSchemaActions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.app.acl.skip('uiSchemas', '*', 'logged-in');
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import Database from '@nocobase/database';
|
import Database, { BelongsToManyRepository } from '@nocobase/database';
|
||||||
import PluginACL from '@nocobase/plugin-acl';
|
import PluginACL from '@nocobase/plugin-acl';
|
||||||
import { MockServer, mockServer } from '@nocobase/test';
|
import { MockServer, mockServer } from '@nocobase/test';
|
||||||
|
|
||||||
@ -69,4 +69,50 @@ describe('role', () => {
|
|||||||
expect(roles.length).toEqual(1);
|
expect(roles.length).toEqual(1);
|
||||||
expect(roles[0].get('name')).toEqual('test2');
|
expect(roles[0].get('name')).toEqual('test2');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should set users default role', async () => {
|
||||||
|
await db.getRepository('roles').create({
|
||||||
|
values: {
|
||||||
|
name: 'test1',
|
||||||
|
title: 'Admin User',
|
||||||
|
allowConfigure: true,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.getRepository('roles').create({
|
||||||
|
values: {
|
||||||
|
name: 'test2',
|
||||||
|
title: 'test2 user',
|
||||||
|
allowConfigure: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await db.getRepository('users').create({
|
||||||
|
values: {
|
||||||
|
token: '123',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const userRolesRepo = db.getRepository<BelongsToManyRepository>('users.roles', user.get('id') as string);
|
||||||
|
await userRolesRepo.add('test1');
|
||||||
|
await userRolesRepo.add('test2');
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.agent()
|
||||||
|
.post('/users:setDefaultRole')
|
||||||
|
.send({
|
||||||
|
defaultRole: 'test2',
|
||||||
|
})
|
||||||
|
.set({
|
||||||
|
Authorization: `Bearer ${user.get('token')}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toEqual(200);
|
||||||
|
|
||||||
|
const userRoles = await userRolesRepo.find();
|
||||||
|
const defaultRole = userRoles.find((userRole) => userRole.get('rolesUsers').default);
|
||||||
|
|
||||||
|
expect(defaultRole['name']).toEqual('test2');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -148,3 +148,34 @@ export async function changePassword(ctx: Context, next: Next) {
|
|||||||
ctx.body = ctx.state.currentUser.toJSON();
|
ctx.body = ctx.state.currentUser.toJSON();
|
||||||
await next();
|
await next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function setDefaultRole(ctx: Context, next: Next) {
|
||||||
|
const {
|
||||||
|
values: { defaultRole },
|
||||||
|
} = ctx.action.params;
|
||||||
|
|
||||||
|
const currentUserId = ctx.state.currentUser.id;
|
||||||
|
|
||||||
|
await ctx.db.getRepository('rolesUsers').update({
|
||||||
|
filter: {
|
||||||
|
userId: currentUserId,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.db.getRepository('rolesUsers').update({
|
||||||
|
filter: {
|
||||||
|
userId: currentUserId,
|
||||||
|
roleName: defaultRole,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.body = 'ok';
|
||||||
|
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
6
packages/plugin-users/src/collections/roles-users.ts
Normal file
6
packages/plugin-users/src/collections/roles-users.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { CollectionOptions } from '@nocobase/database';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'rolesUsers',
|
||||||
|
fields: [{ type: 'boolean', name: 'default' }],
|
||||||
|
} as CollectionOptions;
|
@ -6,6 +6,33 @@ import { Context, Next } from '@nocobase/actions';
|
|||||||
// 因为是否提供匿名访问资源是应用决定的,不是使用插件就一定不能匿名访问。
|
// 因为是否提供匿名访问资源是应用决定的,不是使用插件就一定不能匿名访问。
|
||||||
export function parseToken(options?: any) {
|
export function parseToken(options?: any) {
|
||||||
return async function parseToken(ctx: Context, next: Next) {
|
return async function parseToken(ctx: Context, next: Next) {
|
||||||
|
const user = await findUserByToken(ctx);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
ctx.state.currentUser = user;
|
||||||
|
setCurrentRole(ctx, user);
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCurrentRole(ctx, user) {
|
||||||
|
const userRoles = user.get('roles');
|
||||||
|
let userRole;
|
||||||
|
|
||||||
|
if (userRoles.length == 1) {
|
||||||
|
userRole = userRoles[0].get('name');
|
||||||
|
} else if (userRoles.length > 1) {
|
||||||
|
const defaultRole = userRoles.findIndex((role) => role.get('rolesUsers').default);
|
||||||
|
userRole = defaultRole !== -1 ? userRoles[defaultRole] : userRoles[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userRole) {
|
||||||
|
ctx.state.currentRole = userRole;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findUserByToken(ctx: Context) {
|
||||||
const token = ctx.get('Authorization').replace(/^Bearer\s+/gi, '');
|
const token = ctx.get('Authorization').replace(/^Bearer\s+/gi, '');
|
||||||
const User = ctx.db.getCollection('users');
|
const User = ctx.db.getCollection('users');
|
||||||
const user = await User.repository.findOne({
|
const user = await User.repository.findOne({
|
||||||
@ -15,10 +42,5 @@ export function parseToken(options?: any) {
|
|||||||
appends: ['roles'],
|
appends: ['roles'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user) {
|
return user;
|
||||||
ctx.state.currentUser = user;
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,16 @@ export default class UsersPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.app.resourcer.use(middlewares.parseToken());
|
this.app.resourcer.use(middlewares.parseToken());
|
||||||
|
|
||||||
|
const publicActions = ['check', 'signin', 'signup', 'lostpassword', 'resetpassword', 'getUserByResetToken'];
|
||||||
|
const loggedInActions = ['signout', 'updateProfile', 'changePassword', 'setDefaultRole'];
|
||||||
|
|
||||||
|
publicActions.forEach((action) => this.app.acl.skip('users', action));
|
||||||
|
loggedInActions.forEach((action) => this.app.acl.skip('users', action, 'logged-in'));
|
||||||
|
|
||||||
|
this.app.acl.skip('*', '*', (ctx) => {
|
||||||
|
return ctx.state.currentUser?.id == 1;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
@ -67,13 +77,22 @@ export default class UsersPlugin extends Plugin {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async install() {
|
getRootUserInfo() {
|
||||||
const {
|
const {
|
||||||
adminNickname = 'Super Admin',
|
adminNickname = 'Super Admin',
|
||||||
adminEmail = 'admin@nocobase.com',
|
adminEmail = 'admin@nocobase.com',
|
||||||
adminPassword = 'admin123',
|
adminPassword = 'admin123',
|
||||||
} = this.options;
|
} = this.options;
|
||||||
|
|
||||||
|
return {
|
||||||
|
adminNickname,
|
||||||
|
adminEmail,
|
||||||
|
adminPassword,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async install() {
|
||||||
|
const { adminNickname, adminPassword, adminEmail } = this.getRootUserInfo();
|
||||||
|
|
||||||
const User = this.db.getCollection('users');
|
const User = this.db.getCollection('users');
|
||||||
await User.repository.create({
|
await User.repository.create({
|
||||||
values: {
|
values: {
|
||||||
|
@ -102,6 +102,7 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
});
|
});
|
||||||
|
|
||||||
registerMiddlewares(this, options);
|
registerMiddlewares(this, options);
|
||||||
|
|
||||||
if (options.registerActions !== false) {
|
if (options.registerActions !== false) {
|
||||||
registerActions(this);
|
registerActions(this);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user