refactor(plugin-users): improve extendibility of middlewares (#677)

* refactor(plugin-users): improve extendibility of middlewares

* fix(plugin-users): fix typo

* fix: test error

* fix: allowConfigure condition

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
Junyi 2022-07-25 19:33:23 +08:00 committed by GitHub
parent 5b61587a39
commit 49a4ab4818
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 649 additions and 603 deletions

View File

@ -19,7 +19,9 @@ export class AllowManager {
} }
const roleInstance = await ctx.db.getRepository('roles').findOne({ const roleInstance = await ctx.db.getRepository('roles').findOne({
name: roleName, filter: {
name: roleName
},
}); });
return roleInstance?.get('allowConfigure'); return roleInstance?.get('allowConfigure');

View File

@ -1,6 +1,7 @@
import { ActionName } from './action'; import { ActionName } from './action';
import { requireModule } from './utils'; import { requireModule } from './utils';
import { HandlerType } from './resourcer'; import { HandlerType } from './resourcer';
import compose from 'koa-compose';
export type MiddlewareType = string | string[] | HandlerType | HandlerType[] | MiddlewareOptions | MiddlewareOptions[]; export type MiddlewareType = string | string[] | HandlerType | HandlerType[] | MiddlewareOptions | MiddlewareOptions[];
@ -73,3 +74,19 @@ export class Middleware {
} }
export default Middleware; export default Middleware;
export class MiddlewareManager {
protected middlewares: HandlerType[] = [];
compose() {
return (ctx, next) => compose(this.middlewares)(ctx, next);
}
use(middleware: HandlerType) {
this.middlewares.push(middleware);
}
unuse(middleware: HandlerType) {
this.middlewares.splice(this.middlewares.indexOf(middleware), 1);
}
}

View File

