From 582a03d18e121c55b3c76271672c4407192cf97a Mon Sep 17 00:00:00 2001 From: Patrick Ear Date: Tue, 16 Apr 2024 17:37:41 +0200 Subject: [PATCH 1/4] fix broken zenko-ui when launch alone --- .../next-architecture/ui/AlertProvider.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/react/next-architecture/ui/AlertProvider.tsx b/src/react/next-architecture/ui/AlertProvider.tsx index 205252553..5b9696afb 100644 --- a/src/react/next-architecture/ui/AlertProvider.tsx +++ b/src/react/next-architecture/ui/AlertProvider.tsx @@ -96,10 +96,19 @@ const AlertProvider = ({ children }: { children: React.ReactNode }) => { const metalk8sUI = deployedApps.find( (app: { kind: string }) => app.kind === 'metalk8s-ui', ); - const metalk8sUIConfig = retrieveConfiguration({ - configType: 'run', - name: metalk8sUI.name, - }); + + const metalk8sUIConfig = metalk8sUI + ? retrieveConfiguration({ + configType: 'run', + name: metalk8sUI.name, + }) + : { + spec: { + selfConfiguration: { + url_alertmanager: '', + }, + }, + }; return ( From 5b9195f568514b17606684abb1609ca78a161c19 Mon Sep 17 00:00:00 2001 From: Patrick Ear Date: Mon, 29 Apr 2024 17:49:09 +0200 Subject: [PATCH 2/4] ARTESCA-10989: rework providers to make SelectAccountIAMRole work standalone --- src/react/DataServiceRoleProvider.tsx | 38 ++++++++- .../IAMPensieveAccessibleAccounts.ts | 9 +- .../ui/AccessibleAccountsAdapterProvider.tsx | 8 ++ .../next-architecture/ui/S3ClientProvider.tsx | 85 ++++++++++++++++--- 4 files changed, 120 insertions(+), 20 deletions(-) diff --git a/src/react/DataServiceRoleProvider.tsx b/src/react/DataServiceRoleProvider.tsx index 45a4b0327..b31ec9256 100644 --- a/src/react/DataServiceRoleProvider.tsx +++ b/src/react/DataServiceRoleProvider.tsx @@ -5,6 +5,7 @@ import { getRoleArnStored, setRoleArnStored } from './utils/localStorage'; import { useMutation } from 'react-query'; import { S3ClientProvider, + S3ClientWithoutReduxProvider, useAssumeRoleQuery, useS3ConfigFromAssumeRoleResult, } from './next-architecture/ui/S3ClientProvider'; @@ -94,7 +95,18 @@ export const useCurrentAccount = () => { }; }; -const DataServiceRoleProvider = ({ children }: { children: JSX.Element }) => { +const DataServiceRoleProvider = ({ + children, + /** + * DoNotChangePropsWithRedux is a static props. + * When set, it must not be changed, otherwise it will break the hook rules. + * To be removed when we remove redux. + */ + DoNotChangePropsWithRedux = true, +}: { + children: JSX.Element; + DoNotChangePropsWithRedux?: boolean; +}) => { const [role, setRoleState] = useState<{ roleArn: string }>({ roleArn: '', }); @@ -121,7 +133,7 @@ const DataServiceRoleProvider = ({ children }: { children: JSX.Element }) => { const storedRole = getRoleArnStored(); if (accountName) { const account = accounts.find((account) => account.Name === accountName); - if (account) { + if (account && !role.roleArn) { setRoleState({ roleArn: account?.Roles[0].Arn }); } } else if (!role.roleArn && storedRole && accounts.length) { @@ -138,6 +150,7 @@ const DataServiceRoleProvider = ({ children }: { children: JSX.Element }) => { } else if (!storedRole && !role.roleArn && accounts.length) { setRoleState({ roleArn: accounts[0].Roles[0].Arn }); } + if (role.roleArn) { assumeRoleMutation.mutate(role.roleArn); } @@ -171,8 +184,25 @@ const DataServiceRoleProvider = ({ children }: { children: JSX.Element }) => { return Loading...; } + if (DoNotChangePropsWithRedux) { + return ( + + <_DataServiceRoleContext.Provider + value={{ + role, + setRole, + setRolePromise, + assumedRole, + }} + > + {children} + + + ); + } + return ( - + <_DataServiceRoleContext.Provider value={{ role, @@ -183,7 +213,7 @@ const DataServiceRoleProvider = ({ children }: { children: JSX.Element }) => { > {children} - + ); }; diff --git a/src/react/next-architecture/adapters/accessible-accounts/IAMPensieveAccessibleAccounts.ts b/src/react/next-architecture/adapters/accessible-accounts/IAMPensieveAccessibleAccounts.ts index 0253b8b82..7f4fdfaf4 100644 --- a/src/react/next-architecture/adapters/accessible-accounts/IAMPensieveAccessibleAccounts.ts +++ b/src/react/next-architecture/adapters/accessible-accounts/IAMPensieveAccessibleAccounts.ts @@ -1,4 +1,4 @@ -import { useAccounts } from '../../../utils/hooks'; +import { noopBasedEventDispatcher, useAccounts } from '../../../utils/hooks'; import { useAccountsLocationsAndEndpoints } from '../../domain/business/accounts'; import { AccountInfo, Role } from '../../domain/entities/account'; import { PromiseResult } from '../../domain/entities/promise'; @@ -8,6 +8,7 @@ import { IAccountsLocationsEndpointsAdapter } from '../accounts-locations/IAccou export class IAMPensieveAccessibleAccounts implements IAccessibleAccounts { constructor( private accountsLocationsAndEndpointsAdapter: IAccountsLocationsEndpointsAdapter, + private withEventDispatcher = true, ) {} useListAccessibleAccounts(): { accountInfos: PromiseResult<(AccountInfo & { assumableRoles: Role[] })[]>; @@ -17,7 +18,11 @@ export class IAMPensieveAccessibleAccounts implements IAccessibleAccounts { accountsLocationsEndpointsAdapter: this.accountsLocationsAndEndpointsAdapter, }); - const { accounts: accessibleAccounts, status } = useAccounts(); + const eventDispatcher = this.withEventDispatcher + ? undefined + : noopBasedEventDispatcher; + const { accounts: accessibleAccounts, status } = + useAccounts(eventDispatcher); if (accountStatus === 'error' || status === 'error') { return { diff --git a/src/react/next-architecture/ui/AccessibleAccountsAdapterProvider.tsx b/src/react/next-architecture/ui/AccessibleAccountsAdapterProvider.tsx index 52cf8d2ae..1597a462e 100644 --- a/src/react/next-architecture/ui/AccessibleAccountsAdapterProvider.tsx +++ b/src/react/next-architecture/ui/AccessibleAccountsAdapterProvider.tsx @@ -21,12 +21,20 @@ export const useAccessibleAccountsAdapter = (): IAccessibleAccounts => { export const AccessibleAccountsAdapterProvider = ({ children, + /** + * DoNotChangePropsWithEventDispatcher is a static props. + * When set, it must not be changed, otherwise it will break the hook rules. + * To be removed when we remove redux. + */ + DoNotChangePropsWithEventDispatcher = true, }: { children: JSX.Element; + DoNotChangePropsWithEventDispatcher?: boolean; }) => { const accountAdapter = useAccountsLocationsEndpointsAdapter(); const accessibleAccountsAdapter = new IAMPensieveAccessibleAccounts( accountAdapter, + DoNotChangePropsWithEventDispatcher, ); return ( diff --git a/src/react/next-architecture/ui/S3ClientProvider.tsx b/src/react/next-architecture/ui/S3ClientProvider.tsx index 200824f6b..83e92bc47 100644 --- a/src/react/next-architecture/ui/S3ClientProvider.tsx +++ b/src/react/next-architecture/ui/S3ClientProvider.tsx @@ -92,9 +92,64 @@ export const S3ClientProvider = ({ ); }; +export const S3ClientWithoutReduxProvider = ({ + configuration, + children, +}: PropsWithChildren<{ + configuration: S3.Types.ClientConfiguration; +}>) => { + const { iamEndpoint, iamInternalFQDN, s3InternalFQDN, basePath } = + useConfig(); + const { s3Client, zenkoClient, iamClient } = useMemo(() => { + const s3Config = { + ...configuration, + endpoint: genClientEndpoint(configuration.endpoint as string), + }; + const s3Client = new S3(s3Config); + const zenkoClient = new ZenkoClient( + s3Config.endpoint, + iamInternalFQDN, + s3InternalFQDN, + process.env.NODE_ENV === 'development' ? '' : basePath, + ); + const iamClient = new IAMClient(iamEndpoint); + + if ( + configuration.credentials?.accessKeyId && + configuration.credentials?.secretAccessKey && + configuration.credentials?.sessionToken + ) { + zenkoClient.login({ + accessKey: configuration.credentials.accessKeyId, + secretKey: configuration.credentials.secretAccessKey, + sessionToken: configuration.credentials.sessionToken, + }); + + iamClient.login({ + accessKey: configuration.credentials.accessKeyId, + secretKey: configuration.credentials.secretAccessKey, + sessionToken: configuration.credentials.sessionToken, + }); + } + + return { s3Client, zenkoClient, iamClient }; + }, [configuration]); + + return ( + + + <_IAMContext.Provider value={{ iamClient }}> + {children} + + + + ); +}; + export const useAssumeRoleQuery = () => { const { stsEndpoint } = useConfig(); const token = useAccessToken(); + const user = useAuth(); const roleSessionName = `ui-${user.userData?.id}`; const stsClient = new STSClient({ endpoint: stsEndpoint }); @@ -102,20 +157,22 @@ export const useAssumeRoleQuery = () => { return { queryKey, - getQuery: (roleArn: string) => ({ - queryKey, - queryFn: () => - stsClient.assumeRoleWithWebIdentity({ - idToken: notFalsyTypeGuard(token), - roleArn: roleArn, - RoleSessionName: roleSessionName, - }), - - refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - enabled: !!token && !!roleArn, - }), + getQuery: (roleArn: string) => { + return { + queryKey, + queryFn: () => + stsClient.assumeRoleWithWebIdentity({ + idToken: notFalsyTypeGuard(token), + roleArn: roleArn, + RoleSessionName: roleSessionName, + }), + + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + enabled: !!token && !!roleArn, + }; + }, }; }; From 40801bb1884957f8e3a5208cce04924baadd824d Mon Sep 17 00:00:00 2001 From: Patrick Ear Date: Mon, 29 Apr 2024 17:49:49 +0200 Subject: [PATCH 3/4] ARTESCA-10989 - create SelectAccountIAMRole --- .../ui-elements/SelectAccountIAMRole.tsx | 292 ++++++++++++++++++ src/react/utils/hooks.ts | 6 +- 2 files changed, 296 insertions(+), 2 deletions(-) create mode 100644 src/react/ui-elements/SelectAccountIAMRole.tsx diff --git a/src/react/ui-elements/SelectAccountIAMRole.tsx b/src/react/ui-elements/SelectAccountIAMRole.tsx new file mode 100644 index 000000000..96e4fc48a --- /dev/null +++ b/src/react/ui-elements/SelectAccountIAMRole.tsx @@ -0,0 +1,292 @@ +import { Stack } from '@scality/core-ui'; +import { Select } from '@scality/core-ui/dist/next'; +import { IAM } from 'aws-sdk'; +import { Bucket } from 'aws-sdk/clients/s3'; +import { PropsWithChildren, useState } from 'react'; +import { useQuery, useQueryClient } from 'react-query'; +import { MemoryRouter, Route, useHistory, useParams } from 'react-router-dom'; +import DataServiceRoleProvider, { + useAssumedRole, + useSetAssumedRole, +} from '../DataServiceRoleProvider'; +import { useIAMClient } from '../IAMProvider'; +import { IMetricsAdapter } from '../next-architecture/adapters/metrics/IMetricsAdapter'; +import { useListAccounts } from '../next-architecture/domain/business/accounts'; +import { Account } from '../next-architecture/domain/entities/account'; +import { LatestUsedCapacity } from '../next-architecture/domain/entities/metrics'; +import { + AccessibleAccountsAdapterProvider, + useAccessibleAccountsAdapter, +} from '../next-architecture/ui/AccessibleAccountsAdapterProvider'; +import { AccountsLocationsEndpointsAdapterProvider } from '../next-architecture/ui/AccountsLocationsEndpointsAdapterProvider'; +import { getListRolesQuery } from '../queries'; +import { regexArn } from '../utils/hooks'; + +class NoOppMetricsAdapter implements IMetricsAdapter { + async listBucketsLatestUsedCapacity( + buckets: Bucket[], + ): Promise> { + return {}; + } + async listLocationsLatestUsedCapacity( + locationIds: string[], + ): Promise> { + return {}; + } + async listAccountLocationsLatestUsedCapacity( + accountCanonicalId: string, + ): Promise> { + return {}; + } + async listAccountsLatestUsedCapacity( + accountCanonicalIds: string[], + ): Promise> { + return {}; + } +} + +const filterRoles = ( + accountName: string, + roles: IAM.Role[], + hideAccountRoles: { accountName: string; roleName: string }[], +) => { + return roles.filter( + (role) => + !hideAccountRoles.find( + (hideRole) => + hideRole.accountName === accountName && + hideRole.roleName === role.RoleName, + ), + ); +}; + +/** + * DataServiceRoleProvider is using the the path to figure out what is the current account. + * In order to reuse this logic, we need to have a router and set DataServiceRoleProvider under + * the path /accounts/:accountName + * Without this INTERNAL_DEFAULT_ACCOUNT_NAME_FOR_INITIALIZATION, it won't render. + * + * We assume the user won't have an account with this name. + */ +const INTERNAL_DEFAULT_ACCOUNT_NAME_FOR_INITIALIZATION = + '__INTERNAL_DEFAULT_ACCOUNT_NAME_FOR_INITIALIZATION__'; + +const AssumeDefaultIAMRole = ({ + defaultValue, +}: Pick) => { + const accessibleAccountsAdapter = useAccessibleAccountsAdapter(); + const metricsAdapter = new NoOppMetricsAdapter(); + const accounts = useListAccounts({ + accessibleAccountsAdapter, + metricsAdapter, + }); + const history = useHistory(); + const setAssumeRole = useSetAssumedRole(); + + const isInternalDefaultAccountSelected = + history.location.pathname === + '/accounts/' + INTERNAL_DEFAULT_ACCOUNT_NAME_FOR_INITIALIZATION; + + if ( + accounts.accounts.status === 'success' && + defaultValue && + isInternalDefaultAccountSelected + ) { + const acc = accounts.accounts.value.find( + (acc) => acc.name === defaultValue?.accountName, + ); + + /** + * This set state will trigger a warning because it's not in a useEffect. + * This is fine because the set state is under an if and it should not be called too many times. + * The only time it could break is if for some reason the user use an account that is named like + * INTERNAL_DEFAULT_ACCOUNT_NAME_FOR_INITIALIZATION and use the component with a defaultValue. + */ + setAssumeRole({ + roleArn: acc?.preferredAssumableRoleArn ?? '', + }); + history.replace('/accounts/' + defaultValue?.accountName); + } + + return <>; +}; + +const InternalProvider = ({ + children, + defaultValue, +}: PropsWithChildren< + Pick +>) => { + return ( + + + + + + <> + + {children} + + + + + + + ); +}; + +type SelectAccountIAMRoleProps = { + onChange: (account: Account, role: IAM.Role) => void; + defaultValue?: { accountName: string; roleName: string }; + hideAccountRoles?: { accountName: string; roleName: string }[]; +}; + +type SelectAccountIAMRoleWithAccountProps = SelectAccountIAMRoleProps & { + accounts: Account[]; +}; + +const SelectAccountIAMRoleWithAccount = ( + props: SelectAccountIAMRoleWithAccountProps, +) => { + const history = useHistory(); + const IAMClient = useIAMClient(); + const setAssumedRole = useSetAssumedRole(); + const { accounts, defaultValue, hideAccountRoles, onChange } = props; + const defaultAccountName = useParams<{ accountName: string }>().accountName; + const defaultAccount = + accounts.find((account) => account.name === defaultAccountName) ?? null; + const [account, setAccount] = useState(defaultAccount); + const [role, setRole] = useState(null); + const assumedRole = useAssumedRole(); + + const accountName = account ? account.name : ''; + const rolesQuery = getListRolesQuery(accountName, IAMClient); + const queryClient = useQueryClient(); + + const assumedRoleAccountId = regexArn.exec(assumedRole?.AssumedRoleUser?.Arn) + ?.groups?.['account_id']; + const selectedAccountId = regexArn.exec(account?.preferredAssumableRoleArn) + ?.groups?.['account_id']; + + /** + * When we change account, it will take some time to assume the role for the new account. + * We need this check to make sure we don't show the roles for the old account. + */ + const assumedRoleAccountMatchSelectedAccount = + assumedRoleAccountId === selectedAccountId; + + const listRolesQuery = { + ...rolesQuery, + enabled: + !!IAMClient && + !!IAMClient.client && + accountName !== '' && + assumedRoleAccountMatchSelectedAccount, + }; + const roleQueryData = useQuery(listRolesQuery); + + const roles = filterRoles( + accountName, + roleQueryData?.data?.Roles ?? [], + hideAccountRoles, + ); + + const isDefaultAccountSelected = account?.name === defaultValue?.accountName; + const defaultRole = isDefaultAccountSelected ? defaultValue?.roleName : null; + + return ( + + + + {roles.length > 0 ? ( + + ) : null} + + ); +}; + +const defaultOnChange = () => ({}); +export const _SelectAccountIAMRole = (props: SelectAccountIAMRoleProps) => { + const { + onChange = defaultOnChange, + hideAccountRoles = [], + defaultValue, + } = props; + + const accessibleAccountsAdapter = useAccessibleAccountsAdapter(); + const metricsAdapter = new NoOppMetricsAdapter(); + const accounts = useListAccounts({ + accessibleAccountsAdapter, + metricsAdapter, + }); + + if (accounts.accounts.status === 'success') { + return ( + + ); + } else { + return
Loading accounts...
; + } +}; + +export const SelectAccountIAMRole = (props: SelectAccountIAMRoleProps) => { + return ( + + <_SelectAccountIAMRole {...props} /> + + ); +}; diff --git a/src/react/utils/hooks.ts b/src/react/utils/hooks.ts index b50820d97..5a0a4086b 100644 --- a/src/react/utils/hooks.ts +++ b/src/react/utils/hooks.ts @@ -146,9 +146,10 @@ export function useQueryWithUnmountSupport< }); return query; } - +// arn:aws:sts::142222634614:assumed-role/storage-manager-role/ui-9160673b-2c2a-4a6f-a1ef-a3cb6ce25d7f +// arn:aws:iam::142222634614:role/scality-internal/storage-manager-role export const regexArn = - /arn:aws:iam::(?\d{12}):(?role|policy)\/(?(?:[^/]*\/)*)(?[^/]+)$/; + /arn:aws:(?:iam|sts)::(?\d{12}):(?role|policy|assumed-role)\/(?(?:[^/]*\/)*)(?[^/]+)$/; export const STORAGE_MANAGER_ROLE = 'storage-manager-role'; export const STORAGE_ACCOUNT_OWNER_ROLE = 'storage-account-owner-role'; @@ -233,6 +234,7 @@ export const useAccounts = ( }, (data) => data.Accounts, ); + const uniqueAccountsWithRoles = Object.values( data?.reduce( (agg, current) => ({ From 39797707b4f231808e1ea2a7296d1e95ae8d7b07 Mon Sep 17 00:00:00 2001 From: Patrick Ear Date: Mon, 29 Apr 2024 17:50:12 +0200 Subject: [PATCH 4/4] ARTESCA-10989 - add test for SelectAccountIAMRole --- .jest-setup.js | 3 + .../__tests__/SelectAccountIAMRole.test.tsx | 369 ++++++++++++++++++ 2 files changed, 372 insertions(+) create mode 100644 src/react/ui-elements/__tests__/SelectAccountIAMRole.test.tsx diff --git a/.jest-setup.js b/.jest-setup.js index fac76fbe3..cfd16edec 100644 --- a/.jest-setup.js +++ b/.jest-setup.js @@ -4,3 +4,6 @@ import 'core-js/stable'; import 'regenerator-runtime/runtime'; Enzyme.configure({ adapter: new Adapter() }); +HTMLCanvasElement.prototype.getContext = () => { + // return whatever getContext has to return +}; diff --git a/src/react/ui-elements/__tests__/SelectAccountIAMRole.test.tsx b/src/react/ui-elements/__tests__/SelectAccountIAMRole.test.tsx new file mode 100644 index 000000000..c82e265ff --- /dev/null +++ b/src/react/ui-elements/__tests__/SelectAccountIAMRole.test.tsx @@ -0,0 +1,369 @@ +import { + render, + screen, + waitFor, + waitForElementToBeRemoved, +} from '@testing-library/react'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { TEST_API_BASE_URL } from '../../../react/utils/testUtil'; +import { SelectAccountIAMRole } from '../SelectAccountIAMRole'; + +import userEvent from '@testing-library/user-event'; +import { debug } from 'jest-preview'; +import { + USERS, + getConfigOverlay, +} from '../../../js/mock/managementClientMSWHandlers'; +import { INSTANCE_ID } from '../../../react/actions/__tests__/utils/testUtil'; + +const testAccountId = '064609833007'; + +const genFn = (getPayloadFn: jest.Mock) => { + return rest.post(`${TEST_API_BASE_URL}/`, (req, res, ctx) => { + //@ts-ignore + const params = new URLSearchParams(req.body); + getPayloadFn(params); + + if (params.get('Action') === 'AssumeRoleWithWebIdentity') { + return res( + ctx.status(200), + ctx.xml(` + + + + arn:aws:sts::${testAccountId}:assumed-role/storage-manager-role/ui-9160673b-2c2a-4a6f-a1ef-a3cb6ce25d7f + OES3SPDIYW4L92S8K1QE6MINE31LQG04:ui-9160673b-2c2a-4a6f-a1ef-a3cb6ce25d7f + + + v/0Nq1YMw4nNbvtgQlgi0l6m/PXWjlk1VLmn2I5q + 72SPRZFF71WPWXXUG6XF + eyJzYWx0IjoicVIvVGdIdS9FVjJ4TjN5RmtXSnVLZGE0M0krK0g1L3lFVDU5UkV0enpYYz0iLCJ0YWciOiI4d05WRTIwTlQxWTVKbWtZemo2ZGJ3PT0iLCJjaXBoZXJ0ZXh0IjoiQVNIanI0M0VZc3dzK0QwWDFkVXRXQ2JMbzlFOVZ5SzF5WWt6a21lRjRXOUpCU3hwbmNxS21zWnpIU3ZvYlZEYjNKaDRNTm16bW1yVUd6dTU1bmRwMTk0eTVlVjFSVWMzaHZnSTFxZTRuYmJxNHBPdit5V3VZQ3RtSExUbE5BTHpDK3VhYW1tZDdzWk9BVXNKQlhRcmVHUG5sTFphb0kySTFveXJjbk10QlVpb1AvYnNjNUd6RHFqdTFWMjVQRE9PQWgzM2JFSktHdmorbEoyL2lWV0x5UHBQU1pLZmdZUnd1QjRXczdGaG81dHhaem9uWWhpaG9ocnFtdmFnNUJSNytiN2lGN3ZxZjBVSnFPZXI5Wm9ldDk1dlpqL01qTU04aGhGQXI1MmZnTHpzOHAzVlN3dHV0OENFSTBoVEJJNlVycUY4SWxiUmhFOUtlaHo0cnRiZHRKQzVmVHFRSkVPZWltb0RIbGpZZXZqOVlIZzZPVFhDR2ZhVzRIWDc3T0g5M1BRa0dHc1RCSjVpRTEyZEdYQjhYWWdSM1VackIwUzdQejdLQnpvSUVodTZOWUkrK1NPZ2pwMlFaUmhaWGtkbDdDdU5EMWg2UE9qN2twREY0QXhHbWdwcjBMbmpOdVp1UzJaWlJTck5OZG1WL3B5dWpUM3BtcFNJNUZkNW5Wby9SV1dTSGhoR0FVcWRJS0EyV00xdVJ2TkVFS25rb25keWNuVHRrSHpDVUwrN0RtTXNuL202eTcyZjFReHY2VFQyejRzRVFSUDFhWUcwdnBWSTlXbUpWdW5yTFFVNmxSUmpsb1VFSFVkZ2xCMGd1eTZGZTNYR29YQjdVc1J5UUpxbEJ0elpvdFdkR1AvSjZaMllNODFDSy8zZjJZTXVnNTZlbXQxTmJJZ0hrVWxnaGxpclRsNVdrckVRbG5XTW4zT2dzRk9wMjJKSTV1UGoxSENUMlNhTlBXZEQrY0VCcTZycC9tc1FDOW02Q3prSkMwTWMyclZ0RmdnZitaSEV5dVZvdEVzeUFtY1V5QTdFZzJtY3BXd1pnbHZrYkZQQmI5M2NDN0ZhZGhpNEUzQ0hQbm9BczF5eVNKNkxIOTZZTHJoaXk1Q1h3VWloSTlRdmxuSDNlV3EwaElBZTFGc2N6bThzVWRCTGU2SWlVc3ZJVDlpMjJzcTZnaVpmdld5czVlaU9NZzRQMFBaZCtPK0VDRmdmd3dxdUhYcFdEL1F5RnR1RENVb0xxblNyMU9lOHdCQ2lXNDFYaGtacmEzWTdtVW1QYXlNWTN6MXpOZm1XRllJV0dWZzlBNVFUaUZLWGlZTngxdUtWZGJ3Qk54SmowVmxlTTE4azlDNFR3Z2U3dVYyKzEzcWVkTW5xOUpLa2Uwa1NsNmMxMWM5N1RUbGJ4TUx5YS9WY1JLWkNkbHJaTGZNK0hjSTJWaGdkSzNzWHJIVEN6UENFRC9lMVBRTkg1RVZBVThLRlNHWGEzb1dPWm9VcmlSYlk1L3R5eTQvbHRKTkVhNnV3R2hra0ljR3JLMjltUndkaDJHSE94R1laYmdGL0VVUDYreUs5cjQrVzc5Y1RYc3NRcEpSM1M1bkZpUHE2bHR6NXM1ZlNYalNkcUxSM0gvTVZlcXV6K3RON0czMk1ieW9halZvcVJxcks2WjZIVm1vM3pDZ1M4TURQQk9jVkY3Ymc0QmhXaXFUTjc5a0ZqV0xkWWZSVlB5Qk1VaXBHNmZCcGlBdUZCZEV1S2lLMHBwVkhQNUpZL0h5ZXRBbVgxMzdVK1U5d3prbmw3eXhyOEQ0TkdNL05yaVhBT21hSDN4YVEifQ== + 2023-11-28T10:16:13Z + + www.scality.com + + + 8e94c64ebf4486567b0e + + `), + ); + } + if (params.get('Action') === 'ListRoles') { + return res( + ctx.xml(` + + + + + %7B%22Statement%22%3A%5B%7B%22Action%22%3A%22sts%3AAssumeRole%22%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22AWS%22%3A%22arn%3Aaws%3Aiam%3A%3A000000000000%3Auser%2Fscality-internal%2Fbackbeat-gc-1%22%7D%7D%5D%2C%22Version%22%3A%222012-10-17%22%7D + undefined + /scality-internal/ + backbeat-gc-1 + NPP7LHXVP8THSFDX9J58KJED1VKO5WIZ + arn:aws:iam::232853836441:role/scality-internal/backbeat-gc-1 + 2024-04-17T16:31:36Z + + + %7B%22Statement%22%3A%5B%7B%22Action%22%3A%22sts%3AAssumeRole%22%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22AWS%22%3A%22arn%3Aaws%3Aiam%3A%3A000000000000%3Auser%2Fscality-internal%2Fbackbeat-lifecycle-bp-1%22%7D%7D%5D%2C%22Version%22%3A%222012-10-17%22%7D + undefined + /scality-internal/ + backbeat-lifecycle-bp-1 + B5HBXF8G2DQ7Z7N13LJA87JRJ1SU40QS + arn:aws:iam::232853836441:role/scality-internal/backbeat-lifecycle-bp-1 + 2024-04-17T16:31:36Z + + + %7B%22Statement%22%3A%5B%7B%22Action%22%3A%22sts%3AAssumeRole%22%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22AWS%22%3A%22arn%3Aaws%3Aiam%3A%3A000000000000%3Auser%2Fscality-internal%2Fbackbeat-lifecycle-conductor-1%22%7D%7D%5D%2C%22Version%22%3A%222012-10-17%22%7D + undefined + /scality-internal/ + backbeat-lifecycle-conductor-1 + SBPV35W7A65Q5OCCWR1FD203538EELDB + arn:aws:iam::232853836441:role/scality-internal/backbeat-lifecycle-conductor-1 + 2024-04-17T16:31:36Z + + + %7B%22Statement%22%3A%5B%7B%22Action%22%3A%22sts%3AAssumeRole%22%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22AWS%22%3A%22arn%3Aaws%3Aiam%3A%3A000000000000%3Auser%2Fscality-internal%2Fbackbeat-lifecycle-op-1%22%7D%7D%5D%2C%22Version%22%3A%222012-10-17%22%7D + undefined + /scality-internal/ + backbeat-lifecycle-op-1 + WHS10HK95B2PN9RK8UY2D9Z8377F9E5X + arn:aws:iam::232853836441:role/scality-internal/backbeat-lifecycle-op-1 + 2024-04-17T16:31:36Z + + + %7B%22Statement%22%3A%5B%7B%22Action%22%3A%22sts%3AAssumeRole%22%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22AWS%22%3A%22arn%3Aaws%3Aiam%3A%3A000000000000%3Auser%2Fscality-internal%2Fbackbeat-lifecycle-tp-1%22%7D%7D%5D%2C%22Version%22%3A%222012-10-17%22%7D + undefined + /scality-internal/ + backbeat-lifecycle-tp-1 + YSXDD002ETBE0CJEZQYFDAYJQTWOVJ51 + arn:aws:iam::232853836441:role/scality-internal/backbeat-lifecycle-tp-1 + 2024-04-17T16:31:36Z + + + %7B%22Statement%22%3A%5B%7B%22Action%22%3A%22sts%3AAssumeRole%22%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22AWS%22%3A%22arn%3Aaws%3Aiam%3A%3A000000000000%3Auser%2Fscality-internal%2Fsorbet-fwd-2%22%7D%7D%5D%2C%22Version%22%3A%222012-10-17%22%7D + undefined + /scality-internal/ + cold-storage-archive-role-2 + DMELLEKK4LI9B3F5EWGTXEMRBKU35R3E + arn:aws:iam::232853836441:role/scality-internal/cold-storage-archive-role-2 + 2024-04-17T16:31:36Z + + + %7B%22Statement%22%3A%5B%7B%22Action%22%3A%22sts%3AAssumeRole%22%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22AWS%22%3A%22arn%3Aaws%3Aiam%3A%3A000000000000%3Auser%2Fscality-internal%2Fsorbet-fwd-2%22%7D%7D%5D%2C%22Version%22%3A%222012-10-17%22%7D + undefined + /scality-internal/ + cold-storage-restore-role-2 + H3Y58C2OQTKRH4M1EBXASEKSLOMEGRI1 + arn:aws:iam::232853836441:role/scality-internal/cold-storage-restore-role-2 + 2024-04-17T16:31:36Z + + + %7B%22Statement%22%3A%5B%7B%22Action%22%3A%22sts%3AAssumeRoleWithWebIdentity%22%2C%22Condition%22%3A%7B%22StringEquals%22%3A%7B%22keycloak%3Agroups%22%3A%22100%3A%3ADataConsumer%22%7D%7D%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Federated%22%3A%22https%3A%2F%2Fui.pod-choco.local%2Fauth%2Frealms%2Fartesca%22%7D%7D%2C%7B%22Action%22%3A%22sts%3AAssumeRoleWithWebIdentity%22%2C%22Condition%22%3A%7B%22StringEquals%22%3A%7B%22keycloak%3Agroups%22%3A%22100%3A%3ADataConsumer%22%7D%7D%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Federated%22%3A%22https%3A%2F%2F13.48.197.10%3A8443%2Fauth%2Frealms%2Fartesca%22%7D%7D%5D%2C%22Version%22%3A%222012-10-17%22%7D + Has S3 read and write accesses to 100 S3 Buckets. Cannot create or delete S3 Buckets. + /scality-internal/ + data-consumer-role + YGEX9QWC7RI9KMBQEKS4RA9OND4JZ35U + arn:aws:iam::232853836441:role/scality-internal/data-consumer-role + 2024-04-17T16:31:36Z + + + %7B%22Statement%22%3A%5B%7B%22Action%22%3A%22sts%3AAssumeRoleWithWebIdentity%22%2C%22Condition%22%3A%7B%22StringEquals%22%3A%7B%22keycloak%3Agroups%22%3A%22100%3A%3AStorageAccountOwner%22%7D%7D%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Federated%22%3A%22https%3A%2F%2Fui.pod-choco.local%2Fauth%2Frealms%2Fartesca%22%7D%7D%2C%7B%22Action%22%3A%22sts%3AAssumeRoleWithWebIdentity%22%2C%22Condition%22%3A%7B%22StringEquals%22%3A%7B%22keycloak%3Agroups%22%3A%22100%3A%3AStorageAccountOwner%22%7D%7D%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Federated%22%3A%22https%3A%2F%2F13.48.197.10%3A8443%2Fauth%2Frealms%2Fartesca%22%7D%7D%5D%2C%22Version%22%3A%222012-10-17%22%7D + Manages the 100 account (Policies, Users, Roles, Groups). + /scality-internal/ + storage-account-owner-role + OYYDW5GLCETHME90KWAZCG5Z8KNZA1OT + arn:aws:iam::232853836441:role/scality-internal/storage-account-owner-role + 2024-04-17T16:31:36Z + + + %7B%22Statement%22%3A%5B%7B%22Action%22%3A%22sts%3AAssumeRoleWithWebIdentity%22%2C%22Condition%22%3A%7B%22StringEquals%22%3A%7B%22keycloak%3Aroles%22%3A%22StorageManager%22%7D%7D%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Federated%22%3A%22https%3A%2F%2Fui.pod-choco.local%2Fauth%2Frealms%2Fartesca%22%7D%7D%2C%7B%22Action%22%3A%22sts%3AAssumeRoleWithWebIdentity%22%2C%22Condition%22%3A%7B%22StringEquals%22%3A%7B%22keycloak%3Aroles%22%3A%22StorageManager%22%7D%7D%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Federated%22%3A%22https%3A%2F%2F13.48.197.10%3A8443%2Fauth%2Frealms%2Fartesca%22%7D%7D%5D%2C%22Version%22%3A%222012-10-17%22%7D + Has all permissions and full access to the 100 account resources and manages ARTESCA users. + /scality-internal/ + storage-manager-role + YRA3NTDUTWN6DRN76LSSDM6HA22RWBO9 + arn:aws:iam::232853836441:role/scality-internal/storage-manager-role + 2024-04-17T16:31:36Z + + + false + + + 148012f42345b8eb7c29 + + + `), + ); + } + + if (params.get('Action') === 'GetRolesForWebIdentity') { + const TEST_ACCOUNT = + USERS.find((user) => user.id === testAccountId)?.userName ?? ''; + const TEST_ACCOUNT_CREATION_DATE = + USERS.find((user) => user.id === testAccountId)?.createDate ?? ''; + + return res( + ctx.json({ + IsTruncated: false, + Accounts: [ + { + Name: TEST_ACCOUNT, + CreationDate: TEST_ACCOUNT_CREATION_DATE, + Roles: [ + { + Name: 'storage-manager-role', + Arn: `arn:aws:iam::${testAccountId}:role/scality-internal/storage-manager-role`, + }, + ], + }, + ], + }), + ); + } + }); +}; + +const server = setupServer(getConfigOverlay(TEST_API_BASE_URL, INSTANCE_ID)); + +const LocalWrapper = ({ children }) => { + const queryClient = new QueryClient({ + // In test environnement, we don't want to retry queries + // because we may test the error case + defaultOptions: { + queries: { + retry: false, + }, + mutations: { + retry: false, + }, + }, + }); + + return ( + {children} + ); +}; + +describe('SelectAccountIAMRole', () => { + const seletors = { + accountSelect: () => screen.getByText(/select account/i), + roleSelect: () => screen.getByText(/select role/i), + selectOption: (name: string | RegExp) => + screen.getByRole('option', { + name: new RegExp(name, 'i'), + }), + }; + beforeAll(() => { + server.listen({ onUnhandledRequest: 'error' }); + }); + afterEach(() => { + server.resetHandlers(); + }); + afterAll(() => { + server.close(); + }); + it('renders with normal props', async () => { + const getPayloadFn = jest.fn(); + server.use(genFn(getPayloadFn)); + const onChange = jest.fn(); + render( + + + , + ); + + await waitFor(() => { + expect(seletors.accountSelect()).toBeInTheDocument(); + }); + + await userEvent.click(seletors.accountSelect()); + + expect(screen.getByText('no-bucket')).toBeInTheDocument(); + + await userEvent.click(seletors.selectOption(/no\-bucket/i)); + + await waitFor(() => { + expect(seletors.roleSelect()).toBeInTheDocument(); + }); + + await userEvent.click(seletors.roleSelect()); + await userEvent.click(seletors.selectOption(/backbeat-gc-1/i)); + + const account = { + assumableRoles: [ + { + Arn: `arn:aws:iam::${testAccountId}:role/scality-internal/storage-manager-role`, + Name: 'storage-manager-role', + }, + ], + canManageAccount: true, + canonicalId: + '1e3492312ab47ab0785e3411824352a8fa8aab68cece94973af04167926b8f2c', + creationDate: '2022-03-18T12:51:44.000Z', + id: testAccountId, + name: 'no-bucket', + preferredAssumableRoleArn: `arn:aws:iam::${testAccountId}:role/scality-internal/storage-manager-role`, + usedCapacity: { + status: 'unknown', + }, + }; + const role = { + Arn: 'arn:aws:iam::232853836441:role/scality-internal/backbeat-gc-1', + AssumeRolePolicyDocument: + '%7B%22Statement%22%3A%5B%7B%22Action%22%3A%22sts%3AAssumeRole%22%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22AWS%22%3A%22arn%3Aaws%3Aiam%3A%3A000000000000%3Auser%2Fscality-internal%2Fbackbeat-gc-1%22%7D%7D%5D%2C%22Version%22%3A%222012-10-17%22%7D', + CreateDate: new Date('2024-04-17T16:31:36.000Z'), + Description: 'undefined', + Path: '/scality-internal/', + RoleId: 'NPP7LHXVP8THSFDX9J58KJED1VKO5WIZ', + RoleName: 'backbeat-gc-1', + Tags: [], + }; + expect(onChange).toHaveBeenCalledWith(account, role); + }); + + it('renders with default value', async () => { + const getPayloadFn = jest.fn(); + server.use(genFn(getPayloadFn)); + const onChange = jest.fn(); + render( + + + , + ); + await waitFor(() => { + expect(seletors.accountSelect()).toBeInTheDocument(); + }); + + expect(screen.getByText('no-bucket')).toBeInTheDocument(); + + debug(); + }); + + it('renders with wrong default value', async () => { + const getPayloadFn = jest.fn(); + server.use(genFn(getPayloadFn)); + const onChange = jest.fn(); + render( + + + , + ); + + await waitFor(() => { + expect(seletors.accountSelect()).toBeInTheDocument(); + }); + + expect(seletors.accountSelect()).toBeInTheDocument(); + }); + + it('renders with hidden account roles', async () => { + const getPayloadFn = jest.fn(); + server.use(genFn(getPayloadFn)); + const onChange = jest.fn(); + render( + + + , + ); + + await waitFor(() => { + expect(seletors.accountSelect()).toBeInTheDocument(); + }); + + await userEvent.click(seletors.accountSelect()); + + await userEvent.click(seletors.selectOption(/no\-bucket/i)); + + await waitFor(() => { + expect(seletors.roleSelect()).toBeInTheDocument(); + }); + + await userEvent.click(seletors.roleSelect()); + await userEvent.type(seletors.roleSelect(), 'data-consumer'); + + expect(screen.getByText(/no options/i)).toBeInTheDocument(); + + debug(); + }); +});