Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resourceadm: show alert if user does not have permission (is member of correct Gitea team) to publish resource or edit RRR access lists #12618

Merged
merged 10 commits into from
Apr 5, 2024
6 changes: 4 additions & 2 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -866,7 +866,7 @@
"resourceadm.about_resource_resource_description_label": "Beskrivelse (Bokmål)",
"resourceadm.about_resource_resource_description_text": "Teksten kan bli synlig på flere områder på tvers av offentlige nettløsninger.",
"resourceadm.about_resource_resource_title_label": "Navn på tjenesten (Bokmål)",
"resourceadm.about_resource_resource_title_text": "Navnet vil synes for brukerne, og bør være beskrivende for hva tjenesten handler om. Pass på at navnet er forståelig og gjenkjennbart.",
"resourceadm.about_resource_resource_title_text": "Navnet vil synes for brukere, og være gjenkjennelig og beskrivende for hva tjenesten handler om. Pass på å skille tjenester som har like eller nesten like navn, slik at det blir lett for brukere å forstå.",
"resourceadm.about_resource_resource_type": "Ressurstype",
"resourceadm.about_resource_resource_type_error": "Du mangler å legge til ressurstype.",
"resourceadm.about_resource_resource_type_generic_access_resource": "Generisk tilgangsressurs",
Expand Down Expand Up @@ -1023,6 +1023,7 @@
"resourceadm.listadmin_type": "Type",
"resourceadm.listadmin_undo_remove_from_list": "Angre fjern",
"resourceadm.loading_access_list": "Laster inn tilgangsliste",
"resourceadm.loading_access_list_permission_denied": "Du har ikke tilgang til å administrere tilgangslister i {{envName}}",
"resourceadm.loading_app": "Laster inn ressursregisteret",
"resourceadm.loading_env_list": "Laster inn miljølister",
"resourceadm.loading_lists": "Laster inn lister",
Expand Down Expand Up @@ -1066,7 +1067,8 @@
"resourceadm.resource_navigation_modal_text": "Noen felt på siden har manglende informasjon eller feil i utfylling. Du kan endre eller fikse informasjonen når som helst før ressursen publiseres.",
"resourceadm.resource_navigation_modal_title_policy": "Manglende informasjon i tilgangsregler",
"resourceadm.resource_navigation_modal_title_resource": "Manglende informasjon i ressurs",
"resourceadm.resource_published_success": "Ressursen ble publisert.",
"resourceadm.resource_publish_no_access": "Du har ikke tilgang til å publisere ressurser i {{envName}}.",
"resourceadm.resource_published_success": "Ressursen ble publisert i {{envName}}.",
"resourceadm.right_translation_bar_alert": "For å kunne publisere ressursen må du legge til nynorsk og engelsk oversettelse.",
"resourceadm.right_translation_bar_title": "Oversettelse",
"resourceadm.switch_should": "skal",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { Alert, Button, List, Paragraph } from '@digdir/design-system-react';
import { getResourcePageURL } from '../../utils/urlUtils';
import { useUrlParams } from '../../hooks/useSelectedContext';
import { getAvailableEnvironments } from '../../utils/resourceUtils/resourceUtils';
import { getAvailableEnvironments } from '../../utils/resourceUtils';
import { useResourcePolicyPublishStatusQuery } from '../../hooks/queries';
import { StudioSpinner } from '@studio/components';
import { ArrowForwardIcon } from '@studio/icons';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import type { AxiosError } from 'axios';
import { Alert } from '@digdir/design-system-react';
import { getEnvLabel } from 'resourceadm/utils/resourceUtils';
import { type EnvId } from 'resourceadm/utils/resourceUtils';

interface AccessListErrorMessageProps {
error: AxiosError;
env: EnvId;
}

export const AccessListErrorMessage = ({
error,
env,
}: AccessListErrorMessageProps): React.JSX.Element => {
const { t } = useTranslation();

let errorMessage = t('resourceadm.listadmin_load_list_error');
if (error?.response?.status === 403)
errorMessage = t('resourceadm.loading_access_list_permission_denied', {
envName: t(getEnvLabel(env)),
});

return <Alert severity='danger'>{errorMessage}</Alert>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AccessListErrorMessage } from './AccessListErrorMessage';
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import { Modal, Paragraph } from '@digdir/design-system-react';
import { ResourceNameAndId } from '../../components/ResourceNameAndId';
import { ServerCodes } from 'app-shared/enums/ServerCodes';
import { StudioButton } from '@studio/components';
import { getAvailableEnvironments } from '../../utils/resourceUtils/resourceUtils';
import { getEnvLabel } from '../../utils/resourceUtils';
import type { EnvId } from '../../utils/resourceUtils';

