Skip to content

Commit

Permalink
HAI-2041 Fetch current users permissions in a single request
Browse files Browse the repository at this point in the history
Add a functionality to fecth users permissions for all related hanke. This is used in hanke portfolio listing to prevent multiple whoami http calls.
  • Loading branch information
pitkni committed Oct 31, 2023
1 parent be9c962 commit da3d099
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 95 deletions.
10 changes: 5 additions & 5 deletions src/domain/application/applicationView/ApplicationView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { ApplicationCancel } from '../components/ApplicationCancel';
import AttachmentSummary from '../components/AttachmentSummary';
import useAttachments from '../hooks/useAttachments';
import FeatureFlags from '../../../common/components/featureFlags/FeatureFlags';
import UserRightsCheck from '../../hanke/hankeUsers/UserRightsCheck';
import { CheckRightsByHanke } from '../../hanke/hankeUsers/UserRightsCheck';

type Props = {
application: Application;
Expand Down Expand Up @@ -120,25 +120,25 @@ function ApplicationView({ application, hanke, onEditApplication }: Props) {

<InformationViewHeaderButtons>
{isPending ? (
<UserRightsCheck requiredRight="EDIT_APPLICATIONS" hankeTunnus={hanke?.hankeTunnus}>
<CheckRightsByHanke requiredRight="EDIT_APPLICATIONS" hankeTunnus={hanke?.hankeTunnus}>
<Button
theme="coat"
iconLeft={<IconPen aria-hidden="true" />}
onClick={onEditApplication}
>
{t('hakemus:buttons:editApplication')}
</Button>
</UserRightsCheck>
</CheckRightsByHanke>
) : null}
{hanke ? (
<UserRightsCheck requiredRight="EDIT_APPLICATIONS" hankeTunnus={hanke?.hankeTunnus}>
<CheckRightsByHanke requiredRight="EDIT_APPLICATIONS" hankeTunnus={hanke?.hankeTunnus}>
<ApplicationCancel
applicationId={id}
alluStatus={alluStatus}
hankeTunnus={hanke?.hankeTunnus}
buttonIcon={<IconTrash aria-hidden />}
/>
</UserRightsCheck>
</CheckRightsByHanke>
) : null}
</InformationViewHeaderButtons>
</InformationViewHeader>
Expand Down
4 changes: 2 additions & 2 deletions src/domain/hanke/accessRights/AccessRightsViewContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import ErrorLoadingText from '../../../common/components/errorLoadingText/ErrorL
import { useHankeUsers } from '../hankeUsers/hooks/useHankeUsers';
import useHanke from '../hooks/useHanke';
import AccessRightsView from './AccessRightsView';
import useUserRightsForHanke from '../hankeUsers/hooks/useUserRightsForHanke';
import { usePermissionsForHanke } from '../hankeUsers/hooks/useUserRightsForHanke';

type Props = {
hankeTunnus: string;
Expand All @@ -17,7 +17,7 @@ function AccessRightsViewContainer({ hankeTunnus }: Props) {
const { t } = useTranslation();
const { data: hankeUsers, isLoading, isError, error } = useHankeUsers(hankeTunnus);
const { data: hankeData } = useHanke(hankeTunnus);
const { data: signedInUser } = useUserRightsForHanke(hankeTunnus);
const { data: signedInUser } = usePermissionsForHanke(hankeTunnus);

if (isLoading) {
return (
Expand Down
142 changes: 95 additions & 47 deletions src/domain/hanke/hankeUsers/UserRightsCheck.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,108 @@ import React from 'react';
import { rest } from 'msw';
import { render, screen, waitFor } from '../../../testUtils/render';
import { server } from '../../mocks/test-server';
import { SignedInUser } from './hankeUser';
import UserRightsCheck from './UserRightsCheck';

test('Should render children if user has required right', async () => {
render(
<UserRightsCheck requiredRight="EDIT" hankeTunnus="HAI22-2">
<p>Children</p>
</UserRightsCheck>,
);

await waitFor(() => {
expect(screen.getByText('Children')).toBeInTheDocument();
import { AccessRightLevel, SignedInUser } from './hankeUser';
import { CheckRightsByHanke, CheckRightsByUser } from './UserRightsCheck';
import { signedInUser } from '../../mocks/signedInUser';

describe('CheckRightsByHanke', () => {
test('Should render children if user has required right', async () => {
render(
<CheckRightsByHanke requiredRight="EDIT" hankeTunnus="HAI22-2">
<p>Children</p>
</CheckRightsByHanke>,
);

await waitFor(() => {
expect(screen.getByText('Children')).toBeInTheDocument();
});
});
});

test('Should not render children if user does not have required right', async () => {
server.use(
rest.get('/api/hankkeet/:hankeTunnus/whoami', async (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json<SignedInUser>({
hankeKayttajaId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
kayttooikeustaso: 'KATSELUOIKEUS',
kayttooikeudet: ['VIEW'],
}),
);
}),
);

render(
<UserRightsCheck requiredRight="EDIT" hankeTunnus="HAI22-2">
<p>Children</p>
</UserRightsCheck>,
);

await waitFor(() => {
expect(screen.queryByText('Children')).not.toBeInTheDocument();
test('Should not render children if user does not have required right', async () => {
server.use(
rest.get('/api/hankkeet/:hankeTunnus/whoami', async (_, res, ctx) => {
return res(
ctx.status(200),
ctx.json<SignedInUser>({
hankeKayttajaId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
kayttooikeustaso: 'KATSELUOIKEUS',
kayttooikeudet: ['VIEW'],
}),
);
}),
);

render(
<CheckRightsByHanke requiredRight="EDIT" hankeTunnus="HAI22-2">
<p>Children</p>
</CheckRightsByHanke>,
);

await waitFor(() => {
expect(screen.queryByText('Children')).not.toBeInTheDocument();
});
});

test('Should render children when access right feature is not enabled', async () => {
const OLD_ENV = { ...window._env_ };
window._env_ = { ...OLD_ENV, REACT_APP_FEATURE_ACCESS_RIGHTS: 0 };

render(
<CheckRightsByHanke requiredRight="EDIT" hankeTunnus="HAI22-2">
<p>Children</p>
</CheckRightsByHanke>,
);

await waitFor(() => {
expect(screen.getByText('Children')).toBeInTheDocument();
});
jest.resetModules();
window._env_ = OLD_ENV;
});
});

test('Should render children when access right feature is not enabled', async () => {
const OLD_ENV = window._env_;
window._env_.REACT_APP_FEATURE_ACCESS_RIGHTS = 0;
describe('CheckRightsByUser', () => {
const ALL_RIGHTS_USER = signedInUser(AccessRightLevel.KAIKKI_OIKEUDET);
const VIEW_RIGHT_USER = signedInUser(AccessRightLevel.KATSELUOIKEUS);

test('Should render children on required right', async () => {
render(
<CheckRightsByUser requiredRight="EDIT" signedInUser={ALL_RIGHTS_USER}>
<p>Children</p>
</CheckRightsByUser>,
);

await waitFor(() => {
expect(screen.getByText('Children')).toBeInTheDocument();
});
});

render(
<UserRightsCheck requiredRight="EDIT" hankeTunnus="HAI22-2">
<p>Children</p>
</UserRightsCheck>,
);
test('Should not render children if not enough rights', async () => {
render(
<CheckRightsByUser requiredRight="EDIT" signedInUser={VIEW_RIGHT_USER}>
<p>Children</p>
</CheckRightsByUser>,
);

await waitFor(() => {
expect(screen.getByText('Children')).toBeInTheDocument();
await waitFor(() => {
expect(screen.queryByText('Children')).not.toBeInTheDocument();
});
});

jest.resetModules();
window._env_ = OLD_ENV;
test('Should render children if feature not enabled regardless of permission', async () => {
const OLD_ENV = { ...window._env_ };
window._env_ = { ...OLD_ENV, REACT_APP_FEATURE_ACCESS_RIGHTS: 0 };

render(
<CheckRightsByUser requiredRight="EDIT" signedInUser={VIEW_RIGHT_USER}>
<p>Children</p>
</CheckRightsByUser>,
);

await waitFor(() => {
expect(screen.getByText('Children')).toBeInTheDocument();
});
jest.resetModules();
window._env_ = OLD_ENV;
});
});
26 changes: 21 additions & 5 deletions src/domain/hanke/hankeUsers/UserRightsCheck.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from 'react';
import useUserRightsForHanke from './hooks/useUserRightsForHanke';
import { Rights } from './hankeUser';
import { Rights, SignedInUser } from './hankeUser';
import { useFeatureFlags } from '../../../common/components/featureFlags/FeatureFlagsContext';
import { usePermissionsForHanke } from './hooks/useUserRightsForHanke';

/**
* Check that user has required rights.
* If they have, render children.
*/
function UserRightsCheck({
export function CheckRightsByHanke({
requiredRight,
hankeTunnus,
children,
Expand All @@ -18,7 +18,7 @@ function UserRightsCheck({
hankeTunnus?: string;
children: React.ReactElement | null;
}) {
const { data: signedInUser } = useUserRightsForHanke(hankeTunnus);
const { data: signedInUser } = usePermissionsForHanke(hankeTunnus);
const features = useFeatureFlags();

if (!features.accessRights) {
Expand All @@ -32,4 +32,20 @@ function UserRightsCheck({
return null;
}

export default UserRightsCheck;
export function CheckRightsByUser({
requiredRight,
signedInUser,
children,
}: {
requiredRight: keyof typeof Rights;
signedInUser: SignedInUser;
children: React.ReactElement | null;
}) {
const features = useFeatureFlags();

if (!features.accessRights) {
return children;
}

return signedInUser?.kayttooikeudet?.includes(requiredRight) ? children : null;
}
4 changes: 4 additions & 0 deletions src/domain/hanke/hankeUsers/hankeUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export type SignedInUser = {
kayttooikeudet: UserRights;
};

export type SignedInUserByHanke = {
[hankeTunnus: string]: SignedInUser;
};

export type IdentificationResponse = {
kayttajaId: string;
hankeTunnus: string;
Expand Down
7 changes: 6 additions & 1 deletion src/domain/hanke/hankeUsers/hankeUsersApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import api from '../../api/api';
import { HankeUser, IdentificationResponse, SignedInUser } from './hankeUser';
import { HankeUser, IdentificationResponse, SignedInUser, SignedInUserByHanke } from './hankeUser';

export async function getHankeUsers(hankeTunnus: string) {
const { data } = await api.get<{ kayttajat: HankeUser[] }>(`hankkeet/${hankeTunnus}/kayttajat`);
Expand All @@ -23,6 +23,11 @@ export async function getSignedInUserForHanke(hankeTunnus?: string): Promise<Sig
return data;
}

export async function getSignedInUserByHanke(): Promise<SignedInUserByHanke> {
const { data } = await api.get<SignedInUserByHanke>('hankkeet/my-permissions');
return data;
}

export async function identifyUser(id: string) {
const { data } = await api.post<IdentificationResponse>('kayttajat', { tunniste: id });
return data;
Expand Down
14 changes: 11 additions & 3 deletions src/domain/hanke/hankeUsers/hooks/useUserRightsForHanke.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useQuery } from 'react-query';
import { SignedInUser } from '../hankeUser';
import { getSignedInUserForHanke } from '../hankeUsersApi';
import { SignedInUser, SignedInUserByHanke } from '../hankeUser';
import { getSignedInUserForHanke, getSignedInUserByHanke } from '../hankeUsersApi';
import { useFeatureFlags } from '../../../../common/components/featureFlags/FeatureFlagsContext';

export default function useSignedInUserRightsForHanke(hankeTunnus?: string) {
export function usePermissionsForHanke(hankeTunnus?: string) {
const features = useFeatureFlags();

return useQuery<SignedInUser>(
Expand All @@ -14,3 +14,11 @@ export default function useSignedInUserRightsForHanke(hankeTunnus?: string) {
},
);
}

export function usePermissionsByHanke() {
const features = useFeatureFlags();

return useQuery<SignedInUserByHanke>(['signedInUserByHanke'], () => getSignedInUserByHanke(), {
enabled: features.accessRights,
});
}
21 changes: 12 additions & 9 deletions src/domain/hanke/hankeView/HankeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import {
import FeatureFlags from '../../../common/components/featureFlags/FeatureFlags';
import { useFeatureFlags } from '../../../common/components/featureFlags/FeatureFlagsContext';
import { SignedInUser } from '../hankeUsers/hankeUser';
import UserRightsCheck from '../hankeUsers/UserRightsCheck';
import { CheckRightsByHanke } from '../hankeUsers/UserRightsCheck';

type AreaProps = {
area: HankeAlue;
Expand Down Expand Up @@ -217,7 +217,7 @@ const HankeView: React.FC<Props> = ({

<InformationViewHeaderButtons>
<FeatureFlags flags={['hanke']}>
<UserRightsCheck requiredRight="EDIT" hankeTunnus={hankeData.hankeTunnus}>
<CheckRightsByHanke requiredRight="EDIT" hankeTunnus={hankeData.hankeTunnus}>
<Button
onClick={onEditHanke}
variant="primary"
Expand All @@ -226,8 +226,11 @@ const HankeView: React.FC<Props> = ({
>
{t('hankeList:buttons:edit')}
</Button>
</UserRightsCheck>
<UserRightsCheck requiredRight="EDIT_APPLICATIONS" hankeTunnus={hankeData.hankeTunnus}>
</CheckRightsByHanke>
<CheckRightsByHanke
requiredRight="EDIT_APPLICATIONS"
hankeTunnus={hankeData.hankeTunnus}
>
{isHankePublic ? (
<Button
variant="primary"
Expand All @@ -238,7 +241,7 @@ const HankeView: React.FC<Props> = ({
{t('hankeList:buttons:addApplication')}
</Button>
) : null}
</UserRightsCheck>
</CheckRightsByHanke>
</FeatureFlags>
<FeatureFlags flags={['hanke', 'accessRights']}>
<Button
Expand All @@ -251,22 +254,22 @@ const HankeView: React.FC<Props> = ({
</Button>
</FeatureFlags>
<FeatureFlags flags={['hanke']}>
<UserRightsCheck requiredRight="DELETE" hankeTunnus={hankeData.hankeTunnus}>
<CheckRightsByHanke requiredRight="DELETE" hankeTunnus={hankeData.hankeTunnus}>
<Button variant="primary" iconLeft={<IconCross aria-hidden="true" />} theme="black">
{t('hankeList:buttons:endHanke')}
</Button>
</UserRightsCheck>
</CheckRightsByHanke>
</FeatureFlags>
{!isLoading && isCancelPossible && (
<UserRightsCheck requiredRight="DELETE" hankeTunnus={hankeData.hankeTunnus}>
<CheckRightsByHanke requiredRight="DELETE" hankeTunnus={hankeData.hankeTunnus}>
<Button
onClick={onCancelHanke}
variant="danger"
iconLeft={<IconTrash aria-hidden="true" />}
>
{t('hankeForm:cancelButton')}
</Button>
</UserRightsCheck>
</CheckRightsByHanke>
)}
</InformationViewHeaderButtons>
</InformationViewHeader>
Expand Down
Loading

0 comments on commit da3d099

Please sign in to comment.