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/package-lock.json b/package-lock.json index 26eb46402..eb21b1c9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@hapi/joi-date": "^2.0.1", "@hookform/resolvers": "^2.8.8", "@monaco-editor/react": "^4.4.5", - "@scality/core-ui": "0.121.0", + "@scality/core-ui": "0.122.0", "@scality/module-federation": "github:scality/module-federation#1.1.0", "@types/react-table": "^7.7.10", "@types/react-virtualized": "^9.21.20", @@ -3322,9 +3322,9 @@ "dev": true }, "node_modules/@scality/core-ui": { - "version": "0.121.0", - "resolved": "https://registry.npmjs.org/@scality/core-ui/-/core-ui-0.121.0.tgz", - "integrity": "sha512-hmGLm8KrhRAAoKM+43egEt5qRrQrvRSVXN7VPav3ReFEvUDsbdWqvbI2xQO0zn9A/H9Y1biz4X2jvHMbWtYzPg==", + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@scality/core-ui/-/core-ui-0.122.0.tgz", + "integrity": "sha512-61rXypM9Z845lN7+5jqeuof3ZLZe8tIfcdaVj2FoFLP97nXUuNaKRyBEoWUc0ESSFl9U87ReU1jMOKIOJlWnsg==", "dependencies": { "@floating-ui/dom": "^1.6.3", "@fortawesome/fontawesome-free": "^5.10.2", @@ -3359,46 +3359,6 @@ "vega-tooltip": "0.27.0" } }, - "node_modules/@scality/core-ui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@scality/core-ui/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/@scality/core-ui/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@scality/core-ui/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "node_modules/@scality/core-ui/node_modules/compute-scroll-into-view": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz", @@ -3530,47 +3490,6 @@ "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-1.16.1.tgz", "integrity": "sha512-FdgD72fmZMPJE99FxvFXth0IL4BbLA93WmBg/lvcJmfkK4Uf90WIlvGwaIUdSePIsdpkZjBPyQcHMQ8OcS8Smg==" }, - "node_modules/@scality/core-ui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@scality/core-ui/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@scality/core-ui/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "engines": { - "node": ">=10" - } - }, "node_modules/@scality/module-federation": { "version": "1.1.0", "resolved": "git+ssh://git@github.com/scality/module-federation.git#fcced6f064ecd8f152daf5137113d8fd38648ef6", @@ -19984,7 +19903,6 @@ "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -22367,9 +22285,9 @@ "dev": true }, "@scality/core-ui": { - "version": "0.121.0", - "resolved": "https://registry.npmjs.org/@scality/core-ui/-/core-ui-0.121.0.tgz", - "integrity": "sha512-hmGLm8KrhRAAoKM+43egEt5qRrQrvRSVXN7VPav3ReFEvUDsbdWqvbI2xQO0zn9A/H9Y1biz4X2jvHMbWtYzPg==", + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@scality/core-ui/-/core-ui-0.122.0.tgz", + "integrity": "sha512-61rXypM9Z845lN7+5jqeuof3ZLZe8tIfcdaVj2FoFLP97nXUuNaKRyBEoWUc0ESSFl9U87ReU1jMOKIOJlWnsg==", "requires": { "@floating-ui/dom": "^1.6.3", "@fortawesome/fontawesome-free": "^5.10.2", @@ -22404,37 +22322,6 @@ "vega-tooltip": "0.27.0" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "compute-scroll-into-view": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz", @@ -22536,35 +22423,6 @@ "version": "1.16.1", "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-1.16.1.tgz", "integrity": "sha512-FdgD72fmZMPJE99FxvFXth0IL4BbLA93WmBg/lvcJmfkK4Uf90WIlvGwaIUdSePIsdpkZjBPyQcHMQ8OcS8Smg==" - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" } } }, @@ -35081,7 +34939,6 @@ "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, "requires": { "cliui": "^7.0.2", "escalade": "^3.1.1", diff --git a/package.json b/package.json index 6f64d4248..d01a5e126 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@hapi/joi-date": "^2.0.1", "@hookform/resolvers": "^2.8.8", "@monaco-editor/react": "^4.4.5", - "@scality/core-ui": "0.121.0", + "@scality/core-ui": "0.122.0", "@scality/module-federation": "github:scality/module-federation#1.1.0", "@types/react-table": "^7.7.10", "@types/react-virtualized": "^9.21.20", 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/endpoint/EndpointList.tsx b/src/react/endpoint/EndpointList.tsx index 6970b13f1..52d85c489 100644 --- a/src/react/endpoint/EndpointList.tsx +++ b/src/react/endpoint/EndpointList.tsx @@ -14,6 +14,7 @@ import { cloudServerDashboard, } from './AdvancedMetricsButton'; import { DeleteEndpoint } from './DeleteEndpoint'; + type CellProps = { row: { original: Endpoint; 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..d41e04047 --- /dev/null +++ b/src/react/ui-elements/SelectAccountIAMRole.tsx @@ -0,0 +1,298 @@ +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 NoOpMetricsAdapter 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, + ), + ); +}; + +export const extractAccountIdFromARN = (arn: string) => { + return regexArn.exec(arn)?.groups?.['account_id'] ?? ''; +}; + +/** + * DataServiceRoleProvider is using 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 NoOpMetricsAdapter(); + 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 = extractAccountIdFromARN( + assumedRole?.AssumedRoleUser?.Arn, + ); + const selectedAccountId = extractAccountIdFromARN( + account?.preferredAssumableRoleArn, + ); + + /** + * 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 NoOpMetricsAdapter(); + 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..aed481d40 --- /dev/null +++ b/src/react/ui-elements/__tests__/SelectAccountIAMRole.test.tsx @@ -0,0 +1,618 @@ +import { render, screen, waitFor } 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, + extractAccountIdFromARN, +} 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 testAccountId1 = '064609833007'; +const testAccountId2 = '377232323695'; + +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') { + const accountId = extractAccountIdFromARN(params.get('RoleArn')); + + if (accountId === testAccountId1) { + return res( + ctx.status(200), + ctx.xml(` + + + + arn:aws:sts::${testAccountId1}: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 + + `), + ); + } else if (accountId === testAccountId2) { + return res( + ctx.status(200), + ctx.xml(` + + + + arn:aws:sts::${testAccountId2}: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 testAccount1 = USERS.find((user) => user.id === testAccountId1); + const testAccount2 = USERS.find((user) => user.id === testAccountId2); + const accounts = [testAccount1, testAccount2]; + + return res( + ctx.json({ + IsTruncated: false, + Accounts: accounts.map((account) => { + return { + Name: account.userName, + CreationDate: account.createDate, + Roles: [ + { + Name: 'storage-manager-role', + Arn: `arn:aws:iam::${account.id}: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.getByLabelText(/select account/i), + roleSelect: () => screen.getByLabelText(/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::${testAccountId1}:role/scality-internal/storage-manager-role`, + Name: 'storage-manager-role', + }, + ], + canManageAccount: true, + canonicalId: + '1e3492312ab47ab0785e3411824352a8fa8aab68cece94973af04167926b8f2c', + creationDate: '2022-03-18T12:51:44.000Z', + id: testAccountId1, + name: 'no-bucket', + preferredAssumableRoleArn: `arn:aws:iam::${testAccountId1}: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('test the change of account and role', async () => { + // Select Account A, check that role B does not exist and select role A + // Change Account to Account B and Role B + + 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.click(seletors.selectOption(/backbeat-gc-1/i)); + + expect(onChange).toHaveBeenNthCalledWith( + 1, + { + assumableRoles: [ + { + Arn: 'arn:aws:iam::064609833007:role/scality-internal/storage-manager-role', + Name: 'storage-manager-role', + }, + ], + canManageAccount: true, + canonicalId: + '1e3492312ab47ab0785e3411824352a8fa8aab68cece94973af04167926b8f2c', + creationDate: '2022-03-18T12:51:44.000Z', + id: '064609833007', + name: 'no-bucket', + preferredAssumableRoleArn: + 'arn:aws:iam::064609833007:role/scality-internal/storage-manager-role', + usedCapacity: { + status: 'unknown', + }, + }, + { + 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: [], + }, + ); + + await userEvent.click(seletors.accountSelect()); + + expect(screen.getByText('yanjin')).toBeInTheDocument(); + + server.use( + rest.post(`${TEST_API_BASE_URL}/`, (req, res, ctx) => { + //@ts-ignore + const params = new URLSearchParams(req.body); + getPayloadFn(params); + + 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/ + yanjin-custom-role + NPP7LHXVP8THSFDX9J58KJED1VKO5WIZ + arn:aws:iam::232853836441:role/scality-internal/yanjin-custom-role + 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 + + + `), + ); + } + }), + ); + await userEvent.click(seletors.selectOption(/yanjin/i)); + + await waitFor(() => { + expect(seletors.roleSelect()).toBeInTheDocument(); + }); + + await userEvent.click(seletors.roleSelect()); + + await userEvent.click(seletors.selectOption(/yanjin-custom-role/i)); + + expect(onChange).toHaveBeenNthCalledWith( + 2, + { + assumableRoles: [ + { + Arn: 'arn:aws:iam::377232323695:role/scality-internal/storage-manager-role', + Name: 'storage-manager-role', + }, + ], + canManageAccount: true, + canonicalId: + '8c3b89e95e9768755365a8c2d528e71bc7b1cab781ac118b0824cefe21abaf29', + creationDate: '2022-04-29T09:35:35.000Z', + id: '377232323695', + name: 'yanjin', + preferredAssumableRoleArn: + 'arn:aws:iam::377232323695:role/scality-internal/storage-manager-role', + usedCapacity: { + status: 'unknown', + }, + }, + { + Arn: 'arn:aws:iam::232853836441:role/scality-internal/yanjin-custom-role', + 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: 'yanjin-custom-role', + Tags: [], + }, + ); + debug(); + }); + + 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(); + }); + + 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(); + }); +}); 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) => ({ diff --git a/webpack.common.js b/webpack.common.js index 8a219c2a2..dc1e2995b 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -81,6 +81,8 @@ module.exports = { './FederableApp': './src/react/FederableApp.tsx', './VeeamWelcomeModal': './src/react/ui-elements/Veeam/VeeamWelcomeModal.tsx', + './SelectAccountIAMRole': + './src/react/ui-elements/SelectAccountIAMRole.tsx', }, shared: { ...Object.fromEntries(