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';