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/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/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 (
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,
+ };
+ },
};
};
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/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();
+ });
+});
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) => ({