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

feat(vrack-services): add iam checks on dashboard and listing #15082

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/manager/apps/vrack-services/mocks/iam/iam.mock.ts
Original file line number Diff line number Diff line change
@@ -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,
};
};
16 changes: 15 additions & 1 deletion packages/manager/apps/vrack-services/mocks/iam/iam.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Handler } from '@ovh-ux/manager-core-test-utils';
import { configureIamResponse } from './iam.mock';

export const iamResources = [
{
Expand Down Expand Up @@ -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: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,6 +58,8 @@ export const DisplayName: React.FC<DisplayNameProps> = ({
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}
</EditButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,47 @@
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,
}: {
isListing?: boolean;
vs?: VrackServicesWithIAM;
}) => {
return render(<DisplayName isListing={isListing} {...vs} />);
return render(
<QueryClientProvider client={queryClient}>
<DisplayName isListing={isListing} {...vs} />
</QueryClientProvider>,
);
};

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();
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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();
Expand All @@ -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,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<EditButtonProps> = ({
children,
disabled,
onClick,
iamActions,
urn,
}) => (
<div className="flex items-center">
<div className="grow">
Expand All @@ -33,7 +37,7 @@ export const EditButton: React.FC<EditButtonProps> = ({
</OsdsText>
</div>
<div className="flex-none">
<OsdsButton
<ManagerButton
className="ml-2"
inline
circle
Expand All @@ -44,13 +48,15 @@ export const EditButton: React.FC<EditButtonProps> = ({
{...handleClick(onClick)}
disabled={disabled || undefined}
data-testid="edit-button"
iamActions={iamActions}
urn={urn}
>
<OsdsIcon
color={ODS_THEME_COLOR_INTENT.primary}
name={ODS_ICON_NAME.PEN}
size={ODS_ICON_SIZE.xs}
/>
</OsdsButton>
</ManagerButton>
</div>
</div>
);
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -41,18 +57,22 @@ const renderComponent = ({
vs: VrackServicesWithIAM;
}) => {
return render(
<ShellContext.Provider
value={(shellContext as unknown) as ShellContextType}
>
<VrackId isListing={isListing} {...vs} />
</ShellContext.Provider>,
<QueryClientProvider client={queryClient}>
<ShellContext.Provider
value={(shellContext as unknown) as ShellContextType}
>
<VrackId isListing={isListing} {...vs} />
</ShellContext.Provider>
,
</QueryClientProvider>,
);
};

/** END RENDER */

describe('VrackId Component', () => {
it('should display link to vrack if associated', async () => {
iamActionsMock.mockReturnValue(configureIamResponse({}));
const { getByText, queryByText } = renderComponent({ vs: defaultVs });

await waitFor(() => {
Expand All @@ -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(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -44,6 +45,8 @@ export const useVrackMenuItems = ({
),
);
},
iamActions: [IAM_ACTION.VRACK_SERVICES_VRACK_ATTACH],
urn: vs.iam?.urn,
},
vrackId && {
id: 5,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<VrackServicesWithIAM> = (vs) => {
const navigate = useNavigate();
Expand Down Expand Up @@ -58,6 +59,8 @@ export const ActionCell: React.FC<VrackServicesWithIAM> = (vs) => {
});
navigate(urls.listingEdit.replace(':id', vs.id));
},
iamActions: [IAM_ACTION.VRACK_SERVICES_RESOURCE_EDIT],
urn: vs.iam?.urn,
},
...vrackActionsMenuItems,
{
Expand Down
Loading
Loading