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 { ACLAvailableStrategy, AvailableStrategyOptions, predicate } from './acl-available-strategy';
|
||||
import { ACLRole, RoleActionParams } from './acl-role';
|
||||
import { SkipManager } from './skip-manager';
|
||||
const parse = require('json-templates');
|
||||
|
||||
interface CanResult {
|
||||
@ -46,6 +47,8 @@ export class ACL extends EventEmitter {
|
||||
protected availableStrategy = new Map<string, ACLAvailableStrategy>();
|
||||
protected middlewares = [];
|
||||
|
||||
public skipManager = new SkipManager(this);
|
||||
|
||||
roles = new Map<string, ACLRole>();
|
||||
|
||||
actionAlias = new Map<string, string>();
|
||||
@ -81,6 +84,8 @@ export class ACL extends EventEmitter {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.middlewares.push(this.skipManager.aclMiddleware());
|
||||
}
|
||||
|
||||
define(options: DefineOptions): ACLRole {
|
||||
@ -207,6 +212,10 @@ export class ACL extends EventEmitter {
|
||||
this.middlewares.push(fn);
|
||||
}
|
||||
|
||||
skip(resourceName: string, actionName: string, condition?: any) {
|
||||
this.skipManager.skip(resourceName, actionName, condition);
|
||||
}
|
||||
|
||||
middleware() {
|
||||
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.acl.use(async (ctx, next) => {
|
||||
ctx.permission = {
|
||||
skip: true,
|
||||
};
|
||||
await next();
|
||||
});
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
// @ts-ignore
|
||||
process.argv.push('start', '--port', process.env.API_PORT || 12302);
|
||||
|
@ -77,6 +77,8 @@ export class CollectionManagerPlugin extends Plugin {
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
this.app.acl.skip('collections', 'list', 'logged-in');
|
||||
}
|
||||
|
||||
async load() {
|
||||
|
@ -60,6 +60,8 @@ export class UiSchemaStoragePlugin extends Plugin {
|
||||
name: 'uiSchemas',
|
||||
actions: uiSchemaActions,
|
||||
});
|
||||
|
||||
this.app.acl.skip('uiSchemas', '*', 'logged-in');
|
||||
}
|
||||
|
||||
async load() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Database from '@nocobase/database';
|
||||
import Database, { BelongsToManyRepository } from '@nocobase/database';
|
||||
import PluginACL from '@nocobase/plugin-acl';
|
||||
import { MockServer, mockServer } from '@nocobase/test';
|
||||
|
||||
@ -69,4 +69,50 @@ describe('role', () => {
|
||||
expect(roles.length).toEqual(1);
|
||||
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();
|
||||
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,19 +6,41 @@ import { Context, Next } from '@nocobase/actions';
|
||||
// 因为是否提供匿名访问资源是应用决定的,不是使用插件就一定不能匿名访问。
|
||||
export function parseToken(options?: any) {
|
||||
return async function parseToken(ctx: Context, next: Next) {
|
||||
const token = ctx.get('Authorization').replace(/^Bearer\s+/gi, '');
|
||||
const User = ctx.db.getCollection('users');
|
||||
const user = await User.repository.findOne({
|
||||
filter: {
|
||||
token,
|
||||
},
|
||||
appends: ['roles'],
|
||||
});
|
||||
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 User = ctx.db.getCollection('users');
|
||||
const user = await User.repository.findOne({
|
||||
filter: {
|
||||
token,
|
||||
},
|
||||
appends: ['roles'],
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
@ -59,6 +59,16 @@ export default class UsersPlugin extends Plugin {
|
||||
}
|
||||
|
||||
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() {
|
||||
@ -67,13 +77,22 @@ export default class UsersPlugin extends Plugin {
|
||||
});
|
||||
}
|
||||
|
||||
async install() {
|
||||
getRootUserInfo() {
|
||||
const {
|
||||
adminNickname = 'Super Admin',
|
||||
adminEmail = 'admin@nocobase.com',
|
||||
adminPassword = 'admin123',
|
||||
} = this.options;
|
||||
|
||||
return {
|
||||
adminNickname,
|
||||
adminEmail,
|
||||
adminPassword,
|
||||
};
|
||||
}
|
||||
async install() {
|
||||
const { adminNickname, adminPassword, adminEmail } = this.getRootUserInfo();
|
||||
|
||||
const User = this.db.getCollection('users');
|
||||
await User.repository.create({
|
||||
values: {
|
||||
|
@ -102,6 +102,7 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
});
|
||||
|
||||
registerMiddlewares(this, options);
|
||||
|
||||
if (options.registerActions !== false) {
|
||||
registerActions(this);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user