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 {
|
export interface AvailableActionOptions {
|
||||||
aliases?: string[] | string;
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
type: 'new-data' | 'old-data';
|
type: 'new-data' | 'old-data';
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
|
aliases?: string[] | string;
|
||||||
resource?: string;
|
resource?: string;
|
||||||
|
// 对新数据进行操作
|
||||||
onNewRecord?: boolean;
|
onNewRecord?: boolean;
|
||||||
|
// 允许配置字段
|
||||||
allowConfigureFields?: 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() {
|
getAvailableActions() {
|
||||||
return this.availableActions;
|
return this.availableActions;
|
||||||
}
|
}
|
||||||
@ -216,13 +221,21 @@ export class ACL extends EventEmitter {
|
|||||||
this.skipManager.skip(resourceName, actionName, condition);
|
this.skipManager.skip(resourceName, actionName, condition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parseJsonTemplate(json: any, ctx: any) {
|
||||||
|
return parse(json)({
|
||||||
|
ctx: {
|
||||||
|
state: JSON.parse(JSON.stringify(ctx.state)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
middleware() {
|
middleware() {
|
||||||
const acl = this;
|
const acl = this;
|
||||||
|
|
||||||
const filterParams = (ctx, resourceName, params) => {
|
const filterParams = (ctx, resourceName, params) => {
|
||||||
if (params?.filter?.createdById) {
|
if (params?.filter?.createdById) {
|
||||||
const collection = ctx.db.getCollection(resourceName);
|
const collection = ctx.db.getCollection(resourceName);
|
||||||
if (!collection.getField('createdById')) {
|
if (collection && !collection.getField('createdById')) {
|
||||||
return lodash.omit(params, 'filter.createdById');
|
return lodash.omit(params, 'filter.createdById');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -266,7 +279,7 @@ export class ACL extends EventEmitter {
|
|||||||
|
|
||||||
if (params) {
|
if (params) {
|
||||||
const filteredParams = filterParams(ctx, resourceName, params);
|
const filteredParams = filterParams(ctx, resourceName, params);
|
||||||
const parsedParams = parse(filteredParams)({ ctx: ctxToObject(ctx) });
|
const parsedParams = acl.parseJsonTemplate(filteredParams, ctx);
|
||||||
resourcerAction.mergeParams(parsedParams);
|
resourcerAction.mergeParams(parsedParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import React, { createContext, useContext } from 'react';
|
|||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import { useRequest } from '../api-client';
|
import { useRequest } from '../api-client';
|
||||||
import { useCollection } from '../collection-manager';
|
import { useCollection } from '../collection-manager';
|
||||||
|
import { useRecordIsOwn } from '../record-provider';
|
||||||
import { SchemaComponentOptions, useDesignable } from '../schema-component';
|
import { SchemaComponentOptions, useDesignable } from '../schema-component';
|
||||||
|
|
||||||
export const ACLContext = createContext(null);
|
export const ACLContext = createContext(null);
|
||||||
@ -50,24 +51,50 @@ export const useRoleRecheck = () => {
|
|||||||
const ctx = useContext(ACLContext);
|
const ctx = useContext(ACLContext);
|
||||||
const { allowAll, allowConfigure } = useACLRoleContext();
|
const { allowAll, allowConfigure } = useACLRoleContext();
|
||||||
return () => {
|
return () => {
|
||||||
if (allowAll) {
|
if (allowAll || allowConfigure) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.refresh();
|
ctx.refresh();
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useACLContext = () => {
|
export const useACLContext = () => {
|
||||||
return useContext(ACLContext);
|
return useContext(ACLContext);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useACLRoleContext = () => {
|
export const useACLRoleContext = () => {
|
||||||
const ctx = useContext(ACLContext);
|
const ctx = useContext(ACLContext);
|
||||||
const data = ctx.data?.data;
|
const data = ctx.data?.data;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
getActionParams(path) {
|
getActionParams(path: string, { skipOwnCheck, isOwn }) {
|
||||||
return data?.actions?.[path];
|
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;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ACLCollectionProvider = (props) => {
|
const ACLActionParamsContext = createContext<any>({});
|
||||||
const { name } = useCollection();
|
|
||||||
|
|
||||||
|
export const ACLCollectionProvider = (props) => {
|
||||||
|
const { allowAll, allowConfigure, getActionParams } = useACLRoleContext();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const isOwn = useRecordIsOwn();
|
||||||
|
if (allowAll || allowConfigure) {
|
||||||
return <>{props.children}</>;
|
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) => {
|
export const ACLActionProvider = (props) => {
|
||||||
const { name } = useCollection();
|
const { name } = useCollection();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { allowAll, getActionParams } = useACLRoleContext();
|
const isOwn = useRecordIsOwn();
|
||||||
if (!name || allowAll) {
|
const { allowAll, allowConfigure, getActionParams } = useACLRoleContext();
|
||||||
|
if (!name || allowAll || allowConfigure) {
|
||||||
return <>{props.children}</>;
|
return <>{props.children}</>;
|
||||||
}
|
}
|
||||||
const actionName = fieldSchema['x-action'];
|
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) {
|
if (!params) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return <>{props.children}</>;
|
return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ACLCollectionFieldProvider = (props) => {
|
export const ACLCollectionFieldProvider = (props) => {
|
||||||
@ -106,9 +151,9 @@ export const ACLCollectionFieldProvider = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ACLMenuItemProvider = (props) => {
|
export const ACLMenuItemProvider = (props) => {
|
||||||
const { allowAll, allowMenuItemIds = [] } = useACLRoleContext();
|
const { allowAll, allowConfigure, allowMenuItemIds = [] } = useACLRoleContext();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
if (allowAll) {
|
if (allowAll || allowConfigure) {
|
||||||
return <>{props.children}</>;
|
return <>{props.children}</>;
|
||||||
}
|
}
|
||||||
if (!fieldSchema['x-uid']) {
|
if (!fieldSchema['x-uid']) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { createContext, useContext } from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
|
import { useCurrentUserContext } from '../user';
|
||||||
|
|
||||||
export const RecordContext = createContext({});
|
export const RecordContext = createContext({});
|
||||||
export const RecordIndexContext = createContext(null);
|
export const RecordIndexContext = createContext(null);
|
||||||
@ -21,3 +22,12 @@ export function useRecord<D = any>() {
|
|||||||
export function useRecordIndex() {
|
export function useRecordIndex() {
|
||||||
return useContext(RecordIndexContext);
|
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,
|
RemoteCollectionManagerProvider,
|
||||||
RemoteSchemaComponent,
|
RemoteSchemaComponent,
|
||||||
RemoteSchemaTemplateManagerProvider,
|
RemoteSchemaTemplateManagerProvider,
|
||||||
|
useACLRoleContext,
|
||||||
useDocumentTitle,
|
useDocumentTitle,
|
||||||
useRoute,
|
useRoute,
|
||||||
useSystemSettings
|
useSystemSettings
|
||||||
} from '../../../';
|
} from '../../../';
|
||||||
import { PoweredBy } from '../../../powered-by';
|
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 InternalAdminLayout = (props: any) => {
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@ -26,6 +46,7 @@ const InternalAdminLayout = (props: any) => {
|
|||||||
const sideMenuRef = useRef();
|
const sideMenuRef = useRef();
|
||||||
const defaultSelectedUid = match.params.name;
|
const defaultSelectedUid = match.params.name;
|
||||||
const [schema, setSchema] = useState({});
|
const [schema, setSchema] = useState({});
|
||||||
|
const ctx = useACLRoleContext();
|
||||||
const onSelect = ({ item }) => {
|
const onSelect = ({ item }) => {
|
||||||
const schema = item.props.schema;
|
const schema = item.props.schema;
|
||||||
console.log('onSelect', 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'] = data['x-component-props'] || {};
|
||||||
data['x-component-props']['defaultSelectedUid'] = defaultSelectedUid;
|
data['x-component-props']['defaultSelectedUid'] = defaultSelectedUid;
|
||||||
|
filterByACL(data, ctx);
|
||||||
return data;
|
return data;
|
||||||
}}
|
}}
|
||||||
onSuccess={(data) => {
|
onSuccess={(data) => {
|
||||||
|
@ -3,21 +3,16 @@ import { SchemaOptionsContext } from '@formily/react';
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SchemaComponent, SchemaComponentOptions } from '../../..';
|
import { SchemaComponent, SchemaComponentOptions } from '../../..';
|
||||||
import { useRoleRecheck } from '../../../../acl';
|
|
||||||
import { SchemaInitializer } from '../../../../schema-initializer';
|
import { SchemaInitializer } from '../../../../schema-initializer';
|
||||||
|
|
||||||
export const MenuItemInitializers = (props: any) => {
|
export const MenuItemInitializers = (props: any) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const recheck = useRoleRecheck();
|
|
||||||
return (
|
return (
|
||||||
<SchemaInitializer.Button
|
<SchemaInitializer.Button
|
||||||
insertPosition={'beforeEnd'}
|
insertPosition={'beforeEnd'}
|
||||||
icon={'PlusOutlined'}
|
icon={'PlusOutlined'}
|
||||||
insert={props.insert}
|
insert={props.insert}
|
||||||
style={props.style}
|
style={props.style}
|
||||||
onSuccess={() => {
|
|
||||||
recheck?.();
|
|
||||||
}}
|
|
||||||
{...props}
|
{...props}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
|
@ -12,13 +12,14 @@ const RecordPickerContext = createContext(null);
|
|||||||
|
|
||||||
const useTableSelectorProps = () => {
|
const useTableSelectorProps = () => {
|
||||||
const { multiple, value, setSelectedRows, selectedRows } = useContext(RecordPickerContext);
|
const { multiple, value, setSelectedRows, selectedRows } = useContext(RecordPickerContext);
|
||||||
const { onRowSelectionChange, ...others } = useTsp();
|
const { onRowSelectionChange, rowKey, ...others } = useTsp();
|
||||||
return {
|
return {
|
||||||
...others,
|
...others,
|
||||||
|
rowKey,
|
||||||
rowSelection: {
|
rowSelection: {
|
||||||
type: multiple ? 'checkbox' : 'radio',
|
type: multiple ? 'checkbox' : 'radio',
|
||||||
defaultSelectedRowKeys: selectedRows?.map((item) => item.id),
|
defaultSelectedRowKeys: selectedRows?.map((item) => item[rowKey||'id']),
|
||||||
selectedRowKeys: selectedRows?.map((item) => item.id),
|
selectedRowKeys: selectedRows?.map((item) => item[rowKey||'id']),
|
||||||
},
|
},
|
||||||
onRowSelectionChange(selectedRowKeys, selectedRows) {
|
onRowSelectionChange(selectedRowKeys, selectedRows) {
|
||||||
onRowSelectionChange?.(selectedRowKeys, selectedRows);
|
onRowSelectionChange?.(selectedRowKeys, selectedRows);
|
||||||
|
@ -3,6 +3,7 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea
|
|||||||
import { toArr } from '@formily/shared';
|
import { toArr } from '@formily/shared';
|
||||||
import { Space } from 'antd';
|
import { Space } from 'antd';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { BlockAssociationContext } from '../../../block-provider';
|
||||||
import { CollectionProvider, useCollection } from '../../../collection-manager';
|
import { CollectionProvider, useCollection } from '../../../collection-manager';
|
||||||
import { RecordProvider } from '../../../record-provider';
|
import { RecordProvider } from '../../../record-provider';
|
||||||
import { FormProvider } from '../../core';
|
import { FormProvider } from '../../core';
|
||||||
@ -18,6 +19,7 @@ export const ReadPrettyRecordPicker: React.FC = observer((props: any) => {
|
|||||||
const collectionField = getField(fieldSchema.name);
|
const collectionField = getField(fieldSchema.name);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<BlockAssociationContext.Provider value={`${collectionField.collectionName}.${collectionField.name}`}>
|
||||||
<CollectionProvider name={collectionField.target}>
|
<CollectionProvider name={collectionField.target}>
|
||||||
<ActionContext.Provider value={{ visible, setVisible, openMode: 'drawer' }}>
|
<ActionContext.Provider value={{ visible, setVisible, openMode: 'drawer' }}>
|
||||||
<Space size={0} split={<span style={{ marginRight: 4, color: '#aaa' }}>, </span>}>
|
<Space size={0} split={<span style={{ marginRight: 4, color: '#aaa' }}>, </span>}>
|
||||||
@ -43,6 +45,7 @@ export const ReadPrettyRecordPicker: React.FC = observer((props: any) => {
|
|||||||
</Space>
|
</Space>
|
||||||
</ActionContext.Provider>
|
</ActionContext.Provider>
|
||||||
</CollectionProvider>
|
</CollectionProvider>
|
||||||
|
</BlockAssociationContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { observer, useField, useFieldSchema, useForm } from '@formily/react';
|
import { observer, useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useCollection } from '../../../collection-manager';
|
import { useCollection } from '../../../collection-manager';
|
||||||
|
import { useCompile } from '../../hooks';
|
||||||
import { ActionBar } from '../action';
|
import { ActionBar } from '../action';
|
||||||
|
|
||||||
export const TableField: any = observer((props) => {
|
export const TableField: any = observer((props) => {
|
||||||
@ -8,9 +9,10 @@ export const TableField: any = observer((props) => {
|
|||||||
const { getField } = useCollection();
|
const { getField } = useCollection();
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const collectionField = getField(fieldSchema.name);
|
const collectionField = getField(fieldSchema.name);
|
||||||
|
const compile = useCompile();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!field.title) {
|
if (!field.title) {
|
||||||
field.title = collectionField?.uiSchema?.title;
|
field.title = compile(collectionField?.uiSchema?.title);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
return <div>{props.children}</div>;
|
return <div>{props.children}</div>;
|
||||||
|
@ -68,6 +68,9 @@ export const CalendarActionInitializers = {
|
|||||||
schema: {
|
schema: {
|
||||||
'x-align': 'right',
|
'x-align': 'right',
|
||||||
'x-decorator': 'ACLActionProvider',
|
'x-decorator': 'ACLActionProvider',
|
||||||
|
'x-acl-action-props': {
|
||||||
|
skipScopeCheck: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -16,6 +16,7 @@ export const ReadPrettyFormActionInitializers = {
|
|||||||
component: 'UpdateActionInitializer',
|
component: 'UpdateActionInitializer',
|
||||||
schema: {
|
schema: {
|
||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
|
'x-decorator': 'ACLActionProvider',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
},
|
},
|
||||||
@ -27,6 +28,7 @@ export const ReadPrettyFormActionInitializers = {
|
|||||||
component: 'DestroyActionInitializer',
|
component: 'DestroyActionInitializer',
|
||||||
schema: {
|
schema: {
|
||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
|
'x-decorator': 'ACLActionProvider',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -25,6 +25,9 @@ export const TableActionInitializers = {
|
|||||||
schema: {
|
schema: {
|
||||||
'x-align': 'right',
|
'x-align': 'right',
|
||||||
'x-decorator': 'ACLActionProvider',
|
'x-decorator': 'ACLActionProvider',
|
||||||
|
'x-acl-action-props': {
|
||||||
|
skipScopeCheck: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -34,6 +37,9 @@ export const TableActionInitializers = {
|
|||||||
schema: {
|
schema: {
|
||||||
'x-align': 'right',
|
'x-align': 'right',
|
||||||
'x-decorator': 'ACLActionProvider',
|
'x-decorator': 'ACLActionProvider',
|
||||||
|
'x-acl-action-props': {
|
||||||
|
skipScopeCheck: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -280,14 +280,21 @@ export const createFormBlockSchema = (options) => {
|
|||||||
collection,
|
collection,
|
||||||
resource,
|
resource,
|
||||||
association,
|
association,
|
||||||
|
action,
|
||||||
...others
|
...others
|
||||||
} = options;
|
} = options;
|
||||||
|
const resourceName = resource || association || collection;
|
||||||
const schema: ISchema = {
|
const schema: ISchema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
'x-acl-action-props': {
|
||||||
|
skipScopeCheck: !action,
|
||||||
|
},
|
||||||
|
'x-acl-action': action ? `${resourceName}:update` : `${resourceName}:create`,
|
||||||
'x-decorator': 'FormBlockProvider',
|
'x-decorator': 'FormBlockProvider',
|
||||||
'x-decorator-props': {
|
'x-decorator-props': {
|
||||||
...others,
|
...others,
|
||||||
resource: resource || association || collection,
|
action,
|
||||||
|
resource: resourceName,
|
||||||
collection,
|
collection,
|
||||||
association,
|
association,
|
||||||
// action: 'get',
|
// action: 'get',
|
||||||
@ -338,11 +345,13 @@ export const createReadPrettyFormBlockSchema = (options) => {
|
|||||||
resource,
|
resource,
|
||||||
...others
|
...others
|
||||||
} = options;
|
} = options;
|
||||||
|
const resourceName = resource || association || collection;
|
||||||
const schema: ISchema = {
|
const schema: ISchema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
'x-acl-action': `${resourceName}:get`,
|
||||||
'x-decorator': 'FormBlockProvider',
|
'x-decorator': 'FormBlockProvider',
|
||||||
'x-decorator-props': {
|
'x-decorator-props': {
|
||||||
resource: resource || association || collection,
|
resource: resourceName,
|
||||||
collection,
|
collection,
|
||||||
association,
|
association,
|
||||||
readPretty: true,
|
readPretty: true,
|
||||||
@ -391,6 +400,7 @@ export const createTableBlockSchema = (options) => {
|
|||||||
const schema: ISchema = {
|
const schema: ISchema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-decorator': 'TableBlockProvider',
|
'x-decorator': 'TableBlockProvider',
|
||||||
|
'x-acl-action': `${resource || collection}:list`,
|
||||||
'x-decorator-props': {
|
'x-decorator-props': {
|
||||||
collection,
|
collection,
|
||||||
resource: resource || collection,
|
resource: resource || collection,
|
||||||
@ -461,6 +471,7 @@ export const createTableSelectorSchema = (options) => {
|
|||||||
const { collection, resource, rowKey, ...others } = options;
|
const { collection, resource, rowKey, ...others } = options;
|
||||||
const schema: ISchema = {
|
const schema: ISchema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
'x-acl-action': `${resource || collection}:list`,
|
||||||
'x-decorator': 'TableSelectorProvider',
|
'x-decorator': 'TableSelectorProvider',
|
||||||
'x-decorator-props': {
|
'x-decorator-props': {
|
||||||
collection,
|
collection,
|
||||||
@ -491,7 +502,6 @@ export const createTableSelectorSchema = (options) => {
|
|||||||
'x-initializer': 'TableColumnInitializers',
|
'x-initializer': 'TableColumnInitializers',
|
||||||
'x-component': 'TableV2.Selector',
|
'x-component': 'TableV2.Selector',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
rowKey: 'id',
|
|
||||||
rowSelection: {
|
rowSelection: {
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
},
|
},
|
||||||
@ -509,6 +519,7 @@ export const createCalendarBlockSchema = (options) => {
|
|||||||
const { collection, resource, fieldNames, ...others } = options;
|
const { collection, resource, fieldNames, ...others } = options;
|
||||||
const schema: ISchema = {
|
const schema: ISchema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
'x-acl-action': `${resource || collection}:list`,
|
||||||
'x-decorator': 'CalendarBlockProvider',
|
'x-decorator': 'CalendarBlockProvider',
|
||||||
'x-decorator-props': {
|
'x-decorator-props': {
|
||||||
collection: collection,
|
collection: collection,
|
||||||
@ -595,6 +606,7 @@ export const createKanbanBlockSchema = (options) => {
|
|||||||
const { collection, resource, groupField, ...others } = options;
|
const { collection, resource, groupField, ...others } = options;
|
||||||
const schema: ISchema = {
|
const schema: ISchema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
'x-acl-action': `${resource || collection}:list`,
|
||||||
'x-decorator': 'KanbanBlockProvider',
|
'x-decorator': 'KanbanBlockProvider',
|
||||||
'x-decorator-props': {
|
'x-decorator-props': {
|
||||||
collection: collection,
|
collection: collection,
|
||||||
|
@ -43,7 +43,7 @@ export const CurrentUser = () => {
|
|||||||
</Menu>
|
</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}
|
{data?.data?.nickname || data?.data?.email}
|
||||||
</span>
|
</span>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
@ -92,9 +92,8 @@ describe('middleware', () => {
|
|||||||
it('should limit fields on view actions', async () => {
|
it('should limit fields on view actions', async () => {
|
||||||
await app
|
await app
|
||||||
.agent()
|
.agent()
|
||||||
.resource('roles.resources')
|
.resource('roles.resources', role.get('name'))
|
||||||
.create({
|
.create({
|
||||||
associatedIndex: role.get('name') as string,
|
|
||||||
values: {
|
values: {
|
||||||
name: 'posts',
|
name: 'posts',
|
||||||
usingActionsConfig: true,
|
usingActionsConfig: true,
|
||||||
@ -140,7 +139,7 @@ describe('middleware', () => {
|
|||||||
id: 2,
|
id: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
await app
|
const res = await app
|
||||||
.agent()
|
.agent()
|
||||||
.resource('rolesResourcesScopes')
|
.resource('rolesResourcesScopes')
|
||||||
.create({
|
.create({
|
||||||
@ -152,13 +151,10 @@ describe('middleware', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const scope = await db.getRepository('rolesResourcesScopes').findOne();
|
|
||||||
|
|
||||||
await app
|
await app
|
||||||
.agent()
|
.agent()
|
||||||
.resource('roles.resources')
|
.resource('roles.resources', role.get('name'))
|
||||||
.create({
|
.create({
|
||||||
associatedIndex: role.get('name') as string,
|
|
||||||
values: {
|
values: {
|
||||||
name: 'posts',
|
name: 'posts',
|
||||||
usingActionsConfig: true,
|
usingActionsConfig: true,
|
||||||
@ -170,7 +166,7 @@ describe('middleware', () => {
|
|||||||
{
|
{
|
||||||
name: 'view',
|
name: 'view',
|
||||||
fields: ['title'],
|
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 () => {
|
it('should change fields params to whitelist in create action', async () => {
|
||||||
await app
|
await app
|
||||||
.agent()
|
.agent()
|
||||||
.resource('roles.resources')
|
.resource('roles.resources', role.get('name'))
|
||||||
.create({
|
.create({
|
||||||
associatedIndex: role.get('name') as string,
|
|
||||||
values: {
|
values: {
|
||||||
name: 'posts',
|
name: 'posts',
|
||||||
usingActionsConfig: true,
|
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) {
|
export async function checkAction(ctx, next) {
|
||||||
const currentRole = ctx.state.currentRole;
|
const currentRole = ctx.state.currentRole;
|
||||||
if (currentRole) {
|
if (currentRole) {
|
||||||
@ -8,8 +16,12 @@ export async function checkAction(ctx, next) {
|
|||||||
appends: ['menuUiSchemas'],
|
appends: ['menuUiSchemas'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const role = ctx.app.acl.getRole(currentRole);
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
...ctx.app.acl.getRole(currentRole).toJSON(),
|
...role.toJSON(),
|
||||||
|
resources: [...role.resources.keys()],
|
||||||
|
actionAlias: map2obj(ctx.app.acl.actionAlias),
|
||||||
allowAll: currentRole === 'root',
|
allowAll: currentRole === 'root',
|
||||||
allowConfigure: roleInstance.get('allowConfigure'),
|
allowConfigure: roleInstance.get('allowConfigure'),
|
||||||
allowMenuItemIds: roleInstance.get('menuUiSchemas').map((uiSchema) => uiSchema.get('x-uid')),
|
allowMenuItemIds: roleInstance.get('menuUiSchemas').map((uiSchema) => uiSchema.get('x-uid')),
|
||||||
|
@ -3,6 +3,10 @@ import { CollectionOptions } from '@nocobase/database';
|
|||||||
export default {
|
export default {
|
||||||
name: 'rolesResourcesScopes',
|
name: 'rolesResourcesScopes',
|
||||||
fields: [
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'uid',
|
||||||
|
name: 'key',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Database, Model } from '@nocobase/database';
|
|
||||||
import { ACL, ACLRole } from '@nocobase/acl';
|
import { ACL, ACLRole } from '@nocobase/acl';
|
||||||
|
import { Database, Model } from '@nocobase/database';
|
||||||
import { AssociationFieldAction, AssociationFieldsActions, GrantHelper } from '../server';
|
import { AssociationFieldAction, AssociationFieldsActions, GrantHelper } from '../server';
|
||||||
|
|
||||||
export class RoleResourceActionModel extends Model {
|
export class RoleResourceActionModel extends Model {
|
||||||
@ -27,6 +27,7 @@ export class RoleResourceActionModel extends Model {
|
|||||||
const scope = await this.getScope();
|
const scope = await this.getScope();
|
||||||
|
|
||||||
if (scope) {
|
if (scope) {
|
||||||
|
actionParams['own'] = scope.get('key') === 'own';
|
||||||
actionParams['filter'] = scope.get('scope');
|
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 { Plugin } from '@nocobase/server';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { availableActionResource } from './actions/available-actions';
|
import { availableActionResource } from './actions/available-actions';
|
||||||
@ -260,10 +262,15 @@ export class PluginACL extends Plugin {
|
|||||||
{
|
{
|
||||||
name: 'admin',
|
name: 'admin',
|
||||||
title: 'Admin',
|
title: 'Admin',
|
||||||
|
allowConfigure: true,
|
||||||
|
allowNewMenu: true,
|
||||||
|
strategy: { actions: ['create', 'export', 'view', 'update', 'destroy'] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'member',
|
name: 'member',
|
||||||
title: 'Member',
|
title: 'Member',
|
||||||
|
allowNewMenu: true,
|
||||||
|
strategy: { actions: ['view', 'update:own', 'destroy:own', 'create'] },
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -276,10 +283,12 @@ export class PluginACL extends Plugin {
|
|||||||
await rolesResourcesScopes.createMany({
|
await rolesResourcesScopes.createMany({
|
||||||
records: [
|
records: [
|
||||||
{
|
{
|
||||||
|
key: 'all',
|
||||||
name: '{{t("All records")}}',
|
name: '{{t("All records")}}',
|
||||||
scope: {},
|
scope: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: 'own',
|
||||||
name: '{{t("Own records")}}',
|
name: '{{t("Own records")}}',
|
||||||
scope: {
|
scope: {
|
||||||
createdById: '{{ ctx.state.currentUser.id }}',
|
createdById: '{{ ctx.state.currentUser.id }}',
|
||||||
@ -295,7 +304,6 @@ export class PluginACL extends Plugin {
|
|||||||
this.app.acl.skip('*', '*', (ctx) => {
|
this.app.acl.skip('*', '*', (ctx) => {
|
||||||
return ctx.state.currentRole === 'root';
|
return ctx.state.currentRole === 'root';
|
||||||
});
|
});
|
||||||
|
|
||||||
// root role
|
// root role
|
||||||
this.app.resourcer.use(async (ctx, next) => {
|
this.app.resourcer.use(async (ctx, next) => {
|
||||||
const { actionName, resourceName } = ctx.action.params;
|
const { actionName, resourceName } = ctx.action.params;
|
||||||
@ -308,6 +316,62 @@ export class PluginACL extends Plugin {
|
|||||||
}
|
}
|
||||||
await next();
|
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() {
|
async install() {
|
||||||
|
@ -35,6 +35,8 @@ export default class PluginFileManager extends Plugin {
|
|||||||
if (process.env.NOCOBASE_ENV !== 'production') {
|
if (process.env.NOCOBASE_ENV !== 'production') {
|
||||||
await getStorageConfig(STORAGE_TYPE_LOCAL).middleware(this.app);
|
await getStorageConfig(STORAGE_TYPE_LOCAL).middleware(this.app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.app.acl.skip('attachments', 'upload', 'logged-in');
|
||||||
}
|
}
|
||||||
|
|
||||||
getName(): string {
|
getName(): string {
|
||||||
|
@ -5,6 +5,8 @@ export default {
|
|||||||
title: '{{t("Users")}}',
|
title: '{{t("Users")}}',
|
||||||
sortable: 'sort',
|
sortable: 'sort',
|
||||||
model: 'UserModel',
|
model: 'UserModel',
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
interface: 'input',
|
interface: 'input',
|
||||||
|
@ -51,6 +51,7 @@ export default class UsersPlugin extends Plugin<UserPluginConfig> {
|
|||||||
dataType: 'integer',
|
dataType: 'integer',
|
||||||
dataIndex: 'state.currentUser.id',
|
dataIndex: 'state.currentUser.id',
|
||||||
createOnly: true,
|
createOnly: true,
|
||||||
|
visible: true,
|
||||||
});
|
});
|
||||||
collection.setField('createdBy', {
|
collection.setField('createdBy', {
|
||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
@ -64,6 +65,7 @@ export default class UsersPlugin extends Plugin<UserPluginConfig> {
|
|||||||
type: 'context',
|
type: 'context',
|
||||||
dataType: 'integer',
|
dataType: 'integer',
|
||||||
dataIndex: 'state.currentUser.id',
|
dataIndex: 'state.currentUser.id',
|
||||||
|
visible: true,
|
||||||
});
|
});
|
||||||
collection.setField('updatedBy', {
|
collection.setField('updatedBy', {
|
||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
@ -85,10 +87,6 @@ export default class UsersPlugin extends Plugin<UserPluginConfig> {
|
|||||||
|
|
||||||
publicActions.forEach((action) => this.app.acl.skip('users', action));
|
publicActions.forEach((action) => this.app.acl.skip('users', action));
|
||||||
loggedInActions.forEach((action) => this.app.acl.skip('users', action, 'logged-in'));
|
loggedInActions.forEach((action) => this.app.acl.skip('users', action, 'logged-in'));
|
||||||
|
|
||||||
this.app.acl.skip('*', '*', (ctx) => {
|
|
||||||
return ctx.state.currentUser?.id == 1;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
|
Loading…
Reference in New Issue
Block a user