From 40801bb1884957f8e3a5208cce04924baadd824d Mon Sep 17 00:00:00 2001 From: Patrick Ear Date: Mon, 29 Apr 2024 17:49:49 +0200 Subject: [PATCH] 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) => ({