Skip to content

Commit

Permalink
feat(app-shell): enforce 'view' permission check for main application…
Browse files Browse the repository at this point in the history
… route
  • Loading branch information
emmenko committed Jan 27, 2022
1 parent c703cfc commit 5e749c7
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/thin-cows-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@commercetools-frontend/application-shell': minor
---

Enforce that the Custom Application route is protected by the application "View" permission.
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,51 @@ import type { TProviderProps } from '@commercetools-frontend/application-shell-c
import { Children, ReactNode } from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import invariant from 'tiny-invariant';
import { useIsAuthorized } from '@commercetools-frontend/permissions';
import { PageUnauthorized } from '@commercetools-frontend/application-components';
import { entryPointUriPathToPermissionKeys } from '../../utils/formatters';
import RouteCatchAll from '../route-catch-all';

type Props<AdditionalEnvironmentProperties extends {}> = {
environment: TProviderProps<AdditionalEnvironmentProperties>['environment'];
disableRoutePermissionCheck?: boolean;
render?: () => JSX.Element;
children?: ReactNode;
};

const ApplicationRouteWithPermissionCheck = <
AdditionalEnvironmentProperties extends {}
>(
props: Props<AdditionalEnvironmentProperties>
) => {
const permissionKeys = entryPointUriPathToPermissionKeys(
props.environment.entryPointUriPath
);
// Require View permission to render the application.
const canView = useIsAuthorized({
demandedPermissions: [permissionKeys.View],
});

if (canView) {
return <>{Children.only<ReactNode>(props.children)}</>;
}
return <PageUnauthorized />;
};

const ApplicationRoute = <AdditionalEnvironmentProperties extends {}>(
props: Props<AdditionalEnvironmentProperties>
) => {
if (props.disableRoutePermissionCheck) {
return <>{Children.only<ReactNode>(props.children)}</>;
}

return (
<ApplicationRouteWithPermissionCheck<AdditionalEnvironmentProperties>
{...props}
/>
);
};

