From 49a4ab48180ddc0332bcf43086dd18c5192bf3f2 Mon Sep 17 00:00:00 2001 From: Junyi Date: Mon, 25 Jul 2022 19:33:23 +0800 Subject: [PATCH] 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 --- packages/core/acl/src/allow-manager.ts | 4 +- packages/core/resourcer/src/middleware.ts | 17 ++ packages/plugins/acl/package.json | 1 + .../plugins/acl/src/__tests__/acl.test.ts | 197 ++++++++--------- .../src/__tests__/association-field.test.ts | 205 +++++++++--------- .../acl/src/__tests__/configuration.test.ts | 73 +++++-- .../acl/src/__tests__/middleware.test.ts | 61 +++--- .../plugins/acl/src/__tests__/own.test.ts | 74 +++---- packages/plugins/acl/src/__tests__/prepare.ts | 17 +- .../acl/src/__tests__/role-check.test.ts | 20 +- .../acl/src/__tests__/role-resource.test.ts | 38 ++-- .../src/__tests__/role-user.test.ts} | 5 +- .../plugins/acl/src/__tests__/role.test.ts | 29 ++- .../plugins/acl/src/__tests__/scope.test.ts | 22 +- .../acl/src/__tests__/setCurrentRole.test.ts | 83 +++++++ .../acl/src/actions/user-setDefaultRole.ts | 45 ++++ .../src/collections/roles-users.ts | 0 packages/plugins/acl/src/collections/users.ts | 30 +++ .../acl/src/middlewares/setCurrentRole.ts | 35 +++ packages/plugins/acl/src/server.ts | 44 +++- .../src/__tests__/server-hook-impl.test.ts | 2 + .../src/__tests__/setCurrentRole.test.ts | 104 --------- packages/plugins/users/src/actions/users.ts | 12 - .../plugins/users/src/collections/users.ts | 23 -- .../users/src/middlewares/parseToken.ts | 35 +-- .../plugins/users/src/models/UserModel.ts | 44 ---- packages/plugins/users/src/server.ts | 32 +-- 27 files changed, 649 insertions(+), 603 deletions(-) rename packages/plugins/{users/src/__tests__/role.test.ts => acl/src/__tests__/role-user.test.ts} (96%) create mode 100644 packages/plugins/acl/src/__tests__/setCurrentRole.test.ts create mode 100644 packages/plugins/acl/src/actions/user-setDefaultRole.ts rename packages/plugins/{users => acl}/src/collections/roles-users.ts (100%) create mode 100644 packages/plugins/acl/src/collections/users.ts create mode 100644 packages/plugins/acl/src/middlewares/setCurrentRole.ts delete mode 100644 packages/plugins/users/src/__tests__/setCurrentRole.test.ts delete mode 100644 packages/plugins/users/src/models/UserModel.ts diff --git a/packages/core/acl/src/allow-manager.ts b/packages/core/acl/src/allow-manager.ts index de03b3959..84f80e8e4 100644 --- a/packages/core/acl/src/allow-manager.ts +++ b/packages/core/acl/src/allow-manager.ts @@ -19,7 +19,9 @@ export class AllowManager { } const roleInstance = await ctx.db.getRepository('roles').findOne({ - name: roleName, + filter: { + name: roleName + }, }); return roleInstance?.get('allowConfigure'); diff --git a/packages/core/resourcer/src/middleware.ts b/packages/core/resourcer/src/middleware.ts index 5e5632796..957b05121 100644 --- a/packages/core/resourcer/src/middleware.ts +++ b/packages/core/resourcer/src/middleware.ts @@ -1,6 +1,7 @@ import { ActionName } from './action'; import { requireModule } from './utils'; import { HandlerType } from './resourcer'; +import compose from 'koa-compose'; export type MiddlewareType = string | string[] | HandlerType | HandlerType[] | MiddlewareOptions | MiddlewareOptions[]; @@ -73,3 +74,19 @@ export class 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); + } +} diff --git a/packages/plugins/acl/package.json b/packages/plugins/acl/package.json index c6d8dd129..545ed132b 100644 --- a/packages/plugins/acl/package.json +++ b/packages/plugins/acl/package.json @@ -14,6 +14,7 @@ "dependencies": { "@nocobase/acl": "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" }, "repository": { diff --git a/packages/plugins/acl/src/__tests__/acl.test.ts b/packages/plugins/acl/src/__tests__/acl.test.ts index ccd597bde..8e405504e 100644 --- a/packages/plugins/acl/src/__tests__/acl.test.ts +++ b/packages/plugins/acl/src/__tests__/acl.test.ts @@ -1,13 +1,16 @@ import { ACL } from '@nocobase/acl'; import { Database } from '@nocobase/database'; 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 { prepareApp } from './prepare'; describe('acl', () => { let app: MockServer; let db: Database; let acl: ACL; + let admin; + let adminAgent; let uiSchemaRepository: UiSchemaRepository; @@ -20,38 +23,41 @@ describe('acl', () => { db = app.db; 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'); }); it('should works with universal actions', async () => { await db.getRepository('roles').create({ values: { - name: 'admin', - title: 'Admin User', - allowConfigure: true, - }, - }); - - const role = await db.getRepository('roles').findOne({ - filter: { - name: 'admin', + name: 'new' }, }); expect( acl.can({ - role: 'admin', + role: 'new', resource: 'posts', action: 'create', }), ).toBeNull(); // grant universal action - await app - .agent() + await adminAgent .resource('roles') .update({ - resourceIndex: 'admin', + resourceIndex: 'new', values: { strategy: { actions: ['create'], @@ -61,31 +67,27 @@ describe('acl', () => { expect( acl.can({ - role: 'admin', + role: 'new', resource: 'posts', action: 'create', }), ).toMatchObject({ - role: 'admin', + role: 'new', resource: 'posts', action: 'create', }); }); it('should deny when resource action has no resource', async () => { - const role = await db.getRepository('roles').create({ + await db.getRepository('roles').create({ values: { - name: 'admin', - title: 'Admin User', - allowConfigure: true, + name: 'new', strategy: { actions: ['update:own', 'destroy:own', 'create', 'view'], }, }, }); - changeMockRole('admin'); - // create c1 collection await db.getRepository('collections').create({ values: { @@ -102,9 +104,8 @@ describe('acl', () => { }, }); - await app - .agent() - .resource('roles.resources', 'admin') + await adminAgent + .resource('roles.resources', 'new') .create({ values: { name: 'c1', @@ -115,7 +116,7 @@ describe('acl', () => { expect( acl.can({ - role: 'admin', + role: 'new', resource: 'c1', action: 'list', }), @@ -125,17 +126,13 @@ describe('acl', () => { it('should works with resources actions', async () => { const role = await db.getRepository('roles').create({ values: { - name: 'admin', - title: 'Admin User', - allowConfigure: true, + name: 'new', strategy: { actions: ['list'], }, }, }); - changeMockRole('admin'); - // create c1 collection await db.getRepository('collections').create({ values: { @@ -153,8 +150,7 @@ describe('acl', () => { }); // create c1 published scope - await app - .agent() + const { body: { data: publishedScope } } = await adminAgent .resource('rolesResourcesScopes') .create({ values: { @@ -166,12 +162,11 @@ describe('acl', () => { }, }); - const publishedScope = await db.getRepository('rolesResourcesScopes').findOne(); + // await db.getRepository('rolesResourcesScopes').findOne(); // set admin resources - await app - .agent() - .resource('roles.resources', 'admin') + await adminAgent + .resource('roles.resources', 'new') .create({ values: { name: 'c1', @@ -179,7 +174,7 @@ describe('acl', () => { actions: [ { name: 'create', - scope: publishedScope.get('id'), + scope: publishedScope.id, }, { name: 'view', @@ -191,12 +186,12 @@ describe('acl', () => { expect( acl.can({ - role: 'admin', + role: 'new', resource: 'c1', action: 'create', }), ).toMatchObject({ - role: 'admin', + role: 'new', resource: 'c1', action: 'create', params: { @@ -206,12 +201,12 @@ describe('acl', () => { expect( acl.can({ - role: 'admin', + role: 'new', resource: 'c1', action: 'view', }), ).toMatchObject({ - role: 'admin', + role: 'new', resource: 'c1', action: 'view', params: { @@ -220,8 +215,7 @@ describe('acl', () => { }); // revoke action - const response = await app - .agent() + const response = await adminAgent .resource('roles.resources', role.get('name')) .list({ appends: ['actions'], @@ -232,8 +226,7 @@ describe('acl', () => { const actions = response.body.data[0].actions; const collectionName = response.body.data[0].name; - await app - .agent() + await adminAgent .resource('roles.resources', role.get('name')) .update({ filterByTk: collectionName, @@ -251,7 +244,7 @@ describe('acl', () => { expect( acl.can({ - role: 'admin', + role: 'new', resource: 'c1', action: 'create', }), @@ -259,11 +252,9 @@ describe('acl', () => { }); it('should revoke resource when collection destroy', async () => { - const role = await db.getRepository('roles').create({ + await db.getRepository('roles').create({ values: { - name: 'admin', - title: 'Admin User', - allowConfigure: true, + name: 'new' }, }); @@ -281,11 +272,10 @@ describe('acl', () => { }, }); - await app - .agent() + await adminAgent .resource('roles.resources') .create({ - associatedIndex: role.get('name') as string, + associatedIndex: 'new', values: { name: 'posts', usingActionsConfig: true, @@ -300,7 +290,7 @@ describe('acl', () => { expect( acl.can({ - role: 'admin', + role: 'new', resource: 'posts', action: 'view', }), @@ -314,7 +304,7 @@ describe('acl', () => { expect( acl.can({ - role: 'admin', + role: 'new', resource: 'posts', action: 'view', }), @@ -324,15 +314,7 @@ describe('acl', () => { it('should revoke actions when not using actions config', async () => { await db.getRepository('roles').create({ values: { - name: 'admin', - title: 'Admin User', - allowConfigure: true, - }, - }); - - const role = await db.getRepository('roles').findOne({ - filter: { - name: 'admin', + name: 'new' }, }); @@ -343,11 +325,10 @@ describe('acl', () => { }, }); - await app - .agent() + await adminAgent .resource('roles.resources') .create({ - associatedIndex: role.get('name') as string, + associatedIndex: 'new', values: { name: 'posts', usingActionsConfig: true, @@ -361,25 +342,24 @@ describe('acl', () => { expect( acl.can({ - role: 'admin', + role: 'new', resource: 'posts', action: 'create', }), ).toMatchObject({ - role: 'admin', + role: 'new', resource: 'posts', action: 'create', }); - await app - .agent() - .resource('roles.resources', role.get('name')) + await adminAgent + .resource('roles.resources', 'new') .update({ filterByTk: ( await db.getRepository('rolesResources').findOne({ filter: { name: 'posts', - roleName: 'admin', + roleName: 'new', }, }) ).get('name') as string, @@ -390,21 +370,20 @@ describe('acl', () => { expect( acl.can({ - role: 'admin', + role: 'new', resource: 'posts', action: 'create', }), ).toBeNull(); - await app - .agent() - .resource('roles.resources', role.get('name')) + await adminAgent + .resource('roles.resources', 'new') .update({ filterByTk: ( await db.getRepository('rolesResources').findOne({ filter: { name: 'posts', - roleName: 'admin', + roleName: 'new', }, }) ).get('name') as string, @@ -415,23 +394,21 @@ describe('acl', () => { expect( acl.can({ - role: 'admin', + role: 'new', resource: 'posts', action: 'create', }), ).toMatchObject({ - role: 'admin', + role: 'new', resource: 'posts', action: 'create', }); }); it('should add fields when field created', async () => { - const role = await db.getRepository('roles').create({ + await db.getRepository('roles').create({ values: { - name: 'admin', - title: 'Admin User', - allowConfigure: true, + name: 'new' }, }); @@ -449,11 +426,10 @@ describe('acl', () => { }, }); - await app - .agent() + await adminAgent .resource('roles.resources') .create({ - associatedIndex: role.get('name') as string, + associatedIndex: 'new', values: { name: 'posts', usingActionsConfig: true, @@ -467,7 +443,7 @@ describe('acl', () => { }); const allowFields = acl.can({ - role: 'admin', + role: 'new', resource: 'posts', action: 'view', })['params']['fields']; @@ -483,7 +459,7 @@ describe('acl', () => { }); const newAllowFields = acl.can({ - role: 'admin', + role: 'new', resource: 'posts', action: 'view', })['params']['fields']; @@ -494,18 +470,14 @@ describe('acl', () => { it('should get role menus', async () => { const role = await db.getRepository('roles').create({ values: { - name: 'admin', - title: 'Admin User', - allowConfigure: true, + name: 'new', strategy: { actions: ['view'], }, }, }); - changeMockRole('admin'); - - const menuResponse = await app.agent().resource('roles.menuUiSchemas', 'admin').list(); + const menuResponse = await adminAgent.resource('roles.menuUiSchemas', 'new').list(); expect(menuResponse.statusCode).toEqual(200); }); @@ -513,16 +485,23 @@ describe('acl', () => { it('should toggle role menus', async () => { const role = await db.getRepository('roles').create({ values: { - name: 'admin', - title: 'Admin User', - allowConfigure: true, + name: 'new', strategy: { 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 = { 'x-uid': 'test', @@ -530,9 +509,9 @@ describe('acl', () => { await uiSchemaRepository.insert(schema); - const response = await app - .agent() - .resource('roles.menuUiSchemas', 'admin') + const response = await userAgent + // @ts-ignore + .resource('roles.menuUiSchemas', 'new') .toggle({ values: { tk: 'test' }, }); @@ -543,9 +522,7 @@ describe('acl', () => { it('should sync data to acl before app start', async () => { const role = await db.getRepository('roles').create({ values: { - name: 'admin', - title: 'Admin User', - allowConfigure: true, + name: 'new', resources: [ { name: 'posts', @@ -562,20 +539,20 @@ describe('acl', () => { hooks: false, }); - expect(acl.getRole('admin')).toBeUndefined(); + expect(acl.getRole('new')).toBeUndefined(); await app.start(); - expect(acl.getRole('admin')).toBeDefined(); + expect(acl.getRole('new')).toBeDefined(); expect( acl.can({ - role: 'admin', + role: 'new', resource: 'posts', action: 'view', }), ).toMatchObject({ - role: 'admin', + role: 'new', resource: 'posts', action: 'view', }); diff --git a/packages/plugins/acl/src/__tests__/association-field.test.ts b/packages/plugins/acl/src/__tests__/association-field.test.ts index 4dfb54b05..c4937dbe0 100644 --- a/packages/plugins/acl/src/__tests__/association-field.test.ts +++ b/packages/plugins/acl/src/__tests__/association-field.test.ts @@ -1,5 +1,6 @@ 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 { prepareApp } from './prepare'; @@ -8,7 +9,10 @@ describe('association field acl', () => { let db: Database; let acl: ACL; - let role: Model; + let user; + let userAgent; + let admin; + let adminAgent; afterEach(async () => { await app.destroy(); @@ -19,20 +23,44 @@ describe('association field acl', () => { db = app.db; acl = app.acl; - role = await db.getRepository('roles').create({ + await db.getRepository('roles').create({ values: { - name: 'admin', - title: 'Admin User', + name: 'new', allowConfigure: true, }, }); - await db.getRepository('collections').create({ + await db.getRepository('roles').create({ 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({ values: { @@ -75,56 +103,48 @@ describe('association field acl', () => { context: {}, }); - await app - .agent() - .resource('roles.resources') - .create({ - associatedIndex: 'admin', - values: { - name: 'users', - usingActionsConfig: true, - actions: [ - { - name: 'create', - fields: ['orders'], - }, - { - name: 'view', - fields: ['orders'], - }, - ], - }, - }); + await adminAgent.resource('roles.resources', 'new').create({ + values: { + name: 'users', + usingActionsConfig: true, + actions: [ + { + name: 'create', + fields: ['orders'], + }, + { + name: 'view', + fields: ['orders'], + }, + ], + }, + }); }); it('should revoke target action on association action revoke', async () => { expect( acl.can({ - role: 'admin', + role: 'new', resource: 'orders', action: 'list', }), ).toMatchObject({ - role: 'admin', + role: 'new', resource: 'orders', action: 'list', }); - await app - .agent() - .resource('roles.resources') - .update({ - associatedIndex: 'admin', - values: { - name: 'users', - usingActionsConfig: true, - actions: [], - }, - }); + await adminAgent.resource('roles.resources', 'new').update({ + values: { + name: 'users', + usingActionsConfig: true, + actions: [], + }, + }); expect( acl.can({ - role: 'admin', + role: 'new', resource: 'orders', action: 'list', }), @@ -134,12 +154,12 @@ describe('association field acl', () => { it('should revoke association action on action revoke', async () => { expect( acl.can({ - role: 'admin', + role: 'new', resource: 'users.orders', action: 'add', }), ).toMatchObject({ - role: 'admin', + role: 'new', resource: 'users.orders', action: 'add', }); @@ -152,27 +172,23 @@ describe('association field acl', () => { const actionId = viewAction.get('id') as number; - const response = await app - .agent() - .resource('roles.resources') - .update({ - associatedIndex: 'admin', - values: { - name: 'users', - usingActionsConfig: true, - actions: [ - { - id: actionId, - }, - ], - }, - }); + const response = await adminAgent.resource('roles.resources', 'new').update({ + values: { + name: 'users', + usingActionsConfig: true, + actions: [ + { + id: actionId, + }, + ], + }, + }); expect(response.statusCode).toEqual(200); expect( acl.can({ - role: 'admin', + role: 'new', resource: 'users.orders', action: 'add', }), @@ -180,30 +196,26 @@ describe('association field acl', () => { }); it('should revoke association action on field deleted', async () => { - await app - .agent() - .resource('roles.resources') - .update({ - associatedIndex: 'admin', - values: { - name: 'users', - usingActionsConfig: true, - actions: [ - { - name: 'create', - fields: ['name', 'age'], - }, - ], - }, - }); + await adminAgent.resource('roles.resources', 'new').update({ + values: { + name: 'users', + usingActionsConfig: true, + actions: [ + { + name: 'create', + fields: ['name', 'age'], + }, + ], + }, + }); expect( acl.can({ - role: 'admin', + role: 'new', resource: 'users', action: 'create', }), ).toMatchObject({ - role: 'admin', + role: 'new', resource: 'users', action: 'create', params: { @@ -236,12 +248,12 @@ describe('association field acl', () => { expect( acl.can({ - role: 'admin', + role: 'new', resource: 'users', action: 'create', }), ).toMatchObject({ - role: 'admin', + role: 'new', resource: 'users', action: 'create', params: { @@ -251,45 +263,44 @@ describe('association field acl', () => { }); it('should allow association fields access', async () => { - const createResponse = await app - .agent() - .resource('users') - .create({ - values: { - orders: [ - { - content: 'apple', - }, - ], - }, - }); + const createResponse = await userAgent.resource('users').create({ + values: { + orders: [ + { + content: 'apple', + }, + ], + }, + }); 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 expect(await user.countOrders()).toEqual(1); expect( acl.can({ - role: 'admin', + role: 'new', resource: 'users.orders', action: 'list', }), ).toMatchObject({ - role: 'admin', + role: 'new', resource: 'users.orders', action: 'list', }); expect( acl.can({ - role: 'admin', + role: 'new', resource: 'orders', action: 'list', }), ).toMatchObject({ - role: 'admin', + role: 'new', resource: 'orders', action: 'list', }); diff --git a/packages/plugins/acl/src/__tests__/configuration.test.ts b/packages/plugins/acl/src/__tests__/configuration.test.ts index d769e560e..d7066d2dc 100644 --- a/packages/plugins/acl/src/__tests__/configuration.test.ts +++ b/packages/plugins/acl/src/__tests__/configuration.test.ts @@ -1,15 +1,16 @@ -import { MockServer } from '@nocobase/test'; import { Database } from '@nocobase/database'; -import { ACL } from '@nocobase/acl'; -import { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage'; -import { changeMockRole, prepareApp } from './prepare'; +import UsersPlugin from '@nocobase/plugin-users'; +import { MockServer } from '@nocobase/test'; +import { prepareApp } from './prepare'; describe('configuration', () => { let app: MockServer; let db: Database; - let acl: ACL; - - let uiSchemaRepository: UiSchemaRepository; + let admin; + let adminAgent; + let user; + let userAgent; + let guestAgent; afterEach(async () => { await app.destroy(); @@ -18,28 +19,56 @@ describe('configuration', () => { beforeEach(async () => { app = await prepareApp(); db = app.db; - acl = app.acl; - uiSchemaRepository = db.getRepository('uiSchemas'); - }); - - it('should list collections', async () => { - expect((await app.agent().resource('collections').create()).statusCode).toEqual(403); - expect((await app.agent().resource('collections').list()).statusCode).toEqual(200); - }); - - it('should allow when role has allowConfigure with true value', async () => { await db.getRepository('roles').create({ values: { - name: 'admin1', - title: 'admin allowConfigure', + name: 'test1', allowConfigure: true, }, }); - changeMockRole('admin1'); + await db.getRepository('roles').create({ + values: { + name: 'test2', + }, + }); - expect((await app.agent().resource('collections').create()).statusCode).toEqual(200); - expect((await app.agent().resource('collections').list()).statusCode).toEqual(200); + const UserRepo = db.getCollection('users').repository; + 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); }); }); diff --git a/packages/plugins/acl/src/__tests__/middleware.test.ts b/packages/plugins/acl/src/__tests__/middleware.test.ts index 725da250a..520aa045b 100644 --- a/packages/plugins/acl/src/__tests__/middleware.test.ts +++ b/packages/plugins/acl/src/__tests__/middleware.test.ts @@ -1,33 +1,40 @@ import { ACL } from '@nocobase/acl'; import { Database, Model } from '@nocobase/database'; import { MockServer } from '@nocobase/test'; -import { changeMockUser, prepareApp } from './prepare'; +import UsersPlugin from '@nocobase/plugin-users'; +import { prepareApp } from './prepare'; describe('middleware', () => { let app: MockServer; let role: Model; let db: Database; let acl: ACL; + let admin; + let adminAgent; beforeEach(async () => { app = await prepareApp(); db = app.db; acl = app.acl; - await db.getRepository('roles').create({ - values: { - name: 'admin', - title: 'Admin User', - allowConfigure: true, - }, - }); - role = await db.getRepository('roles').findOne({ filter: { 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({ values: { name: 'posts', @@ -82,7 +89,7 @@ describe('middleware', () => { }, }); - const response = await app.agent().resource('posts').create({ + const response = await adminAgent.resource('posts').create({ values: {}, }); @@ -90,8 +97,7 @@ describe('middleware', () => { }); it('should limit fields on view actions', async () => { - await app - .agent() + await adminAgent .resource('roles.resources', role.get('name')) .create({ values: { @@ -110,8 +116,7 @@ describe('middleware', () => { }, }); - await app - .agent() + await adminAgent .resource('posts') .create({ values: { @@ -124,10 +129,10 @@ describe('middleware', () => { expect(post.get('title')).toEqual('post-title'); 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); - const data = response.body.data[0]; + const [data] = response.body.data; expect(data['id']).not.toBeUndefined(); expect(data['title']).toEqual('post-title'); @@ -135,12 +140,7 @@ describe('middleware', () => { }); it('should parse template value on action params', async () => { - changeMockUser({ - id: 2, - }); - - const res = await app - .agent() + const res = await adminAgent .resource('rolesResourcesScopes') .create({ values: { @@ -151,8 +151,7 @@ describe('middleware', () => { }, }); - await app - .agent() + await adminAgent .resource('roles.resources', role.get('name')) .create({ values: { @@ -172,8 +171,7 @@ describe('middleware', () => { }, }); - await app - .agent() + await adminAgent .resource('posts') .create({ values: { @@ -183,8 +181,7 @@ describe('middleware', () => { }, }); - await app - .agent() + await adminAgent .resource('posts') .create({ 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; expect(data.length).toEqual(1); }); it('should change fields params to whitelist in create action', async () => { - await app - .agent() + await adminAgent .resource('roles.resources', role.get('name')) .create({ values: { @@ -216,8 +212,7 @@ describe('middleware', () => { }, }); - await app - .agent() + await adminAgent .resource('posts') .create({ values: { diff --git a/packages/plugins/acl/src/__tests__/own.test.ts b/packages/plugins/acl/src/__tests__/own.test.ts index bab333111..0d5f76b41 100644 --- a/packages/plugins/acl/src/__tests__/own.test.ts +++ b/packages/plugins/acl/src/__tests__/own.test.ts @@ -1,11 +1,9 @@ import { ACL } from '@nocobase/acl'; 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 { mockServer, MockServer } from '@nocobase/test'; -import supertest from 'supertest'; +import { MockServer } from '@nocobase/test'; + +import { prepareApp } from './prepare'; describe('own test', () => { let app: MockServer; @@ -21,28 +19,15 @@ describe('own test', () => { let role; let agent; + let adminAgent; + let userAgent; afterEach(async () => { await app.destroy(); }); beforeEach(async () => { - app = mockServer({ - 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(); + app = await prepareApp(); db = app.db; const PostCollection = db.collection({ @@ -68,9 +53,9 @@ describe('own test', () => { fields: [{ type: 'string', name: 'name' }], }); - await app.db.sync(); + await db.sync(); - agent = supertest.agent(app.callback()); + agent = app.agent(); acl = app.acl; @@ -86,6 +71,8 @@ describe('own test', () => { adminToken = pluginUser.jwtService.sign({ userId: admin.get('id') }); + adminAgent = app.agent().auth(adminToken, { type: 'bearer' }); + user = await db.getRepository('users').create({ values: { nickname: 'test', @@ -94,10 +81,12 @@ describe('own test', () => { }); userToken = pluginUser.jwtService.sign({ userId: user.get('id') }); + + userAgent = app.agent().auth(userToken, { type: 'bearer' }); }); it('should list without createBy', async () => { - let response = await agent + await adminAgent .patch('/roles/admin') .send({ strategy: { @@ -106,33 +95,38 @@ describe('own test', () => { }) .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); }); it('should delete with createdBy', async () => { - let response = await agent - .patch('/roles/admin') - .send({ - strategy: { - actions: ['view:own', 'create', 'destroy:own'], - }, - }) - .set({ Authorization: 'Bearer ' + adminToken }); + await adminAgent + .resource('roles') + .update({ + filterByTk: 'admin', + values: { + strategy: { + actions: ['view:own', 'create', 'destroy:own'], + }, + } + }); - response = await agent - .get('/posts:create') - .send({ - title: 't1', - }) - .set({ Authorization: 'Bearer ' + userToken }); + let response = await userAgent + .resource('posts') + .create({ + values: { + title: 't1', + } + }); expect(response.statusCode).toEqual(200); const data = response.body; 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(await db.getRepository('posts').count()).toEqual(0); }); diff --git a/packages/plugins/acl/src/__tests__/prepare.ts b/packages/plugins/acl/src/__tests__/prepare.ts index abdd5f70b..933f3237a 100644 --- a/packages/plugins/acl/src/__tests__/prepare.ts +++ b/packages/plugins/acl/src/__tests__/prepare.ts @@ -1,18 +1,10 @@ +import PluginUsers from '@nocobase/plugin-users'; import PluginCollectionManager from '@nocobase/plugin-collection-manager'; import PluginUiSchema from '@nocobase/plugin-ui-schema-storage'; import { mockServer } from '@nocobase/test'; 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() { const app = mockServer({ @@ -21,15 +13,10 @@ export async function prepareApp() { await app.cleanDb(); + app.plugin(PluginUsers); app.plugin(PluginUiSchema); app.plugin(PluginCollectionManager); - app.resourcer.use(async (ctx, next) => { - ctx.state.currentRole = mockRole; - ctx.state.currentUser = mockUser; - await next(); - }); - app.plugin(PluginACL); await app.loadAndInstall(); diff --git a/packages/plugins/acl/src/__tests__/role-check.test.ts b/packages/plugins/acl/src/__tests__/role-check.test.ts index 88f0055e1..5c43cf8c7 100644 --- a/packages/plugins/acl/src/__tests__/role-check.test.ts +++ b/packages/plugins/acl/src/__tests__/role-check.test.ts @@ -1,6 +1,8 @@ import { MockServer } from '@nocobase/test'; -import { changeMockRole, changeMockUser, prepareApp } from './prepare'; import { Database } from '@nocobase/database'; +import UsersPlugin from '@nocobase/plugin-users'; + +import { prepareApp } from './prepare'; describe('role check action', () => { let app: MockServer; @@ -21,14 +23,18 @@ describe('role check action', () => { name: 'test', }, }); - - changeMockUser({ - id: 2, + const user = await db.getRepository('users').create({ + values: { + 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'); - - const response = await app.agent().get('/roles:check'); + // @ts-ignore + const response = await agent.resource('roles').check(); expect(response.statusCode).toEqual(200); }); diff --git a/packages/plugins/acl/src/__tests__/role-resource.test.ts b/packages/plugins/acl/src/__tests__/role-resource.test.ts index 0c05e7298..63a66eaac 100644 --- a/packages/plugins/acl/src/__tests__/role-resource.test.ts +++ b/packages/plugins/acl/src/__tests__/role-resource.test.ts @@ -1,4 +1,5 @@ import { Database, Model } from '@nocobase/database'; +import UsersPlugin from '@nocobase/plugin-users'; import { CollectionRepository } from '@nocobase/plugin-collection-manager'; import { MockServer } from '@nocobase/test'; import { prepareApp } from './prepare'; @@ -7,6 +8,8 @@ describe('role resource api', () => { let app: MockServer; let db: Database; let role: Model; + let admin; + let adminAgent; afterEach(async () => { await app.destroy(); @@ -16,19 +19,23 @@ describe('role resource api', () => { app = await prepareApp(); db = app.db; - await db.getRepository('roles').create({ - values: { - name: 'admin', - title: 'Admin User', - allowConfigure: true, - }, - }); - role = await db.getRepository('roles').findOne({ filter: { 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 () => { @@ -91,8 +98,7 @@ describe('role resource api', () => { }); // get collections list - let response = await app - .agent() + let response = await adminAgent .resource('roles.collections', 'admin') .list({ filter: { @@ -119,8 +125,7 @@ describe('role resource api', () => { ]); // set resource actions - response = await app - .agent() + response = await adminAgent .resource('roles.resources', 'admin') .create({ values: { @@ -137,8 +142,7 @@ describe('role resource api', () => { expect(response.statusCode).toEqual(200); // get collections list - response = await app - .agent() + response = await adminAgent .resource('roles.collections') .list({ associatedIndex: role.get('name') as string, @@ -149,8 +153,7 @@ describe('role resource api', () => { expect(response.body.data[0]['usingConfig']).toEqual('resourceAction'); - response = await app - .agent() + response = await adminAgent .resource('roles.resources') .list({ associatedIndex: role.get('name') as string, @@ -164,8 +167,7 @@ describe('role resource api', () => { expect(resourceAction['name']).toEqual('create'); // update resource actions - response = await app - .agent() + response = await adminAgent .resource('roles.resources') .update({ associatedIndex: role.get('name') as string, diff --git a/packages/plugins/users/src/__tests__/role.test.ts b/packages/plugins/acl/src/__tests__/role-user.test.ts similarity index 96% rename from packages/plugins/users/src/__tests__/role.test.ts rename to packages/plugins/acl/src/__tests__/role-user.test.ts index 6426b4350..6d14a5326 100644 --- a/packages/plugins/users/src/__tests__/role.test.ts +++ b/packages/plugins/acl/src/__tests__/role-user.test.ts @@ -2,7 +2,6 @@ import Database, { BelongsToManyRepository } from '@nocobase/database'; import PluginACL from '@nocobase/plugin-acl'; import UsersPlugin from '@nocobase/plugin-users'; import { MockServer, mockServer } from '@nocobase/test'; -import { userPluginConfig } from './utils'; describe('role', () => { let api: MockServer; @@ -13,7 +12,7 @@ describe('role', () => { beforeEach(async () => { api = mockServer(); await api.cleanDb(); - api.plugin(UsersPlugin, userPluginConfig); + api.plugin(UsersPlugin); api.plugin(PluginACL); await api.loadAndInstall(); @@ -113,7 +112,7 @@ describe('role', () => { .set({ Authorization: `Bearer ${userToken}`, }); - + expect(response.statusCode).toEqual(200); const userRoles = await userRolesRepo.find(); diff --git a/packages/plugins/acl/src/__tests__/role.test.ts b/packages/plugins/acl/src/__tests__/role.test.ts index 3ba5f8709..056ac7a16 100644 --- a/packages/plugins/acl/src/__tests__/role.test.ts +++ b/packages/plugins/acl/src/__tests__/role.test.ts @@ -1,6 +1,6 @@ import { MockServer } from '@nocobase/test'; -import { CollectionRepository } from '@nocobase/plugin-collection-manager'; import { Database, Model } from '@nocobase/database'; +import UsersPlugin from '@nocobase/plugin-users'; import { prepareApp } from './prepare'; @@ -19,32 +19,37 @@ describe('role api', () => { describe('grant', () => { let role: Model; + let admin: Model; + let adminAgent; beforeEach(async () => { - await db.getRepository('roles').create({ - values: { - name: 'admin', - title: 'Admin User', - allowConfigure: true, - }, - }); - role = await db.getRepository('roles').findOne({ filter: { 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 () => { - const response = await app.agent().resource('availableActions').list(); + const response = await adminAgent.resource('availableActions').list(); expect(response.statusCode).toEqual(200); }); it('should grant universal role actions', async () => { // grant role actions - const response = await app - .agent() + const response = await adminAgent .resource('roles') .update({ values: { diff --git a/packages/plugins/acl/src/__tests__/scope.test.ts b/packages/plugins/acl/src/__tests__/scope.test.ts index d8de629d8..d35b096de 100644 --- a/packages/plugins/acl/src/__tests__/scope.test.ts +++ b/packages/plugins/acl/src/__tests__/scope.test.ts @@ -1,11 +1,15 @@ import { prepareApp } from './prepare'; import { MockServer } from '@nocobase/test'; import { Database } from '@nocobase/database'; +import UsersPlugin from '@nocobase/plugin-users'; describe('scope api', () => { let app: MockServer; let db: Database; + let admin; + let adminAgent; + afterEach(async () => { await app.destroy(); }); @@ -13,18 +17,22 @@ describe('scope api', () => { beforeEach(async () => { app = await prepareApp(); db = app.db; - await db.getRepository('roles').create({ + + const UserRepo = db.getCollection('users').repository; + admin = await UserRepo.create({ values: { - name: 'admin', - title: 'Admin User', - allowConfigure: true, - }, + 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 create scope of resource', async () => { - const response = await app - .agent() + const response = await adminAgent .resource('rolesResourcesScopes') .create({ values: { diff --git a/packages/plugins/acl/src/__tests__/setCurrentRole.test.ts b/packages/plugins/acl/src/__tests__/setCurrentRole.test.ts new file mode 100644 index 000000000..21cf2aef3 --- /dev/null +++ b/packages/plugins/acl/src/__tests__/setCurrentRole.test.ts @@ -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'); + }); +}); diff --git a/packages/plugins/acl/src/actions/user-setDefaultRole.ts b/packages/plugins/acl/src/actions/user-setDefaultRole.ts new file mode 100644 index 000000000..2b0f67c63 --- /dev/null +++ b/packages/plugins/acl/src/actions/user-setDefaultRole.ts @@ -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(); +} diff --git a/packages/plugins/users/src/collections/roles-users.ts b/packages/plugins/acl/src/collections/roles-users.ts similarity index 100% rename from packages/plugins/users/src/collections/roles-users.ts rename to packages/plugins/acl/src/collections/roles-users.ts diff --git a/packages/plugins/acl/src/collections/users.ts b/packages/plugins/acl/src/collections/users.ts new file mode 100644 index 000000000..b85f574dd --- /dev/null +++ b/packages/plugins/acl/src/collections/users.ts @@ -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', + }, + }, + }, + } + ], +}); diff --git a/packages/plugins/acl/src/middlewares/setCurrentRole.ts b/packages/plugins/acl/src/middlewares/setCurrentRole.ts new file mode 100644 index 000000000..3be755be0 --- /dev/null +++ b/packages/plugins/acl/src/middlewares/setCurrentRole.ts @@ -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(); +} diff --git a/packages/plugins/acl/src/server.ts b/packages/plugins/acl/src/server.ts index 4ee5f39ee..f6e20cacf 100644 --- a/packages/plugins/acl/src/server.ts +++ b/packages/plugins/acl/src/server.ts @@ -1,10 +1,13 @@ import { Context } from '@nocobase/actions'; import { Collection } from '@nocobase/database'; +import UsersPlugin from '@nocobase/plugin-users'; import { Plugin } from '@nocobase/server'; import { resolve } from 'path'; import { availableActionResource } from './actions/available-actions'; import { checkAction } from './actions/role-check'; import { roleCollectionsResource } from './actions/role-collections'; +import { setDefaultRole } from './actions/user-setDefaultRole'; +import { setCurrentRole } from './middlewares/setCurrentRole'; import { RoleModel } from './model/RoleModel'; import { RoleResourceActionModel } from './model/RoleResourceActionModel'; 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(`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) => { const { transaction } = options; @@ -271,7 +290,7 @@ export class PluginACL extends Plugin { title: '{{t("Admin")}}', allowConfigure: true, allowNewMenu: true, - strategy: { actions: ['create', 'export', 'view', 'update', 'destroy'] }, + strategy: { actions: ['create', 'view', 'update', 'destroy'] }, }, { 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', ['create', 'update', 'destroy'], 'allowConfigure'); @@ -392,6 +416,24 @@ export class PluginACL extends Plugin { if (repo) { 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() { diff --git a/packages/plugins/ui-schema-storage/src/__tests__/server-hook-impl.test.ts b/packages/plugins/ui-schema-storage/src/__tests__/server-hook-impl.test.ts index afcf64a68..db93e4561 100644 --- a/packages/plugins/ui-schema-storage/src/__tests__/server-hook-impl.test.ts +++ b/packages/plugins/ui-schema-storage/src/__tests__/server-hook-impl.test.ts @@ -1,4 +1,5 @@ import { BelongsToManyRepository, Database } from '@nocobase/database'; +import PluginUsers from '@nocobase/plugin-users'; import PluginACL from '@nocobase/plugin-acl'; import PluginCollectionManager from '@nocobase/plugin-collection-manager'; import UiSchemaStoragePlugin, { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage'; @@ -24,6 +25,7 @@ describe('server hooks', () => { app.plugin(UiSchemaStoragePlugin); app.plugin(PluginCollectionManager); + app.plugin(PluginUsers); app.plugin(PluginACL); await app.loadAndInstall(); diff --git a/packages/plugins/users/src/__tests__/setCurrentRole.test.ts b/packages/plugins/users/src/__tests__/setCurrentRole.test.ts deleted file mode 100644 index e93410bb6..000000000 --- a/packages/plugins/users/src/__tests__/setCurrentRole.test.ts +++ /dev/null @@ -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'); - }); -}); diff --git a/packages/plugins/users/src/actions/users.ts b/packages/plugins/users/src/actions/users.ts index 8495238a7..e514e59ab 100644 --- a/packages/plugins/users/src/actions/users.ts +++ b/packages/plugins/users/src/actions/users.ts @@ -150,15 +150,3 @@ 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: { roleName }, - } = ctx.action.params; - - await ctx.state.currentUser.setDefaultRole(roleName); - - ctx.body = 'ok'; - - await next(); -} diff --git a/packages/plugins/users/src/collections/users.ts b/packages/plugins/users/src/collections/users.ts index 42cd619bc..8a7626f6c 100644 --- a/packages/plugins/users/src/collections/users.ts +++ b/packages/plugins/users/src/collections/users.ts @@ -51,29 +51,6 @@ export default { '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', name: 'appLang', diff --git a/packages/plugins/users/src/middlewares/parseToken.ts b/packages/plugins/users/src/middlewares/parseToken.ts index b83aeb11d..23e0db0fc 100644 --- a/packages/plugins/users/src/middlewares/parseToken.ts +++ b/packages/plugins/users/src/middlewares/parseToken.ts @@ -1,40 +1,17 @@ import { Context, Next } from '@nocobase/actions'; +import { MiddlewareManager } from '@nocobase/resourcer'; import UsersPlugin from '../server'; 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); if (user) { ctx.state.currentUser = user; - setCurrentRole(ctx); } return next(); - }; -} - -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; - } + }); + return middleware; } async function findUserByToken(ctx: Context, plugin: UsersPlugin) { @@ -46,7 +23,7 @@ async function findUserByToken(ctx: Context, plugin: UsersPlugin) { try { const { userId } = await plugin.jwtService.decode(token); const collection = ctx.db.getCollection('users'); - const appends = ['roles']; + const appends = []; for (const [, field] of collection.fields) { if (field.type === 'belongsTo') { appends.push(field.name); diff --git a/packages/plugins/users/src/models/UserModel.ts b/packages/plugins/users/src/models/UserModel.ts deleted file mode 100644 index c8f2d2d3d..000000000 --- a/packages/plugins/users/src/models/UserModel.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/plugins/users/src/server.ts b/packages/plugins/users/src/server.ts index 821f77119..0524dda5f 100644 --- a/packages/plugins/users/src/server.ts +++ b/packages/plugins/users/src/server.ts @@ -7,7 +7,6 @@ import * as actions from './actions/users'; import { JwtOptions, JwtService } from './jwt-service'; import { enUS, zhCN } from './locale'; import * as middlewares from './middlewares'; -import { UserModel } from './models/UserModel'; export interface UserPluginConfig { jwt: JwtOptions; @@ -16,9 +15,12 @@ export interface UserPluginConfig { export default class UsersPlugin extends Plugin { public jwtService: JwtService; + public tokenMiddleware; + constructor(app, options) { super(app, options); this.jwtService = new JwtService(options?.jwt || {}); + this.tokenMiddleware = middlewares.parseToken({ plugin: this }); } async beforeLoad() { @@ -43,23 +45,6 @@ export default class UsersPlugin extends Plugin { }; }, }); - 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) => { let { createdBy, updatedBy } = collection.options; @@ -100,10 +85,10 @@ export default class UsersPlugin extends Plugin { 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 loggedInActions = ['signout', 'updateProfile', 'changePassword', 'setDefaultRole']; + const loggedInActions = ['signout', 'updateProfile', 'changePassword']; publicActions.forEach((action) => this.app.acl.allow('users', action)); loggedInActions.forEach((action) => this.app.acl.allow('users', action, 'loggedIn')); @@ -132,17 +117,14 @@ export default class UsersPlugin extends Plugin { async install(options) { const { rootNickname, rootPassword, rootEmail } = this.getInstallingData(options); const User = this.db.getCollection('users'); - const user = await User.repository.create({ + const user = await User.repository.create({ values: { email: rootEmail, password: rootPassword, - nickname: rootNickname, - roles: ['root', 'admin', 'member'], + nickname: rootNickname }, }); - await user.setDefaultRole('root'); - const repo = this.db.getRepository('collections'); if (repo) { await repo.db2cm('users');