Skip to content

Commit

Permalink
Merge pull request #244 from Flowpack/feature/base-privileges
Browse files Browse the repository at this point in the history
FEATURE: Implement manage tags and collections privileges
  • Loading branch information
Sebobo authored Oct 29, 2024
2 parents 9e91555 + af9e0cd commit 5fa0849
Show file tree
Hide file tree
Showing 15 changed files with 124 additions and 20 deletions.
10 changes: 10 additions & 0 deletions Classes/GraphQL/Resolver/Type/QueryResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'),
];
}

Expand Down
13 changes: 13 additions & 0 deletions Configuration/Policy.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
15 changes: 15 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
3 changes: 3 additions & 0 deletions Resources/Private/GraphQL/schema.root.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ type Config {
uploadMaxFileSize: FileSize!
uploadMaxFileUploadLimit: Int!
currentServerTime: DateTime!
canManageAssetCollections: Boolean!
canManageTags: Boolean!
canManageAssets: Boolean!
}

"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);

Expand All @@ -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'}
>
<span className="fa-layers fa-fw">
<Icon icon="folder" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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}
>
<span className="fa-layers fa-fw">
<Icon icon="tag" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -30,6 +31,7 @@ const AssetCollectionTreeNode: React.FC<AssetCollectionTreeNodeProps> = ({
children = null,
renderChildCollections = true,
}) => {
const { config } = useConfigQuery();
const { assetCollection } = useAssetCollectionQuery(assetCollectionId);
const { assetCollections } = useAssetCollectionsQuery();
const [collapsed, setCollapsed] = useRecoilState(assetCollectionTreeCollapsedItemState(assetCollectionId));
Expand Down Expand Up @@ -82,7 +84,8 @@ const AssetCollectionTreeNode: React.FC<AssetCollectionTreeNodeProps> = ({
);

// TODO: Also check assetSource.readonly
const dragForbidden = !assetCollectionId || assetCollectionId === UNASSIGNED_COLLECTION_ID;
const dragForbidden =
!config.canManageAssetCollections || !assetCollectionId || assetCollectionId === UNASSIGNED_COLLECTION_ID;

return (
<Tree.Node>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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();
Expand Down Expand Up @@ -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}
/>
Expand Down
16 changes: 15 additions & 1 deletion Resources/Private/JavaScript/core/src/hooks/useConfigQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConfigQueryResult>(CONFIG);
return { config: data?.config, loading };
return { config: { ...DEFAULT_CONFIG, ...data?.config }, loading };
};

export default useConfigQuery;
3 changes: 3 additions & 0 deletions Resources/Private/JavaScript/core/src/queries/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const CONFIG = gql`
uploadMaxFileSize
uploadMaxFileUploadLimit
currentServerTime
canManageAssetCollections
canManageTags
canManageAssets
}
}
`;
Expand Down
3 changes: 3 additions & 0 deletions Resources/Private/JavaScript/dev-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 '.';
Expand All @@ -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();
Expand Down Expand Up @@ -65,15 +67,23 @@ const AssetCollectionInspector = () => {
return (
<InspectorContainer>
<Property label={translate('inspector.title', 'Title')}>
<TextInput type="text" value={title} onChange={handleChange} onEnterKey={handleApply} />
<TextInput
type="text"
value={title}
onChange={handleChange}
onEnterKey={handleApply}
disabled={!config.canManageAssetCollections}
/>
</Property>

<Actions
handleApply={handleApply}
handleDiscard={handleDiscard}
hasUnpublishedChanges={hasUnpublishedChanges}
inputValid={!!title}
/>
{config.canManageAssetCollections && (
<Actions
handleApply={handleApply}
handleDiscard={handleDiscard}
hasUnpublishedChanges={hasUnpublishedChanges}
inputValid={!!title}
/>
)}

<TagSelectBoxAssetCollection />
<ParentCollectionSelectBox />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -96,7 +98,7 @@ const ParentCollectionSelectBox = () => {
</Headline>
<SelectBox
className={classes.collectionSelectBox}
disabled={loading}
disabled={!config.canManageAssetCollections || loading}
placeholder={translate('inspector.collections.placeholder', 'Select a collection')}
value={selectedAssetCollection.parent?.id}
optionValueField="id"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useRecoilValue } from 'recoil';
import { TextInput } from '@neos-project/react-ui-components';

import { useIntl, useNotify } from '@media-ui/core';
import { useConfigQuery } from '@media-ui/core/src/hooks';
import { selectedInspectorViewState } from '@media-ui/core/src/state';
import { useSelectedTag, useUpdateTag } from '@media-ui/feature-asset-tags';

Expand All @@ -16,6 +17,7 @@ const TagInspector = () => {
const selectedTag = useSelectedTag();
const selectedInspectorView = useRecoilValue(selectedInspectorViewState);
const Notify = useNotify();
const { config } = useConfigQuery();
const { translate } = useIntl();
const [label, setLabel] = useState<string>(null);

Expand Down Expand Up @@ -54,14 +56,22 @@ const TagInspector = () => {
return (
<InspectorContainer>
<Property label={translate('inspector.label', 'Label')}>
<TextInput type="text" value={label || ''} onChange={setLabel} onEnterKey={handleApply} />
<TextInput
type="text"
value={label || ''}
onChange={setLabel}
onEnterKey={handleApply}
disabled={!config.canManageTags}
/>
</Property>

<Actions
handleApply={handleApply}
handleDiscard={handleDiscard}
hasUnpublishedChanges={hasUnpublishedChanges}
/>
{config.canManageTags && (
<Actions
handleApply={handleApply}
handleDiscard={handleDiscard}
hasUnpublishedChanges={hasUnpublishedChanges}
/>
)}
</InspectorContainer>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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();
Expand Down Expand Up @@ -56,7 +58,14 @@ const TagSelectBoxAssetCollection = () => {

if (!selectedAssetCollection) return null;

return <TagSelectBox values={tagIds} options={allTags} onChange={handleChange} />;
return (
<TagSelectBox
disabled={!config.canManageAssetCollections}
values={tagIds}
options={allTags}
onChange={handleChange}
/>
);
};

export default React.memo(TagSelectBoxAssetCollection);

0 comments on commit 5fa0849

Please sign in to comment.