const ApplicationEntryPoint = <AdditionalEnvironmentProperties extends {}>(
props: Props<AdditionalEnvironmentProperties>
) => {
Expand All @@ -33,7 +70,7 @@ const ApplicationEntryPoint = <AdditionalEnvironmentProperties extends {}>(
)
}
<Route path={`/:projectKey/${entryPointUriPath}`}>
{Children.only<ReactNode>(props.children)}
<ApplicationRoute<AdditionalEnvironmentProperties> {...props} />
</Route>
{/* Catch-all route */}
<RouteCatchAll />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,16 +244,65 @@ describe.each`
renderApp(<TestComponent />, {
renderNodeAsChildren,
route,
disableRoutePermissionCheck: true,
});
await screen.findByText('Application name: my-app');
});

if (renderNodeAsChildren) {
describe('when route permission check is enabled (default)', () => {
it('should render page if application has view permission', async () => {
mockServer.use(
...getDefaultMockResolvers({
projects: [
ProjectMock.build({
...createTestAppliedPermissions({
allAppliedPermissions: [
{
name: 'canViewAvengers',
value: true,
},
],
}),
}),
],
})
);
const TestComponent = () => {
const applicationName = useApplicationContext(
(context) => context.environment.applicationName
);
return <p>{`Application name: ${applicationName}`}</p>;
};
renderApp(<TestComponent />, {
renderNodeAsChildren,
route,
});
await screen.findByText('Application name: my-app');
});
it('should render unauthorized page if application does not have view permission', async () => {
const TestComponent = () => {
const applicationName = useApplicationContext(
(context) => context.environment.applicationName
);
return <p>{`Application name: ${applicationName}`}</p>;
};
renderApp(<TestComponent />, {
renderNodeAsChildren,
route,
});
await screen.findByText(/you are not authorized to view it/);
});
});
}

describe('when user navigates to "/account" route', () => {
if (renderNodeAsChildren) {
it('should trigger a page reload (when served by proxy)', async () => {
const { history } = renderApp(null, {
renderNodeAsChildren,
environment: { servedByProxy: true },
disableRoutePermissionCheck: true,
});
await screen.findByText('OK');
history.push('/account');
Expand All @@ -265,6 +314,7 @@ describe.each`
it('should render using the "render" prop', async () => {
const { history } = renderApp(null, {
renderNodeAsChildren,
disableRoutePermissionCheck: true,
});
await screen.findByText('OK');
history.push('/account');
Expand All @@ -278,7 +328,9 @@ describe.each`

describe('when route does not contain a project key (e.g. /account)', () => {
it('should not render NavBar', async () => {
const { history, queryByLeftNavigation } = renderApp();
const { history, queryByLeftNavigation } = renderApp(null, {
disableRoutePermissionCheck: true,
});
await screen.findByText('OK');
history.push('/account');
await waitFor(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ type Props<AdditionalEnvironmentProperties extends {}> = {
event: SyntheticEvent<HTMLAnchorElement>,
track: TrackFn
) => void;
disableRoutePermissionCheck?: boolean;
render?: () => JSX.Element;
children?: ReactNode;
};
Expand Down Expand Up @@ -460,6 +461,9 @@ export const RestrictedApplication = <
match={routerProps.match}
location={routerProps.location}
environment={applicationEnvironment}
disableRoutePermissionCheck={
props.disableRoutePermissionCheck
}
// This effectively renders the
// children, which is the application
// specific part
Expand Down Expand Up @@ -530,6 +534,9 @@ const ApplicationShell = <AdditionalEnvironmentProperties extends {}>(
render={props.render}
applicationMessages={props.applicationMessages}
onMenuItemClick={props.onMenuItemClick}
disableRoutePermissionCheck={
props.disableRoutePermissionCheck
}
>
{props.children}
</RestrictedApplication>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type Props<AdditionalEnvironmentProperties extends {}> = Pick<
> & {
user: TFetchLoggedInUserQuery['user'];
environment: TProviderProps<AdditionalEnvironmentProperties>['environment'];
disableRoutePermissionCheck?: boolean;
render?: () => JSX.Element;
children?: ReactNode;
};
Expand Down Expand Up @@ -176,6 +177,9 @@ const ProjectContainer = <AdditionalEnvironmentProperties extends {}>(
*/}
<ApplicationEntryPoint<AdditionalEnvironmentProperties>
environment={props.environment}
disableRoutePermissionCheck={
props.disableRoutePermissionCheck
}
render={props.render}
>
{props.children}
Expand Down
6 changes: 5 additions & 1 deletion packages/application-shell/src/test-utils/test-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ export type TRenderAppOptions<AdditionalEnvironmentProperties = {}> = {
apolloClient?: ApolloClient<NormalizedCacheObject>;
enableApolloMocks: boolean;
route: string;
disableRoutePermissionCheck: boolean;
disableAutomaticEntryPointRoutes: boolean;
history: ReturnType<typeof createEnhancedHistory>;
flags: TFlags;
Expand All @@ -286,6 +287,9 @@ type TApplicationProvidersProps = {
};

function createApplicationProviders<AdditionalEnvironmentProperties = {}>({
// application
disableAutomaticEntryPointRoutes = false,
disableRoutePermissionCheck = false,
// react-intl
locale = 'en',
// Apollo
Expand All @@ -294,7 +298,6 @@ function createApplicationProviders<AdditionalEnvironmentProperties = {}>({
enableApolloMocks = false,
// react-router
route,
disableAutomaticEntryPointRoutes = false,
history,
// flopflip
flags = {},
Expand Down Expand Up @@ -340,6 +343,7 @@ function createApplicationProviders<AdditionalEnvironmentProperties = {}>({
<Suspense fallback={<LoadingFallback />}>
<ApplicationEntryPoint
environment={mergedEnvironment}
disableRoutePermissionCheck={disableRoutePermissionCheck}
render={
disableAutomaticEntryPointRoutes
? // eslint-disable-next-line testing-library/no-node-access
Expand Down

0 comments on commit 5e749c7

Please sign in to comment.