From caa7c12af78059634213cae73b090053dc724a98 Mon Sep 17 00:00:00 2001 From: Quentin Pavy Date: Thu, 23 Jan 2025 15:38:12 +0100 Subject: [PATCH] feat(vrack-services): add iam checks on dashboard and listing ref: MANAGER-16523 Signed-off-by: Quentin Pavy --- .../apps/vrack-services/mocks/iam/iam.mock.ts | 21 +++++++++ .../apps/vrack-services/mocks/iam/iam.ts | 16 ++++++- .../display-name/DisplayName.component.tsx | 3 ++ .../display-name/DisplayName.spec.tsx | 40 +++++++++++++++- .../display-name/EditButton.component.tsx | 14 ++++-- .../src/components/vrack-id/VrackId.spec.tsx | 46 +++++++++++++++++-- .../vrack-id/useVrackMenuItems.hook.ts | 5 ++ .../pages/associate/AssociateVrack.spec.tsx | 22 +++++++++ .../pages/listing/ActionCell.component.tsx | 3 ++ .../src/pages/listing/Listing.spec.tsx | 32 +++++++++++++ .../src/test-utils/setupTests.tsx | 9 ++++ .../src/utils/iamActions.constants.ts | 5 ++ 12 files changed, 204 insertions(+), 12 deletions(-) create mode 100644 packages/manager/apps/vrack-services/mocks/iam/iam.mock.ts create mode 100644 packages/manager/apps/vrack-services/src/utils/iamActions.constants.ts diff --git a/packages/manager/apps/vrack-services/mocks/iam/iam.mock.ts b/packages/manager/apps/vrack-services/mocks/iam/iam.mock.ts new file mode 100644 index 000000000000..da46b59ec677 --- /dev/null +++ b/packages/manager/apps/vrack-services/mocks/iam/iam.mock.ts @@ -0,0 +1,21 @@ +import { IAM_ACTION } from '../../src/utils/iamActions.constants'; + +const defaultUrn = 'urn:v1:eu:resource:vrackServices:vrs-ar7-72w-poh-3qe'; + +export const configureIamResponse = ({ + unauthorizedActions = [], + urn, +}: { + unauthorizedActions?: string[]; + urn?: string; +}) => { + const allActions = Object.values(IAM_ACTION); + const authorizedActions = allActions.filter( + (action) => !unauthorizedActions.includes(action), + ); + return { + urn: urn || defaultUrn, + authorizedActions, + unauthorizedActions, + }; +}; diff --git a/packages/manager/apps/vrack-services/mocks/iam/iam.ts b/packages/manager/apps/vrack-services/mocks/iam/iam.ts index dd39da629e94..1631c61fec94 100644 --- a/packages/manager/apps/vrack-services/mocks/iam/iam.ts +++ b/packages/manager/apps/vrack-services/mocks/iam/iam.ts @@ -1,4 +1,5 @@ import { Handler } from '@ovh-ux/manager-core-test-utils'; +import { configureIamResponse } from './iam.mock'; export const iamResources = [ { @@ -44,9 +45,22 @@ export const iamResources = [ export type GetIamMocksParams = { iamKo?: boolean; + unauthorizedActions?: string[]; + urn?: string; }; -export const getIamMocks = ({ iamKo }: GetIamMocksParams): Handler[] => [ +export const getIamMocks = ({ + iamKo, + unauthorizedActions, + urn, +}: GetIamMocksParams): Handler[] => [ + { + url: '/iam/resource/:urn/authorization/check', + response: () => configureIamResponse({ urn, unauthorizedActions }), + api: 'v2', + method: 'post', + status: 200, + }, { url: '/iam/resource', response: () => { diff --git a/packages/manager/apps/vrack-services/src/components/display-name/DisplayName.component.tsx b/packages/manager/apps/vrack-services/src/components/display-name/DisplayName.component.tsx index c92d1cef41c0..e55215633cec 100644 --- a/packages/manager/apps/vrack-services/src/components/display-name/DisplayName.component.tsx +++ b/packages/manager/apps/vrack-services/src/components/display-name/DisplayName.component.tsx @@ -11,6 +11,7 @@ import { EditButton } from './EditButton.component'; import { VrackServicesWithIAM, getDisplayName, isEditable } from '@/data'; import { urls } from '@/routes/routes.constants'; import { InfoIcon } from './InfoIcon.component'; +import { IAM_ACTION } from '@/utils/iamActions.constants'; export type DisplayNameProps = { isListing?: boolean; @@ -57,6 +58,8 @@ export const DisplayName: React.FC = ({ navigate(urls.overviewEdit.replace(':id', vs.id)); }} data-testid="display-name-edit-button" + iamActions={[IAM_ACTION.VRACK_SERVICES_RESOURCE_EDIT]} + urn={vs.iam?.urn} > {name} diff --git a/packages/manager/apps/vrack-services/src/components/display-name/DisplayName.spec.tsx b/packages/manager/apps/vrack-services/src/components/display-name/DisplayName.spec.tsx index eebb15d9ea33..1588c769b4ba 100644 --- a/packages/manager/apps/vrack-services/src/components/display-name/DisplayName.spec.tsx +++ b/packages/manager/apps/vrack-services/src/components/display-name/DisplayName.spec.tsx @@ -2,15 +2,30 @@ import '@/test-utils/setupTests'; // ----- import React from 'react'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render } from '@testing-library/react'; import { DisplayName } from '@/components/display-name/DisplayName.component'; import vrackServicesList from '../../../mocks/vrack-services/get-vrack-services.json'; import { VrackServicesWithIAM } from '@/data'; import '@testing-library/jest-dom'; +import { configureIamResponse } from '../../../mocks/iam/iam.mock'; +import { IAM_ACTION } from '@/utils/iamActions.constants'; const defaultVs = vrackServicesList[0] as VrackServicesWithIAM; +const queryClient = new QueryClient(); + +const iamActionsMock = vi.fn(); + +vi.mock('@ovh-ux/manager-react-components', async (importOriginal) => { + const original: typeof import('@ovh-ux/manager-react-components') = await importOriginal(); + return { + ...original, + useAuthorizationIam: iamActionsMock, + }; +}); + const renderComponent = ({ isListing, vs = defaultVs, @@ -18,11 +33,16 @@ const renderComponent = ({ isListing?: boolean; vs?: VrackServicesWithIAM; }) => { - return render(); + return render( + + + , + ); }; describe('DisplayName Component', () => { it('In listing, should display the display name with link', async () => { + iamActionsMock.mockReturnValue(configureIamResponse({})); const { getByText, queryByTestId } = renderComponent({ isListing: true }); expect(queryByTestId('display-name-link')).toBeDefined(); @@ -31,6 +51,7 @@ describe('DisplayName Component', () => { }); it('In listing, should display the display name with info icon', async () => { + iamActionsMock.mockReturnValue(configureIamResponse({})); const { queryByTestId } = renderComponent({ isListing: true, vs: vrackServicesList[2] as VrackServicesWithIAM, @@ -39,6 +60,7 @@ describe('DisplayName Component', () => { }); it('In listing, should display the display name with loader', async () => { + iamActionsMock.mockReturnValue(configureIamResponse({})); const { queryByTestId } = renderComponent({ isListing: true, vs: vrackServicesList[3] as VrackServicesWithIAM, @@ -47,6 +69,7 @@ describe('DisplayName Component', () => { }); it('In Dashboard, should display the display name with edit action', async () => { + iamActionsMock.mockReturnValue(configureIamResponse({})); const { getByText, queryByTestId } = renderComponent({}); expect(getByText(defaultVs.iam.displayName)).toBeDefined(); @@ -55,6 +78,19 @@ describe('DisplayName Component', () => { }); it('In Dashboard, should display the display name with disabled edit action', async () => { + iamActionsMock.mockReturnValue(configureIamResponse({})); + const { queryByTestId } = renderComponent({ + vs: vrackServicesList[2] as VrackServicesWithIAM, + }); + expect(queryByTestId('edit-button')).toHaveProperty('disabled'); + }); + + it('In Dashboard, should display the display name with disabled edit action when user have no iam right', async () => { + iamActionsMock.mockReturnValue( + configureIamResponse({ + unauthorizedActions: [IAM_ACTION.VRACK_SERVICES_RESOURCE_EDIT], + }), + ); const { queryByTestId } = renderComponent({ vs: vrackServicesList[2] as VrackServicesWithIAM, }); diff --git a/packages/manager/apps/vrack-services/src/components/display-name/EditButton.component.tsx b/packages/manager/apps/vrack-services/src/components/display-name/EditButton.component.tsx index b6e118fcad94..947cc061350d 100644 --- a/packages/manager/apps/vrack-services/src/components/display-name/EditButton.component.tsx +++ b/packages/manager/apps/vrack-services/src/components/display-name/EditButton.component.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { OsdsButton, OsdsIcon, OsdsText } from '@ovhcloud/ods-components/react'; +import { OsdsIcon, OsdsText } from '@ovhcloud/ods-components/react'; import { ODS_BUTTON_SIZE, ODS_BUTTON_TYPE, @@ -10,17 +10,21 @@ import { ODS_TEXT_LEVEL, } from '@ovhcloud/ods-components'; import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; -import { handleClick } from '@ovh-ux/manager-react-components'; +import { handleClick, ManagerButton } from '@ovh-ux/manager-react-components'; export type EditButtonProps = React.PropsWithChildren<{ disabled?: boolean; onClick: () => void; + iamActions?: string[]; + urn?: string; }>; export const EditButton: React.FC = ({ children, disabled, onClick, + iamActions, + urn, }) => (
@@ -33,7 +37,7 @@ export const EditButton: React.FC = ({
- = ({ {...handleClick(onClick)} disabled={disabled || undefined} data-testid="edit-button" + iamActions={iamActions} + urn={urn} > - +
); diff --git a/packages/manager/apps/vrack-services/src/components/vrack-id/VrackId.spec.tsx b/packages/manager/apps/vrack-services/src/components/vrack-id/VrackId.spec.tsx index 2bfc99499e89..be788db61cc0 100644 --- a/packages/manager/apps/vrack-services/src/components/vrack-id/VrackId.spec.tsx +++ b/packages/manager/apps/vrack-services/src/components/vrack-id/VrackId.spec.tsx @@ -8,13 +8,29 @@ import { ShellContext, ShellContextType, } from '@ovh-ux/manager-react-shell-client'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { getButtonByLabel } from '@ovh-ux/manager-core-test-utils'; import { VrackId } from './VrackId.component'; import { VrackServicesWithIAM } from '@/data'; import vrackServicesList from '../../../mocks/vrack-services/get-vrack-services.json'; +import { configureIamResponse } from '../../../mocks/iam/iam.mock'; +import { IAM_ACTION } from '@/utils/iamActions.constants'; const defaultVs = vrackServicesList[5] as VrackServicesWithIAM; const vsWithoutVrack = vrackServicesList[0] as VrackServicesWithIAM; +const queryClient = new QueryClient(); + +const iamActionsMock = vi.fn(); + +vi.mock('@ovh-ux/manager-react-components', async (importOriginal) => { + const original: typeof import('@ovh-ux/manager-react-components') = await importOriginal(); + return { + ...original, + useAuthorizationIam: iamActionsMock, + }; +}); + /** Render */ const shellContext = { @@ -41,11 +57,14 @@ const renderComponent = ({ vs: VrackServicesWithIAM; }) => { return render( - - - , + + + + + , + , ); }; @@ -53,6 +72,7 @@ const renderComponent = ({ describe('VrackId Component', () => { it('should display link to vrack if associated', async () => { + iamActionsMock.mockReturnValue(configureIamResponse({})); const { getByText, queryByText } = renderComponent({ vs: defaultVs }); await waitFor(() => { @@ -67,7 +87,23 @@ describe('VrackId Component', () => { }); }); + it('should display disable link to vrack if user has no iam right', async () => { + iamActionsMock.mockReturnValue( + configureIamResponse({ + unauthorizedActions: [IAM_ACTION.VRACK_SERVICES_VRACK_ATTACH], + }), + ); + const { container } = renderComponent({ vs: defaultVs }); + + await getButtonByLabel({ + container, + label: 'vrackActionAssociateToAnother', + disabled: true, + }); + }); + it('should display action to link vrack if not associated', async () => { + iamActionsMock.mockReturnValue(configureIamResponse({})); const { getByText, queryByText } = renderComponent({ vs: vsWithoutVrack }); await waitFor(() => { diff --git a/packages/manager/apps/vrack-services/src/components/vrack-id/useVrackMenuItems.hook.ts b/packages/manager/apps/vrack-services/src/components/vrack-id/useVrackMenuItems.hook.ts index 49e43df485df..c7cf7e0c61ce 100644 --- a/packages/manager/apps/vrack-services/src/components/vrack-id/useVrackMenuItems.hook.ts +++ b/packages/manager/apps/vrack-services/src/components/vrack-id/useVrackMenuItems.hook.ts @@ -8,6 +8,7 @@ import { import { useNavigate } from 'react-router-dom'; import { VrackServicesWithIAM, isEditable } from '@/data'; import { urls } from '@/routes/routes.constants'; +import { IAM_ACTION } from '@/utils/iamActions.constants'; export type UseVrackMenuItemsParams = { vs: VrackServicesWithIAM; @@ -44,6 +45,8 @@ export const useVrackMenuItems = ({ ), ); }, + iamActions: [IAM_ACTION.VRACK_SERVICES_VRACK_ATTACH], + urn: vs.iam?.urn, }, vrackId && { id: 5, @@ -65,6 +68,8 @@ export const useVrackMenuItems = ({ .replace(':vrackId', vs.currentState.vrackId), ); }, + iamActions: [IAM_ACTION.VRACK_SERVICES_VRACK_ATTACH], + urn: vs.iam?.urn, }, vrackId && { id: 6, diff --git a/packages/manager/apps/vrack-services/src/pages/associate/AssociateVrack.spec.tsx b/packages/manager/apps/vrack-services/src/pages/associate/AssociateVrack.spec.tsx index a8b83012b590..5851940bfbc4 100644 --- a/packages/manager/apps/vrack-services/src/pages/associate/AssociateVrack.spec.tsx +++ b/packages/manager/apps/vrack-services/src/pages/associate/AssociateVrack.spec.tsx @@ -18,6 +18,7 @@ import { import vrackServicesList from '../../../mocks/vrack-services/get-vrack-services.json'; import { urls } from '@/routes/routes.constants'; import { vrackList } from '../../../mocks/vrack/vrack'; +import { IAM_ACTION } from '@/utils/iamActions.constants'; describe('Vrack Services associate vrack test suite', () => { it('from dashboard should associate vrack using vrack modal', async () => { @@ -66,6 +67,27 @@ describe('Vrack Services associate vrack test suite', () => { await assertModalVisibility({ container, isVisible: false }); }); + it('from dashboard should disable vrack association if user have not the iam right to do it', async () => { + const { container } = await renderTest({ + initialRoute: urls.overview.replace(':id', vrackServicesList[0].id), + nbVs: 1, + unauthorizedActions: [IAM_ACTION.VRACK_SERVICES_VRACK_ATTACH], + }); + + const actionMenuButton = await getButtonByIcon({ + container, + iconName: ODS_ICON_NAME.ELLIPSIS, + }); + + await waitFor(() => fireEvent.click(actionMenuButton)); + + await getButtonByLabel({ + container, + label: labels.common.associateVrackButtonLabel, + disabled: true, + }); + }); + it('from dashboard, should propose user to create vrack if no eligible vrack', async () => { const { container } = await renderTest({ initialRoute: urls.overviewAssociate.replace( diff --git a/packages/manager/apps/vrack-services/src/pages/listing/ActionCell.component.tsx b/packages/manager/apps/vrack-services/src/pages/listing/ActionCell.component.tsx index 9f46414fc4a2..a30fa46bd5c4 100644 --- a/packages/manager/apps/vrack-services/src/pages/listing/ActionCell.component.tsx +++ b/packages/manager/apps/vrack-services/src/pages/listing/ActionCell.component.tsx @@ -11,6 +11,7 @@ import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; import { isEditable, VrackServicesWithIAM } from '@/data'; import { urls } from '@/routes/routes.constants'; import { useVrackMenuItems } from '@/components/vrack-id/useVrackMenuItems.hook'; +import { IAM_ACTION } from '@/utils/iamActions.constants'; export const ActionCell: React.FC = (vs) => { const navigate = useNavigate(); @@ -58,6 +59,8 @@ export const ActionCell: React.FC = (vs) => { }); navigate(urls.listingEdit.replace(':id', vs.id)); }, + iamActions: [IAM_ACTION.VRACK_SERVICES_RESOURCE_EDIT], + urn: vs.iam?.urn, }, ...vrackActionsMenuItems, { diff --git a/packages/manager/apps/vrack-services/src/pages/listing/Listing.spec.tsx b/packages/manager/apps/vrack-services/src/pages/listing/Listing.spec.tsx index 87a73a3a0ebb..92cbe9d3b98d 100644 --- a/packages/manager/apps/vrack-services/src/pages/listing/Listing.spec.tsx +++ b/packages/manager/apps/vrack-services/src/pages/listing/Listing.spec.tsx @@ -13,6 +13,7 @@ import { labels, renderTest, } from '../../test-utils'; +import { IAM_ACTION } from '@/utils/iamActions.constants'; describe('Vrack Services listing test suite', () => { it('should redirect to the onboarding page when the VRS list is empty', async () => { @@ -100,4 +101,35 @@ describe('Vrack Services listing test suite', () => { await assertTextVisibility(labels.dashboard.dashboardPageDescription); }); + + it('should disable iam actions', async () => { + const { container } = await renderTest({ + nbVs: 1, + unauthorizedActions: [ + IAM_ACTION.VRACK_SERVICES_RESOURCE_EDIT, + IAM_ACTION.VRACK_SERVICES_VRACK_ATTACH, + ], + }); + + await assertTextVisibility(labels.listing.createVrackServicesButtonLabel); + + const actionMenu = await getButtonByIcon({ + container, + iconName: ODS_ICON_NAME.ELLIPSIS, + }); + + await waitFor(() => fireEvent.click(actionMenu)); + + await getButtonByLabel({ + container, + label: labels.common['action-editDisplayName'], + disabled: true, + }); + + await getButtonByLabel({ + container, + label: labels.common.associateVrackButtonLabel, + disabled: true, + }); + }); }); diff --git a/packages/manager/apps/vrack-services/src/test-utils/setupTests.tsx b/packages/manager/apps/vrack-services/src/test-utils/setupTests.tsx index a782a87100c5..358015d14db6 100644 --- a/packages/manager/apps/vrack-services/src/test-utils/setupTests.tsx +++ b/packages/manager/apps/vrack-services/src/test-utils/setupTests.tsx @@ -2,6 +2,7 @@ import { vi } from 'vitest'; import React from 'react'; import '@testing-library/jest-dom'; import { NavLinkProps } from 'react-router-dom'; +import { configureIamResponse } from '../../mocks/iam/iam.mock'; vi.mock('react-i18next', () => ({ useTranslation: () => ({ @@ -30,3 +31,11 @@ vi.mock('react-router-dom', async (importOriginal) => { NavLink: ({ ...params }: NavLinkProps) => <>{params.children}, }; }); + +vi.mock('@ovh-ux/manager-react-components', async (importOriginal) => { + const original: typeof import('@ovh-ux/manager-react-components') = await importOriginal(); + return { + ...original, + useAuthorizationIam: vi.fn().mockReturnValue(configureIamResponse({})), + }; +}); diff --git a/packages/manager/apps/vrack-services/src/utils/iamActions.constants.ts b/packages/manager/apps/vrack-services/src/utils/iamActions.constants.ts new file mode 100644 index 000000000000..ff93ba0f7fb5 --- /dev/null +++ b/packages/manager/apps/vrack-services/src/utils/iamActions.constants.ts @@ -0,0 +1,5 @@ +export const IAM_ACTION = { + VRACK_SERVICES_RESOURCE_EDIT: 'vrackServices:apiovh:resource/edit', + VRACK_SERVICES_VRACK_ATTACH: 'vrackServices:apiovh:vrack/attach', + VRACK_SERVICES_RESOURCE_GET: 'vrackServices:apiovh:resource/get', +};