Skip to content

Commit

Permalink
Enhance IAM Role Selector with ability to select only interresting ro…
Browse files Browse the repository at this point in the history
…les and review design
  • Loading branch information
JBWatenbergScality committed Sep 23, 2024
1 parent e313cc4 commit d14c774
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 53 deletions.
8 changes: 8 additions & 0 deletions src/js/IAMClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,14 @@ export default class IAMClient implements IAMClientInterface {
.promise();
}

getRole(roleName: string) {
return notFalsyTypeGuard(this.client)
.getRole({
RoleName: roleName,
})
.promise();
}

listOwnAccessKeys() {
return notFalsyTypeGuard(this.client).listAccessKeys().promise();
}
Expand Down
4 changes: 3 additions & 1 deletion src/react/DataServiceRoleProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export const useCurrentAccount = () => {

const DataServiceRoleProvider = ({
children,
inlineLoader = false,
/**
* DoNotChangePropsWithRedux is a static props.
* When set, it must not be changed, otherwise it will break the hook rules.
Expand All @@ -105,6 +106,7 @@ const DataServiceRoleProvider = ({
DoNotChangePropsWithRedux = true,
}: {
children: JSX.Element;
inlineLoader?: boolean;
DoNotChangePropsWithRedux?: boolean;
}) => {
const [role, setRoleState] = useState<{ roleArn: string }>({
Expand Down Expand Up @@ -181,7 +183,7 @@ const DataServiceRoleProvider = ({

if (role.roleArn && !assumedRole) {
//@ts-expect-error fix this when you are working on it
return <Loader>Loading...</Loader>;
return inlineLoader ? <div>loading...</div> : <Loader>Loading...</Loader>;
}

if (DoNotChangePropsWithRedux) {
Expand Down
186 changes: 134 additions & 52 deletions src/react/ui-elements/SelectAccountIAMRole.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Stack } from '@scality/core-ui';
import { Form, FormGroup, FormSection, 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 { useMutation, useQuery, useQueryClient } from 'react-query';
import { MemoryRouter, Route, useHistory, useParams } from 'react-router-dom';
import DataServiceRoleProvider, {
useAssumedRole,
Expand All @@ -20,7 +20,7 @@ import {
} from '../next-architecture/ui/AccessibleAccountsAdapterProvider';
import { AccountsLocationsEndpointsAdapterProvider } from '../next-architecture/ui/AccountsLocationsEndpointsAdapterProvider';
import { getListRolesQuery } from '../queries';
import { regexArn } from '../utils/hooks';
import { SCALITY_INTERNAL_ROLES, regexArn } from '../utils/hooks';

class NoOpMetricsAdapter implements IMetricsAdapter {
async listBucketsLatestUsedCapacity(
Expand Down Expand Up @@ -128,7 +128,7 @@ const InternalProvider = ({
]}
>
<Route path="/accounts/:accountName">
<DataServiceRoleProvider DoNotChangePropsWithRedux={false}>
<DataServiceRoleProvider DoNotChangePropsWithRedux={false} inlineLoader>
<AccountsLocationsEndpointsAdapterProvider>
<AccessibleAccountsAdapterProvider
DoNotChangePropsWithEventDispatcher={false}
Expand All @@ -146,9 +146,16 @@ const InternalProvider = ({
};

type SelectAccountIAMRoleProps = {
onChange: (account: Account, role: IAM.Role) => void;
onChange: (
account: Account,
role: IAM.Role,
keycloakRoleName: string,
) => void;
defaultValue?: { accountName: string; roleName: string };
hideAccountRoles?: { accountName: string; roleName: string }[];
menuPosition?: 'absolute' | 'fixed';
identityProviderUrl?: string;
filterOutInternalRoles?: boolean;
};

type SelectAccountIAMRoleWithAccountProps = SelectAccountIAMRoleProps & {
Expand All @@ -169,6 +176,12 @@ const SelectAccountIAMRoleWithAccount = (
const [role, setRole] = useState<IAM.Role | null>(null);
const assumedRole = useAssumedRole();

const getIAMRoleMutation = useMutation({
mutationFn: (roleName: string) => {
return IAMClient.getRole(roleName);
},
});

const accountName = account ? account.name : '';
const rolesQuery = getListRolesQuery(accountName, IAMClient);
const queryClient = useQueryClient();
Expand Down Expand Up @@ -197,66 +210,132 @@ const SelectAccountIAMRoleWithAccount = (
};
const roleQueryData = useQuery(listRolesQuery);

const roles = filterRoles(
const allRolesExceptHiddenOnes = filterRoles(
accountName,
roleQueryData?.data?.Roles ?? [],
hideAccountRoles,
);
const roles = props.filterOutInternalRoles
? allRolesExceptHiddenOnes.filter((role) => {
return (
SCALITY_INTERNAL_ROLES.includes(role.RoleName) ||
!role.Arn.includes('role/scality-internal')
);
})
: allRolesExceptHiddenOnes;

const isDefaultAccountSelected = account?.name === defaultValue?.accountName;
const defaultRole = isDefaultAccountSelected ? defaultValue?.roleName : null;

return (
<Stack>
<Select
id="select-account"
value={account?.name ?? defaultValue?.accountName}
onChange={(accountName) => {
const selectedAccount = accounts.find(
(account) => account.name === accountName,
);
<Form layout={{ kind: 'tab' }}>
<FormSection>
<FormGroup
label="Account"
id="select-account"
content={
<Select
id="select-account"
value={account?.name ?? defaultValue?.accountName}
onChange={(accountName) => {
const selectedAccount = accounts.find(
(account) => account.name === accountName,
);

setAssumedRole({
roleArn: selectedAccount.preferredAssumableRoleArn,
});
history.push(`/accounts/${accountName}`);
setAssumedRole({
roleArn: selectedAccount.preferredAssumableRoleArn,
});
history.push(`/accounts/${accountName}`);

setAccount(selectedAccount);
setRole(null);
queryClient.invalidateQueries(rolesQuery.queryKey);
}}
size="1/2"
placeholder="Select Account"
>
{accounts.map((account) => (
<Select.Option key={`${account.name}`} value={account.name}>
{account.name}
</Select.Option>
))}
</Select>
setAccount(selectedAccount);
setRole(null);
queryClient.invalidateQueries(rolesQuery.queryKey);
}}
menuPosition={props.menuPosition}
placeholder="Select Account"
>
{accounts.map((account) => (
<Select.Option key={`${account.name}`} value={account.name}>
{account.name}
</Select.Option>
))}
</Select>
}
/>

{roles.length > 0 ? (
<Select
<FormGroup
label="Role"
id="select-account-role"
value={role?.RoleName ?? defaultRole}
onChange={(roleName) => {
const selectedRole = roles.find(
(role) => role.RoleName === roleName,
);
onChange(account, selectedRole);
setRole(selectedRole);
}}
size="2/3"
placeholder="Select Role"
>
{roles.map((role) => (
<Select.Option key={`${role.RoleName}`} value={role.RoleName}>
{role.RoleName}
</Select.Option>
))}
</Select>
) : null}
</Stack>
content={
roles.length > 0 ? (
<Select
id="select-account-role"
value={role?.RoleName ?? defaultRole}
onChange={(roleName) => {
const selectedRole = roles.find(
(role) => role.RoleName === roleName,
);
getIAMRoleMutation.mutate(roleName, {
onSuccess: (data) => {
const assumeRolePolicyDocument: {
Statement: {
Effect: 'Allow' | 'Deny';
Principal: { Federated?: string };
Action: 'sts:AssumeRoleWithWebIdentity';
Condition: {
StringEquals: { ['keycloak:roles']: string };
};
}[];
} = JSON.parse(
decodeURIComponent(data.Role.AssumeRolePolicyDocument),
);
const keycloakRoleName =
assumeRolePolicyDocument.Statement.find(
(statement) =>
(props.identityProviderUrl
? statement.Principal?.Federated?.startsWith(
props.identityProviderUrl,
)
: true) &&
statement.Condition.StringEquals[
'keycloak:roles'
] &&
statement.Effect === 'Allow' &&
statement.Action ===
'sts:AssumeRoleWithWebIdentity',
).Condition.StringEquals['keycloak:roles'];
onChange(account, selectedRole, keycloakRoleName);
},
});
setRole(selectedRole);
}}
menuPosition={props.menuPosition}
placeholder="Select Role"
>
{roles.map((role) => (
<Select.Option key={`${role.RoleName}`} value={role.RoleName}>
{role.RoleName}
</Select.Option>
))}
</Select>
) : (
<Select
id="select-account-role"
value={'Please select an account'}
disabled
onChange={() => {}}

Check failure on line 326 in src/react/ui-elements/SelectAccountIAMRole.tsx

View workflow job for this annotation

GitHub Actions / tests

Unexpected empty arrow function
menuPosition={props.menuPosition}
placeholder="Select Role"
>
<Select.Option value={'Please select an account'}>
Please select an account
</Select.Option>
</Select>
)
}
/>
</FormSection>
</Form>
);
};

Expand All @@ -282,6 +361,9 @@ export const _SelectAccountIAMRole = (props: SelectAccountIAMRoleProps) => {
defaultValue={defaultValue}
hideAccountRoles={hideAccountRoles}
onChange={onChange}
menuPosition={props.menuPosition}
filterOutInternalRoles={props.filterOutInternalRoles}
identityProviderUrl={props.identityProviderUrl}
/>
);
} else {
Expand Down
2 changes: 2 additions & 0 deletions src/react/utils/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,12 @@ export const regexArn =
export const STORAGE_MANAGER_ROLE = 'storage-manager-role';
export const STORAGE_ACCOUNT_OWNER_ROLE = 'storage-account-owner-role';
const DATA_CONSUMER_ROLE = 'data-consumer-role';
const DATA_ACCESSOR_ROLE = 'data-accessor-role';
export const SCALITY_INTERNAL_ROLES = [
STORAGE_MANAGER_ROLE,
STORAGE_ACCOUNT_OWNER_ROLE,
DATA_CONSUMER_ROLE,
DATA_ACCESSOR_ROLE,
];

const reduxBasedEventDispatcher = () => {
Expand Down

0 comments on commit d14c774

Please sign in to comment.