From 271e91b452ff4c383264a8ca27c13df350b60973 Mon Sep 17 00:00:00 2001 From: chenos Date: Tue, 12 Apr 2022 12:02:58 +0800 Subject: [PATCH] feat: improve acl module (#283) * feat: improve code * fix: rowKey * fix: ctx.state.currentUser * fix: improve code * fix: menu item permission * fix: x-acl-action * fix: skipScopeCheck * feat: relationship resource permission * fix: createdById --- packages/acl/src/acl-available-action.ts | 7 +- packages/acl/src/acl.ts | 17 ++++- packages/client/src/acl/ACLProvider.tsx | 75 +++++++++++++++---- packages/client/src/record-provider/index.tsx | 10 +++ .../route-switch/antd/admin-layout/index.tsx | 22 ++++++ .../antd/menu/MenuItemInitializers/index.tsx | 5 -- .../antd/record-picker/InputRecordPicker.tsx | 7 +- .../record-picker/ReadPrettyRecordPicker.tsx | 53 ++++++------- .../antd/table-v2/TableField.tsx | 4 +- .../buttons/CalendarActionInitializers.tsx | 3 + .../ReadPrettyFormActionInitializers.tsx | 2 + .../buttons/TableActionInitializers.tsx | 6 ++ .../client/src/schema-initializer/utils.ts | 18 ++++- packages/client/src/user/CurrentUser.tsx | 2 +- .../src/__tests__/middleware.test.ts | 15 ++-- packages/plugin-acl/src/actions/role-check.ts | 14 +++- .../src/collections/rolesResourcesScopes.ts | 4 + .../src/model/RoleResourceActionModel.ts | 3 +- packages/plugin-acl/src/server.ts | 68 ++++++++++++++++- packages/plugin-file-manager/src/server.ts | 2 + .../plugin-users/src/collections/users.ts | 2 + packages/plugin-users/src/server.ts | 6 +- 22 files changed, 271 insertions(+), 74 deletions(-) diff --git a/packages/acl/src/acl-available-action.ts b/packages/acl/src/acl-available-action.ts index 1c697f6ba..a61f23f89 100644 --- a/packages/acl/src/acl-available-action.ts +++ b/packages/acl/src/acl-available-action.ts @@ -1,9 +1,14 @@ export interface AvailableActionOptions { - aliases?: string[] | string; + /** + * @deprecated + */ type: 'new-data' | 'old-data'; displayName?: string; + aliases?: string[] | string; resource?: string; + // 对新数据进行操作 onNewRecord?: boolean; + // 允许配置字段 allowConfigureFields?: boolean; } diff --git a/packages/acl/src/acl.ts b/packages/acl/src/acl.ts index 3d141ec1a..20c9ba109 100644 --- a/packages/acl/src/acl.ts +++ b/packages/acl/src/acl.ts @@ -138,6 +138,11 @@ export class ACL extends EventEmitter { } } + getAvailableAction(name: string) { + const actionName = this.actionAlias.get(name) || name; + return this.availableActions.get(actionName); + } + getAvailableActions() { return this.availableActions; } @@ -216,13 +221,21 @@ export class ACL extends EventEmitter { this.skipManager.skip(resourceName, actionName, condition); } + parseJsonTemplate(json: any, ctx: any) { + return parse(json)({ + ctx: { + state: JSON.parse(JSON.stringify(ctx.state)), + }, + }); + } + middleware() { const acl = this; const filterParams = (ctx, resourceName, params) => { if (params?.filter?.createdById) { const collection = ctx.db.getCollection(resourceName); - if (!collection.getField('createdById')) { + if (collection && !collection.getField('createdById')) { return lodash.omit(params, 'filter.createdById'); } } @@ -266,7 +279,7 @@ export class ACL extends EventEmitter { if (params) { const filteredParams = filterParams(ctx, resourceName, params); - const parsedParams = parse(filteredParams)({ ctx: ctxToObject(ctx) }); + const parsedParams = acl.parseJsonTemplate(filteredParams, ctx); resourcerAction.mergeParams(parsedParams); } diff --git a/packages/client/src/acl/ACLProvider.tsx b/packages/client/src/acl/ACLProvider.tsx index 8e476d686..efea6de0f 100644 --- a/packages/client/src/acl/ACLProvider.tsx +++ b/packages/client/src/acl/ACLProvider.tsx @@ -5,6 +5,7 @@ import React, { createContext, useContext } from 'react'; import { Redirect } from 'react-router-dom'; import { useRequest } from '../api-client'; import { useCollection } from '../collection-manager'; +import { useRecordIsOwn } from '../record-provider'; import { SchemaComponentOptions, useDesignable } from '../schema-component'; export const ACLContext = createContext(null); @@ -50,24 +51,50 @@ export const useRoleRecheck = () => { const ctx = useContext(ACLContext); const { allowAll, allowConfigure } = useACLRoleContext(); return () => { - if (allowAll) { + if (allowAll || allowConfigure) { return; } ctx.refresh(); - } -} + }; +}; export const useACLContext = () => { return useContext(ACLContext); -} +}; export const useACLRoleContext = () => { const ctx = useContext(ACLContext); const data = ctx.data?.data; + return { ...data, - getActionParams(path) { - return data?.actions?.[path]; + getActionParams(path: string, { skipOwnCheck, isOwn }) { + const [resourceName, act] = path.split(':'); + const currentAction = data?.actionAlias?.[act] || act; + const hasResource = data?.resources?.includes(resourceName); + const params = data?.actions?.[`${resourceName}:${currentAction}`]; + if (hasResource) { + if (!skipOwnCheck && params?.own) { + return isOwn ? params : null; + } + return params; + } + const strategyActions = data?.strategy?.actions || []; + const strategyAction = strategyActions?.find((action) => { + const [value] = action.split(':'); + return value === currentAction; + }); + if (!strategyAction) { + return; + } + if (skipOwnCheck) { + return {}; + } + const [, actionScope] = strategyAction.split(':'); + if (actionScope === 'own') { + return isOwn; + } + return {}; }, }; }; @@ -80,25 +107,43 @@ export const ACLAllowConfigure = (props) => { return null; }; -export const ACLCollectionProvider = (props) => { - const { name } = useCollection(); +const ACLActionParamsContext = createContext({}); - return <>{props.children}; +export const ACLCollectionProvider = (props) => { + const { allowAll, allowConfigure, getActionParams } = useACLRoleContext(); + const fieldSchema = useFieldSchema(); + const isOwn = useRecordIsOwn(); + if (allowAll || allowConfigure) { + return <>{props.children}; + } + const path = fieldSchema['x-acl-action']; + const skipScopeCheck = fieldSchema['x-acl-action-props']?.skipScopeCheck; + if (!path) { + return <>{props.children}; + } + const params = getActionParams(path, { isOwn, skipOwnCheck: skipScopeCheck === false ? false : true }); + if (!params) { + return null; + } + return {props.children}; }; export const ACLActionProvider = (props) => { const { name } = useCollection(); const fieldSchema = useFieldSchema(); - const { allowAll, getActionParams } = useACLRoleContext(); - if (!name || allowAll) { + const isOwn = useRecordIsOwn(); + const { allowAll, allowConfigure, getActionParams } = useACLRoleContext(); + if (!name || allowAll || allowConfigure) { return <>{props.children}; } const actionName = fieldSchema['x-action']; - const params = getActionParams([`${name}:${actionName}`]); + const path = fieldSchema['x-acl-action'] || `${name}:${actionName}`; + const skipScopeCheck = fieldSchema['x-acl-action-props']?.skipScopeCheck; + const params = getActionParams(path, { skipOwnCheck: skipScopeCheck, isOwn }); if (!params) { return null; } - return <>{props.children}; + return {props.children}; }; export const ACLCollectionFieldProvider = (props) => { @@ -106,9 +151,9 @@ export const ACLCollectionFieldProvider = (props) => { }; export const ACLMenuItemProvider = (props) => { - const { allowAll, allowMenuItemIds = [] } = useACLRoleContext(); + const { allowAll, allowConfigure, allowMenuItemIds = [] } = useACLRoleContext(); const fieldSchema = useFieldSchema(); - if (allowAll) { + if (allowAll || allowConfigure) { return <>{props.children}; } if (!fieldSchema['x-uid']) { diff --git a/packages/client/src/record-provider/index.tsx b/packages/client/src/record-provider/index.tsx index 48e68a2c4..e614270da 100644 --- a/packages/client/src/record-provider/index.tsx +++ b/packages/client/src/record-provider/index.tsx @@ -1,4 +1,5 @@ import React, { createContext, useContext } from 'react'; +import { useCurrentUserContext } from '../user'; export const RecordContext = createContext({}); export const RecordIndexContext = createContext(null); @@ -21,3 +22,12 @@ export function useRecord() { export function useRecordIndex() { return useContext(RecordIndexContext); } + +export const useRecordIsOwn = () => { + const record = useRecord(); + const ctx = useCurrentUserContext(); + if (!record?.createdById) { + return false; + } + return record?.createdById === ctx?.data?.data?.id; +}; diff --git a/packages/client/src/route-switch/antd/admin-layout/index.tsx b/packages/client/src/route-switch/antd/admin-layout/index.tsx index b8773acc7..f5372ab87 100644 --- a/packages/client/src/route-switch/antd/admin-layout/index.tsx +++ b/packages/client/src/route-switch/antd/admin-layout/index.tsx @@ -12,12 +12,32 @@ import { RemoteCollectionManagerProvider, RemoteSchemaComponent, RemoteSchemaTemplateManagerProvider, + useACLRoleContext, useDocumentTitle, useRoute, useSystemSettings } from '../../../'; import { PoweredBy } from '../../../powered-by'; +const filterByACL = (schema, options) => { + const { allowAll, allowConfigure, allowMenuItemIds = [] } = options; + if (allowAll || allowConfigure) { + return schema; + } + const filterSchema = (s) => { + for (const key in s.properties) { + if (Object.prototype.hasOwnProperty.call(s.properties, key)) { + const element = s.properties[key]; + if (element['x-uid'] && !allowMenuItemIds.includes(element['x-uid'])) { + delete s.properties[key]; + } + } + } + }; + filterSchema(schema); + return schema; +}; + const InternalAdminLayout = (props: any) => { const route = useRoute(); const history = useHistory(); @@ -26,6 +46,7 @@ const InternalAdminLayout = (props: any) => { const sideMenuRef = useRef(); const defaultSelectedUid = match.params.name; const [schema, setSchema] = useState({}); + const ctx = useACLRoleContext(); const onSelect = ({ item }) => { const schema = item.props.schema; console.log('onSelect', schema); @@ -75,6 +96,7 @@ const InternalAdminLayout = (props: any) => { } data['x-component-props'] = data['x-component-props'] || {}; data['x-component-props']['defaultSelectedUid'] = defaultSelectedUid; + filterByACL(data, ctx); return data; }} onSuccess={(data) => { diff --git a/packages/client/src/schema-component/antd/menu/MenuItemInitializers/index.tsx b/packages/client/src/schema-component/antd/menu/MenuItemInitializers/index.tsx index 2e757d38c..b35d976f9 100644 --- a/packages/client/src/schema-component/antd/menu/MenuItemInitializers/index.tsx +++ b/packages/client/src/schema-component/antd/menu/MenuItemInitializers/index.tsx @@ -3,21 +3,16 @@ import { SchemaOptionsContext } from '@formily/react'; import React, { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { SchemaComponent, SchemaComponentOptions } from '../../..'; -import { useRoleRecheck } from '../../../../acl'; import { SchemaInitializer } from '../../../../schema-initializer'; export const MenuItemInitializers = (props: any) => { const { t } = useTranslation(); - const recheck = useRoleRecheck(); return ( { - recheck?.(); - }} {...props} items={[ { diff --git a/packages/client/src/schema-component/antd/record-picker/InputRecordPicker.tsx b/packages/client/src/schema-component/antd/record-picker/InputRecordPicker.tsx index 0a073b8d7..57f9b8b80 100644 --- a/packages/client/src/schema-component/antd/record-picker/InputRecordPicker.tsx +++ b/packages/client/src/schema-component/antd/record-picker/InputRecordPicker.tsx @@ -12,13 +12,14 @@ const RecordPickerContext = createContext(null); const useTableSelectorProps = () => { const { multiple, value, setSelectedRows, selectedRows } = useContext(RecordPickerContext); - const { onRowSelectionChange, ...others } = useTsp(); + const { onRowSelectionChange, rowKey, ...others } = useTsp(); return { ...others, + rowKey, rowSelection: { type: multiple ? 'checkbox' : 'radio', - defaultSelectedRowKeys: selectedRows?.map((item) => item.id), - selectedRowKeys: selectedRows?.map((item) => item.id), + defaultSelectedRowKeys: selectedRows?.map((item) => item[rowKey||'id']), + selectedRowKeys: selectedRows?.map((item) => item[rowKey||'id']), }, onRowSelectionChange(selectedRowKeys, selectedRows) { onRowSelectionChange?.(selectedRowKeys, selectedRows); diff --git a/packages/client/src/schema-component/antd/record-picker/ReadPrettyRecordPicker.tsx b/packages/client/src/schema-component/antd/record-picker/ReadPrettyRecordPicker.tsx index ea050bd45..5d647a4ee 100644 --- a/packages/client/src/schema-component/antd/record-picker/ReadPrettyRecordPicker.tsx +++ b/packages/client/src/schema-component/antd/record-picker/ReadPrettyRecordPicker.tsx @@ -3,6 +3,7 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea import { toArr } from '@formily/shared'; import { Space } from 'antd'; import React, { useState } from 'react'; +import { BlockAssociationContext } from '../../../block-provider'; import { CollectionProvider, useCollection } from '../../../collection-manager'; import { RecordProvider } from '../../../record-provider'; import { FormProvider } from '../../core'; @@ -18,31 +19,33 @@ export const ReadPrettyRecordPicker: React.FC = observer((props: any) => { const collectionField = getField(fieldSchema.name); return (
- - - , }> - {toArr(field.value).map((record, index) => { - return ( - - { - console.log('setVisible'); - setVisible(true); - }} - > - {record?.[fieldNames?.label || 'label']} - - - - - - - - ); - })} - - - + + + + , }> + {toArr(field.value).map((record, index) => { + return ( + + { + console.log('setVisible'); + setVisible(true); + }} + > + {record?.[fieldNames?.label || 'label']} + + + + + + + + ); + })} + + + +
); }); diff --git a/packages/client/src/schema-component/antd/table-v2/TableField.tsx b/packages/client/src/schema-component/antd/table-v2/TableField.tsx index de5570574..1b77f5c2c 100644 --- a/packages/client/src/schema-component/antd/table-v2/TableField.tsx +++ b/packages/client/src/schema-component/antd/table-v2/TableField.tsx @@ -1,6 +1,7 @@ import { observer, useField, useFieldSchema, useForm } from '@formily/react'; import React, { useEffect } from 'react'; import { useCollection } from '../../../collection-manager'; +import { useCompile } from '../../hooks'; import { ActionBar } from '../action'; export const TableField: any = observer((props) => { @@ -8,9 +9,10 @@ export const TableField: any = observer((props) => { const { getField } = useCollection(); const field = useField(); const collectionField = getField(fieldSchema.name); + const compile = useCompile(); useEffect(() => { if (!field.title) { - field.title = collectionField?.uiSchema?.title; + field.title = compile(collectionField?.uiSchema?.title); } }, []); return
{props.children}
; diff --git a/packages/client/src/schema-initializer/buttons/CalendarActionInitializers.tsx b/packages/client/src/schema-initializer/buttons/CalendarActionInitializers.tsx index 5e9a7bf4c..d7515b059 100644 --- a/packages/client/src/schema-initializer/buttons/CalendarActionInitializers.tsx +++ b/packages/client/src/schema-initializer/buttons/CalendarActionInitializers.tsx @@ -68,6 +68,9 @@ export const CalendarActionInitializers = { schema: { 'x-align': 'right', 'x-decorator': 'ACLActionProvider', + 'x-acl-action-props': { + skipScopeCheck: true, + }, }, }, ], diff --git a/packages/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx b/packages/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx index 391197288..f894cc19d 100644 --- a/packages/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx +++ b/packages/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx @@ -16,6 +16,7 @@ export const ReadPrettyFormActionInitializers = { component: 'UpdateActionInitializer', schema: { 'x-component': 'Action', + 'x-decorator': 'ACLActionProvider', 'x-component-props': { type: 'primary', }, @@ -27,6 +28,7 @@ export const ReadPrettyFormActionInitializers = { component: 'DestroyActionInitializer', schema: { 'x-component': 'Action', + 'x-decorator': 'ACLActionProvider', }, }, ], diff --git a/packages/client/src/schema-initializer/buttons/TableActionInitializers.tsx b/packages/client/src/schema-initializer/buttons/TableActionInitializers.tsx index d2c68954f..384e0c92e 100644 --- a/packages/client/src/schema-initializer/buttons/TableActionInitializers.tsx +++ b/packages/client/src/schema-initializer/buttons/TableActionInitializers.tsx @@ -25,6 +25,9 @@ export const TableActionInitializers = { schema: { 'x-align': 'right', 'x-decorator': 'ACLActionProvider', + 'x-acl-action-props': { + skipScopeCheck: true, + }, }, }, { @@ -34,6 +37,9 @@ export const TableActionInitializers = { schema: { 'x-align': 'right', 'x-decorator': 'ACLActionProvider', + 'x-acl-action-props': { + skipScopeCheck: true, + }, }, }, ], diff --git a/packages/client/src/schema-initializer/utils.ts b/packages/client/src/schema-initializer/utils.ts index 133f2b531..49e9365d2 100644 --- a/packages/client/src/schema-initializer/utils.ts +++ b/packages/client/src/schema-initializer/utils.ts @@ -280,14 +280,21 @@ export const createFormBlockSchema = (options) => { collection, resource, association, + action, ...others } = options; + const resourceName = resource || association || collection; const schema: ISchema = { type: 'void', + 'x-acl-action-props': { + skipScopeCheck: !action, + }, + 'x-acl-action': action ? `${resourceName}:update` : `${resourceName}:create`, 'x-decorator': 'FormBlockProvider', 'x-decorator-props': { ...others, - resource: resource || association || collection, + action, + resource: resourceName, collection, association, // action: 'get', @@ -338,11 +345,13 @@ export const createReadPrettyFormBlockSchema = (options) => { resource, ...others } = options; + const resourceName = resource || association || collection; const schema: ISchema = { type: 'void', + 'x-acl-action': `${resourceName}:get`, 'x-decorator': 'FormBlockProvider', 'x-decorator-props': { - resource: resource || association || collection, + resource: resourceName, collection, association, readPretty: true, @@ -391,6 +400,7 @@ export const createTableBlockSchema = (options) => { const schema: ISchema = { type: 'void', 'x-decorator': 'TableBlockProvider', + 'x-acl-action': `${resource || collection}:list`, 'x-decorator-props': { collection, resource: resource || collection, @@ -461,6 +471,7 @@ export const createTableSelectorSchema = (options) => { const { collection, resource, rowKey, ...others } = options; const schema: ISchema = { type: 'void', + 'x-acl-action': `${resource || collection}:list`, 'x-decorator': 'TableSelectorProvider', 'x-decorator-props': { collection, @@ -491,7 +502,6 @@ export const createTableSelectorSchema = (options) => { 'x-initializer': 'TableColumnInitializers', 'x-component': 'TableV2.Selector', 'x-component-props': { - rowKey: 'id', rowSelection: { type: 'checkbox', }, @@ -509,6 +519,7 @@ export const createCalendarBlockSchema = (options) => { const { collection, resource, fieldNames, ...others } = options; const schema: ISchema = { type: 'void', + 'x-acl-action': `${resource || collection}:list`, 'x-decorator': 'CalendarBlockProvider', 'x-decorator-props': { collection: collection, @@ -595,6 +606,7 @@ export const createKanbanBlockSchema = (options) => { const { collection, resource, groupField, ...others } = options; const schema: ISchema = { type: 'void', + 'x-acl-action': `${resource || collection}:list`, 'x-decorator': 'KanbanBlockProvider', 'x-decorator-props': { collection: collection, diff --git a/packages/client/src/user/CurrentUser.tsx b/packages/client/src/user/CurrentUser.tsx index bbe38cbc5..10b6c2325 100644 --- a/packages/client/src/user/CurrentUser.tsx +++ b/packages/client/src/user/CurrentUser.tsx @@ -43,7 +43,7 @@ export const CurrentUser = () => { } > - + {data?.data?.nickname || data?.data?.email} diff --git a/packages/plugin-acl/src/__tests__/middleware.test.ts b/packages/plugin-acl/src/__tests__/middleware.test.ts index daee98d1a..725da250a 100644 --- a/packages/plugin-acl/src/__tests__/middleware.test.ts +++ b/packages/plugin-acl/src/__tests__/middleware.test.ts @@ -92,9 +92,8 @@ describe('middleware', () => { it('should limit fields on view actions', async () => { await app .agent() - .resource('roles.resources') + .resource('roles.resources', role.get('name')) .create({ - associatedIndex: role.get('name') as string, values: { name: 'posts', usingActionsConfig: true, @@ -140,7 +139,7 @@ describe('middleware', () => { id: 2, }); - await app + const res = await app .agent() .resource('rolesResourcesScopes') .create({ @@ -152,13 +151,10 @@ describe('middleware', () => { }, }); - const scope = await db.getRepository('rolesResourcesScopes').findOne(); - await app .agent() - .resource('roles.resources') + .resource('roles.resources', role.get('name')) .create({ - associatedIndex: role.get('name') as string, values: { name: 'posts', usingActionsConfig: true, @@ -170,7 +166,7 @@ describe('middleware', () => { { name: 'view', fields: ['title'], - scope: scope.get('id'), + scope: res.body.data.id, }, ], }, @@ -206,9 +202,8 @@ describe('middleware', () => { it('should change fields params to whitelist in create action', async () => { await app .agent() - .resource('roles.resources') + .resource('roles.resources', role.get('name')) .create({ - associatedIndex: role.get('name') as string, values: { name: 'posts', usingActionsConfig: true, diff --git a/packages/plugin-acl/src/actions/role-check.ts b/packages/plugin-acl/src/actions/role-check.ts index 6b71e47a3..927d6009b 100644 --- a/packages/plugin-acl/src/actions/role-check.ts +++ b/packages/plugin-acl/src/actions/role-check.ts @@ -1,3 +1,11 @@ +const map2obj = (map: Map) => { + const obj = {}; + for(let [key, value] of map){ + obj[key] = value; + } + return obj; +} + export async function checkAction(ctx, next) { const currentRole = ctx.state.currentRole; if (currentRole) { @@ -8,8 +16,12 @@ export async function checkAction(ctx, next) { appends: ['menuUiSchemas'], }); + const role = ctx.app.acl.getRole(currentRole); + ctx.body = { - ...ctx.app.acl.getRole(currentRole).toJSON(), + ...role.toJSON(), + resources: [...role.resources.keys()], + actionAlias: map2obj(ctx.app.acl.actionAlias), allowAll: currentRole === 'root', allowConfigure: roleInstance.get('allowConfigure'), allowMenuItemIds: roleInstance.get('menuUiSchemas').map((uiSchema) => uiSchema.get('x-uid')), diff --git a/packages/plugin-acl/src/collections/rolesResourcesScopes.ts b/packages/plugin-acl/src/collections/rolesResourcesScopes.ts index 5d2b1d14f..3281d4e78 100644 --- a/packages/plugin-acl/src/collections/rolesResourcesScopes.ts +++ b/packages/plugin-acl/src/collections/rolesResourcesScopes.ts @@ -3,6 +3,10 @@ import { CollectionOptions } from '@nocobase/database'; export default { name: 'rolesResourcesScopes', fields: [ + { + type: 'uid', + name: 'key', + }, { type: 'string', name: 'name', diff --git a/packages/plugin-acl/src/model/RoleResourceActionModel.ts b/packages/plugin-acl/src/model/RoleResourceActionModel.ts index 654e3df22..fdaf5a0ca 100644 --- a/packages/plugin-acl/src/model/RoleResourceActionModel.ts +++ b/packages/plugin-acl/src/model/RoleResourceActionModel.ts @@ -1,5 +1,5 @@ -import { Database, Model } from '@nocobase/database'; import { ACL, ACLRole } from '@nocobase/acl'; +import { Database, Model } from '@nocobase/database'; import { AssociationFieldAction, AssociationFieldsActions, GrantHelper } from '../server'; export class RoleResourceActionModel extends Model { @@ -27,6 +27,7 @@ export class RoleResourceActionModel extends Model { const scope = await this.getScope(); if (scope) { + actionParams['own'] = scope.get('key') === 'own'; actionParams['filter'] = scope.get('scope'); } diff --git a/packages/plugin-acl/src/server.ts b/packages/plugin-acl/src/server.ts index 1900f361b..9fbcf8103 100644 --- a/packages/plugin-acl/src/server.ts +++ b/packages/plugin-acl/src/server.ts @@ -1,3 +1,5 @@ +import { Context } from '@nocobase/actions'; +import { Collection } from '@nocobase/database'; import { Plugin } from '@nocobase/server'; import { resolve } from 'path'; import { availableActionResource } from './actions/available-actions'; @@ -260,10 +262,15 @@ export class PluginACL extends Plugin { { name: 'admin', title: 'Admin', + allowConfigure: true, + allowNewMenu: true, + strategy: { actions: ['create', 'export', 'view', 'update', 'destroy'] }, }, { name: 'member', title: 'Member', + allowNewMenu: true, + strategy: { actions: ['view', 'update:own', 'destroy:own', 'create'] }, default: true, }, { @@ -276,10 +283,12 @@ export class PluginACL extends Plugin { await rolesResourcesScopes.createMany({ records: [ { + key: 'all', name: '{{t("All records")}}', scope: {}, }, { + key: 'own', name: '{{t("Own records")}}', scope: { createdById: '{{ ctx.state.currentUser.id }}', @@ -295,8 +304,7 @@ export class PluginACL extends Plugin { this.app.acl.skip('*', '*', (ctx) => { return ctx.state.currentRole === 'root'; }); - - // root role + // root role this.app.resourcer.use(async (ctx, next) => { const { actionName, resourceName } = ctx.action.params; if (actionName === 'list' && resourceName === 'roles') { @@ -308,6 +316,62 @@ export class PluginACL extends Plugin { } await next(); }); + + this.app.acl.use(async (ctx: Context, next) => { + const { actionName, resourceName } = ctx.action; + if (actionName === 'get' || actionName === 'list') { + if (!Array.isArray(ctx?.permission?.can?.params?.fields)) { + return next(); + } + let collection: Collection; + if (resourceName.includes('.')) { + const [collectionName, associationName] = resourceName.split('.'); + const field = ctx.db.getCollection(collectionName)?.getField?.(associationName); + if (field.target) { + collection = ctx.db.getCollection(field.target); + } + } else { + collection = ctx.db.getCollection(resourceName); + } + if (collection && collection.hasField('createdById')) { + ctx.permission.can.params.fields.push('createdById'); + } + } + return next(); + }); + + const parseJsonTemplate = this.app.acl.parseJsonTemplate; + this.app.acl.use(async (ctx: Context, next) => { + const { actionName, resourceName, resourceOf } = ctx.action; + if (resourceName.includes('.') && resourceOf) { + if (!ctx?.permission?.can?.params) { + return next(); + } + // 关联数据去掉 filter + delete ctx.permission.can.params.filter; + // 关联数据能不能处理取决于 source 是否有权限 + const [collectionName] = resourceName.split('.'); + const action = ctx.can({ resource: collectionName, action: actionName }); + const availableAction = this.app.acl.getAvailableAction(actionName); + if (availableAction?.options?.onNewRecord) { + if (action) { + ctx.permission.skip = true; + } else { + ctx.permission.can = false; + } + } else { + const filter = parseJsonTemplate(action?.params?.filter || {}, ctx); + const sourceInstance = await ctx.db.getRepository(collectionName).findOne({ + filterByTk: resourceOf, + filter, + }); + if (!sourceInstance) { + ctx.permission.can = false; + } + } + } + await next(); + }); } async install() { diff --git a/packages/plugin-file-manager/src/server.ts b/packages/plugin-file-manager/src/server.ts index 887c75493..e2ce2ea66 100644 --- a/packages/plugin-file-manager/src/server.ts +++ b/packages/plugin-file-manager/src/server.ts @@ -35,6 +35,8 @@ export default class PluginFileManager extends Plugin { if (process.env.NOCOBASE_ENV !== 'production') { await getStorageConfig(STORAGE_TYPE_LOCAL).middleware(this.app); } + + this.app.acl.skip('attachments', 'upload', 'logged-in'); } getName(): string { diff --git a/packages/plugin-users/src/collections/users.ts b/packages/plugin-users/src/collections/users.ts index 707cca7df..4905d5545 100644 --- a/packages/plugin-users/src/collections/users.ts +++ b/packages/plugin-users/src/collections/users.ts @@ -5,6 +5,8 @@ export default { title: '{{t("Users")}}', sortable: 'sort', model: 'UserModel', + createdBy: true, + updatedBy: true, fields: [ { interface: 'input', diff --git a/packages/plugin-users/src/server.ts b/packages/plugin-users/src/server.ts index c072bc168..7d022ffd0 100644 --- a/packages/plugin-users/src/server.ts +++ b/packages/plugin-users/src/server.ts @@ -51,6 +51,7 @@ export default class UsersPlugin extends Plugin { dataType: 'integer', dataIndex: 'state.currentUser.id', createOnly: true, + visible: true, }); collection.setField('createdBy', { type: 'belongsTo', @@ -64,6 +65,7 @@ export default class UsersPlugin extends Plugin { type: 'context', dataType: 'integer', dataIndex: 'state.currentUser.id', + visible: true, }); collection.setField('updatedBy', { type: 'belongsTo', @@ -85,10 +87,6 @@ export default class UsersPlugin extends Plugin { publicActions.forEach((action) => this.app.acl.skip('users', action)); loggedInActions.forEach((action) => this.app.acl.skip('users', action, 'logged-in')); - - this.app.acl.skip('*', '*', (ctx) => { - return ctx.state.currentUser?.id == 1; - }); } async load() {