export interface NewAccessListModalProps {
org: string;
env: string;
env: EnvId;
navigateUrl: string;
onClose: () => void;
}
Expand Down Expand Up @@ -57,7 +58,7 @@ export const NewAccessListModal = forwardRef<HTMLDialogElement, NewAccessListMod
<Modal ref={ref} onClose={onClose}>
<Modal.Header>
{t('resourceadm.listadmin_create_list_header', {
env: t(getAvailableEnvironments(org).find((listEnv) => listEnv.id === env).label),
env: t(getEnvLabel(env)),
})}
</Modal.Header>
<Modal.Content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,34 @@ describe('ResourceAccessLists', () => {
});

it('should show error when loading fails', async () => {
const getResourceAccessListsMock = jest.fn().mockImplementation(() => Promise.reject({}));
const getResourceAccessListsMock = jest
.fn()
.mockImplementation(() => Promise.reject({ response: { status: 500 } }));
renderResourceAccessLists(getResourceAccessListsMock);

const spinnerTitle = screen.queryByText(textMock('resourceadm.loading_lists'));
await waitForElementToBeRemoved(spinnerTitle);

expect(screen.getByText(textMock('resourceadm.listadmin_load_list_error'))).toBeInTheDocument();
});

it('should show error when user does not have permission to change access lists', async () => {
const getResourceAccessListsMock = jest
.fn()
.mockImplementation(() => Promise.reject({ response: { status: 403 } }));
renderResourceAccessLists(getResourceAccessListsMock);

const spinnerTitle = screen.queryByText(textMock('resourceadm.loading_lists'));
await waitForElementToBeRemoved(spinnerTitle);

expect(
screen.getByText(
textMock('resourceadm.loading_access_list_permission_denied', {
envName: textMock('resourceadm.deploy_test_env'),
}),
),
).toBeInTheDocument();
});
});

const renderResourceAccessLists = (getResourceAccessListsMock?: jest.Mock) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { useEffect, useState, useRef } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Alert, Checkbox, Heading, Link as DigdirLink, Button } from '@digdir/design-system-react';
import type { AxiosError } from 'axios';
import { Checkbox, Heading, Link as DigdirLink, Button } from '@digdir/design-system-react';
import classes from './ResourceAccessLists.module.css';
import { StudioSpinner, StudioButton } from '@studio/components';
import { PencilWritingIcon, PlusIcon } from '@studio/icons';
Expand All @@ -12,9 +13,11 @@ import { getResourcePageURL } from '../../utils/urlUtils';
import { NewAccessListModal } from '../NewAccessListModal';
import type { Resource } from 'app-shared/types/ResourceAdm';
import { useUrlParams } from '../../hooks/useSelectedContext';
import type { EnvId } from '../../utils/resourceUtils';
import { AccessListErrorMessage } from '../AccessListErrorMessage';

export interface ResourceAccessListsProps {
env: string;
env: EnvId;
resourceData: Resource;
}

