From 0f5434bf5dc361e35ca647b3361bf1750b3a4c45 Mon Sep 17 00:00:00 2001 From: Sebastian Helzle Date: Fri, 25 Oct 2024 20:00:32 +0200 Subject: [PATCH 1/2] FEATURE: Implement manage tags and collections privileges --- .../GraphQL/Resolver/Type/QueryResolver.php | 10 ++++++++ Configuration/Policy.yaml | 13 ++++++++++ Readme.md | 15 ++++++++++++ Resources/Private/GraphQL/schema.root.graphql | 3 +++ .../components/AddAssetCollectionButton.tsx | 4 +++- .../src/components/AddTagButton.tsx | 4 +++- .../components/AssetCollectionTreeNode.tsx | 5 +++- .../src/components/DeleteButton.tsx | 7 +++++- .../core/src/hooks/useConfigQuery.ts | 16 ++++++++++++- .../JavaScript/core/src/queries/config.ts | 3 +++ .../Inspector/AssetCollectionInspector.tsx | 24 +++++++++++++------ .../Inspector/ParentCollectionSelectBox.tsx | 4 +++- .../SideBarRight/Inspector/TagInspector.tsx | 22 ++++++++++++----- .../Inspector/TagSelectBoxAssetCollection.tsx | 11 ++++++++- 14 files changed, 121 insertions(+), 20 deletions(-) diff --git a/Classes/GraphQL/Resolver/Type/QueryResolver.php b/Classes/GraphQL/Resolver/Type/QueryResolver.php index 44773a07f..0a09d813d 100644 --- a/Classes/GraphQL/Resolver/Type/QueryResolver.php +++ b/Classes/GraphQL/Resolver/Type/QueryResolver.php @@ -23,6 +23,7 @@ use Flowpack\Media\Ui\Service\UsageDetailsService; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\Doctrine\PersistenceManager; +use Neos\Flow\Security\Authorization\PrivilegeManagerInterface; use Neos\Media\Domain\Model\Asset; use Neos\Media\Domain\Model\AssetCollection; use Neos\Media\Domain\Model\AssetSource\AssetProxy\AssetProxyInterface; @@ -107,6 +108,12 @@ class QueryResolver implements ResolverInterface */ protected $assetProxyIteratorBuilder; + /** + * @Flow\Inject + * @var PrivilegeManagerInterface + */ + protected $privilegeManager; + /** * Returns total count of asset proxies in the given asset source * @noinspection PhpUnusedParameterInspection @@ -182,6 +189,9 @@ public function config($_): array 'uploadMaxFileSize' => $this->getMaximumFileUploadSize(), 'uploadMaxFileUploadLimit' => $this->getMaximumFileUploadLimit(), 'currentServerTime' => (new \DateTime())->format(DATE_W3C), + 'canManageTags' => $this->privilegeManager->isPrivilegeTargetGranted('Flowpack.Media.Ui:ManageTags'), + 'canManageAssetCollections' => $this->privilegeManager->isPrivilegeTargetGranted('Flowpack.Media.Ui:ManageAssetCollections'), + 'canManageAssets' => $this->privilegeManager->isPrivilegeTargetGranted('Flowpack.Media.Ui:ManageAssets'), ]; } diff --git a/Configuration/Policy.yaml b/Configuration/Policy.yaml index 545548972..1fec68de1 100644 --- a/Configuration/Policy.yaml +++ b/Configuration/Policy.yaml @@ -1,12 +1,21 @@ privilegeTargets: 'Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege': 'Flowpack.Media.Ui:ManageAssets': + label: 'Manage anything related to assets' matcher: 'method(Flowpack\Media\Ui\GraphQL\Resolver\Type\MutationResolver->.*())' + 'Flowpack.Media.Ui:ManageTags': + label: 'Manage tags' + matcher: 'method(Flowpack\Media\Ui\GraphQL\Resolver\Type\MutationResolver->(create|delete|update)Tag())' + 'Flowpack.Media.Ui:ManageAssetCollections': + label: 'Manage asset collections' + matcher: 'method(Flowpack\Media\Ui\GraphQL\Resolver\Type\MutationResolver->(create|delete|update)AssetCollection())' 'Flowpack.Media.Ui:Queries': + label: 'Query asset data' matcher: 'method(public Flowpack\Media\Ui\GraphQL\Resolver\(.*)Resolver->.*()) || method(t3n\GraphQL\Controller\GraphQLController->queryAction())' 'Neos\Neos\Security\Authorization\Privilege\ModulePrivilege': 'Flowpack.Media.Ui:Backend.Module.Management.Media': + label: 'Access to the media management module' matcher: 'management/mediaui' roles: @@ -16,5 +25,9 @@ roles: permission: GRANT - privilegeTarget: 'Flowpack.Media.Ui:ManageAssets' permission: GRANT + - privilegeTarget: 'Flowpack.Media.Ui:ManageTags' + permission: GRANT + - privilegeTarget: 'Flowpack.Media.Ui:ManageAssetCollections' + permission: GRANT - privilegeTarget: 'Flowpack.Media.Ui:Queries' permission: GRANT diff --git a/Readme.md b/Readme.md index 1177f8a9a..3671c2328 100644 --- a/Readme.md +++ b/Readme.md @@ -84,6 +84,21 @@ Neos: useNewMediaSelection: false ``` +#### Privileges + +By default any editor (Neos.Neos:AbstractEditor) can access the new media module and manage assets, tags and collections. + +To adjust the privileges you can use the following privilege targets for any role: + +```yaml +- privilegeTarget: 'Flowpack.Media.Ui:ManageAssets' + permission: GRANT +- privilegeTarget: 'Flowpack.Media.Ui:ManageTags' + permission: GRANT +- privilegeTarget: 'Flowpack.Media.Ui:ManageAssetCollections' + permission: GRANT +``` + #### Hierarchical asset collections This package will enable a hierarchical asset collection structure via AOP (until the feature is in the Neos core). diff --git a/Resources/Private/GraphQL/schema.root.graphql b/Resources/Private/GraphQL/schema.root.graphql index 579d8ac42..9fd350ca5 100644 --- a/Resources/Private/GraphQL/schema.root.graphql +++ b/Resources/Private/GraphQL/schema.root.graphql @@ -164,6 +164,9 @@ type Config { uploadMaxFileSize: FileSize! uploadMaxFileUploadLimit: Int! currentServerTime: DateTime! + canManageAssetCollections: Boolean! + canManageTags: Boolean! + canManageAssets: Boolean! } """ diff --git a/Resources/Private/JavaScript/asset-collections/src/components/AddAssetCollectionButton.tsx b/Resources/Private/JavaScript/asset-collections/src/components/AddAssetCollectionButton.tsx index 0ef58629b..6f7e4c6ee 100644 --- a/Resources/Private/JavaScript/asset-collections/src/components/AddAssetCollectionButton.tsx +++ b/Resources/Private/JavaScript/asset-collections/src/components/AddAssetCollectionButton.tsx @@ -4,6 +4,7 @@ import { useRecoilValue, useSetRecoilState } from 'recoil'; import { Button, Icon } from '@neos-project/react-ui-components'; import { useIntl } from '@media-ui/core'; +import { useConfigQuery } from '@media-ui/core/src/hooks'; import { createAssetCollectionDialogVisibleState } from '../state/createAssetCollectionDialogVisibleState'; import { assetCollectionTreeViewState } from '../state/assetCollectionTreeViewState'; @@ -12,6 +13,7 @@ import classes from './AddAssetCollectionButton.module.css'; const AddAssetCollectionButton: React.FC = () => { const { translate } = useIntl(); + const { config } = useConfigQuery(); const setCreateAssetCollectionDialogState = useSetRecoilState(createAssetCollectionDialogVisibleState); const assetCollectionTreeView = useRecoilValue(assetCollectionTreeViewState); @@ -22,7 +24,7 @@ const AddAssetCollectionButton: React.FC = () => { hoverStyle="brand" title={translate('assetCollectionTree.toolbar.createAssetCollection', 'Create new asset collection')} onClick={() => setCreateAssetCollectionDialogState(true)} - disabled={assetCollectionTreeView !== 'collections'} + disabled={!config.canManageAssetCollections || assetCollectionTreeView !== 'collections'} > diff --git a/Resources/Private/JavaScript/asset-collections/src/components/AddTagButton.tsx b/Resources/Private/JavaScript/asset-collections/src/components/AddTagButton.tsx index 6b3ed2e70..48e9c3a36 100644 --- a/Resources/Private/JavaScript/asset-collections/src/components/AddTagButton.tsx +++ b/Resources/Private/JavaScript/asset-collections/src/components/AddTagButton.tsx @@ -4,12 +4,14 @@ import { useRecoilValue, useSetRecoilState } from 'recoil'; import { Button, Icon } from '@neos-project/react-ui-components'; import { useIntl } from '@media-ui/core'; +import { useConfigQuery } from '@media-ui/core/src/hooks'; import { createTagDialogState, selectedTagIdState } from '@media-ui/feature-asset-tags'; import classes from './AddTagButton.module.css'; const AddTagButton: React.FC = () => { const { translate } = useIntl(); + const { config } = useConfigQuery(); const setCreateTagDialogState = useSetRecoilState(createTagDialogState); const selectedTagId = useRecoilValue(selectedTagIdState); @@ -24,7 +26,7 @@ const AddTagButton: React.FC = () => { hoverStyle="brand" title={translate('assetCollectionTree.toolbar.createTag', 'Create new tag')} onClick={onClickCreate} - disabled={selectedTagId !== null} + disabled={!config.canManageTags || selectedTagId !== null} > diff --git a/Resources/Private/JavaScript/asset-collections/src/components/AssetCollectionTreeNode.tsx b/Resources/Private/JavaScript/asset-collections/src/components/AssetCollectionTreeNode.tsx index d139a694c..82a9d9341 100644 --- a/Resources/Private/JavaScript/asset-collections/src/components/AssetCollectionTreeNode.tsx +++ b/Resources/Private/JavaScript/asset-collections/src/components/AssetCollectionTreeNode.tsx @@ -6,6 +6,7 @@ import { Tree } from '@neos-project/react-ui-components'; import dndTypes from '@media-ui/core/src/constants/dndTypes'; import { selectedAssetCollectionAndTagState } from '@media-ui/core/src/state'; import { IconStack } from '@media-ui/core/src/components'; +import { useConfigQuery } from '@media-ui/core/src/hooks'; import TagTreeNode from './TagTreeNode'; import { useAssetCollectionQuery, UNASSIGNED_COLLECTION_ID } from '../hooks/useAssetCollectionQuery'; @@ -30,6 +31,7 @@ const AssetCollectionTreeNode: React.FC = ({ children = null, renderChildCollections = true, }) => { + const { config } = useConfigQuery(); const { assetCollection } = useAssetCollectionQuery(assetCollectionId); const { assetCollections } = useAssetCollectionsQuery(); const [collapsed, setCollapsed] = useRecoilState(assetCollectionTreeCollapsedItemState(assetCollectionId)); @@ -82,7 +84,8 @@ const AssetCollectionTreeNode: React.FC = ({ ); // TODO: Also check assetSource.readonly - const dragForbidden = !assetCollectionId || assetCollectionId === UNASSIGNED_COLLECTION_ID; + const dragForbidden = + !config.canManageAssetCollections || !assetCollectionId || assetCollectionId === UNASSIGNED_COLLECTION_ID; return ( diff --git a/Resources/Private/JavaScript/asset-collections/src/components/DeleteButton.tsx b/Resources/Private/JavaScript/asset-collections/src/components/DeleteButton.tsx index b4400fc6e..894bbf8dd 100644 --- a/Resources/Private/JavaScript/asset-collections/src/components/DeleteButton.tsx +++ b/Resources/Private/JavaScript/asset-collections/src/components/DeleteButton.tsx @@ -4,6 +4,7 @@ import { useSetRecoilState } from 'recoil'; import { IconButton } from '@neos-project/react-ui-components'; import { useIntl, useMediaUi, useNotify } from '@media-ui/core'; +import { useConfigQuery } from '@media-ui/core/src/hooks'; import { selectedAssetCollectionAndTagState } from '@media-ui/core/src/state'; import { useDeleteTag, useSelectedTag } from '@media-ui/feature-asset-tags'; @@ -12,6 +13,7 @@ import useSelectedAssetCollection from '../hooks/useSelectedAssetCollection'; const DeleteButton: React.FC = () => { const { translate } = useIntl(); + const { config } = useConfigQuery(); const Notify = useNotify(); const { approvalAttainmentStrategy } = useMediaUi(); const selectedAssetCollection = useSelectedAssetCollection(); @@ -71,7 +73,10 @@ const DeleteButton: React.FC = () => { size="regular" style="transparent" hoverStyle="error" - disabled={!selectedAssetCollection && !selectedTag} + disabled={ + (!selectedAssetCollection || !config.canManageAssetCollections) && + (!selectedTag || !config.canManageTags) + } title={translate('assetCollectionTree.toolbar.delete', 'Delete')} onClick={onClickDelete} /> diff --git a/Resources/Private/JavaScript/core/src/hooks/useConfigQuery.ts b/Resources/Private/JavaScript/core/src/hooks/useConfigQuery.ts index 856d8db6a..a6fe2a32e 100644 --- a/Resources/Private/JavaScript/core/src/hooks/useConfigQuery.ts +++ b/Resources/Private/JavaScript/core/src/hooks/useConfigQuery.ts @@ -7,12 +7,26 @@ interface ConfigQueryResult { uploadMaxFileSize: number; uploadMaxFileUploadLimit: number; currentServerTime: Date; + canManageAssetCollections: boolean; + canManageTags: boolean; + canManageAssets: boolean; }; } +const DEFAULT_CONFIG: ConfigQueryResult = { + config: { + uploadMaxFileSize: 0, + uploadMaxFileUploadLimit: 0, + currentServerTime: new Date(), + canManageAssetCollections: false, + canManageTags: false, + canManageAssets: false, + }, +}; + const useConfigQuery = () => { const { data, loading } = useQuery(CONFIG); - return { config: data?.config, loading }; + return { config: { ...DEFAULT_CONFIG, ...data?.config }, loading }; }; export default useConfigQuery; diff --git a/Resources/Private/JavaScript/core/src/queries/config.ts b/Resources/Private/JavaScript/core/src/queries/config.ts index f1b4bb412..d6dbb5f88 100644 --- a/Resources/Private/JavaScript/core/src/queries/config.ts +++ b/Resources/Private/JavaScript/core/src/queries/config.ts @@ -6,6 +6,9 @@ const CONFIG = gql` uploadMaxFileSize uploadMaxFileUploadLimit currentServerTime + canManageAssetCollections + canManageTags + canManageAssets } } `; diff --git a/Resources/Private/JavaScript/media-module/src/components/SideBarRight/Inspector/AssetCollectionInspector.tsx b/Resources/Private/JavaScript/media-module/src/components/SideBarRight/Inspector/AssetCollectionInspector.tsx index a819f8346..6ddecac5f 100644 --- a/Resources/Private/JavaScript/media-module/src/components/SideBarRight/Inspector/AssetCollectionInspector.tsx +++ b/Resources/Private/JavaScript/media-module/src/components/SideBarRight/Inspector/AssetCollectionInspector.tsx @@ -5,6 +5,7 @@ import { TextInput } from '@neos-project/react-ui-components'; import { useIntl, useNotify } from '@media-ui/core'; import { selectedInspectorViewState } from '@media-ui/core/src/state'; +import { useConfigQuery } from '@media-ui/core/src/hooks'; import { useSelectedAssetCollection, useUpdateAssetCollection } from '@media-ui/feature-asset-collections'; import { TagSelectBoxAssetCollection } from '.'; @@ -15,6 +16,7 @@ import ParentCollectionSelectBox from './ParentCollectionSelectBox'; // TASK: Move into media module package const AssetCollectionInspector = () => { + const { config } = useConfigQuery(); const selectedAssetCollection = useSelectedAssetCollection(); const selectedInspectorView = useRecoilValue(selectedInspectorViewState); const Notify = useNotify(); @@ -65,15 +67,23 @@ const AssetCollectionInspector = () => { return ( - + - + {config.canManageAssetCollections && ( + + )} diff --git a/Resources/Private/JavaScript/media-module/src/components/SideBarRight/Inspector/ParentCollectionSelectBox.tsx b/Resources/Private/JavaScript/media-module/src/components/SideBarRight/Inspector/ParentCollectionSelectBox.tsx index a9ffabacc..5c2859a59 100644 --- a/Resources/Private/JavaScript/media-module/src/components/SideBarRight/Inspector/ParentCollectionSelectBox.tsx +++ b/Resources/Private/JavaScript/media-module/src/components/SideBarRight/Inspector/ParentCollectionSelectBox.tsx @@ -4,6 +4,7 @@ import { Headline, SelectBox } from '@neos-project/react-ui-components'; import { useIntl, useMediaUi, useNotify } from '@media-ui/core'; import { IconLabel } from '@media-ui/core/src/components'; +import { useConfigQuery } from '@media-ui/core/src/hooks'; import { collectionPath, useAssetCollectionsQuery, @@ -17,6 +18,7 @@ import * as classes from './ParentCollectionSelectBox.module.css'; const ParentCollectionSelectBox = () => { const Notify = useNotify(); + const { config } = useConfigQuery(); const { translate } = useIntl(); const { approvalAttainmentStrategy } = useMediaUi(); const { assetCollections } = useAssetCollectionsQuery(); @@ -96,7 +98,7 @@ const ParentCollectionSelectBox = () => { { const selectedTag = useSelectedTag(); const selectedInspectorView = useRecoilValue(selectedInspectorViewState); const Notify = useNotify(); + const { config } = useConfigQuery(); const { translate } = useIntl(); const [label, setLabel] = useState(null); @@ -54,14 +56,22 @@ const TagInspector = () => { return ( - + - + {config.canManageTags && ( + + )} ); }; diff --git a/Resources/Private/JavaScript/media-module/src/components/SideBarRight/Inspector/TagSelectBoxAssetCollection.tsx b/Resources/Private/JavaScript/media-module/src/components/SideBarRight/Inspector/TagSelectBoxAssetCollection.tsx index 09fd97b05..af165b155 100644 --- a/Resources/Private/JavaScript/media-module/src/components/SideBarRight/Inspector/TagSelectBoxAssetCollection.tsx +++ b/Resources/Private/JavaScript/media-module/src/components/SideBarRight/Inspector/TagSelectBoxAssetCollection.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useMemo } from 'react'; import { useIntl, useNotify } from '@media-ui/core'; +import { useConfigQuery } from '@media-ui/core/src/hooks'; import { useTagsQuery } from '@media-ui/feature-asset-tags'; import { useSelectedAssetCollection, useUpdateAssetCollection } from '@media-ui/feature-asset-collections'; @@ -21,6 +22,7 @@ const tagsMatchAssetCollection = (tags: Tag[], assetCollection: AssetCollection) const TagSelectBoxAssetCollection = () => { const Notify = useNotify(); + const { config } = useConfigQuery(); const { translate } = useIntl(); const { tags: allTags } = useTagsQuery(); const { updateAssetCollection } = useUpdateAssetCollection(); @@ -56,7 +58,14 @@ const TagSelectBoxAssetCollection = () => { if (!selectedAssetCollection) return null; - return ; + return ( + + ); }; export default React.memo(TagSelectBoxAssetCollection); From af9e0cd974e002d981e5fbca91a63d3290d02ee7 Mon Sep 17 00:00:00 2001 From: Sebastian Helzle Date: Sat, 26 Oct 2024 09:34:00 +0200 Subject: [PATCH 2/2] TASK: Adjust dev-server to new config --- Resources/Private/JavaScript/dev-server/src/server.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Resources/Private/JavaScript/dev-server/src/server.ts b/Resources/Private/JavaScript/dev-server/src/server.ts index fea486d79..a1d9df7d4 100644 --- a/Resources/Private/JavaScript/dev-server/src/server.ts +++ b/Resources/Private/JavaScript/dev-server/src/server.ts @@ -164,6 +164,9 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); uploadMaxFileSize: 1024 * 1024, uploadMaxFileUploadLimit: 2, currentServerTime: new Date(), + canManageAssetCollections: true, + canManageTags: true, + canManageAssets: true, }), }, Mutation: {