From 86de0733ad17a1aefcdee0ac8d87edb0f19aa81f Mon Sep 17 00:00:00 2001 From: ChengLei Shao Date: Wed, 12 Apr 2023 17:44:33 +0800 Subject: [PATCH] fix: meta acl with association query (#1695) * fix: meta acl with association query * fix: test --- .../acl/src/__tests__/list-action.test.ts | 53 ++++++++++++++++++- packages/plugins/acl/src/__tests__/prepare.ts | 4 +- packages/plugins/acl/src/server.ts | 39 +++++++++----- 3 files changed, 79 insertions(+), 17 deletions(-) diff --git a/packages/plugins/acl/src/__tests__/list-action.test.ts b/packages/plugins/acl/src/__tests__/list-action.test.ts index a26a10049..9dce691fb 100644 --- a/packages/plugins/acl/src/__tests__/list-action.test.ts +++ b/packages/plugins/acl/src/__tests__/list-action.test.ts @@ -1,8 +1,9 @@ import { Database } from '@nocobase/database'; import { prepareApp } from './prepare'; +import { MockServer } from '@nocobase/test'; describe('list action with acl', () => { - let app; + let app: MockServer; let Post; @@ -17,6 +18,11 @@ describe('list action with acl', () => { type: 'bigInt', name: 'createdById', }, + { + type: 'belongsTo', + name: 'createdBy', + target: 'users', + }, ], }); @@ -85,6 +91,7 @@ describe('list action with acl', () => { }, ); + //@ts-ignore const response = await app.agent().set('X-With-ACL-Meta', true).resource('tests').list({}); const data = response.body; @@ -93,6 +100,48 @@ describe('list action with acl', () => { expect(data.meta.allowedActions.destroy).toEqual(['t3']); }); + it('should list items meta permissions by association field', async () => { + const userRole = app.acl.define({ + role: 'user', + }); + + userRole.grantAction('posts:view', {}); + + userRole.grantAction('posts:update', { + filter: { + 'createdBy.id': '{{ ctx.state.currentUser.id }}', + }, + }); + + await Post.repository.create({ + values: [ + { title: 'p1', createdById: 1 }, + { title: 'p2', createdById: 1 }, + { title: 'p3', createdById: 2 }, + ], + }); + + app.resourcer.use( + (ctx, next) => { + ctx.state.currentRole = 'user'; + ctx.state.currentUser = { + id: 1, + }; + + return next(); + }, + { + before: 'acl', + }, + ); + + const response = await (app as any).agent().set('X-With-ACL-Meta', true).resource('posts').list(); + const data = response.body; + expect(data.meta.allowedActions.view).toEqual([1, 2, 3]); + expect(data.meta.allowedActions.update).toEqual([1, 2]); + expect(data.meta.allowedActions.destroy).toEqual([]); + }); + it('should list items with meta permission', async () => { const userRole = app.acl.define({ role: 'user', @@ -126,6 +175,7 @@ describe('list action with acl', () => { }, ); + // @ts-ignore const response = await app.agent().set('X-With-ACL-Meta', true).resource('posts').list({}); const data = response.body; @@ -167,6 +217,7 @@ describe('list action with acl', () => { }, ); + // @ts-ignore const getResponse = await app.agent().set('X-With-ACL-Meta', true).resource('posts').get({ filterByTk: 1, }); diff --git a/packages/plugins/acl/src/__tests__/prepare.ts b/packages/plugins/acl/src/__tests__/prepare.ts index 8a2dcc007..270e86f4e 100644 --- a/packages/plugins/acl/src/__tests__/prepare.ts +++ b/packages/plugins/acl/src/__tests__/prepare.ts @@ -1,7 +1,7 @@ -import { mockServer } from '@nocobase/test'; +import { mockServer, MockServer } from '@nocobase/test'; import PluginACL from '../server'; -export async function prepareApp() { +export async function prepareApp(): Promise { const app = mockServer({ registerActions: true, acl: true, diff --git a/packages/plugins/acl/src/server.ts b/packages/plugins/acl/src/server.ts index 22459fe7d..e535a2be1 100644 --- a/packages/plugins/acl/src/server.ts +++ b/packages/plugins/acl/src/server.ts @@ -585,20 +585,12 @@ export class PluginACL extends Plugin { const withACLMeta = async (ctx: any, next) => { await next(); - if (!ctx.action) { + if (!ctx.action || !ctx.get('X-With-ACL-Meta') || ctx.status !== 200) { return; } const { resourceName, actionName } = ctx.action; - if (!ctx.get('X-With-ACL-Meta')) { - return; - } - - if (ctx.status !== 200) { - return; - } - if (!['list', 'get'].includes(actionName)) { return; } @@ -620,11 +612,11 @@ export class PluginACL extends Plugin { listData = lodash.castArray(listData); } - const actions = ['view', 'update', 'destroy']; + const inspectActions = ['view', 'update', 'destroy']; const actionsParams = []; - for (const action of actions) { + for (const action of inspectActions) { const actionCtx: any = { db: ctx.db, action: { @@ -707,10 +699,16 @@ export class PluginACL extends Plugin { { where: (() => { const filterObj = queryParams.where; + if (!this.db.options.underscored) { return filterObj; } + const isAssociationKey = (key) => { + return key.startsWith('$') && key.endsWith('$'); + }; + + // change camelCase to snake_case const iterate = (rootObj, path = []) => { const obj = path.length == 0 ? rootObj : lodash.get(rootObj, path); @@ -738,8 +736,21 @@ export class PluginACL extends Plugin { } if (typeof key === 'string' && key !== snakeCase(key)) { - lodash.set(rootObj, [...path, snakeCase(key)], lodash.cloneDeep(obj[key])); + const setKey = isAssociationKey(key) + ? (() => { + const parts = key.split('.'); + + parts[parts.length - 1] = lodash.snakeCase(parts[parts.length - 1]); + + const result = parts.join('.'); + + return result.endsWith('$') ? result : `${result}$`; + })() + : snakeCase(key); + const setValue = lodash.cloneDeep(obj[key]); lodash.unset(rootObj, [...path, key]); + + lodash.set(rootObj, [...path, setKey], setValue); } }); }; @@ -776,7 +787,7 @@ export class PluginACL extends Plugin { include: conditions.map((condition) => condition.include).flat(), }); - const allowedActions = actions + const allowedActions = inspectActions .map((action) => { if (allAllowed.includes(action)) { return [action, ids]; @@ -831,4 +842,4 @@ export class PluginACL extends Plugin { } } -export default PluginACL; \ No newline at end of file +export default PluginACL;