Expand Down Expand Up @@ -74,7 +77,7 @@ export const ResourceAccessLists = ({
}

if (accessListsError) {
return <Alert severity='danger'>{t('resourceadm.listadmin_load_list_error')}</Alert>;
return <AccessListErrorMessage error={accessListsError as AxiosError} env={env} />;
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { textMock } from '../../../testing/mocks/i18nMock';
import userEvent from '@testing-library/user-event';
import { act } from 'react-dom/test-utils';
import type { QueryClient } from '@tanstack/react-query';
import type { Environment } from '../../utils/resourceUtils/resourceUtils';
import type { Environment } from '../../utils/resourceUtils';
import { queriesMock } from 'app-shared/mocks/queriesMock';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext';
Expand Down Expand Up @@ -40,7 +40,11 @@ describe('ResourceDeployEnvCard', () => {
await act(() => user.click(deployButton));
await waitFor(() => {
expect(
screen.getByText(textMock('resourceadm.resource_published_success')),
screen.getByText(
textMock('resourceadm.resource_published_success', {
envName: textMock(mockTestEnv.label),
}),
),
).toBeInTheDocument();
});
});
Expand Down Expand Up @@ -89,11 +93,39 @@ describe('ResourceDeployEnvCard', () => {
await act(() => user.click(deployButton));
expect(queriesMock.publishResource).toHaveBeenCalledTimes(1);
});

it('should show error if publish fails with error 403', async () => {
const user = userEvent.setup();
renderResourceDeployEnvCard(
{},
{
publishResource: jest
.fn()
.mockImplementation(() => Promise.reject({ response: { status: 403 } })),
},
);

const deployButton = screen.getByRole('button', {
name: textMock('resourceadm.deploy_card_publish', { env: textMock(mockTestEnv.label) }),
});

await act(() => user.click(deployButton));

await waitFor(() => {
expect(
screen.getByText(
textMock('resourceadm.resource_publish_no_access', {
envName: textMock(mockTestEnv.label),
}),
),
).toBeInTheDocument();
});
});
});

