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
This commit is contained in:
parent
118899887c
commit
271e91b452
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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<any>({});
|
||||
|
||||
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 <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
|
||||
};
|
||||
|
||||
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 <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
|
||||
};
|
||||
|
||||
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']) {
|
||||
|
@ -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<D = any>() {
|
||||
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;
|
||||
};
|
||||
|
@ -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) => {
|
||||
|
@ -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 (
|
||||
<SchemaInitializer.Button
|
||||
insertPosition={'beforeEnd'}
|
||||
icon={'PlusOutlined'}
|
||||
insert={props.insert}
|
||||
style={props.style}
|
||||
onSuccess={() => {
|
||||
recheck?.();
|
||||
}}
|
||||
{...props}
|
||||
items={[
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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 (
|
||||
<div>
|
||||
<CollectionProvider name={collectionField.target}>
|
||||
<ActionContext.Provider value={{ visible, setVisible, openMode: 'drawer' }}>
|
||||
<Space size={0} split={<span style={{ marginRight: 4, color: '#aaa' }}>, </span>}>
|
||||
{toArr(field.value).map((record, index) => {
|
||||
return (
|
||||
<span>
|
||||
<a
|
||||
onClick={() => {
|
||||
console.log('setVisible');
|
||||
setVisible(true);
|
||||
}}
|
||||
>
|
||||
{record?.[fieldNames?.label || 'label']}
|
||||
</a>
|
||||
<RecordProvider record={record}>
|
||||
<FormProvider>
|
||||
<RecursionField schema={fieldSchema} onlyRenderProperties />
|
||||
</FormProvider>
|
||||
</RecordProvider>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
</ActionContext.Provider>
|
||||
</CollectionProvider>
|
||||
<BlockAssociationContext.Provider value={`${collectionField.collectionName}.${collectionField.name}`}>
|
||||
<CollectionProvider name={collectionField.target}>
|
||||
<ActionContext.Provider value={{ visible, setVisible, openMode: 'drawer' }}>
|
||||
<Space size={0} split={<span style={{ marginRight: 4, color: '#aaa' }}>, </span>}>
|
||||
{toArr(field.value).map((record, index) => {
|
||||
return (
|
||||
<span>
|
||||
<a
|
||||
onClick={() => {
|
||||
console.log('setVisible');
|
||||
setVisible(true);
|
||||
}}
|
||||
>
|
||||
{record?.[fieldNames?.label || 'label']}
|
||||
</a>
|
||||
<RecordProvider record={record}>
|
||||
<FormProvider>
|
||||
<RecursionField schema={fieldSchema} onlyRenderProperties />
|
||||
</FormProvider>
|
||||
</RecordProvider>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
</ActionContext.Provider>
|
||||
</CollectionProvider>
|
||||
</BlockAssociationContext.Provider>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -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 <div>{props.children}</div>;
|
||||
|
@ -68,6 +68,9 @@ export const CalendarActionInitializers = {
|
||||
schema: {
|
||||
'x-align': 'right',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -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,
|
||||
|
@ -43,7 +43,7 @@ export const CurrentUser = () => {
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<span style={{ border: 0, padding: '16px', color: 'rgba(255, 255, 255, 0.65)' }}>
|
||||
<span style={{ cursor: 'pointer', border: 0, padding: '16px', color: 'rgba(255, 255, 255, 0.65)' }}>
|
||||
{data?.data?.nickname || data?.data?.email}
|
||||
</span>
|
||||
</Dropdown>
|
||||
|
@ -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,
|
||||
|
@ -1,3 +1,11 @@
|
||||
const map2obj = (map: Map<string, string>) => {
|
||||
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')),
|
||||
|
@ -3,6 +3,10 @@ import { CollectionOptions } from '@nocobase/database';
|
||||
export default {
|
||||
name: 'rolesResourcesScopes',
|
||||
fields: [
|
||||
{
|
||||
type: 'uid',
|
||||
name: 'key',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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 {
|
||||
|
@ -5,6 +5,8 @@ export default {
|
||||
title: '{{t("Users")}}',
|
||||
sortable: 'sort',
|
||||
model: 'UserModel',
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
fields: [
|
||||
{
|
||||
interface: 'input',
|
||||
|
@ -51,6 +51,7 @@ export default class UsersPlugin extends Plugin<UserPluginConfig> {
|
||||
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<UserPluginConfig> {
|
||||
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<UserPluginConfig> {
|
||||
|
||||
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() {
|
||||
|
Loading…
Reference in New Issue
Block a user