Add page type and attributes permissions handling (#1425)

* WIP add new configuration menu

* Add missing permissions

* Changed filtering to one based on arrays of permissions

* Add hasAnyPermissions and hasAllPermissions functions

* Simplify getConfigMenuItemsPermissions function

* Simplify routing logic
This commit is contained in:
Wojciech Mista 2021-09-24 14:01:54 +02:00 committed by GitHub
parent d4ffd88752
commit b98f069aab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 102 additions and 55 deletions

View file

@ -66,6 +66,13 @@ export const PERMISSIONS = {
CONFIGURATION_SELECTORS.productTypes CONFIGURATION_SELECTORS.productTypes
] ]
}, },
pageTypeAndAttribute: {
parent: configurationAsParent,
permissionSelectors: [
CONFIGURATION_SELECTORS.pageTypes,
CONFIGURATION_SELECTORS.attributes
]
},
settings: { settings: {
parent: configurationAsParent, parent: configurationAsParent,
permissionSelectors: [ permissionSelectors: [

View file

@ -33,9 +33,6 @@ export const PERMISSIONS_OPTIONS = {
user: ONE_PERMISSION_USERS.page, user: ONE_PERMISSION_USERS.page,
permissions: [PERMISSIONS.page] permissions: [PERMISSIONS.page]
}, },
pageTypeAndAttribute: {
user: ONE_PERMISSION_USERS.pageTypeAndAttribute
},
plugin: { plugin: {
user: ONE_PERMISSION_USERS.plugin, user: ONE_PERMISSION_USERS.plugin,
permissions: [PERMISSIONS.plugin] permissions: [PERMISSIONS.plugin]
@ -48,6 +45,10 @@ export const PERMISSIONS_OPTIONS = {
user: ONE_PERMISSION_USERS.productTypeAndAttribute, user: ONE_PERMISSION_USERS.productTypeAndAttribute,
permissions: [PERMISSIONS.productTypeAndAttribute] permissions: [PERMISSIONS.productTypeAndAttribute]
}, },
pageTypeAndAttribute: {
user: ONE_PERMISSION_USERS.pageTypeAndAttribute,
permissions: [PERMISSIONS.pageTypeAndAttribute]
},
settings: { settings: {
user: ONE_PERMISSION_USERS.settings, user: ONE_PERMISSION_USERS.settings,
permissions: [PERMISSIONS.settings] permissions: [PERMISSIONS.settings]

View file

@ -4,24 +4,37 @@ import { Route, RouteProps } from "react-router-dom";
import NotFound from "../../NotFound"; import NotFound from "../../NotFound";
import { PermissionEnum } from "../../types/globalTypes"; import { PermissionEnum } from "../../types/globalTypes";
import { hasPermission } from "../misc"; import { hasAllPermissions, hasAnyPermissions } from "../misc";
type MatchPermissionType = "all" | "any";
interface SectionRouteProps extends RouteProps { interface SectionRouteProps extends RouteProps {
permissions?: PermissionEnum[]; permissions?: PermissionEnum[];
matchPermission?: MatchPermissionType;
} }
const matchAll = (match: MatchPermissionType) => match === "all";
export const SectionRoute: React.FC<SectionRouteProps> = ({ export const SectionRoute: React.FC<SectionRouteProps> = ({
permissions, permissions,
matchPermission = "all",
...props ...props
}) => { }) => {
const { user } = useUser(); const { user } = useUser();
const hasPermissions = const hasSectionPermissions = () => {
!permissions || if (!permissions) {
permissions return true;
.map(permission => hasPermission(permission, user)) }
.reduce((prev, curr) => prev && curr);
return hasPermissions ? <Route {...props} /> : <NotFound />; if (matchAll(matchPermission)) {
return hasAllPermissions(permissions, user);
}
return hasAnyPermissions(permissions, user);
};
return hasSectionPermissions() ? <Route {...props} /> : <NotFound />;
}; };
SectionRoute.displayName = "Route"; SectionRoute.displayName = "Route";
export default SectionRoute; export default SectionRoute;

View file

@ -4,3 +4,9 @@ import { PermissionEnum } from "../types/globalTypes";
export const hasPermission = (permission: PermissionEnum, user: User) => export const hasPermission = (permission: PermissionEnum, user: User) =>
user.userPermissions.map(perm => perm.code).includes(permission); user.userPermissions.map(perm => perm.code).includes(permission);
export const hasAnyPermissions = (permissions: PermissionEnum[], user: User) =>
permissions?.some(permission => hasPermission(permission, user)) || false;
export const hasAllPermissions = (permissions: PermissionEnum[], user: User) =>
permissions?.every(permission => hasPermission(permission, user)) || false;

View file

@ -6,11 +6,8 @@ import discountsIcon from "@assets/images/menu-discounts-icon.svg";
import homeIcon from "@assets/images/menu-home-icon.svg"; import homeIcon from "@assets/images/menu-home-icon.svg";
import ordersIcon from "@assets/images/menu-orders-icon.svg"; import ordersIcon from "@assets/images/menu-orders-icon.svg";
import translationIcon from "@assets/images/menu-translation-icon.svg"; import translationIcon from "@assets/images/menu-translation-icon.svg";
import { import { configurationMenuUrl } from "@saleor/configuration";
configurationMenuUrl, import { getConfigMenuItemsPermissions } from "@saleor/configuration/utils";
createConfigurationMenu
} from "@saleor/configuration";
import { MenuItem } from "@saleor/configuration/ConfigurationPage";
import { User } from "@saleor/fragments/types/User"; import { User } from "@saleor/fragments/types/User";
import { giftCardsListUrl } from "@saleor/giftCards/urls"; import { giftCardsListUrl } from "@saleor/giftCards/urls";
import { commonMessages, sectionNames } from "@saleor/intl"; import { commonMessages, sectionNames } from "@saleor/intl";
@ -33,8 +30,6 @@ interface FilterableMenuItem extends Omit<SidebarMenuItem, "children"> {
} }
function createMenuStructure(intl: IntlShape, user: User): SidebarMenuItem[] { function createMenuStructure(intl: IntlShape, user: User): SidebarMenuItem[] {
const configurationMenu = createConfigurationMenu(intl);
const menuItems: FilterableMenuItem[] = [ const menuItems: FilterableMenuItem[] = [
{ {
ariaLabel: "home", ariaLabel: "home",
@ -152,12 +147,7 @@ function createMenuStructure(intl: IntlShape, user: User): SidebarMenuItem[] {
ariaLabel: "configure", ariaLabel: "configure",
iconSrc: configurationIcon, iconSrc: configurationIcon,
label: intl.formatMessage(sectionNames.configuration), label: intl.formatMessage(sectionNames.configuration),
permissions: configurationMenu permissions: getConfigMenuItemsPermissions(intl),
.reduce(
(sections, section) => [...sections, ...section.menuItems],
[] as MenuItem[]
)
.map(section => section.permission),
id: "configure", id: "configure",
url: configurationMenuUrl url: configurationMenuUrl
} }

View file

@ -8,7 +8,7 @@ import { makeStyles } from "@saleor/macaw-ui";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { hasPermission } from "../auth/misc"; import { hasAnyPermissions } from "../auth/misc";
import Container from "../components/Container"; import Container from "../components/Container";
import PageHeader from "../components/PageHeader"; import PageHeader from "../components/PageHeader";
import VersionInfo from "../components/VersionInfo"; import VersionInfo from "../components/VersionInfo";
@ -17,7 +17,7 @@ import { PermissionEnum } from "../types/globalTypes";
export interface MenuItem { export interface MenuItem {
description: string; description: string;
icon: React.ReactElement<IconProps>; icon: React.ReactElement<IconProps>;
permission: PermissionEnum; permissions: PermissionEnum[];
title: string; title: string;
url?: string; url?: string;
testId?: string; testId?: string;
@ -123,6 +123,7 @@ export const ConfigurationPage: React.FC<ConfigurationPageProps> = props => {
); );
const intl = useIntl(); const intl = useIntl();
return ( return (
<Container> <Container>
{!isSmUp && renderVersionInfo} {!isSmUp && renderVersionInfo}
@ -135,7 +136,7 @@ export const ConfigurationPage: React.FC<ConfigurationPageProps> = props => {
{menus {menus
.filter(menu => .filter(menu =>
menu.menuItems.some(menuItem => menu.menuItems.some(menuItem =>
hasPermission(menuItem.permission, user) hasAnyPermissions(menuItem.permissions, user)
) )
) )
.map((menu, menuIndex) => ( .map((menu, menuIndex) => (
@ -145,7 +146,9 @@ export const ConfigurationPage: React.FC<ConfigurationPageProps> = props => {
</div> </div>
<div className={classes.configurationItem}> <div className={classes.configurationItem}>
{menu.menuItems {menu.menuItems
.filter(menuItem => hasPermission(menuItem.permission, user)) .filter(menuItem =>
hasAnyPermissions(menuItem.permissions, user)
)
.map((item, itemIndex) => ( .map((item, itemIndex) => (
<Card <Card
className={item.url ? classes.card : classes.cardDisabled} className={item.url ? classes.card : classes.cardDisabled}

View file

@ -50,7 +50,10 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
id: "configurationMenuAttributes" id: "configurationMenuAttributes"
}), }),
icon: <Attributes fontSize="inherit" viewBox="0 0 44 44" />, icon: <Attributes fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES, permissions: [
PermissionEnum.MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES,
PermissionEnum.MANAGE_PAGE_TYPES_AND_ATTRIBUTES
],
title: intl.formatMessage(sectionNames.attributes), title: intl.formatMessage(sectionNames.attributes),
url: attributeListUrl(), url: attributeListUrl(),
testId: "configurationMenuAttributes" testId: "configurationMenuAttributes"
@ -61,7 +64,7 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
id: "configurationMenuProductTypes" id: "configurationMenuProductTypes"
}), }),
icon: <ProductTypes fontSize="inherit" viewBox="0 0 44 44" />, icon: <ProductTypes fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES, permissions: [PermissionEnum.MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES],
title: intl.formatMessage(sectionNames.productTypes), title: intl.formatMessage(sectionNames.productTypes),
url: productTypeListUrl(), url: productTypeListUrl(),
testId: "configurationMenuProductTypes" testId: "configurationMenuProductTypes"
@ -79,7 +82,7 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
id: "configurationMenuTaxes" id: "configurationMenuTaxes"
}), }),
icon: <Taxes fontSize="inherit" viewBox="0 0 44 44" />, icon: <Taxes fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_SETTINGS, permissions: [PermissionEnum.MANAGE_SETTINGS],
title: intl.formatMessage(sectionNames.taxes), title: intl.formatMessage(sectionNames.taxes),
url: taxSection, url: taxSection,
testId: "configurationMenuTaxes" testId: "configurationMenuTaxes"
@ -97,7 +100,7 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
id: "configurationMenuStaff" id: "configurationMenuStaff"
}), }),
icon: <StaffMembers fontSize="inherit" viewBox="0 0 44 44" />, icon: <StaffMembers fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_STAFF, permissions: [PermissionEnum.MANAGE_STAFF],
title: intl.formatMessage(sectionNames.staff), title: intl.formatMessage(sectionNames.staff),
url: staffListUrl(), url: staffListUrl(),
testId: "configurationMenuStaff" testId: "configurationMenuStaff"
@ -109,7 +112,7 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
id: "configurationMenuPermissionGroups" id: "configurationMenuPermissionGroups"
}), }),
icon: <PermissionGroups fontSize="inherit" viewBox="0 0 44 44" />, icon: <PermissionGroups fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_STAFF, permissions: [PermissionEnum.MANAGE_STAFF],
title: intl.formatMessage(sectionNames.permissionGroups), title: intl.formatMessage(sectionNames.permissionGroups),
url: permissionGroupListUrl(), url: permissionGroupListUrl(),
testId: "configurationMenuPermissionGroups" testId: "configurationMenuPermissionGroups"
@ -127,7 +130,7 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
id: "configurationMenuShipping" id: "configurationMenuShipping"
}), }),
icon: <ShippingMethods fontSize="inherit" viewBox="0 0 44 44" />, icon: <ShippingMethods fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_SHIPPING, permissions: [PermissionEnum.MANAGE_SHIPPING],
title: intl.formatMessage(sectionNames.shipping), title: intl.formatMessage(sectionNames.shipping),
url: shippingZonesListUrl(), url: shippingZonesListUrl(),
testId: "configurationMenuShipping" testId: "configurationMenuShipping"
@ -138,7 +141,7 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
id: "configurationMenuWarehouses" id: "configurationMenuWarehouses"
}), }),
icon: <Warehouses fontSize="inherit" viewBox="0 0 44 44" />, icon: <Warehouses fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_PRODUCTS, permissions: [PermissionEnum.MANAGE_PRODUCTS],
title: intl.formatMessage(sectionNames.warehouses), title: intl.formatMessage(sectionNames.warehouses),
url: warehouseSection, url: warehouseSection,
testId: "configurationMenuWarehouses" testId: "configurationMenuWarehouses"
@ -156,7 +159,7 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
id: "configurationMenuChannels" id: "configurationMenuChannels"
}), }),
icon: <Channels fontSize="inherit" viewBox="0 0 44 44" />, icon: <Channels fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_CHANNELS, permissions: [PermissionEnum.MANAGE_CHANNELS],
title: intl.formatMessage(sectionNames.channels), title: intl.formatMessage(sectionNames.channels),
url: channelsListUrl(), url: channelsListUrl(),
testId: "configurationMenuChannels" testId: "configurationMenuChannels"
@ -174,7 +177,10 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
id: "configurationMenuPageTypes" id: "configurationMenuPageTypes"
}), }),
icon: <PageTypes fontSize="inherit" viewBox="0 0 44 44" />, icon: <PageTypes fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_PAGES, permissions: [
PermissionEnum.MANAGE_PAGES,
PermissionEnum.MANAGE_PAGE_TYPES_AND_ATTRIBUTES
],
title: intl.formatMessage(sectionNames.pageTypes), title: intl.formatMessage(sectionNames.pageTypes),
url: pageTypeListUrl(), url: pageTypeListUrl(),
testId: "configurationMenuPageTypes" testId: "configurationMenuPageTypes"
@ -185,7 +191,7 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
id: "configurationMenuPages" id: "configurationMenuPages"
}), }),
icon: <Pages fontSize="inherit" viewBox="0 0 44 44" />, icon: <Pages fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_PAGES, permissions: [PermissionEnum.MANAGE_PAGES],
title: intl.formatMessage(sectionNames.pages), title: intl.formatMessage(sectionNames.pages),
url: pageListUrl(), url: pageListUrl(),
testId: "configurationMenuPages" testId: "configurationMenuPages"
@ -203,7 +209,7 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
id: "configurationMenuNavigation" id: "configurationMenuNavigation"
}), }),
icon: <Navigation fontSize="inherit" viewBox="0 0 44 44" />, icon: <Navigation fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_MENUS, permissions: [PermissionEnum.MANAGE_MENUS],
title: intl.formatMessage(sectionNames.navigation), title: intl.formatMessage(sectionNames.navigation),
url: menuListUrl(), url: menuListUrl(),
testId: "configurationMenuNavigation" testId: "configurationMenuNavigation"
@ -214,7 +220,7 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
id: "configurationMenuSiteSettings" id: "configurationMenuSiteSettings"
}), }),
icon: <SiteSettings fontSize="inherit" viewBox="0 0 44 44" />, icon: <SiteSettings fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_SETTINGS, permissions: [PermissionEnum.MANAGE_SETTINGS],
title: intl.formatMessage(sectionNames.siteSettings), title: intl.formatMessage(sectionNames.siteSettings),
url: siteSettingsUrl(), url: siteSettingsUrl(),
testId: "configurationMenuSiteSettings" testId: "configurationMenuSiteSettings"
@ -231,7 +237,7 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
preserveAspectRatio="xMinYMin meet" preserveAspectRatio="xMinYMin meet"
/> />
), ),
permission: PermissionEnum.MANAGE_PLUGINS, permissions: [PermissionEnum.MANAGE_PLUGINS],
title: intl.formatMessage(sectionNames.plugins), title: intl.formatMessage(sectionNames.plugins),
url: pluginListUrl(), url: pluginListUrl(),
testId: "configurationPluginsPages" testId: "configurationPluginsPages"