@ -14,6 +14,7 @@
"dependencies": { "dependencies": {
"@nocobase/acl": "0.7.3-alpha.1", "@nocobase/acl": "0.7.3-alpha.1",
"@nocobase/database": "0.7.3-alpha.1", "@nocobase/database": "0.7.3-alpha.1",
"@nocobase/plugin-users": "0.7.3-alpha.1",
"@nocobase/server": "0.7.3-alpha.1" "@nocobase/server": "0.7.3-alpha.1"
}, },
"repository": { "repository": {

View File

@ -1,13 +1,16 @@
import { ACL } from '@nocobase/acl'; import { ACL } from '@nocobase/acl';
import { Database } from '@nocobase/database'; import { Database } from '@nocobase/database';
import { MockServer } from '@nocobase/test'; import { MockServer } from '@nocobase/test';
import { changeMockRole, prepareApp } from './prepare'; import UsersPlugin from '@nocobase/plugin-users';
import { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage'; import { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage';
import { prepareApp } from './prepare';
describe('acl', () => { describe('acl', () => {
let app: MockServer; let app: MockServer;
let db: Database; let db: Database;
let acl: ACL; let acl: ACL;
let admin;
let adminAgent;
let uiSchemaRepository: UiSchemaRepository; let uiSchemaRepository: UiSchemaRepository;
@ -20,38 +23,41 @@ describe('acl', () => {
db = app.db; db = app.db;
acl = app.acl; acl = app.acl;
const UserRepo = db.getCollection('users').repository;
admin = await UserRepo.create({
values: {
roles: ['admin']
}
});
const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin;
adminAgent = app.agent().auth(userPlugin.jwtService.sign({
userId: admin.get('id'),
}), { type: 'bearer' });
uiSchemaRepository = db.getRepository('uiSchemas'); uiSchemaRepository = db.getRepository('uiSchemas');
}); });
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: {
name: 'admin', name: 'new'
title: 'Admin User',
allowConfigure: true,
},
});
const role = await db.getRepository('roles').findOne({
filter: {
name: 'admin',
}, },
}); });
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'posts', resource: 'posts',
action: 'create', action: 'create',
}), }),
).toBeNull(); ).toBeNull();
// grant universal action // grant universal action
await app await adminAgent
.agent()
.resource('roles') .resource('roles')
.update({ .update({
resourceIndex: 'admin', resourceIndex: 'new',
values: { values: {
strategy: { strategy: {
actions: ['create'], actions: ['create'],
@ -61,31 +67,27 @@ describe('acl', () => {
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'posts', resource: 'posts',
action: 'create', action: 'create',
}), }),
).toMatchObject({ ).toMatchObject({
role: 'admin', role: 'new',
resource: 'posts', resource: 'posts',
action: 'create', action: 'create',
}); });
}); });
it('should deny when resource action has no resource', async () => { it('should deny when resource action has no resource', async () => {
const role = await db.getRepository('roles').create({ await db.getRepository('roles').create({
values: { values: {
name: 'admin', name: 'new',
title: 'Admin User',
allowConfigure: true,
strategy: { strategy: {
actions: ['update:own', 'destroy:own', 'create', 'view'], actions: ['update:own', 'destroy:own', 'create', 'view'],
}, },
}, },
}); });
changeMockRole('admin');
// create c1 collection // create c1 collection
await db.getRepository('collections').create({ await db.getRepository('collections').create({
values: { values: {
@ -102,9 +104,8 @@ describe('acl', () => {
}, },
}); });
await app await adminAgent
.agent() .resource('roles.resources', 'new')
.resource('roles.resources', 'admin')
.create({ .create({
values: { values: {
name: 'c1', name: 'c1',
@ -115,7 +116,7 @@ describe('acl', () => {
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'c1', resource: 'c1',
action: 'list', action: 'list',
}), }),
@ -125,17 +126,13 @@ describe('acl', () => {
it('should works with resources actions', async () => { it('should works with resources actions', async () => {
const role = await db.getRepository('roles').create({ const role = await db.getRepository('roles').create({
values: { values: {
name: 'admin', name: 'new',
title: 'Admin User',
allowConfigure: true,
strategy: { strategy: {
actions: ['list'], actions: ['list'],
}, },
}, },
}); });
changeMockRole('admin');
// create c1 collection // create c1 collection
await db.getRepository('collections').create({ await db.getRepository('collections').create({
values: { values: {
@ -153,8 +150,7 @@ describe('acl', () => {
}); });
// create c1 published scope // create c1 published scope
await app const { body: { data: publishedScope } } = await adminAgent
.agent()
.resource('rolesResourcesScopes') .resource('rolesResourcesScopes')
.create({ .create({
values: { values: {
@ -166,12 +162,11 @@ describe('acl', () => {
}, },
}); });
const publishedScope = await db.getRepository('rolesResourcesScopes').findOne(); // await db.getRepository('rolesResourcesScopes').findOne();
// set admin resources // set admin resources
await app await adminAgent
.agent() .resource('roles.resources', 'new')
.resource('roles.resources', 'admin')
.create({ .create({
values: { values: {
name: 'c1', name: 'c1',
@ -179,7 +174,7 @@ describe('acl', () => {
actions: [ actions: [
{ {
name: 'create', name: 'create',
scope: publishedScope.get('id'), scope: publishedScope.id,
}, },
{ {
name: 'view', name: 'view',
@ -191,12 +186,12 @@ describe('acl', () => {
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'c1', resource: 'c1',
action: 'create', action: 'create',
}), }),
).toMatchObject({ ).toMatchObject({
role: 'admin', role: 'new',
resource: 'c1', resource: 'c1',
action: 'create', action: 'create',
params: { params: {
@ -206,12 +201,12 @@ describe('acl', () => {
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'c1', resource: 'c1',
action: 'view', action: 'view',
}), }),
).toMatchObject({ ).toMatchObject({
role: 'admin', role: 'new',
resource: 'c1', resource: 'c1',
action: 'view', action: 'view',
params: { params: {
@ -220,8 +215,7 @@ describe('acl', () => {
}); });
// revoke action // revoke action
const response = await app const response = await adminAgent
.agent()
.resource('roles.resources', role.get('name')) .resource('roles.resources', role.get('name'))
.list({ .list({
appends: ['actions'], appends: ['actions'],
@ -232,8 +226,7 @@ describe('acl', () => {
const actions = response.body.data[0].actions; const actions = response.body.data[0].actions;
const collectionName = response.body.data[0].name; const collectionName = response.body.data[0].name;
await app await adminAgent
.agent()
.resource('roles.resources', role.get('name')) .resource('roles.resources', role.get('name'))
.update({ .update({
filterByTk: collectionName, filterByTk: collectionName,
@ -251,7 +244,7 @@ describe('acl', () => {
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'c1', resource: 'c1',
action: 'create', action: 'create',
}), }),
@ -259,11 +252,9 @@ describe('acl', () => {
}); });
it('should revoke resource when collection destroy', async () => { it('should revoke resource when collection destroy', async () => {
const role = await db.getRepository('roles').create({ await db.getRepository('roles').create({
values: { values: {
name: 'admin', name: 'new'
title: 'Admin User',
allowConfigure: true,
}, },
}); });
@ -281,11 +272,10 @@ describe('acl', () => {
}, },
}); });
await app await adminAgent
.agent()
.resource('roles.resources') .resource('roles.resources')
.create({ .create({
associatedIndex: role.get('name') as string, associatedIndex: 'new',
values: { values: {
name: 'posts', name: 'posts',
usingActionsConfig: true, usingActionsConfig: true,
@ -300,7 +290,7 @@ describe('acl', () => {
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'posts', resource: 'posts',
action: 'view', action: 'view',
}), }),
@ -314,7 +304,7 @@ describe('acl', () => {
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'posts', resource: 'posts',
action: 'view', action: 'view',
}), }),
@ -324,15 +314,7 @@ describe('acl', () => {
it('should revoke actions when not using actions config', async () => { it('should revoke actions when not using actions config', async () => {
await db.getRepository('roles').create({ await db.getRepository('roles').create({
values: { values: {
name: 'admin', name: 'new'
title: 'Admin User',
allowConfigure: true,
},
});
const role = await db.getRepository('roles').findOne({
filter: {
name: 'admin',
}, },
}); });
@ -343,11 +325,10 @@ describe('acl', () => {
}, },
}); });
await app await adminAgent
.agent()
.resource('roles.resources') .resource('roles.resources')
.create({ .create({
associatedIndex: role.get('name') as string, associatedIndex: 'new',
values: { values: {
name: 'posts', name: 'posts',
usingActionsConfig: true, usingActionsConfig: true,
@ -361,25 +342,24 @@ describe('acl', () => {
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'posts', resource: 'posts',
action: 'create', action: 'create',
}), }),
).toMatchObject({ ).toMatchObject({
role: 'admin', role: 'new',
resource: 'posts', resource: 'posts',
action: 'create', action: 'create',
}); });
await app await adminAgent
.agent() .resource('roles.resources', 'new')
.resource('roles.resources', role.get('name'))
.update({ .update({
filterByTk: ( filterByTk: (
await db.getRepository('rolesResources').findOne({ await db.getRepository('rolesResources').findOne({
filter: { filter: {
name: 'posts', name: 'posts',
roleName: 'admin', roleName: 'new',
}, },
}) })
).get('name') as string, ).get('name') as string,
@ -390,21 +370,20 @@ describe('acl', () => {
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'posts', resource: 'posts',
action: 'create', action: 'create',
}), }),
).toBeNull(); ).toBeNull();
await app await adminAgent
.agent() .resource('roles.resources', 'new')
.resource('roles.resources', role.get('name'))
.update({ .update({
filterByTk: ( filterByTk: (
await db.getRepository('rolesResources').findOne({ await db.getRepository('rolesResources').findOne({
filter: { filter: {
name: 'posts', name: 'posts',
roleName: 'admin', roleName: 'new',
}, },
}) })
).get('name') as string, ).get('name') as string,
@ -415,23 +394,21 @@ describe('acl', () => {
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'posts', resource: 'posts',
action: 'create', action: 'create',
}), }),
).toMatchObject({ ).toMatchObject({
role: 'admin', role: 'new',
resource: 'posts', resource: 'posts',
action: 'create', action: 'create',
}); });
}); });
it('should add fields when field created', async () => { it('should add fields when field created', async () => {
const role = await db.getRepository('roles').create({ await db.getRepository('roles').create({
values: { values: {
name: 'admin', name: 'new'
title: 'Admin User',
allowConfigure: true,
}, },
}); });
@ -449,11 +426,10 @@ describe('acl', () => {
}, },
}); });
await app await adminAgent
.agent()
.resource('roles.resources') .resource('roles.resources')
.create({ .create({
associatedIndex: role.get('name') as string, associatedIndex: 'new',
values: { values: {
name: 'posts', name: 'posts',
usingActionsConfig: true, usingActionsConfig: true,
@ -467,7 +443,7 @@ describe('acl', () => {
}); });
const allowFields = acl.can({ const allowFields = acl.can({
role: 'admin', role: 'new',
resource: 'posts', resource: 'posts',
action: 'view', action: 'view',
})['params']['fields']; })['params']['fields'];
@ -483,7 +459,7 @@ describe('acl', () => {
}); });
const newAllowFields = acl.can({ const newAllowFields = acl.can({
role: 'admin', role: 'new',
resource: 'posts', resource: 'posts',
action: 'view', action: 'view',
})['params']['fields']; })['params']['fields'];
@ -494,18 +470,14 @@ describe('acl', () => {
it('should get role menus', async () => { it('should get role menus', async () => {
const role = await db.getRepository('roles').create({ const role = await db.getRepository('roles').create({
values: { values: {
name: 'admin', name: 'new',
title: 'Admin User',
allowConfigure: true,
strategy: { strategy: {
actions: ['view'], actions: ['view'],
}, },
}, },
}); });
changeMockRole('admin'); const menuResponse = await adminAgent.resource('roles.menuUiSchemas', 'new').list();
const menuResponse = await app.agent().resource('roles.menuUiSchemas', 'admin').list();
expect(menuResponse.statusCode).toEqual(200); expect(menuResponse.statusCode).toEqual(200);
}); });
@ -513,16 +485,23 @@ describe('acl', () => {
it('should toggle role menus', async () => { it('should toggle role menus', async () => {
const role = await db.getRepository('roles').create({ const role = await db.getRepository('roles').create({
values: { values: {
name: 'admin', name: 'new',
title: 'Admin User',
allowConfigure: true,
strategy: { strategy: {
actions: ['*'], actions: ['*'],
}, },
}, },
}); });
const UserRepo = db.getCollection('users').repository;
const user = await UserRepo.create({
values: {
roles: ['new']
}
});
changeMockRole('admin'); const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin;
const userAgent = app.agent().auth(userPlugin.jwtService.sign({
userId: user.get('id'),
}), { type: 'bearer' });
const schema = { const schema = {
'x-uid': 'test', 'x-uid': 'test',
@ -530,9 +509,9 @@ describe('acl', () => {
await uiSchemaRepository.insert(schema); await uiSchemaRepository.insert(schema);
const response = await app const response = await userAgent
.agent() // @ts-ignore
.resource('roles.menuUiSchemas', 'admin') .resource('roles.menuUiSchemas', 'new')
.toggle({ .toggle({
values: { tk: 'test' }, values: { tk: 'test' },
}); });
@ -543,9 +522,7 @@ describe('acl', () => {
it('should sync data to acl before app start', async () => { it('should sync data to acl before app start', async () => {
const role = await db.getRepository('roles').create({ const role = await db.getRepository('roles').create({
values: { values: {
name: 'admin', name: 'new',
title: 'Admin User',
allowConfigure: true,
resources: [ resources: [
{ {
name: 'posts', name: 'posts',
@ -562,20 +539,20 @@ describe('acl', () => {
hooks: false, hooks: false,
}); });
expect(acl.getRole('admin')).toBeUndefined(); expect(acl.getRole('new')).toBeUndefined();
await app.start(); await app.start();
expect(acl.getRole('admin')).toBeDefined(); expect(acl.getRole('new')).toBeDefined();
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'posts', resource: 'posts',
action: 'view', action: 'view',
}), }),
).toMatchObject({ ).toMatchObject({
role: 'admin', role: 'new',
resource: 'posts', resource: 'posts',
action: 'view', action: 'view',
}); });

View File

@ -1,5 +1,6 @@
import { ACL } from '@nocobase/acl'; import { ACL } from '@nocobase/acl';
import { Database, HasManyRepository, Model } from '@nocobase/database'; import { Database, HasManyRepository } from '@nocobase/database';
import UsersPlugin from '@nocobase/plugin-users';
import { MockServer } from '@nocobase/test'; import { MockServer } from '@nocobase/test';
import { prepareApp } from './prepare'; import { prepareApp } from './prepare';
@ -8,7 +9,10 @@ describe('association field acl', () => {
let db: Database; let db: Database;
let acl: ACL; let acl: ACL;
let role: Model; let user;
let userAgent;
let admin;
let adminAgent;
afterEach(async () => { afterEach(async () => {
await app.destroy(); await app.destroy();
@ -19,20 +23,44 @@ describe('association field acl', () => {
db = app.db; db = app.db;
acl = app.acl; acl = app.acl;
role = await db.getRepository('roles').create({ await db.getRepository('roles').create({
values: { values: {
name: 'admin', name: 'new',
title: 'Admin User',
allowConfigure: true, allowConfigure: true,
}, },
}); });
await db.getRepository('collections').create({ await db.getRepository('roles').create({
values: { values: {
name: 'users', name: 'testAdmin',
allowConfigure: true,
}, },
context: {},
}); });
const UserRepo = db.getCollection('users').repository;
user = await UserRepo.create({
values: {
roles: ['new'],
},
});
admin = await UserRepo.create({
values: {
roles: ['testAdmin'],
},
});
const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin;
userAgent = app.agent().auth(
userPlugin.jwtService.sign({
userId: user.get('id'),
}),
{ type: 'bearer' },
);
adminAgent = app.agent().auth(
userPlugin.jwtService.sign({
userId: admin.get('id'),
}),
{ type: 'bearer' },
);
await db.getRepository('collections').create({ await db.getRepository('collections').create({
values: { values: {
@ -75,56 +103,48 @@ describe('association field acl', () => {
context: {}, context: {},
}); });
await app await adminAgent.resource('roles.resources', 'new').create({
.agent() values: {
.resource('roles.resources') name: 'users',
.create({ usingActionsConfig: true,
associatedIndex: 'admin', actions: [
values: { {
name: 'users', name: 'create',
usingActionsConfig: true, fields: ['orders'],
actions: [ },
{ {
name: 'create', name: 'view',
fields: ['orders'], fields: ['orders'],
}, },
{ ],
name: 'view', },
fields: ['orders'], });
},
],
},
});
}); });
it('should revoke target action on association action revoke', async () => { it('should revoke target action on association action revoke', async () => {
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'orders', resource: 'orders',
action: 'list', action: 'list',
}), }),
).toMatchObject({ ).toMatchObject({
role: 'admin', role: 'new',
resource: 'orders', resource: 'orders',
action: 'list', action: 'list',
}); });
await app await adminAgent.resource('roles.resources', 'new').update({
.agent() values: {
.resource('roles.resources') name: 'users',
.update({ usingActionsConfig: true,
associatedIndex: 'admin', actions: [],
values: { },
name: 'users', });
usingActionsConfig: true,
actions: [],
},
});
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'orders', resource: 'orders',
action: 'list', action: 'list',
}), }),
@ -134,12 +154,12 @@ describe('association field acl', () => {
it('should revoke association action on action revoke', async () => { it('should revoke association action on action revoke', async () => {
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'users.orders', resource: 'users.orders',
action: 'add', action: 'add',
}), }),
).toMatchObject({ ).toMatchObject({
role: 'admin', role: 'new',
resource: 'users.orders', resource: 'users.orders',
action: 'add', action: 'add',
}); });
@ -152,27 +172,23 @@ describe('association field acl', () => {
const actionId = viewAction.get('id') as number; const actionId = viewAction.get('id') as number;
const response = await app const response = await adminAgent.resource('roles.resources', 'new').update({
.agent() values: {
.resource('roles.resources') name: 'users',
.update({ usingActionsConfig: true,
associatedIndex: 'admin', actions: [
values: { {
name: 'users', id: actionId,
usingActionsConfig: true, },
actions: [ ],
{ },
id: actionId, });
},
],
},
});
expect(response.statusCode).toEqual(200); expect(response.statusCode).toEqual(200);
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'users.orders', resource: 'users.orders',
action: 'add', action: 'add',
}), }),
@ -180,30 +196,26 @@ describe('association field acl', () => {
}); });
it('should revoke association action on field deleted', async () => { it('should revoke association action on field deleted', async () => {
await app await adminAgent.resource('roles.resources', 'new').update({
.agent() values: {
.resource('roles.resources') name: 'users',
.update({ usingActionsConfig: true,
associatedIndex: 'admin', actions: [
values: { {
name: 'users', name: 'create',
usingActionsConfig: true, fields: ['name', 'age'],
actions: [ },
{ ],
name: 'create', },
fields: ['name', 'age'], });
},
],
},
});
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'users', resource: 'users',
action: 'create', action: 'create',
}), }),
).toMatchObject({ ).toMatchObject({
role: 'admin', role: 'new',
resource: 'users', resource: 'users',
action: 'create', action: 'create',
params: { params: {
@ -236,12 +248,12 @@ describe('association field acl', () => {
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'users', resource: 'users',
action: 'create', action: 'create',
}), }),
).toMatchObject({ ).toMatchObject({
role: 'admin', role: 'new',
resource: 'users', resource: 'users',
action: 'create', action: 'create',
params: { params: {
@ -251,45 +263,44 @@ describe('association field acl', () => {
}); });
it('should allow association fields access', async () => { it('should allow association fields access', async () => {
const createResponse = await app const createResponse = await userAgent.resource('users').create({
.agent() values: {
.resource('users') orders: [
.create({ {
values: { content: 'apple',
orders: [ },
{ ],
content: 'apple', },
}, });
],
},
});
expect(createResponse.statusCode).toEqual(200); expect(createResponse.statusCode).toEqual(200);
const user = await db.getRepository('users').findOne(); const user = await db.getRepository('users').findOne({
filterByTk: createResponse.body.data.id,
});
// @ts-ignore // @ts-ignore
expect(await user.countOrders()).toEqual(1); expect(await user.countOrders()).toEqual(1);
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'users.orders', resource: 'users.orders',
action: 'list', action: 'list',
}), }),
).toMatchObject({ ).toMatchObject({
role: 'admin', role: 'new',
resource: 'users.orders', resource: 'users.orders',
action: 'list', action: 'list',
}); });
expect( expect(
acl.can({ acl.can({
role: 'admin', role: 'new',
resource: 'orders', resource: 'orders',
action: 'list', action: 'list',
}), }),
).toMatchObject({ ).toMatchObject({
role: 'admin', role: 'new',
resource: 'orders', resource: 'orders',
action: 'list', action: 'list',
}); });

View File

@ -1,15 +1,16 @@
import { MockServer } from '@nocobase/test';
import { Database } from '@nocobase/database'; import { Database } from '@nocobase/database';
import { ACL } from '@nocobase/acl'; import UsersPlugin from '@nocobase/plugin-users';
import { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage'; import { MockServer } from '@nocobase/test';
import { changeMockRole, prepareApp } from './prepare'; import { prepareApp } from './prepare';
describe('configuration', () => { describe('configuration', () => {
let app: MockServer; let app: MockServer;
let db: Database; let db: Database;
let acl: ACL; let admin;
let adminAgent;
let uiSchemaRepository: UiSchemaRepository; let user;
let userAgent;
let guestAgent;
afterEach(async () => { afterEach(async () => {
await app.destroy(); await app.destroy();
@ -18,28 +19,56 @@ describe('configuration', () => {
beforeEach(async () => { beforeEach(async () => {
app = await prepareApp(); app = await prepareApp();
db = app.db; 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({ await db.getRepository('roles').create({
values: { values: {
name: 'admin1', name: 'test1',
title: 'admin allowConfigure',
allowConfigure: true, allowConfigure: true,
}, },
}); });
changeMockRole('admin1'); await db.getRepository('roles').create({
values: {
name: 'test2',
},
});
expect((await app.agent().resource('collections').create()).statusCode).toEqual(200); const UserRepo = db.getCollection('users').repository;
expect((await app.agent().resource('collections').list()).statusCode).toEqual(200); admin = await UserRepo.create({
values: {
roles: ['test1']
}
});
user = await UserRepo.create({
values: {
roles: ['test2']
}
});
const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin;
adminAgent = app.agent().auth(userPlugin.jwtService.sign({
userId: admin.get('id'),
}), { type: 'bearer' });
userAgent = app.agent().auth(userPlugin.jwtService.sign({
userId: user.get('id'),
}), { type: 'bearer' });
guestAgent = app.agent();
});
it('should list collections', async () => {
expect((await userAgent.resource('collections').create()).statusCode).toEqual(403);
expect((await userAgent.resource('collections').list()).statusCode).toEqual(200);
});
it('should not create/list collections', async () => {
expect((await guestAgent.resource('collections').create()).statusCode).toEqual(403);
expect((await guestAgent.resource('collections').list()).statusCode).toEqual(403);
});
it('should allow when role has allowConfigure with true value', async () => {
expect((await adminAgent.resource('collections').create()).statusCode).toEqual(200);
expect((await adminAgent.resource('collections').list()).statusCode).toEqual(200);
}); });
}); });

View File

@ -1,33 +1,40 @@
import { ACL } from '@nocobase/acl'; import { ACL } from '@nocobase/acl';
import { Database, Model } from '@nocobase/database'; import { Database, Model } from '@nocobase/database';
import { MockServer } from '@nocobase/test'; import { MockServer } from '@nocobase/test';
import { changeMockUser, prepareApp } from './prepare'; import UsersPlugin from '@nocobase/plugin-users';
import { prepareApp } from './prepare';
describe('middleware', () => { describe('middleware', () => {
let app: MockServer; let app: MockServer;
let role: Model; let role: Model;
let db: Database; let db: Database;
let acl: ACL; let acl: ACL;
let admin;
let adminAgent;
beforeEach(async () => { beforeEach(async () => {
app = await prepareApp(); app = await prepareApp();
db = app.db; db = app.db;
acl = app.acl; acl = app.acl;
await db.getRepository('roles').create({
values: {
name: 'admin',
title: 'Admin User',
allowConfigure: true,
},
});
role = await db.getRepository('roles').findOne({ role = await db.getRepository('roles').findOne({
filter: { filter: {
name: 'admin', name: 'admin',
}, },
}); });
const UserRepo = db.getCollection('users').repository;
admin = await UserRepo.create({
values: {
roles: ['admin']
}
});
const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin;
adminAgent = app.agent().auth(userPlugin.jwtService.sign({
userId: admin.get('id'),
}), { type: 'bearer' });
await db.getRepository('collections').create({ await db.getRepository('collections').create({
values: { values: {
name: 'posts', name: 'posts',
@ -82,7 +89,7 @@ describe('middleware', () => {
}, },
}); });
const response = await app.agent().resource('posts').create({ const response = await adminAgent.resource('posts').create({
values: {}, values: {},
}); });
@ -90,8 +97,7 @@ describe('middleware', () => {
}); });
it('should limit fields on view actions', async () => { it('should limit fields on view actions', async () => {
await app await adminAgent
.agent()
.resource('roles.resources', role.get('name')) .resource('roles.resources', role.get('name'))
.create({ .create({
values: { values: {
@ -110,8 +116,7 @@ describe('middleware', () => {
}, },
}); });
await app await adminAgent
.agent()
.resource('posts') .resource('posts')
.create({ .create({
values: { values: {
@ -124,10 +129,10 @@ describe('middleware', () => {
expect(post.get('title')).toEqual('post-title'); expect(post.get('title')).toEqual('post-title');
expect(post.get('description')).toEqual('post-description'); expect(post.get('description')).toEqual('post-description');
const response = await app.agent().resource('posts').list({}); const response = await adminAgent.resource('posts').list({});
expect(response.statusCode).toEqual(200); expect(response.statusCode).toEqual(200);
const data = response.body.data[0]; const [data] = response.body.data;
expect(data['id']).not.toBeUndefined(); expect(data['id']).not.toBeUndefined();
expect(data['title']).toEqual('post-title'); expect(data['title']).toEqual('post-title');
@ -135,12 +140,7 @@ describe('middleware', () => {
}); });
it('should parse template value on action params', async () => { it('should parse template value on action params', async () => {
changeMockUser({ const res = await adminAgent
id: 2,
});
const res = await app
.agent()
.resource('rolesResourcesScopes') .resource('rolesResourcesScopes')
.create({ .create({
values: { values: {
@ -151,8 +151,7 @@ describe('middleware', () => {
}, },
}); });
await app await adminAgent
.agent()
.resource('roles.resources', role.get('name')) .resource('roles.resources', role.get('name'))
.create({ .create({
values: { values: {
@ -172,8 +171,7 @@ describe('middleware', () => {
}, },
}); });
await app await adminAgent
.agent()
.resource('posts') .resource('posts')
.create({ .create({
values: { values: {
@ -183,8 +181,7 @@ describe('middleware', () => {
}, },
}); });
await app await adminAgent
.agent()
.resource('posts') .resource('posts')
.create({ .create({
values: { values: {
@ -194,14 +191,13 @@ describe('middleware', () => {
}, },
}); });
const response = await app.agent().resource('posts').list(); const response = await adminAgent.resource('posts').list();
const data = response.body.data; const data = response.body.data;
expect(data.length).toEqual(1); expect(data.length).toEqual(1);
}); });
it('should change fields params to whitelist in create action', async () => { it('should change fields params to whitelist in create action', async () => {
await app await adminAgent
.agent()
.resource('roles.resources', role.get('name')) .resource('roles.resources', role.get('name'))
.create({ .create({
values: { values: {
@ -216,8 +212,7 @@ describe('middleware', () => {
}, },
}); });
await app await adminAgent
.agent()
.resource('posts') .resource('posts')
.create({ .create({
values: { values: {

View File

@ -1,11 +1,9 @@
import { ACL } from '@nocobase/acl'; import { ACL } from '@nocobase/acl';
import { Database } from '@nocobase/database'; import { Database } from '@nocobase/database';
import PluginACL from '@nocobase/plugin-acl';
import PluginCollectionManager from '@nocobase/plugin-collection-manager';
import PluginUiSchema from '@nocobase/plugin-ui-schema-storage';
import PluginUser from '@nocobase/plugin-users'; import PluginUser from '@nocobase/plugin-users';
import { mockServer, MockServer } from '@nocobase/test'; import { MockServer } from '@nocobase/test';
import supertest from 'supertest';
import { prepareApp } from './prepare';
describe('own test', () => { describe('own test', () => {
let app: MockServer; let app: MockServer;
@ -21,28 +19,15 @@ describe('own test', () => {
let role; let role;
let agent; let agent;
let adminAgent;
let userAgent;
afterEach(async () => { afterEach(async () => {
await app.destroy(); await app.destroy();
}); });
beforeEach(async () => { beforeEach(async () => {
app = mockServer({ app = await prepareApp();
registerActions: true,
});
await app.cleanDb();
app.plugin(PluginUiSchema);
app.plugin(PluginCollectionManager);
app.plugin(PluginUser, {
jwt: {
secret: process.env.APP_KEY || 'test-key',
},
});
app.plugin(PluginACL);
await app.loadAndInstall();
db = app.db; db = app.db;
const PostCollection = db.collection({ const PostCollection = db.collection({
@ -68,9 +53,9 @@ describe('own test', () => {
fields: [{ type: 'string', name: 'name' }], fields: [{ type: 'string', name: 'name' }],
}); });
await app.db.sync(); await db.sync();
agent = supertest.agent(app.callback()); agent = app.agent();
acl = app.acl; acl = app.acl;
@ -86,6 +71,8 @@ describe('own test', () => {
adminToken = pluginUser.jwtService.sign({ userId: admin.get('id') }); adminToken = pluginUser.jwtService.sign({ userId: admin.get('id') });
adminAgent = app.agent().auth(adminToken, { type: 'bearer' });
user = await db.getRepository('users').create({ user = await db.getRepository('users').create({
values: { values: {
nickname: 'test', nickname: 'test',
@ -94,10 +81,12 @@ describe('own test', () => {
}); });
userToken = pluginUser.jwtService.sign({ userId: user.get('id') }); userToken = pluginUser.jwtService.sign({ userId: user.get('id') });
userAgent = app.agent().auth(userToken, { type: 'bearer' });
}); });
it('should list without createBy', async () => { it('should list without createBy', async () => {
let response = await agent await adminAgent
.patch('/roles/admin') .patch('/roles/admin')
.send({ .send({
strategy: { strategy: {
@ -106,33 +95,38 @@ describe('own test', () => {
}) })
.set({ Authorization: 'Bearer ' + adminToken }); .set({ Authorization: 'Bearer ' + adminToken });
response = await agent.get('/tests:list').set({ Authorization: 'Bearer ' + userToken }); const response = await userAgent.get('/tests:list');
expect(response.statusCode).toEqual(200); expect(response.statusCode).toEqual(200);
}); });
it('should delete with createdBy', async () => { it('should delete with createdBy', async () => {
let response = await agent await adminAgent
.patch('/roles/admin') .resource('roles')
.send({ .update({
strategy: { filterByTk: 'admin',
actions: ['view:own', 'create', 'destroy:own'], values: {
}, strategy: {
}) actions: ['view:own', 'create', 'destroy:own'],
.set({ Authorization: 'Bearer ' + adminToken }); },
}
});
response = await agent let response = await userAgent
.get('/posts:create') .resource('posts')
.send({ .create({
title: 't1', values: {
}) title: 't1',
.set({ Authorization: 'Bearer ' + userToken }); }
});
expect(response.statusCode).toEqual(200); expect(response.statusCode).toEqual(200);
const data = response.body; const data = response.body;
const id = data.data['id']; const id = data.data['id'];
response = await agent.delete(`/posts/${id}`).set({ Authorization: 'Bearer ' + userToken }); response = await userAgent.resource('posts').destroy({
filterByTk: id
});
expect(response.statusCode).toEqual(200); expect(response.statusCode).toEqual(200);
expect(await db.getRepository('posts').count()).toEqual(0); expect(await db.getRepository('posts').count()).toEqual(0);
}); });

View File

@ -1,18 +1,10 @@
import PluginUsers from '@nocobase/plugin-users';
import PluginCollectionManager from '@nocobase/plugin-collection-manager'; import PluginCollectionManager from '@nocobase/plugin-collection-manager';
import PluginUiSchema from '@nocobase/plugin-ui-schema-storage'; import PluginUiSchema from '@nocobase/plugin-ui-schema-storage';
import { mockServer } from '@nocobase/test'; import { mockServer } from '@nocobase/test';
import PluginACL from '../server'; import PluginACL from '../server';
let mockRole: string = 'admin';
let mockUser = {};
export function changeMockRole(role: string) {
mockRole = role;
}
export function changeMockUser(user: any) {
mockUser = user;
}
export async function prepareApp() { export async function prepareApp() {
const app = mockServer({ const app = mockServer({
@ -21,15 +13,10 @@ export async function prepareApp() {
await app.cleanDb(); await app.cleanDb();
app.plugin(PluginUsers);
app.plugin(PluginUiSchema); app.plugin(PluginUiSchema);
app.plugin(PluginCollectionManager); app.plugin(PluginCollectionManager);
app.resourcer.use(async (ctx, next) => {
ctx.state.currentRole = mockRole;
ctx.state.currentUser = mockUser;
await next();
});
app.plugin(PluginACL); app.plugin(PluginACL);
await app.loadAndInstall(); await app.loadAndInstall();

View File

@ -1,6 +1,8 @@
import { MockServer } from '@nocobase/test'; import { MockServer } from '@nocobase/test';
import { changeMockRole, changeMockUser, prepareApp } from './prepare';
import { Database } from '@nocobase/database'; import { Database } from '@nocobase/database';
import UsersPlugin from '@nocobase/plugin-users';
import { prepareApp } from './prepare';
describe('role check action', () => { describe('role check action', () => {
let app: MockServer; let app: MockServer;
@ -21,14 +23,18 @@ describe('role check action', () => {
name: 'test', name: 'test',
}, },
}); });
const user = await db.getRepository('users').create({
changeMockUser({ values: {
id: 2, roles: ['test']
}
}); });
const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin;
const agent = app.agent().auth(userPlugin.jwtService.sign({
userId: user.get('id'),
}), { type: 'bearer' });
changeMockRole('test'); // @ts-ignore
const response = await agent.resource('roles').check();
const response = await app.agent().get('/roles:check');
expect(response.statusCode).toEqual(200); expect(response.statusCode).toEqual(200);
}); });

View File

@ -1,4 +1,5 @@
import { Database, Model } from '@nocobase/database'; import { Database, Model } from '@nocobase/database';
import UsersPlugin from '@nocobase/plugin-users';
import { CollectionRepository } from '@nocobase/plugin-collection-manager'; import { CollectionRepository } from '@nocobase/plugin-collection-manager';
import { MockServer } from '@nocobase/test'; import { MockServer } from '@nocobase/test';
import { prepareApp } from './prepare'; import { prepareApp } from './prepare';
@ -7,6 +8,8 @@ describe('role resource api', () => {
let app: MockServer; let app: MockServer;
let db: Database; let db: Database;
let role: Model; let role: Model;
let admin;
let adminAgent;
afterEach(async () => { afterEach(async () => {
await app.destroy(); await app.destroy();
@ -16,19 +19,23 @@ describe('role resource api', () => {
app = await prepareApp(); app = await prepareApp();
db = app.db; db = app.db;
await db.getRepository('roles').create({
values: {
name: 'admin',
title: 'Admin User',
allowConfigure: true,
},
});
role = await db.getRepository('roles').findOne({ role = await db.getRepository('roles').findOne({
filter: { filter: {
name: 'admin', name: 'admin',
}, },
}); });
const UserRepo = db.getCollection('users').repository;
admin = await UserRepo.create({
values: {
roles: ['admin']
}
});
const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin;
adminAgent = app.agent().auth(userPlugin.jwtService.sign({
userId: admin.get('id'),
}), { type: 'bearer' });
}); });
it('should grant resource by createRepository', async () => { it('should grant resource by createRepository', async () => {
@ -91,8 +98,7 @@ describe('role resource api', () => {
}); });
// get collections list // get collections list
let response = await app let response = await adminAgent
.agent()
.resource('roles.collections', 'admin') .resource('roles.collections', 'admin')
.list({ .list({
filter: { filter: {
@ -119,8 +125,7 @@ describe('role resource api', () => {
]); ]);
// set resource actions // set resource actions
response = await app response = await adminAgent
.agent()
.resource('roles.resources', 'admin') .resource('roles.resources', 'admin')
.create({ .create({
values: { values: {
@ -137,8 +142,7 @@ describe('role resource api', () => {
expect(response.statusCode).toEqual(200); expect(response.statusCode).toEqual(200);
// get collections list // get collections list
response = await app response = await adminAgent
.agent()
.resource('roles.collections') .resource('roles.collections')
.list({ .list({
associatedIndex: role.get('name') as string, associatedIndex: role.get('name') as string,
@ -149,8 +153,7 @@ describe('role resource api', () => {
expect(response.body.data[0]['usingConfig']).toEqual('resourceAction'); expect(response.body.data[0]['usingConfig']).toEqual('resourceAction');
response = await app response = await adminAgent
.agent()
.resource('roles.resources') .resource('roles.resources')
.list({ .list({
associatedIndex: role.get('name') as string, associatedIndex: role.get('name') as string,
@ -164,8 +167,7 @@ describe('role resource api', () => {
expect(resourceAction['name']).toEqual('create'); expect(resourceAction['name']).toEqual('create');
// update resource actions // update resource actions
response = await app response = await adminAgent
.agent()
.resource('roles.resources') .resource('roles.resources')
.update({ .update({
associatedIndex: role.get('name') as string, associatedIndex: role.get('name') as string,

View File

@ -2,7 +2,6 @@ import Database, { BelongsToManyRepository } from '@nocobase/database';
import PluginACL from '@nocobase/plugin-acl'; import PluginACL from '@nocobase/plugin-acl';
import UsersPlugin from '@nocobase/plugin-users'; import UsersPlugin from '@nocobase/plugin-users';
import { MockServer, mockServer } from '@nocobase/test'; import { MockServer, mockServer } from '@nocobase/test';
import { userPluginConfig } from './utils';
describe('role', () => { describe('role', () => {
let api: MockServer; let api: MockServer;
@ -13,7 +12,7 @@ describe('role', () => {
beforeEach(async () => { beforeEach(async () => {
api = mockServer(); api = mockServer();
await api.cleanDb(); await api.cleanDb();
api.plugin(UsersPlugin, userPluginConfig); api.plugin(UsersPlugin);
api.plugin(PluginACL); api.plugin(PluginACL);
await api.loadAndInstall(); await api.loadAndInstall();

View File

@ -1,6 +1,6 @@
import { MockServer } from '@nocobase/test'; import { MockServer } from '@nocobase/test';
import { CollectionRepository } from '@nocobase/plugin-collection-manager';
import { Database, Model } from '@nocobase/database'; import { Database, Model } from '@nocobase/database';
import UsersPlugin from '@nocobase/plugin-users';
import { prepareApp } from './prepare'; import { prepareApp } from './prepare';
@ -19,32 +19,37 @@ describe('role api', () => {
describe('grant', () => { describe('grant', () => {
let role: Model; let role: Model;
let admin: Model;
let adminAgent;
beforeEach(async () => { beforeEach(async () => {
await db.getRepository('roles').create({
values: {
name: 'admin',
title: 'Admin User',
allowConfigure: true,
},
});
role = await db.getRepository('roles').findOne({ role = await db.getRepository('roles').findOne({
filter: { filter: {
name: 'admin', name: 'admin',
}, },
}); });
const UserRepo = db.getCollection('users').repository;
admin = await UserRepo.create({
values: {
roles: ['admin']
}
});
const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin;
adminAgent = app.agent().auth(userPlugin.jwtService.sign({
userId: admin.get('id'),
}), { type: 'bearer' });
}); });
it('should list actions', async () => { it('should list actions', async () => {
const response = await app.agent().resource('availableActions').list(); const response = await adminAgent.resource('availableActions').list();
expect(response.statusCode).toEqual(200); expect(response.statusCode).toEqual(200);
}); });
it('should grant universal role actions', async () => { it('should grant universal role actions', async () => {
// grant role actions // grant role actions
const response = await app const response = await adminAgent
.agent()
.resource('roles') .resource('roles')
.update({ .update({
values: { values: {

View File

@ -1,11 +1,15 @@
import { prepareApp } from './prepare'; import { prepareApp } from './prepare';
import { MockServer } from '@nocobase/test'; import { MockServer } from '@nocobase/test';
import { Database } from '@nocobase/database'; import { Database } from '@nocobase/database';
import UsersPlugin from '@nocobase/plugin-users';
describe('scope api', () => { describe('scope api', () => {
let app: MockServer; let app: MockServer;
let db: Database; let db: Database;
let admin;
let adminAgent;
afterEach(async () => { afterEach(async () => {
await app.destroy(); await app.destroy();
}); });
@ -13,18 +17,22 @@ describe('scope api', () => {
beforeEach(async () => { beforeEach(async () => {
app = await prepareApp(); app = await prepareApp();
db = app.db; db = app.db;
await db.getRepository('roles').create({
const UserRepo = db.getCollection('users').repository;
admin = await UserRepo.create({
values: { values: {
name: 'admin', roles: ['admin']
title: 'Admin User', }
allowConfigure: true,
},
}); });
const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin;
adminAgent = app.agent().auth(userPlugin.jwtService.sign({
userId: admin.get('id'),
}), { type: 'bearer' });
}); });
it('should create scope of resource', async () => { it('should create scope of resource', async () => {
const response = await app const response = await adminAgent
.agent()
.resource('rolesResourcesScopes') .resource('rolesResourcesScopes')
.create({ .create({
values: { values: {

View File

@ -0,0 +1,83 @@
import Database from '@nocobase/database';
import UsersPlugin from '@nocobase/plugin-users';
import { MockServer } from '@nocobase/test';
import { setCurrentRole } from '../middlewares/setCurrentRole';
import { prepareApp } from './prepare';
describe('role', () => {
let api: MockServer;
let db: Database;
let usersPlugin: UsersPlugin;
let ctx;
beforeEach(async () => {
api = await prepareApp();
db = api.db;
usersPlugin = api.getPlugin('@nocobase/plugin-users');
ctx = {
db,
state: {
currentRole: '',
}
}
});
afterEach(async () => {
await api.destroy();
});
it('should set role with X-Role when exists', async () => {
ctx.state.currentUser = await db.getRepository('users').findOne({
appends: ['roles'],
});
ctx.get = function(name) {
if (name === 'X-Role') {
return 'admin';
}
};
await setCurrentRole(ctx, () => {});
expect(ctx.state.currentRole).toBe('admin');
});
it('should set role with default', async () => {
ctx.state.currentUser = await db.getRepository('users').findOne({
appends: ['roles'],
});
ctx.get = function (name) {
if (name === 'X-Role') {
return '';
}
};
await setCurrentRole(ctx, () => {});
expect(ctx.state.currentRole).toBe('root');
});
it('should set role with default when x-role does not exist', async () => {
ctx.state.currentUser = await db.getRepository('users').findOne({
appends: ['roles'],
});
ctx.get = function (name) {
if (name === 'X-Role') {
return 'abc';
}
};
await setCurrentRole(ctx, () => {});
expect(ctx.state.currentRole).toBe('root');
});
it('should set role with anonymous', async () => {
ctx.state.currentUser = await db.getRepository('users').findOne({
appends: ['roles'],
});
ctx.get = function (name) {
if (name === 'X-Role') {
return 'anonymous';
}
};
await setCurrentRole(ctx, () => {});
expect(ctx.state.currentRole).toBe('anonymous');
});
});

View File

@ -0,0 +1,45 @@
import { Context, Next } from '@nocobase/actions';
export async function setDefaultRole(ctx: Context, next: Next) {
const {
values: { roleName },
} = ctx.action.params;
const {
db,
state: { currentUser },
action: { params: { values } }
} = ctx;
if (values.roleName == 'anonymous') {
return next();
}
const repository = db.getRepository('rolesUsers');
await db.sequelize.transaction(async transaction => {
await repository.update({
filter: {
userId: currentUser.get('id'),
},
values: {
default: false,
},
transaction,
});
await repository.update({
filter: {
userId: currentUser.get('id'),
roleName,
},
values: {
default: true,
},
transaction,
});
});
ctx.body = 'ok';
await next();
}

View File

@ -0,0 +1,30 @@
import { extend } from '@nocobase/database';
export default extend({
name: 'users',
fields: [
{
interface: 'm2m',
type: 'belongsToMany',
name: 'roles',
target: 'roles',
foreignKey: 'userId',
otherKey: 'roleName',
sourceKey: 'id',
targetKey: 'name',
through: 'rolesUsers',
uiSchema: {
type: 'array',
title: '{{t("Roles")}}',
'x-component': 'RecordPicker',
'x-component-props': {
multiple: true,
fieldNames: {
label: 'title',
value: 'name',
},
},
},
}
],
});

View File

@ -0,0 +1,35 @@
export async function setCurrentRole(ctx, next) {
if (!ctx.state.currentUser) {
return next();
}
let currentRole = ctx.get('X-Role');
if (currentRole === 'anonymous') {
ctx.state.currentRole = currentRole;
return next();
}
const RolesUsers = ctx.db.getCollection('rolesUsers').model;
const userRoles = await RolesUsers.findAll({
where: {
userId: ctx.state.currentUser.id
}
});
if (userRoles.length == 1) {
currentRole = userRoles[0].roleName;
} else if (userRoles.length > 1) {
const role = userRoles.find((item) => item.roleName === currentRole);
if (!role) {
const defaultRole = userRoles.find((item) => item.default);
currentRole = (defaultRole || userRoles[0])?.roleName;
}
}
if (currentRole) {
ctx.state.currentRole = currentRole;
}
await next();
}

View File

@ -1,10 +1,13 @@
import { Context } from '@nocobase/actions'; import { Context } from '@nocobase/actions';
import { Collection } from '@nocobase/database'; import { Collection } from '@nocobase/database';
import UsersPlugin from '@nocobase/plugin-users';
import { Plugin } from '@nocobase/server'; import { Plugin } from '@nocobase/server';
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';
import { roleCollectionsResource } from './actions/role-collections'; import { roleCollectionsResource } from './actions/role-collections';
import { setDefaultRole } from './actions/user-setDefaultRole';
import { setCurrentRole } from './middlewares/setCurrentRole';
import { RoleModel } from './model/RoleModel'; import { RoleModel } from './model/RoleModel';
import { RoleResourceActionModel } from './model/RoleResourceActionModel'; import { RoleResourceActionModel } from './model/RoleResourceActionModel';
import { RoleResourceModel } from './model/RoleResourceModel'; import { RoleResourceModel } from './model/RoleResourceModel';
@ -134,6 +137,22 @@ export class PluginACL extends Plugin {
this.app.resourcer.registerActionHandler('roles:check', checkAction); this.app.resourcer.registerActionHandler('roles:check', checkAction);
this.app.resourcer.registerActionHandler(`users:setDefaultRole`, setDefaultRole);
this.db.on('users.afterCreateWithAssociations', async (model, options) => {
const { transaction } = options;
const repository = this.app.db.getRepository('roles');
const defaultRole = await repository.findOne({
filter: {
default: true,
},
transaction,
});
if (defaultRole && (await model.countRoles({ transaction })) == 0) {
await model.addRoles(defaultRole, { transaction });
}
});
this.app.db.on('roles.afterSaveWithAssociations', async (model, options) => { this.app.db.on('roles.afterSaveWithAssociations', async (model, options) => {
const { transaction } = options; const { transaction } = options;
@ -271,7 +290,7 @@ export class PluginACL extends Plugin {
title: '{{t("Admin")}}', title: '{{t("Admin")}}',
allowConfigure: true, allowConfigure: true,
allowNewMenu: true, allowNewMenu: true,
strategy: { actions: ['create', 'export', 'view', 'update', 'destroy'] }, strategy: { actions: ['create', 'view', 'update', 'destroy'] },
}, },
{ {
name: 'member', name: 'member',
@ -301,6 +320,11 @@ export class PluginACL extends Plugin {
}); });
}); });
const usersPlugin = this.app.pm.get('@nocobase/plugin-users') as UsersPlugin;
usersPlugin.tokenMiddleware.use(setCurrentRole);
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', ['create', 'update', 'destroy'], 'allowConfigure');
@ -392,6 +416,24 @@ export class PluginACL extends Plugin {
if (repo) { if (repo) {
await repo.db2cm('roles'); await repo.db2cm('roles');
} }
const User = this.db.getCollection('users');
await User.repository.update({
values: {
roles: ['root', 'admin', 'member']
}
});
const RolesUsers = this.db.getCollection('rolesUsers');
await RolesUsers.repository.update({
filter: {
userId: 1,
roleName: 'root'
},
values: {
default: true
}
});
} }
async load() { async load() {

View File

@ -1,4 +1,5 @@
import { BelongsToManyRepository, Database } from '@nocobase/database'; import { BelongsToManyRepository, Database } from '@nocobase/database';
import PluginUsers from '@nocobase/plugin-users';
import PluginACL from '@nocobase/plugin-acl'; import PluginACL from '@nocobase/plugin-acl';
import PluginCollectionManager from '@nocobase/plugin-collection-manager'; import PluginCollectionManager from '@nocobase/plugin-collection-manager';
import UiSchemaStoragePlugin, { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage'; import UiSchemaStoragePlugin, { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage';
@ -24,6 +25,7 @@ describe('server hooks', () => {
app.plugin(UiSchemaStoragePlugin); app.plugin(UiSchemaStoragePlugin);
app.plugin(PluginCollectionManager); app.plugin(PluginCollectionManager);
app.plugin(PluginUsers);
app.plugin(PluginACL); app.plugin(PluginACL);
await app.loadAndInstall(); await app.loadAndInstall();

View File

@ -1,104 +0,0 @@
import Database from '@nocobase/database';
import PluginACL from '@nocobase/plugin-acl';
import UsersPlugin from '@nocobase/plugin-users';
import { MockServer, mockServer } from '@nocobase/test';
import { setCurrentRole } from '../middlewares/parseToken';
import { userPluginConfig } from './utils';
describe('role', () => {
let api: MockServer;
let db: Database;
let usersPlugin: UsersPlugin;
beforeEach(async () => {
api = mockServer();
await api.cleanDb();
api.plugin(UsersPlugin, userPluginConfig);
api.plugin(PluginACL);
await api.loadAndInstall();
db = api.db;
usersPlugin = api.getPlugin('@nocobase/plugin-users');
});
afterEach(async () => {
await api.destroy();
});
it('should set role with X-Role when exists', async () => {
const currentUser = await db.getRepository('users').findOne({
appends: ['roles'],
});
const ctx = {
get(name) {
if (name === 'X-Role') {
return 'admin';
}
},
state: {
currentUser,
currentRole: '',
}
}
setCurrentRole(ctx);
expect(ctx.state.currentRole).toBe('admin');
});
it('should set role with default', async () => {
const currentUser = await db.getRepository('users').findOne({
appends: ['roles'],
});
const ctx = {
get(name) {
if (name === 'X-Role') {
return '';
}
},
state: {
currentUser,
currentRole: '',
}
}
setCurrentRole(ctx);
expect(ctx.state.currentRole).toBe('root');
});
it('should set role with default when x-role does not exist', async () => {
const currentUser = await db.getRepository('users').findOne({
appends: ['roles'],
});
const ctx = {
get(name) {
if (name === 'X-Role') {
return 'abc';
}
},
state: {
currentUser,
currentRole: '',
}
}
setCurrentRole(ctx);
expect(ctx.state.currentRole).toBe('root');
});
it('should set role with anonymous', async () => {
const currentUser = await db.getRepository('users').findOne({
appends: ['roles'],
});
const ctx = {
get(name) {
if (name === 'X-Role') {
return 'anonymous';
}
},
state: {
currentUser,
currentRole: '',
}
}
setCurrentRole(ctx);
expect(ctx.state.currentRole).toBe('anonymous');
});
});

View File

@ -150,15 +150,3 @@ 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: { roleName },
} = ctx.action.params;
await ctx.state.currentUser.setDefaultRole(roleName);
ctx.body = 'ok';
await next();
}

View File

@ -51,29 +51,6 @@ export default {
'x-component': 'Password', 'x-component': 'Password',
}, },
}, },
{
interface: 'm2m',
type: 'belongsToMany',
name: 'roles',
target: 'roles',
foreignKey: 'userId',
otherKey: 'roleName',
sourceKey: 'id',
targetKey: 'name',
through: 'rolesUsers',
uiSchema: {
type: 'array',
title: '{{t("Roles")}}',
'x-component': 'RecordPicker',
'x-component-props': {
multiple: true,
fieldNames: {
label: 'title',
value: 'name',
},
},
},
},
{ {
type: 'string', type: 'string',
name: 'appLang', name: 'appLang',

View File

@ -1,40 +1,17 @@
import { Context, Next } from '@nocobase/actions'; import { Context, Next } from '@nocobase/actions';
import { MiddlewareManager } from '@nocobase/resourcer';
import UsersPlugin from '../server'; import UsersPlugin from '../server';
export function parseToken(options?: { plugin: UsersPlugin }) { export function parseToken(options?: { plugin: UsersPlugin }) {
return async function parseToken(ctx: Context, next: Next) { const middleware = new MiddlewareManager();
middleware.use(async function (ctx: Context, next: Next) {
const user = await findUserByToken(ctx, options.plugin); const user = await findUserByToken(ctx, options.plugin);
if (user) { if (user) {
ctx.state.currentUser = user; ctx.state.currentUser = user;
setCurrentRole(ctx);
} }
return next(); return next();
}; });
} return middleware;
export function setCurrentRole(ctx) {
let currentRole = ctx.get('X-Role');
if (currentRole === 'anonymous') {
ctx.state.currentRole = currentRole;
return;
}
const userRoles = ctx.state.currentUser.roles;
if (userRoles.length == 1) {
currentRole = userRoles[0].name;
} else if (userRoles.length > 1) {
const role = userRoles.find((role) => role.name === currentRole);
if (!role) {
const defaultRole = userRoles.find((role) => role?.rolesUsers?.default);
currentRole = (defaultRole || userRoles[0])?.name;
}
}
if (currentRole) {
ctx.state.currentRole = currentRole;
}
} }
async function findUserByToken(ctx: Context, plugin: UsersPlugin) { async function findUserByToken(ctx: Context, plugin: UsersPlugin) {
@ -46,7 +23,7 @@ async function findUserByToken(ctx: Context, plugin: UsersPlugin) {
try { try {
const { userId } = await plugin.jwtService.decode(token); const { userId } = await plugin.jwtService.decode(token);
const collection = ctx.db.getCollection('users'); const collection = ctx.db.getCollection('users');
const appends = ['roles']; const appends = [];
for (const [, field] of collection.fields) { for (const [, field] of collection.fields) {
if (field.type === 'belongsTo') { if (field.type === 'belongsTo') {
appends.push(field.name); appends.push(field.name);

View File

@ -1,44 +0,0 @@
import Database, { Model, Transactionable } from '@nocobase/database';
export class UserModel extends Model {
async setDefaultRole(roleName: string, options: Transactionable = {}) {
if (roleName == 'anonymous') {
return false;
}
const db = (this.constructor as any).database as Database;
const repository = db.getRepository('rolesUsers');
if (!repository) {
return false;
}
const transaction = options.transaction || (await db.sequelize.transaction());
try {
await repository.update({
filter: {
userId: this.get('id'),
},
values: {
default: false,
},
transaction,
});
await repository.update({
filter: {
userId: this.get('id'),
roleName,
},
values: {
default: true,
},
transaction,
});
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
return true;
}
}

View File

@ -7,7 +7,6 @@ import * as actions from './actions/users';
import { JwtOptions, JwtService } from './jwt-service'; import { JwtOptions, JwtService } from './jwt-service';
import { enUS, zhCN } from './locale'; import { enUS, zhCN } from './locale';
import * as middlewares from './middlewares'; import * as middlewares from './middlewares';
import { UserModel } from './models/UserModel';
export interface UserPluginConfig { export interface UserPluginConfig {
jwt: JwtOptions; jwt: JwtOptions;
@ -16,9 +15,12 @@ export interface UserPluginConfig {
export default class UsersPlugin extends Plugin<UserPluginConfig> { export default class UsersPlugin extends Plugin<UserPluginConfig> {
public jwtService: JwtService; public jwtService: JwtService;
public tokenMiddleware;
constructor(app, options) { constructor(app, options) {
super(app, options); super(app, options);
this.jwtService = new JwtService(options?.jwt || {}); this.jwtService = new JwtService(options?.jwt || {});
this.tokenMiddleware = middlewares.parseToken({ plugin: this });
} }
async beforeLoad() { async beforeLoad() {
@ -43,23 +45,6 @@ export default class UsersPlugin extends Plugin<UserPluginConfig> {
}; };
}, },
}); });
this.db.registerModels({ UserModel });
this.db.on('users.afterCreateWithAssociations', async (model, options) => {
const { transaction } = options;
const repository = this.app.db.getRepository('roles');
if (!repository) {
return;
}
const defaultRole = await repository.findOne({
filter: {
default: true,
},
transaction,
});
if (defaultRole && (await model.countRoles({ transaction })) == 0) {
await model.addRoles(defaultRole, { transaction });
}
});
this.db.on('afterDefineCollection', (collection: Collection) => { this.db.on('afterDefineCollection', (collection: Collection) => {
let { createdBy, updatedBy } = collection.options; let { createdBy, updatedBy } = collection.options;
@ -100,10 +85,10 @@ export default class UsersPlugin extends Plugin<UserPluginConfig> {
this.app.resourcer.registerActionHandler(`users:${key}`, action); this.app.resourcer.registerActionHandler(`users:${key}`, action);
} }
this.app.resourcer.use(middlewares.parseToken({ plugin: this })); this.app.resourcer.use(this.tokenMiddleware.compose());
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'];
publicActions.forEach((action) => this.app.acl.allow('users', action)); publicActions.forEach((action) => this.app.acl.allow('users', action));
loggedInActions.forEach((action) => this.app.acl.allow('users', action, 'loggedIn')); loggedInActions.forEach((action) => this.app.acl.allow('users', action, 'loggedIn'));
@ -132,17 +117,14 @@ export default class UsersPlugin extends Plugin<UserPluginConfig> {
async install(options) { async install(options) {
const { rootNickname, rootPassword, rootEmail } = this.getInstallingData(options); const { rootNickname, rootPassword, rootEmail } = this.getInstallingData(options);
const User = this.db.getCollection('users'); const User = this.db.getCollection('users');
const user = await User.repository.create<UserModel>({ const user = await User.repository.create({
values: { values: {
email: rootEmail, email: rootEmail,
password: rootPassword, password: rootPassword,
nickname: rootNickname, nickname: rootNickname
roles: ['root', 'admin', 'member'],
}, },
}); });
await user.setDefaultRole('root');
const repo = this.db.getRepository<any>('collections'); const repo = this.db.getRepository<any>('collections');
if (repo) { if (repo) {
await repo.db2cm('users'); await repo.db2cm('users');