diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.tsx
index 26cbbd77acc18..0b9f82b4c2779 100644
--- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.tsx
+++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.tsx
@@ -16,114 +16,40 @@
* along with this program. If not, see .
*/
-import React, { useEffect, useState } from 'react';
-import { Link as InternalRouteLink } from 'react-router-dom';
-import { useLocation } from 'react-router';
-import styled from 'styled-components';
-import { Box, ButtonSecondary, Text, Link, Flex, ButtonPrimary } from 'design';
+import { Box, ButtonPrimary, ButtonSecondary, Flex, Link, Text } from 'design';
import * as Icons from 'design/Icon';
+import { Link as InternalRouteLink } from 'react-router-dom';
import FieldInput from 'shared/components/FieldInput';
+import Validation from 'shared/components/Validation';
import { requiredIamRoleName } from 'shared/components/Validation/rules';
-import Validation, { Validator } from 'shared/components/Validation';
-import useAttempt from 'shared/hooks/useAttemptNext';
+import styled from 'styled-components';
-import {
- IntegrationEnrollEvent,
- IntegrationEnrollEventData,
- IntegrationEnrollKind,
- userEventService,
-} from 'teleport/services/userEvent';
+import { TextSelectCopyMulti } from 'teleport/components/TextSelectCopy';
+import cfg from 'teleport/config';
import { Header } from 'teleport/Discover/Shared';
+import {
+ ShowConfigurationScript,
+ RoleArnInput,
+} from 'teleport/Integrations/shared';
import { AWS_RESOURCE_GROUPS_TAG_EDITOR_LINK } from 'teleport/Discover/Shared/const';
-import { DiscoverUrlLocationState } from 'teleport/Discover/useDiscover';
-import { TextSelectCopyMulti } from 'teleport/components/TextSelectCopy';
import useStickyClusterId from 'teleport/useStickyClusterId';
-import {
- Integration,
- IntegrationKind,
- integrationService,
-} from 'teleport/services/integrations';
-import cfg from 'teleport/config';
-
import { FinishDialog } from './FinishDialog';
+import { useAwsOidcIntegration } from './useAwsOidcIntegration';
export function AwsOidc() {
- const [integrationName, setIntegrationName] = useState('');
- const [roleArn, setRoleArn] = useState('');
- const [roleName, setRoleName] = useState('');
- const [scriptUrl, setScriptUrl] = useState('');
- const [createdIntegration, setCreatedIntegration] = useState();
- const { attempt, run } = useAttempt('');
-
+ const {
+ integrationConfig,
+ setIntegrationConfig,
+ scriptUrl,
+ setScriptUrl,
+ handleOnCreate,
+ createdIntegration,
+ attempt,
+ generateAwsOidcConfigIdpScript,
+ } = useAwsOidcIntegration();
const { clusterId } = useStickyClusterId();
- const location = useLocation();
-
- const [eventData] = useState({
- id: crypto.randomUUID(),
- kind: IntegrationEnrollKind.AwsOidc,
- });
-
- useEffect(() => {
- // If a user came from the discover wizard,
- // discover wizard will send of appropriate events.
- if (location.state?.discover) {
- return;
- }
-
- emitEvent(IntegrationEnrollEvent.Started);
- // Only send event once on init.
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- function handleOnCreate(validator: Validator) {
- if (!validator.validate()) {
- return;
- }
-
- run(() =>
- integrationService
- .createIntegration({
- name: integrationName,
- subKind: IntegrationKind.AwsOidc,
- awsoidc: {
- roleArn,
- },
- })
- .then(res => {
- setCreatedIntegration(res);
-
- if (location.state?.discover) {
- return;
- }
- emitEvent(IntegrationEnrollEvent.Complete);
- })
- );
- }
-
- function emitEvent(event: IntegrationEnrollEvent) {
- userEventService.captureIntegrationEnrollEvent({
- event,
- eventData,
- });
- }
-
- function generateAwsOidcConfigIdpScript(validator: Validator) {
- if (!validator.validate()) {
- return;
- }
-
- validator.reset();
-
- const newScriptUrl = cfg.getAwsOidcConfigureIdpScriptUrl({
- integrationName,
- roleName,
- });
-
- setScriptUrl(newScriptUrl);
- }
-
return (
Set up your AWS account
@@ -168,7 +94,7 @@ export function AwsOidc() {
`\n` +
`teleport.dev/origin: integration_awsoidc\n` +
`teleport.dev/integration: ` +
- integrationName,
+ integrationConfig.name,
},
]}
/>
@@ -178,23 +104,33 @@ export function AwsOidc() {
{({ validator }) => (
<>
-
+ Step 1 setIntegrationName(e.target.value)}
+ onChange={e =>
+ setIntegrationConfig({
+ ...integrationConfig,
+ name: e.target.value,
+ })
+ }
disabled={!!scriptUrl}
/>
setRoleName(e.target.value)}
+ onChange={e =>
+ setIntegrationConfig({
+ ...integrationConfig,
+ roleName: e.target.value,
+ })
+ }
disabled={!!scriptUrl}
/>
@@ -218,54 +154,27 @@ export function AwsOidc() {
{scriptUrl && (
<>
-
+ Step 2
-
- Open{' '}
-
- AWS CloudShell
- {' '}
- and copy and paste the command that configures the
- permissions for you:
-
-
-
-
+
-
+ Step 3
- After configuring is finished, go to your{' '}
-
- IAM Role dashboard
- {' '}
- and copy and paste the ARN below.
- setRoleArn(e.target.value)}
+
+ setIntegrationConfig({
+ ...integrationConfig,
+ roleArn: v,
+ })
+ }
disabled={attempt.status === 'processing'}
- toolTipContent={`Unique AWS resource identifier and uses the format: arn:aws:iam:::role/`}
/>
>
)}
- {attempt.status === 'failed' && (
+ {attempt.status === 'error' && (
Error: {attempt.statusText}
@@ -275,7 +184,9 @@ export function AwsOidc() {
handleOnCreate(validator)}
disabled={
- !scriptUrl || attempt.status === 'processing' || !roleArn
+ !scriptUrl ||
+ attempt.status === 'processing' ||
+ !integrationConfig.roleArn
}
>
Create Integration
@@ -303,24 +214,6 @@ const Container = styled(Box)`
padding: ${p => p.theme.space[3]}px;
`;
-const requiredRoleArn = (roleName: string) => (roleArn: string) => () => {
- const regex = new RegExp(
- '^arn:aws.*:iam::\\d{12}:role\\/(' + roleName + ')$'
- );
-
- if (regex.test(roleArn)) {
- return {
- valid: true,
- };
- }
-
- return {
- valid: false,
- message:
- 'invalid role ARN, double check you copied and pasted the correct output',
- };
-};
-
const RouteLink = styled(InternalRouteLink)`
color: ${({ theme }) => theme.colors.buttons.link.default};
diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/useAwsOidcIntegration.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/useAwsOidcIntegration.tsx
new file mode 100644
index 0000000000000..c5fc5156ac6dd
--- /dev/null
+++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/useAwsOidcIntegration.tsx
@@ -0,0 +1,138 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { useEffect, useState } from 'react';
+import { useLocation } from 'react-router';
+import { Validator } from 'shared/components/Validation';
+import { useAsync } from 'shared/hooks/useAsync';
+
+import { DiscoverUrlLocationState } from 'teleport/Discover/useDiscover';
+import {
+ IntegrationEnrollEvent,
+ IntegrationEnrollEventData,
+ IntegrationEnrollKind,
+ userEventService,
+} from 'teleport/services/userEvent';
+import cfg from 'teleport/config';
+import {
+ Integration,
+ IntegrationCreateRequest,
+ IntegrationKind,
+ integrationService,
+} from 'teleport/services/integrations';
+
+type integrationConfig = {
+ name: string;
+ roleName: string;
+ roleArn: string;
+};
+
+export function useAwsOidcIntegration() {
+ const [integrationConfig, setIntegrationConfig] = useState(
+ {
+ name: '',
+ roleName: '',
+ roleArn: '',
+ }
+ );
+ const [scriptUrl, setScriptUrl] = useState('');
+ const [createdIntegration, setCreatedIntegration] = useState();
+
+ const location = useLocation();
+
+ const [eventData] = useState({
+ id: crypto.randomUUID(),
+ kind: IntegrationEnrollKind.AwsOidc,
+ });
+
+ useEffect(() => {
+ // If a user came from the discover wizard,
+ // discover wizard will send of appropriate events.
+ if (location.state?.discover) {
+ return;
+ }
+
+ emitEvent(IntegrationEnrollEvent.Started);
+ // Only send event once on init.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ function emitEvent(event: IntegrationEnrollEvent) {
+ userEventService.captureIntegrationEnrollEvent({
+ event,
+ eventData,
+ });
+ }
+
+ const [createIntegrationAttempt, runCreateIntegration] = useAsync(
+ async (req: IntegrationCreateRequest) => {
+ const resp = await integrationService.createIntegration(req);
+ setCreatedIntegration(resp);
+ return resp;
+ }
+ );
+
+ async function handleOnCreate(validator: Validator) {
+ if (!validator.validate()) {
+ return;
+ }
+
+ const [, err] = await runCreateIntegration({
+ name: integrationConfig.name,
+ subKind: IntegrationKind.AwsOidc,
+ awsoidc: {
+ roleArn: integrationConfig.roleArn,
+ },
+ });
+ if (err) {
+ return;
+ }
+
+ if (location.state?.discover) {
+ return;
+ }
+ emitEvent(IntegrationEnrollEvent.Complete);
+ }
+
+ function generateAwsOidcConfigIdpScript(validator: Validator) {
+ if (!validator.validate()) {
+ return;
+ }
+
+ validator.reset();
+
+ const newScriptUrl = cfg.getAwsOidcConfigureIdpScriptUrl({
+ integrationName: integrationConfig.name,
+ roleName: integrationConfig.roleName,
+ });
+
+ setScriptUrl(newScriptUrl);
+ }
+
+ return {
+ integrationConfig,
+ setIntegrationConfig,
+ scriptUrl,
+ setScriptUrl,
+ createdIntegration,
+ handleOnCreate,
+ runCreateIntegration,
+ generateAwsOidcConfigIdpScript,
+ attempt: createIntegrationAttempt,
+ };
+}
diff --git a/web/packages/teleport/src/Integrations/shared/RoleArnInput.story.tsx b/web/packages/teleport/src/Integrations/shared/RoleArnInput.story.tsx
new file mode 100644
index 0000000000000..7807e8d14ad5c
--- /dev/null
+++ b/web/packages/teleport/src/Integrations/shared/RoleArnInput.story.tsx
@@ -0,0 +1,93 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { useState } from 'react';
+import { ButtonSecondary } from 'design/Button';
+import Validation from 'shared/components/Validation';
+
+import { StyledBox } from 'teleport/Discover/Shared';
+
+import { RoleArnInput } from './RoleArnInput';
+
+export default {
+ title: 'Teleport/Integrations/Shared/AwsOidc/RoleArnInput',
+};
+
+export const Enabled = () => {
+ const [roleArn, setRoleArn] = useState('');
+ return (
+
+
+
+
+
+ );
+};
+
+export const Disabled = () => {
+ const [roleArn, setRoleArn] = useState(
+ 'arn:aws:iam::1234567890:role/test-role'
+ );
+ return (
+
+
+
+
+
+ );
+};
+
+export const Error = () => {
+ const [roleArn, setRoleArn] = useState('');
+ return (
+
+ {({ validator }) => (
+ <>
+
+
+
+ {
+ if (!validator.validate()) {
+ return;
+ }
+ }}
+ >
+ Test Validation
+
+ >
+ )}
+
+ );
+};
diff --git a/web/packages/teleport/src/Integrations/shared/RoleArnInput.tsx b/web/packages/teleport/src/Integrations/shared/RoleArnInput.tsx
new file mode 100644
index 0000000000000..0d67a35df4620
--- /dev/null
+++ b/web/packages/teleport/src/Integrations/shared/RoleArnInput.tsx
@@ -0,0 +1,82 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { Link, Text } from 'design';
+import FieldInput from 'shared/components/FieldInput';
+
+export function RoleArnInput({
+ description,
+ roleName,
+ roleArn,
+ setRoleArn,
+ disabled,
+}: {
+ description?: React.ReactNode;
+ roleName: string;
+ roleArn: string;
+ setRoleArn: (arn: string) => void;
+ disabled: boolean;
+}) {
+ return (
+ <>
+ {description || (
+
+ Once Teleport completes setting up OIDC identity provider and creating
+ a role named "{roleName}" in AWS cloud shell (step 2), go to your{' '}
+
+ IAM Role dashboard
+ {' '}
+ and copy and paste the role ARN below. Teleport will use this role to
+ identity itself to AWS.
+
+ )}
+ setRoleArn(e.target.value)}
+ disabled={disabled}
+ toolTipContent={`Unique AWS resource identifier and uses the format: arn:aws:iam:::role/`}
+ />
+ >
+ );
+}
+
+const requiredRoleArn = (roleName: string) => (roleArn: string) => () => {
+ const regex = new RegExp(
+ '^arn:aws.*:iam::\\d{12}:role\\/(' + roleName + ')$'
+ );
+
+ if (regex.test(roleArn)) {
+ return {
+ valid: true,
+ };
+ }
+
+ return {
+ valid: false,
+ message:
+ 'invalid role ARN, double check you copied and pasted the correct output',
+ };
+};
diff --git a/web/packages/teleport/src/Integrations/shared/ShowConfigurationScript.story.tsx b/web/packages/teleport/src/Integrations/shared/ShowConfigurationScript.story.tsx
new file mode 100644
index 0000000000000..f2e11d1c52afe
--- /dev/null
+++ b/web/packages/teleport/src/Integrations/shared/ShowConfigurationScript.story.tsx
@@ -0,0 +1,48 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { Text } from 'design';
+
+import { StyledBox } from 'teleport/Discover/Shared';
+
+import { ShowConfigurationScript } from './ShowConfigurationScript';
+
+export default {
+ title: 'Teleport/Integrations/Shared/AwsOidc/ShowConfigurationScript',
+};
+
+export const Enabled = () => {
+ return (
+
+
+
+ );
+};
+
+export const CustomDescription = () => {
+ const description = Custom description;
+
+ return (
+
+
+
+ );
+};
diff --git a/web/packages/teleport/src/Integrations/shared/ShowConfigurationScript.tsx b/web/packages/teleport/src/Integrations/shared/ShowConfigurationScript.tsx
new file mode 100644
index 0000000000000..8a8408fb672bc
--- /dev/null
+++ b/web/packages/teleport/src/Integrations/shared/ShowConfigurationScript.tsx
@@ -0,0 +1,56 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { Box, Link, Text } from 'design';
+
+import { TextSelectCopyMulti } from 'teleport/components/TextSelectCopy';
+
+export function ShowConfigurationScript({
+ scriptUrl,
+ description,
+}: {
+ scriptUrl: string;
+ description?: React.ReactNode;
+}) {
+ return (
+ <>
+ {description || (
+
+ Open{' '}
+
+ AWS CloudShell
+ {' '}
+ and copy and paste the command that configures the permissions for
+ you:
+
+ )}
+
+
+
+ >
+ );
+}
diff --git a/web/packages/teleport/src/Integrations/shared/index.ts b/web/packages/teleport/src/Integrations/shared/index.ts
new file mode 100644
index 0000000000000..d41606b55ba01
--- /dev/null
+++ b/web/packages/teleport/src/Integrations/shared/index.ts
@@ -0,0 +1,20 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+export { ShowConfigurationScript } from './ShowConfigurationScript';
+export { RoleArnInput } from './RoleArnInput';