View file

@ -0,0 +1,17 @@
import { PermissionEnum } from "@saleor/types/globalTypes";
import { IntlShape } from "react-intl";
import { createConfigurationMenu } from ".";
export const getConfigMenuItemsPermissions = (
intl: IntlShape
): PermissionEnum[] =>
createConfigurationMenu(intl)
.reduce(
(prev, { menuItems }) => [
...prev,
...menuItems.map(({ permissions }) => permissions)
],
[]
)
.flat();

View file

@ -26,7 +26,6 @@ import AuthProvider, { useAuth } from "./auth/AuthProvider";
import LoginLoading from "./auth/components/LoginLoading/LoginLoading"; import LoginLoading from "./auth/components/LoginLoading/LoginLoading";
import SectionRoute from "./auth/components/SectionRoute"; import SectionRoute from "./auth/components/SectionRoute";
import authLink from "./auth/link"; import authLink from "./auth/link";
import { hasPermission } from "./auth/misc";
import CategorySection from "./categories"; import CategorySection from "./categories";
import ChannelsSection from "./channels"; import ChannelsSection from "./channels";
import { channelsSection } from "./channels/urls"; import { channelsSection } from "./channels/urls";
@ -41,7 +40,8 @@ import MessageManagerProvider from "./components/messages";
import { ShopProvider } from "./components/Shop"; import { ShopProvider } from "./components/Shop";
import { WindowTitle } from "./components/WindowTitle"; import { WindowTitle } from "./components/WindowTitle";
import { API_URI, APP_MOUNT_URI, DEMO_MODE, GTM_ID } from "./config"; import { API_URI, APP_MOUNT_URI, DEMO_MODE, GTM_ID } from "./config";
import ConfigurationSection, { createConfigurationMenu } from "./configuration"; import ConfigurationSection from "./configuration";
import { getConfigMenuItemsPermissions } from "./configuration/utils";
import AppStateProvider from "./containers/AppState"; import AppStateProvider from "./containers/AppState";
import BackgroundTasksProvider from "./containers/BackgroundTasks"; import BackgroundTasksProvider from "./containers/BackgroundTasks";
import ServiceWorker from "./containers/ServiceWorker/ServiceWorker"; import ServiceWorker from "./containers/ServiceWorker/ServiceWorker";
@ -71,6 +71,7 @@ import TranslationsSection from "./translations";
import { PermissionEnum } from "./types/globalTypes"; import { PermissionEnum } from "./types/globalTypes";
import WarehouseSection from "./warehouses"; import WarehouseSection from "./warehouses";
import { warehouseSection } from "./warehouses/urls"; import { warehouseSection } from "./warehouses/urls";
if (process.env.GTM_ID) { if (process.env.GTM_ID) {
TagManager.initialize({ gtmId: GTM_ID }); TagManager.initialize({ gtmId: GTM_ID });
} }
@ -151,8 +152,7 @@ const Routes: React.FC = () => {
hasToken, hasToken,
isAuthenticated, isAuthenticated,
tokenAuthLoading, tokenAuthLoading,
tokenVerifyLoading, tokenVerifyLoading
user
} = useAuth(); } = useAuth();
const { channel } = useAppChannel(false); const { channel } = useAppChannel(false);
@ -220,9 +220,13 @@ const Routes: React.FC = () => {
component={PageSection} component={PageSection}
/> />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_PAGES]} permissions={[
PermissionEnum.MANAGE_PAGES,
PermissionEnum.MANAGE_PAGE_TYPES_AND_ATTRIBUTES
]}
path="/page-types" path="/page-types"
component={PageTypesSection} component={PageTypesSection}
matchPermission="any"
/> />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_PLUGINS]} permissions={[PermissionEnum.MANAGE_PLUGINS]}
@ -283,10 +287,12 @@ const Routes: React.FC = () => {
/> />
<SectionRoute <SectionRoute
permissions={[ permissions={[
PermissionEnum.MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES PermissionEnum.MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES,
PermissionEnum.MANAGE_PAGE_TYPES_AND_ATTRIBUTES
]} ]}
path={attributeSection} path={attributeSection}
component={AttributeSection} component={AttributeSection}
matchPermission="any"
/> />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_APPS]} permissions={[PermissionEnum.MANAGE_APPS]}
@ -303,15 +309,13 @@ const Routes: React.FC = () => {
path={channelsSection} path={channelsSection}
component={ChannelsSection} component={ChannelsSection}
/> />
{createConfigurationMenu(intl).filter(menu => <SectionRoute
menu.menuItems.map(item => hasPermission(item.permission, user)) matchPermission="any"
).length > 0 && ( permissions={getConfigMenuItemsPermissions(intl)}
<SectionRoute exact
exact path="/configuration"
path="/configuration" component={ConfigurationSection}
component={ConfigurationSection} />
/>
)}
<Route component={NotFound} /> <Route component={NotFound} />
</Switch> </Switch>
</ErrorBoundary> </ErrorBoundary>