const renderResourceDeployEnvCard = (
props: Partial<ResourceDeployEnvCardProps> = {},
queries: Partial<ResourceDeployEnvCardProps> = {},
queries: Partial<ServicesContextProps> = {},
queryClient: QueryClient = createQueryClientMock(),
) => {
const allQueries: ServicesContextProps = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React from 'react';
import React, { useState } from 'react';
import { toast } from 'react-toastify';
import { useTranslation } from 'react-i18next';
import { Tag, Paragraph, Spinner } from '@digdir/design-system-react';
import type { AxiosError } from 'axios';
import { Tag, Paragraph, Spinner, Alert } from '@digdir/design-system-react';
import classes from './ResourceDeployEnvCard.module.css';
import { ArrowRightIcon } from '@studio/icons';
import { StudioButton } from '@studio/components';
import { usePublishResourceMutation } from '../../hooks/mutations';
import type { Environment } from '../../utils/resourceUtils/resourceUtils';
import { type Environment } from '../../utils/resourceUtils';
import { useUrlParams } from '../../hooks/useSelectedContext';

export type ResourceDeployEnvCardProps = {
Expand Down Expand Up @@ -36,6 +37,7 @@ export const ResourceDeployEnvCard = ({
}: ResourceDeployEnvCardProps): React.JSX.Element => {
const { t } = useTranslation();

const [hasNoPublishAccess, setHasNoPublishAccess] = useState<boolean>(false);
const { selectedContext, repo, resourceId } = useUrlParams();

// Query function for publishing a resource
Expand All @@ -45,7 +47,12 @@ export const ResourceDeployEnvCard = ({
const handlePublish = () => {
publishResource(env.id, {
onSuccess: () => {
toast.success(t('resourceadm.resource_published_success'));
toast.success(t('resourceadm.resource_published_success', { envName: t(env.label) }));
},
onError: (error: Error) => {
if ((error as AxiosError).response.status === 403) {
setHasNoPublishAccess(true);
}
},
});
};
Expand Down Expand Up @@ -76,9 +83,20 @@ export const ResourceDeployEnvCard = ({
</>
)}
</div>
<StudioButton disabled={!isDeployPossible} onClick={handlePublish} size='small'>
<StudioButton
disabled={!isDeployPossible || hasNoPublishAccess}
onClick={handlePublish}
size='small'
>
{t('resourceadm.deploy_card_publish', { env: t(env.label) })}
</StudioButton>
{hasNoPublishAccess && (
<Alert severity='danger'>
<Paragraph size='small'>
{t('resourceadm.resource_publish_no_access', { envName: t(env.label) })}
</Paragraph>
</Alert>
)}
</>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
mapKeywordStringToKeywordTypeArray,
mapKeywordsArrayToString,
resourceTypeMap,
} from '../../utils/resourceUtils/resourceUtils';
} from '../../utils/resourceUtils';
import { useTranslation } from 'react-i18next';
import {
ResourceCheckboxGroup,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { useRepoStatusQuery } from 'app-shared/hooks/queries';
import { useTranslation, Trans } from 'react-i18next';
import { mergeQueryStatuses } from 'app-shared/utils/tanstackQueryUtils';
import { useUrlParams } from '../../hooks/useSelectedContext';
import { getAvailableEnvironments } from '../../utils/resourceUtils/resourceUtils';
import { getAvailableEnvironments } from '../../utils/resourceUtils';

export type DeployResourcePageProps = {
navigateToPageWithError: (page: NavigationBarPage) => void;
Expand Down
29 changes: 24 additions & 5 deletions frontend/resourceadm/pages/ListAdminPage/ListAdminPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,34 @@ describe('ListAdminPage', () => {

expect(await screen.findByText('Test-list2')).toBeInTheDocument();
});

it('should show error when user does not have permission to edit access lists', async () => {
(useParams as jest.Mock).mockReturnValue({
selectedContext: 'ttd',
env: 'tt02',
});

renderListAdminPage(true);

expect(
await screen.findByText(
textMock('resourceadm.loading_access_list_permission_denied', {
envName: textMock('resourceadm.deploy_test_env'),
}),
),
).toBeInTheDocument();
});
});

const renderListAdminPage = () => {
const renderListAdminPage = (isError?: boolean) => {
const allQueries: ServicesContextProps = {
...queriesMock,
getAccessLists: jest
.fn()
.mockImplementationOnce(() => Promise.resolve(accessListResults))
.mockImplementationOnce(() => Promise.resolve(accessListResultsPage2)),
getAccessLists: isError
? jest.fn().mockImplementationOnce(() => Promise.reject({ response: { status: 403 } }))
: jest
.fn()
.mockImplementationOnce(() => Promise.resolve(accessListResults))
.mockImplementationOnce(() => Promise.resolve(accessListResultsPage2)),
};

return render(
Expand Down
21 changes: 13 additions & 8 deletions frontend/resourceadm/pages/ListAdminPage/ListAdminPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useRef, useEffect, useCallback } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import type { AxiosError } from 'axios';
import { Heading, Link as DigdirLink, ToggleGroup, Button } from '@digdir/design-system-react';
import { StudioSpinner, StudioButton } from '@studio/components';
import { PencilWritingIcon, PlusIcon } from '@studio/icons';
Expand All @@ -9,8 +10,9 @@ import { useGetAccessListsQuery } from '../../hooks/queries/useGetAccessListsQue
import { NewAccessListModal } from '../../components/NewAccessListModal';
import { getAccessListPageUrl, getResourceDashboardURL } from '../../utils/urlUtils';
import { useUrlParams } from '../../hooks/useSelectedContext';
import type { EnvId } from '../../utils/resourceUtils/resourceUtils';
import { getAvailableEnvironments } from '../../utils/resourceUtils/resourceUtils';
import type { EnvId } from '../../utils/resourceUtils';
import { getAvailableEnvironments, getEnvLabel } from '../../utils/resourceUtils';
import { AccessListErrorMessage } from 'resourceadm/components/AccessListErrorMessage';

export const ListAdminPage = (): React.JSX.Element => {
const { t } = useTranslation();
Expand All @@ -22,6 +24,7 @@ export const ListAdminPage = (): React.JSX.Element => {
isLoading: isLoadingEnvListData,
hasNextPage,
isFetchingNextPage,
error: listFetchError,
fetchNextPage,
} = useGetAccessListsQuery(selectedContext, selectedEnv);

Expand Down Expand Up @@ -66,10 +69,16 @@ export const ListAdminPage = (): React.JSX.Element => {
<NewAccessListModal
ref={createAccessListModalRef}
org={selectedContext}
env={selectedEnv}
env={selectedEnv as EnvId}
navigateUrl={getAccessListPageUrl(selectedContext, repo, selectedEnv)}
onClose={() => createAccessListModalRef.current?.close()}
/>
{listFetchError && (
<AccessListErrorMessage
error={listFetchError as AxiosError}
env={selectedEnv as EnvId}
/>
)}
{isLoadingEnvListData && (
<StudioSpinner
showSpinnerTitle={false}
Expand All @@ -80,11 +89,7 @@ export const ListAdminPage = (): React.JSX.Element => {
<div>
<Heading level={2} size='xsmall'>
{t('resourceadm.listadmin_lists_in', {
environment: t(
getAvailableEnvironments(selectedContext).find(
(listEnv) => listEnv.id === selectedEnv,
).label,
),
environment: t(getEnvLabel(selectedEnv as EnvId)),
})}
</Heading>
{envListData.pages.map((list) => {
Expand Down
Loading
Loading