diff --git a/web/packages/design/src/Alert/Alert.jsx b/web/packages/design/src/Alert/Alert.jsx
index 09682267e8f87..7e792ff987810 100644
--- a/web/packages/design/src/Alert/Alert.jsx
+++ b/web/packages/design/src/Alert/Alert.jsx
@@ -49,15 +49,23 @@ const kind = props => {
case 'outline-danger':
return {
background: fade(theme.colors.error.main, 0.1),
- border: `${theme.radii[1]}px solid ${theme.colors.error.main}`,
+ border: `${theme.borders[2]} ${theme.colors.error.main}`,
borderRadius: `${theme.radii[3]}px`,
boxShadow: 'none',
justifyContent: 'normal',
};
case 'outline-info':
return {
- background: fade(theme.colors.link, 0.1),
- border: `${theme.radii[1]}px solid ${theme.colors.link}`,
+ background: fade(theme.colors.accent.main, 0.1),
+ border: `${theme.borders[2]} ${theme.colors.accent.main}`,
+ borderRadius: `${theme.radii[3]}px`,
+ boxShadow: 'none',
+ justifyContent: 'normal',
+ };
+ case 'outline-warn':
+ return {
+ background: fade(theme.colors.warning.main, 0.1),
+ border: `${theme.borders[2]} ${theme.colors.warning.main}`,
borderRadius: `${theme.radii[3]}px`,
boxShadow: 'none',
justifyContent: 'normal',
@@ -100,6 +108,7 @@ Alert.propTypes = {
'success',
'outline-danger',
'outline-info',
+ 'outline-warn',
]),
...color.propTypes,
...space.propTypes,
@@ -121,3 +130,4 @@ export const OutlineDanger = props => (
);
export const OutlineInfo = props => ;
+export const OutlineWarn = props => ;
diff --git a/web/packages/design/src/Alert/Alert.story.js b/web/packages/design/src/Alert/Alert.story.js
index a62d8ff16e9bc..6818f0c0bfc9f 100644
--- a/web/packages/design/src/Alert/Alert.story.js
+++ b/web/packages/design/src/Alert/Alert.story.js
@@ -33,5 +33,7 @@ export const Alerts = () => (
Some informational message
This is success
Text align it yourself
+ Text align it yourself
+ Text align it yourself
);
diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx
index 94e5010b2a695..69ee1c3647860 100644
--- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx
+++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabase.tsx
@@ -44,6 +44,7 @@ import {
createDiscoveryConfig,
} from 'teleport/services/discovery';
import useTeleport from 'teleport/useTeleport';
+import { splitAwsIamArn } from 'teleport/services/integrations/aws';
import {
AutoEnrollDialog,
@@ -221,13 +222,11 @@ export function EnrollRdsDatabase() {
if (!requiredVpcsAndSubnets) {
try {
const { spec, name: integrationName } = agentMeta.awsIntegration;
- const accountId = spec.roleArn
- .split('arn:aws:iam::')[1]
- .substring(0, 12);
+ const { awsAccountId } = splitAwsIamArn(spec.roleArn);
requiredVpcsAndSubnets =
await integrationService.fetchAwsRdsRequiredVpcs(integrationName, {
region: tableData.currRegion,
- accountId,
+ accountId: awsAccountId,
});
setRequiredVpcs(requiredVpcsAndSubnets);
diff --git a/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.test.tsx b/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.test.tsx
index 42e376d9ed322..7339646b588af 100644
--- a/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.test.tsx
+++ b/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.test.tsx
@@ -15,7 +15,8 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
-import { render, screen, fireEvent } from 'design/utils/testing';
+import { render, screen, fireEvent, waitFor } from 'design/utils/testing';
+import userEvent from '@testing-library/user-event';
import {
Integration,
@@ -25,7 +26,7 @@ import {
import { EditAwsOidcIntegrationDialog } from './EditAwsOidcIntegrationDialog';
-test('edit without s3 fields', async () => {
+test('user acknowledging script was ran when s3 bucket fields are edited', async () => {
render(
null}
@@ -36,7 +37,7 @@ test('edit without s3 fields', async () => {
name: 'some-integration-name',
spec: {
roleArn: 'arn:aws:iam::123456789012:role/johndoe',
- issuerS3Bucket: '',
+ issuerS3Bucket: 'test-value',
issuerS3Prefix: '',
},
statusCode: IntegrationStatusCode.Running,
@@ -45,18 +46,29 @@ test('edit without s3 fields', async () => {
);
// Initial state.
- expect(screen.getByText(/required/i)).toBeInTheDocument();
expect(screen.queryByTestId('scriptbox')).not.toBeInTheDocument();
expect(screen.queryByTestId('checkbox')).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: /generate command/i })
+ ).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
- // Click on generate command:
+ // Fill in the s3 prefix field.
+ fireEvent.change(screen.getByPlaceholderText(/prefix/i), {
+ target: { value: 'test-value' },
+ });
+ await waitFor(() =>
+ expect(
+ screen.getByRole('button', { name: /generate command/i })
+ ).toBeEnabled()
+ );
+ // When clicking on generate command:
// - script rendered
// - checkbox to confirm user has ran command
// - edit button replaces generate command button
// - save button still disabled
- fireEvent.click(screen.getByRole('button', { name: /generate command/i }));
- screen.getByRole('button', { name: /edit/i });
+ userEvent.click(screen.getByRole('button', { name: /generate command/i }));
+ await screen.findByRole('button', { name: /edit/i });
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
expect(
screen.queryByRole('button', { name: /generate command/i })
@@ -65,32 +77,48 @@ test('edit without s3 fields', async () => {
expect(screen.getByTestId('scriptbox')).toBeInTheDocument();
// Click on checkbox should enable save button and disable edit button.
- fireEvent.click(screen.getByRole('checkbox'));
- expect(screen.getByRole('button', { name: /save/i })).toBeEnabled();
+ userEvent.click(screen.getByRole('checkbox'));
+ await waitFor(() =>
+ expect(screen.getByRole('button', { name: /save/i })).toBeEnabled()
+ );
expect(screen.getByRole('button', { name: /edit/i })).toBeDisabled();
// Unchecking the checkbox should disable save button.
- fireEvent.click(screen.getByRole('checkbox'));
- expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
+ userEvent.click(screen.getByRole('checkbox'));
+ await waitFor(() =>
+ expect(screen.getByRole('button', { name: /save/i })).toBeDisabled()
+ );
// Click on edit, should replace it with generate command
- fireEvent.click(screen.getByRole('button', { name: /edit/i }));
- expect(
- screen.getByRole('button', { name: /generate command/i })
- ).toBeEnabled();
+ userEvent.click(screen.getByRole('button', { name: /edit/i }));
+ await waitFor(() =>
+ expect(
+ screen.getByRole('button', { name: /generate command/i })
+ ).toBeEnabled()
+ );
});
-test('edit with s3 fields', async () => {
+test('render warning on save when leaving s3 fields empty', async () => {
+ const edit = jest.fn(() => Promise.resolve());
render(
null}
- edit={() => null}
- integration={integration}
+ edit={edit}
+ integration={{
+ resourceType: 'integration',
+ kind: IntegrationKind.AwsOidc,
+ name: 'some-integration-name',
+ spec: {
+ roleArn: 'arn:aws:iam::123456789012:role/johndoe',
+ issuerS3Bucket: '',
+ issuerS3Prefix: '',
+ },
+ statusCode: IntegrationStatusCode.Running,
+ }}
/>
);
// Initial state.
- expect(screen.queryByText(/required/i)).not.toBeInTheDocument();
expect(screen.queryByTestId('scriptbox')).not.toBeInTheDocument();
expect(screen.queryByTestId('checkbox')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
@@ -98,21 +126,104 @@ test('edit with s3 fields', async () => {
screen.queryByRole('button', { name: /generate command/i })
).not.toBeInTheDocument();
- // Changing role arn should not render generate command.
+ // Enable the generate command button by changing a field.
fireEvent.change(screen.getByPlaceholderText(/arn:aws:iam:/i), {
- target: { value: 'something else' },
+ target: { value: 'arn:aws:iam::123456789012:role/someonelse' },
});
- expect(screen.getByRole('button', { name: /save/i })).toBeEnabled();
+ await waitFor(() =>
+ expect(
+ screen.getByRole('button', { name: /generate command/i })
+ ).toBeEnabled()
+ );
+
+ expect(screen.queryByTestId('checkbox')).not.toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
+
+ userEvent.click(screen.getByRole('button', { name: /generate command/i }));
+ await screen.findByRole('button', { name: /edit/i });
+ expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
+
+ userEvent.click(screen.getByTestId('checkbox'));
+ await waitFor(() =>
+ expect(screen.getByRole('button', { name: /save/i })).toBeEnabled()
+ );
+
+ // Clicking on save without defining s3 fields, should render
+ // a warning.
+ userEvent.click(screen.getByRole('button', { name: /save/i }));
+ await screen.findByText(/recommended to use an S3 bucket/i);
+ expect(edit).not.toHaveBeenCalled();
+
+ // Canceling and saving should re-render the warning.
+ userEvent.click(screen.getByRole('button', { name: /cancel/i }));
+ await screen.findByRole('button', { name: /save/i });
+
+ userEvent.click(screen.getByRole('button', { name: /save/i }));
+ await screen.findByText(/recommended to use an S3 bucket/i);
+
+ userEvent.click(screen.getByRole('button', { name: /continue/i }));
+ await waitFor(() => expect(edit).toHaveBeenCalledTimes(1));
+});
+
+test('render warning on save when deleting existing s3 fields', async () => {
+ const edit = jest.fn(() => Promise.resolve());
+ render(
+ null}
+ edit={edit}
+ integration={{
+ resourceType: 'integration',
+ kind: IntegrationKind.AwsOidc,
+ name: 'some-integration-name',
+ spec: {
+ roleArn: 'arn:aws:iam::123456789012:role/johndoe',
+ issuerS3Bucket: 'delete-me',
+ issuerS3Prefix: 'delete-me',
+ },
+ statusCode: IntegrationStatusCode.Running,
+ }}
+ />
+ );
+
expect(
screen.queryByRole('button', { name: /generate command/i })
).not.toBeInTheDocument();
- // Changing the s3 fields should render generate command.
+ // Delete the s3 fields.
fireEvent.change(screen.getByPlaceholderText(/bucket/i), {
- target: { value: 's3-bucket-something' },
+ target: { value: '' },
});
- fireEvent.click(screen.getByRole('button', { name: /generate command/i }));
+ fireEvent.change(screen.getByPlaceholderText(/prefix/i), {
+ target: { value: '' },
+ });
+ await waitFor(() =>
+ expect(
+ screen.getByRole('button', { name: /generate command/i })
+ ).toBeEnabled()
+ );
+
+ expect(screen.queryByTestId('checkbox')).not.toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
+
+ userEvent.click(screen.getByRole('button', { name: /generate command/i }));
+ await screen.findByRole('button', { name: /edit/i });
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
+
+ userEvent.click(screen.getByTestId('checkbox'));
+ await waitFor(() =>
+ expect(screen.getByRole('button', { name: /save/i })).toBeEnabled()
+ );
+
+ // Test for warning render.
+ userEvent.click(screen.getByRole('button', { name: /save/i }));
+ await screen.findByText(/recommended to use an S3 bucket/i);
+ expect(edit).not.toHaveBeenCalled();
+ expect(
+ screen.getByText(/recommended to use an S3 bucket/i)
+ ).toBeInTheDocument();
+
+ userEvent.click(screen.getByRole('button', { name: /continue/i }));
+ await waitFor(() => expect(edit).toHaveBeenCalledTimes(1));
});
test('edit invalid fields', async () => {
@@ -131,22 +242,18 @@ test('edit invalid fields', async () => {
target: { value: 'role something else' },
});
- fireEvent.click(screen.getByRole('button', { name: /save/i }));
- expect(screen.getByText(/invalid role ARN format/i)).toBeInTheDocument();
+ await waitFor(() =>
+ expect(
+ screen.getByRole('button', { name: /generate command/i })
+ ).toBeEnabled()
+ );
- // invalid s3 fields
- fireEvent.change(screen.getByPlaceholderText(/bucket/i), {
- target: { value: '' },
- });
- fireEvent.change(screen.getByPlaceholderText(/prefix/i), {
- target: { value: '' },
- });
- fireEvent.click(screen.getByRole('button', { name: /generate command/i }));
- expect(screen.queryAllByText(/required/i)).toHaveLength(2);
+ userEvent.click(screen.getByRole('button', { name: /generate command/i }));
+ await screen.findByText(/invalid role ARN format/i);
});
-test('edit submit', async () => {
- const mockEditFn = jest.fn();
+test('edit submit called with proper fields', async () => {
+ const mockEditFn = jest.fn(() => Promise.resolve());
render(
null}
@@ -170,9 +277,21 @@ test('edit submit', async () => {
target: { value: 'other-prefix' },
});
- fireEvent.click(screen.getByRole('button', { name: /generate command/i }));
- fireEvent.click(screen.getByRole('checkbox'));
- fireEvent.click(screen.getByRole('button', { name: /save/i }));
+ await waitFor(() =>
+ expect(
+ screen.getByRole('button', { name: /generate command/i })
+ ).toBeEnabled()
+ );
+
+ userEvent.click(screen.getByRole('button', { name: /generate command/i }));
+ await screen.findByRole('button', { name: /edit/i });
+
+ userEvent.click(screen.getByTestId('checkbox'));
+ await waitFor(() =>
+ expect(screen.getByRole('button', { name: /save/i })).toBeEnabled()
+ );
+ userEvent.click(screen.getByRole('button', { name: /save/i }));
+ await waitFor(() => expect(mockEditFn).toHaveBeenCalledTimes(1));
expect(mockEditFn).toHaveBeenCalledWith({
roleArn: 'arn:aws:iam::123456789011:role/other',
diff --git a/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx b/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx
index 515c3a7cf982f..d1ced2fea7b6a 100644
--- a/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx
+++ b/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx
@@ -25,7 +25,6 @@ import {
Alert,
Text,
Box,
- Flex,
Link,
} from 'design';
import Dialog, {
@@ -38,19 +37,16 @@ import useAttempt from 'shared/hooks/useAttemptNext';
import FieldInput from 'shared/components/FieldInput';
import Validation, { Validator } from 'shared/components/Validation';
import { requiredRoleArn } from 'shared/components/Validation/rules';
-import { ToolTipInfo } from 'shared/components/ToolTip';
import { CheckboxInput } from 'design/Checkbox';
import { TextSelectCopyMulti } from 'shared/components/TextSelectCopy';
import { Integration } from 'teleport/services/integrations';
import cfg from 'teleport/config';
+import { splitAwsIamArn } from 'teleport/services/integrations/aws';
import { EditableIntegrationFields } from './Operations/useIntegrationOperation';
import { S3BucketConfiguration } from './Enroll/AwsOidc/S3BucketConfiguration';
-import {
- getDefaultS3BucketName,
- getDefaultS3PrefixName,
-} from './Enroll/AwsOidc/Shared/utils';
+import { S3BucketWarningBanner } from './Enroll/AwsOidc/S3BucketWarningBanner';
type Props = {
close(): void;
@@ -62,14 +58,13 @@ export function EditAwsOidcIntegrationDialog(props: Props) {
const { close, edit, integration } = props;
const { attempt, run } = useAttempt();
+ const [showS3BucketWarning, setShowS3BucketWarning] = useState(false);
const [roleArn, setRoleArn] = useState(integration.spec.roleArn);
const [s3Bucket, setS3Bucket] = useState(
- () => integration.spec.issuerS3Bucket || getDefaultS3BucketName()
+ () => integration.spec.issuerS3Bucket
);
const [s3Prefix, setS3Prefix] = useState(
- () =>
- integration.spec.issuerS3Prefix ||
- getDefaultS3PrefixName(integration.spec.roleArn.split(':role/')[1])
+ () => integration.spec.issuerS3Prefix
);
const [scriptUrl, setScriptUrl] = useState('');
@@ -90,10 +85,12 @@ export function EditAwsOidcIntegrationDialog(props: Props) {
validator.reset();
- const roleName = roleArn.split(':role/')[1];
+ const { arnResourceName } = splitAwsIamArn(
+ roleArn || props.integration.spec.roleArn
+ );
const newScriptUrl = cfg.getAwsOidcConfigureIdpScriptUrl({
integrationName: integration.name,
- roleName,
+ roleName: arnResourceName,
s3Bucket: s3Bucket,
s3Prefix: s3Prefix,
});
@@ -102,17 +99,29 @@ export function EditAwsOidcIntegrationDialog(props: Props) {
}
const isProcessing = attempt.status === 'processing';
- const requiresS3 =
- !integration.spec.issuerS3Bucket || !integration.spec.issuerS3Prefix;
+ const requiresS3BucketWarning = !s3Bucket && !s3Prefix;
const showGenerateCommand =
- requiresS3 ||
integration.spec.issuerS3Bucket !== s3Bucket ||
- integration.spec.issuerS3Prefix !== s3Prefix;
+ integration.spec.issuerS3Prefix !== s3Prefix ||
+ integration.spec.roleArn !== roleArn;
+
+ const changeDetected =
+ integration.spec.issuerS3Bucket !== s3Bucket ||
+ integration.spec.issuerS3Prefix !== s3Prefix ||
+ integration.spec.roleArn !== roleArn;
return (
{({ validator }) => (
-