diff --git a/lib/web/ui/app.go b/lib/web/ui/app.go index 80761b368a8..22e336a63d4 100644 --- a/lib/web/ui/app.go +++ b/lib/web/ui/app.go @@ -58,6 +58,9 @@ type App struct { SAMLApp bool `json:"samlApp,omitempty"` // RequireRequest indicates if a returned resource is only accessible after an access request RequiresRequest bool `json:"requiresRequest,omitempty"` + // Integration is the integration name that must be used to access this Application. + // Only applicable to AWS App Access. + Integration string `json:"integration,omitempty"` } // UserGroupAndDescription is a user group name and its description. @@ -135,6 +138,7 @@ func MakeApp(app types.Application, c MakeAppsConfig) App { UserGroups: userGroupAndDescriptions, SAMLApp: false, RequiresRequest: c.RequiresRequest, + Integration: app.GetIntegration(), } if app.IsAWSConsole() { diff --git a/web/packages/shared/components/Select/types.ts b/web/packages/shared/components/Select/types.ts index a622bafbb83..0220fe3dced 100644 --- a/web/packages/shared/components/Select/types.ts +++ b/web/packages/shared/components/Select/types.ts @@ -32,7 +32,7 @@ export type Props = { hideSelectedOptions?: boolean; controlShouldRenderValue?: boolean; maxMenuHeight?: number; - onChange(e: Option | Option[]): void; + onChange(e: Option | Option[], action?: ActionMeta): void; onKeyDown?(e: KeyboardEvent | React.KeyboardEvent): void; value: null | Option | Option[]; isMulti?: boolean; @@ -92,7 +92,7 @@ export type GroupOption = { }; export type ActionMeta = { - action: 'set-value' | 'input-change' | 'input-blur' | 'menu-close'; + action: 'set-value' | 'input-change' | 'input-blur' | 'menu-close' | 'clear'; }; /** diff --git a/web/packages/shared/components/Validation/rules.ts b/web/packages/shared/components/Validation/rules.ts index b3423979c34..84a78020007 100644 --- a/web/packages/shared/components/Validation/rules.ts +++ b/web/packages/shared/components/Validation/rules.ts @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import { IAM_ROLE_NAME_REGEX } from 'teleport/services/integrations/aws'; + /** * The result of validating a field. */ @@ -95,13 +97,6 @@ const requiredConfirmedPassword = }; }; -/** - * ROLE_ARN_REGEX uses the same regex matcher used in the backend: - * https://github.com/gravitational/teleport/blob/2cba82cb332e769ebc8a658d32ff24ddda79daff/api/utils/aws/identifiers.go#L43 - * - * The regex checks for alphanumerics and select few characters. - */ -const IAM_ROLE_NAME_REGEX = /^[\w+=,.@-]+$/; const isIamRoleNameValid = roleName => { return ( roleName && roleName.length <= 64 && roleName.match(IAM_ROLE_NAME_REGEX) diff --git a/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/AppCreatedDialog.tsx b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/AppCreatedDialog.tsx new file mode 100644 index 00000000000..ff53f39b67a --- /dev/null +++ b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/AppCreatedDialog.tsx @@ -0,0 +1,51 @@ +/** + * 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 React from 'react'; +import { Text, Flex, ButtonPrimary } from 'design'; +import * as Icons from 'design/Icon'; +import Dialog, { DialogContent } from 'design/DialogConfirmation'; + +export function AppCreatedDialog({ + toNextStep, + appName, +}: { + toNextStep: () => void; + appName: string; +}) { + return ( + + + + + + Successfully created an application server named "{appName}". + + + toNextStep()}> + Next + + + + ); +} diff --git a/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.story.tsx b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.story.tsx new file mode 100644 index 00000000000..eb3d36105bb --- /dev/null +++ b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.story.tsx @@ -0,0 +1,144 @@ +/** + * 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 React from 'react'; +import { MemoryRouter } from 'react-router'; +import { initialize, mswLoader } from 'msw-storybook-addon'; +import { rest } from 'msw'; +import { Info } from 'design/Alert'; + +import { ContextProvider } from 'teleport'; +import cfg from 'teleport/config'; +import { createTeleportContext } from 'teleport/mocks/contexts'; +import { + DiscoverProvider, + DiscoverContextState, +} from 'teleport/Discover/useDiscover'; +import { + IntegrationKind, + IntegrationStatusCode, +} from 'teleport/services/integrations'; +import { ResourceKind } from 'teleport/Discover/Shared'; +import { DiscoverEventResource } from 'teleport/services/userEvent'; + +import { CreateAppAccess } from './CreateAppAccess'; + +initialize(); + +export default { + title: 'Teleport/Discover/Application/AwsConsole/CreateApp', + loaders: [mswLoader], +}; + +export const Success = () => ; +Success.parameters = { + msw: { + handlers: [ + rest.post(cfg.api.awsAppAccessPath, (req, res, ctx) => + res(ctx.json({ name: 'app-1' })) + ), + ], + }, +}; + +export const Loading = () => { + cfg.isCloud = true; + return ; +}; +Loading.parameters = { + msw: { + handlers: [ + rest.post(cfg.api.awsAppAccessPath, (req, res, ctx) => + res(ctx.delay('infinite')) + ), + ], + }, +}; + +export const Failed = () => ; +Failed.parameters = { + msw: { + handlers: [ + rest.post(cfg.api.awsAppAccessPath, (req, res, ctx) => + res( + ctx.status(403), + ctx.json({ message: 'Some kind of error message' }) + ) + ), + ], + }, +}; + +const Component = () => { + const ctx = createTeleportContext(); + const discoverCtx: DiscoverContextState = { + agentMeta: { + resourceName: 'aws-console', + agentMatcherLabels: [], + awsIntegration: { + kind: IntegrationKind.AwsOidc, + name: 'some-oidc-name', + resourceType: 'integration', + spec: { + roleArn: 'arn:aws:iam::123456789012:role/test-role-arn', + issuerS3Bucket: '', + issuerS3Prefix: '', + }, + statusCode: IntegrationStatusCode.Running, + }, + }, + currentStep: 0, + nextStep: () => null, + prevStep: () => null, + onSelectResource: () => null, + resourceSpec: { + name: '', + kind: ResourceKind.Application, + icon: null, + keywords: '', + event: DiscoverEventResource.ApplicationHttp, + appMeta: { + awsConsole: true, + }, + }, + exitFlow: () => null, + viewConfig: null, + indexedViews: [], + setResourceSpec: () => null, + updateAgentMeta: () => null, + emitErrorEvent: () => null, + emitEvent: () => null, + eventState: null, + }; + + cfg.proxyCluster = 'localhost'; + return ( + + + + Devs: Click next to see next state + + + + + ); +}; diff --git a/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.test.tsx b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.test.tsx new file mode 100644 index 00000000000..fd5c85926de --- /dev/null +++ b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.test.tsx @@ -0,0 +1,135 @@ +/** + * 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 React from 'react'; +import { MemoryRouter } from 'react-router'; +import { render, screen, userEvent } from 'design/utils/testing'; + +import { ContextProvider } from 'teleport'; +import { + IntegrationKind, + IntegrationStatusCode, + integrationService, +} from 'teleport/services/integrations'; +import { createTeleportContext } from 'teleport/mocks/contexts'; +import cfg from 'teleport/config'; +import TeleportContext from 'teleport/teleportContext'; +import { + DiscoverContextState, + DiscoverProvider, +} from 'teleport/Discover/useDiscover'; +import { FeaturesContextProvider } from 'teleport/FeaturesContext'; + +import { + DiscoverEventResource, + userEventService, +} from 'teleport/services/userEvent'; +import { app } from 'teleport/Discover/AwsMangementConsole/fixtures'; +import { ResourceKind } from 'teleport/Discover/Shared'; + +import { CreateAppAccess } from './CreateAppAccess'; + +beforeEach(() => { + jest.spyOn(integrationService, 'createAwsAppAccess').mockResolvedValue(app); + jest + .spyOn(userEventService, 'captureDiscoverEvent') + .mockResolvedValue(undefined as never); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +test('create app access', async () => { + const { ctx, discoverCtx } = getMockedContexts(); + + renderCreateAppAccess(ctx, discoverCtx); + await screen.findByText(/bash/i); + + userEvent.click(screen.getByRole('button', { name: /next/i })); + await screen.findByText(/aws-console/i); + expect(integrationService.createAwsAppAccess).toHaveBeenCalledTimes(1); +}); + +function getMockedContexts() { + const ctx = createTeleportContext(); + const discoverCtx: DiscoverContextState = { + agentMeta: { + resourceName: 'aws-console', + agentMatcherLabels: [], + awsIntegration: { + kind: IntegrationKind.AwsOidc, + name: 'some-oidc-name', + resourceType: 'integration', + spec: { + roleArn: 'arn:aws:iam::123456789012:role/test-role-arn', + issuerS3Bucket: '', + issuerS3Prefix: '', + }, + statusCode: IntegrationStatusCode.Running, + }, + }, + currentStep: 0, + nextStep: jest.fn(), + prevStep: () => null, + onSelectResource: () => null, + resourceSpec: { + kind: ResourceKind.Application, + appMeta: { awsConsole: true }, + name: '', + icon: undefined, + keywords: '', + event: DiscoverEventResource.ApplicationHttp, + }, + exitFlow: () => null, + viewConfig: null, + indexedViews: [], + setResourceSpec: () => null, + updateAgentMeta: () => null, + emitErrorEvent: () => null, + emitEvent: jest.fn(), + eventState: null, + }; + + jest + .spyOn(userEventService, 'captureDiscoverEvent') + .mockResolvedValue(undefined as never); + + return { ctx, discoverCtx }; +} + +function renderCreateAppAccess( + ctx: TeleportContext, + discoverCtx: DiscoverContextState +) { + return render( + + + + + + + + + + ); +} diff --git a/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx new file mode 100644 index 00000000000..497b448306c --- /dev/null +++ b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx @@ -0,0 +1,148 @@ +/** + * 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 React from 'react'; +import styled from 'styled-components'; +import { Box, Text, Flex, Link } from 'design'; +import TextEditor from 'shared/components/TextEditor'; +import { Danger } from 'design/Alert'; +import { ToolTipInfo } from 'shared/components/ToolTip'; +import { useAsync } from 'shared/hooks/useAsync'; + +import { TextSelectCopyMulti } from 'teleport/components/TextSelectCopy'; +import { useDiscover } from 'teleport/Discover/useDiscover'; +import { integrationService } from 'teleport/services/integrations'; +import cfg from 'teleport/config'; +import { Container } from 'teleport/Discover/Shared/CommandBox'; +import { splitAwsIamArn } from 'teleport/services/integrations/aws'; + +import { ActionButtons, Header, Mark } from '../../Shared'; + +import { AppCreatedDialog } from './AppCreatedDialog'; + +const IAM_POLICY_NAME = 'AWSAppAccess'; + +export function CreateAppAccess() { + const { agentMeta, updateAgentMeta, emitErrorEvent, nextStep } = + useDiscover(); + const { awsIntegration } = agentMeta; + + const [attempt, createApp] = useAsync(async () => { + try { + const app = await integrationService.createAwsAppAccess( + awsIntegration.name + ); + updateAgentMeta({ + ...agentMeta, + app, + resourceName: app.name, + }); + } catch (err) { + emitErrorEvent(err.message); + throw err; + } + }); + + const iamRoleName = splitAwsIamArn( + agentMeta.awsIntegration.spec.roleArn + ).arnResourceName; + const scriptUrl = cfg.getAwsIamConfigureScriptAppAccessUrl({ + iamRoleName, + }); + + return ( + +
Enable Access to AWS with Teleport Application Access
+ + An application will be created that will use the selected AWS OIDC + Integration {agentMeta.awsIntegration.name} for proxying + access to AWS Management Console, AWS CLI, and AWS APIs. + + {attempt.status === 'error' && ( + {attempt.statusText} + )} + + + First configure your AWS IAM permissions + + The following IAM permissions will be added as an inline policy + named {IAM_POLICY_NAME} to IAM role{' '} + {iamRoleName} + + + + + + + + + Run the command below on your{' '} + + AWS CloudShell + {' '} + to configure your IAM permissions. + + + + + + {attempt.status === 'success' && ( + + )} +
+ ); +} + +const EditorWrapper = styled(Flex)` + flex-directions: column; + height: ${p => p.$height}px; + margin-top: ${p => p.theme.space[3]}px; + width: 450px; +`; + +const inlinePolicyJson = `{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AssumeTaggedRole", + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": "*", + "Condition": { + "StringEquals": {"iam:ResourceTag/teleport.dev/integration": "true"} + } + } + ] +}`; diff --git a/web/packages/teleport/src/Discover/AwsMangementConsole/SetupAccess/SetupAccess.story.tsx b/web/packages/teleport/src/Discover/AwsMangementConsole/SetupAccess/SetupAccess.story.tsx new file mode 100644 index 00000000000..9f35c5aba61 --- /dev/null +++ b/web/packages/teleport/src/Discover/AwsMangementConsole/SetupAccess/SetupAccess.story.tsx @@ -0,0 +1,216 @@ +/** + * 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 React from 'react'; +import { MemoryRouter } from 'react-router'; +import { initialize, mswLoader } from 'msw-storybook-addon'; +import { rest } from 'msw'; +import { AwsRole } from 'shared/services/apps'; + +import { ContextProvider } from 'teleport'; +import cfg from 'teleport/config'; +import { createTeleportContext, getAcl } from 'teleport/mocks/contexts'; +import { + DiscoverProvider, + DiscoverContextState, +} from 'teleport/Discover/useDiscover'; +import { + IntegrationKind, + IntegrationStatusCode, +} from 'teleport/services/integrations'; +import { ResourceKind } from 'teleport/Discover/Shared'; +import { DiscoverEventResource } from 'teleport/services/userEvent'; + +import { app } from '../fixtures'; + +import { SetupAccess } from './SetupAccess'; + +export default { + title: 'Teleport/Discover/Application/AwsConsole/SetupAccess', + loaders: [mswLoader], +}; + +initialize(); + +const awsRoles: AwsRole[] = [ + { + name: 'test1', + display: 'test1', + arn: 'arn:aws:iam::123456789012:role/test1', + accountId: '123456789012', + }, + { + name: 'test2', + display: 'test2', + arn: 'arn:aws:iam::123456789012:role/test2', + accountId: '123456789012', + }, +]; + +const defaultUserGet = rest.get(cfg.api.userWithUsernamePath, (req, res, ctx) => + res( + ctx.json({ + name: 'user-1', + roles: [], + authType: 'local', + isLocal: true, + traits: { + awsRoleArns: [], + }, + allTraits: {}, + }) + ) +); + +export const NoTraits = () => ( + + + + + +); +NoTraits.parameters = { + msw: { + handlers: [defaultUserGet], + }, +}; + +export const WithTraits = () => ( + + + + + +); +WithTraits.parameters = { + msw: { + handlers: [ + rest.get(cfg.api.userWithUsernamePath, (req, res, ctx) => + res( + ctx.json({ + name: 'user-1', + roles: [], + authType: 'local', + isLocal: true, + traits: { + awsRoleArns: ['arn:aws:iam::123456789012:role/dynamic1'], + }, + allTraits: {}, + }) + ) + ), + ], + }, +}; + +export const NoAccess = () => ( + + + + + +); +NoAccess.parameters = { + msw: { + handlers: [defaultUserGet], + }, +}; + +export const SsoUser = () => ( + + + + + +); +SsoUser.parameters = { + msw: { + handlers: [defaultUserGet], + }, +}; + +const Provider = ({ + children, + awsRoles, + noAccess = false, + isSso = false, +}: { + children: React.ReactNode; + awsRoles: AwsRole[]; + noAccess?: boolean; + isSso?: boolean; +}) => { + const ctx = createTeleportContext(); + if (noAccess) { + ctx.storeUser.state.acl = getAcl({ noAccess: true }); + } + if (isSso) { + ctx.storeUser.state.authType = 'sso'; + } + const discoverCtx: DiscoverContextState = { + prevStep: () => {}, + nextStep: () => {}, + agentMeta: { + app: { + ...app, + awsRoles, + }, + awsIntegration: { + resourceType: 'integration', + kind: IntegrationKind.AwsOidc, + name: 'some-aws-oidc-name', + statusCode: IntegrationStatusCode.Running, + spec: { + roleArn: 'arn:aws:iam::123456789012:role/some-iam-role-name', + issuerS3Bucket: '', + issuerS3Prefix: '', + }, + }, + }, + currentStep: 0, + onSelectResource: () => null, + resourceSpec: { + kind: ResourceKind.Application, + appMeta: { awsConsole: true }, + name: '', + icon: undefined, + keywords: '', + event: DiscoverEventResource.ApplicationHttp, + }, + exitFlow: () => null, + viewConfig: null, + indexedViews: [], + setResourceSpec: () => null, + updateAgentMeta: () => null, + emitErrorEvent: () => null, + emitEvent: () => null, + eventState: null, + }; + + return ( + + + {children} + + + ); +}; diff --git a/web/packages/teleport/src/Discover/AwsMangementConsole/SetupAccess/SetupAccess.tsx b/web/packages/teleport/src/Discover/AwsMangementConsole/SetupAccess/SetupAccess.tsx new file mode 100644 index 00000000000..c32128aca69 --- /dev/null +++ b/web/packages/teleport/src/Discover/AwsMangementConsole/SetupAccess/SetupAccess.tsx @@ -0,0 +1,178 @@ +/** + * 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 React, { useState, useEffect } from 'react'; +import { LabelInput, Text, Link } from 'design'; +import { useTheme } from 'styled-components'; +import { Cross } from 'design/Icon'; + +import { OutlineInfo } from 'design/Alert/Alert'; +import { FieldSelectCreatable } from 'shared/components/FieldSelect'; +import Validation, { Validator } from 'shared/components/Validation'; + +import { Option } from 'teleport/Discover/Shared/SelectCreatable'; +import { IAM_ROLE_ARN_REGEX } from 'teleport/services/integrations/aws'; +import { + useUserTraits, + SetupAccessWrapper, +} from 'teleport/Discover/Shared/SetupAccess'; +import { Mark } from 'teleport/Discover/Shared'; +import { styles } from 'teleport/Discover/Shared/SelectCreatable/SelectCreatable'; +import { AWS_TAG_INFO_LINK } from 'teleport/Discover/Shared/const'; + +export function SetupAccess() { + const { + onProceed, + initSelectedOptions, + getFixedOptions, + getSelectableOptions, + ...restOfProps + } = useUserTraits(); + + const theme = useTheme(); + + const [selectedArns, setSelectedArns] = useState([]); + const [inputValue, setInputValue] = useState(''); + + useEffect(() => { + if (restOfProps.attempt.status === 'success') { + setSelectedArns(initSelectedOptions('awsRoleArns')); + } + }, [restOfProps.attempt.status]); + + function handleKeyDown(event: React.KeyboardEvent, validator: Validator) { + if (!inputValue) return; + switch (event.key) { + case 'Enter': + case 'Tab': + if (!validator.validate()) return; + setSelectedArns(prevArns => [ + ...prevArns, + { value: inputValue, label: inputValue }, + ]); + setInputValue(''); + event.preventDefault(); + } + } + + function handleOnProceed(validator: Validator) { + if (!validator.validate()) return; + onProceed({ awsRoleArns: selectedArns }); + } + + const canAddTraits = !restOfProps.isSsoUser && restOfProps.canEditUser; + const headerSubtitle = 'Allow access to AWS Management Console.'; + const hasTraits = selectedArns.length > 0; + + const preContent = ( + + + Only{' '} + + IAM roles with tag + {' '} + key teleport.dev/integration and value true{' '} + are allowed to be used by the integration. + + + ); + + return ( + + {({ validator }) => ( + handleOnProceed(validator)} + preContent={preContent} + onPrev={null} + > + p.theme.fontSizes[2]}px; + padding-bottom: ${p => p.theme.space[1]}px; + } + `} + mb={1} + components={{ + DropdownIndicator: null, + CrossIcon: () => , + }} + stylesConfig={styles(theme)} + rule={validArns} + label="Copy and paste IAM role ARNs that you want to assume when accessing the + AWS console" + inputValue={inputValue} + onInputChange={setInputValue} + onKeyDown={(e: React.KeyboardEvent) => handleKeyDown(e, validator)} + isMulti + isSearchable + isClearable={selectedArns.some(v => !v.isFixed)} + placeholder="Copy and paste ARNs" + value={selectedArns} + isDisabled={!canAddTraits} + onChange={(value: Option[], action) => { + if (action.action === 'clear') { + setSelectedArns(getFixedOptions('awsRoleArns')); + } else { + setSelectedArns(value || []); + } + }} + options={getSelectableOptions('awsRoleArns')} + autoFocus + toolTipContent={ + <> + ARN is found in the format{' '} + {`arn:aws:iam:::role/`} + + } + /> + + )} + + ); +} + +export const validArns = (createdArns: Option[]) => () => { + if (!createdArns) { + return { + valid: true, + }; + } + + const badFilters = createdArns.filter(o => { + // Ignore statically (role) configured arns since + // user cannot fix it on this screen. + return !o.isFixed && !IAM_ROLE_ARN_REGEX.test(o.value); + }); + + if (badFilters.length > 0) { + return { + valid: false, + message: `The following ARNs have invalid format: ${badFilters + .map(f => f.value) + .join(', ')}`, + }; + } + + return { valid: true }; +}; diff --git a/web/packages/teleport/src/Discover/AwsMangementConsole/TestConnection/TestConnection.story.tsx b/web/packages/teleport/src/Discover/AwsMangementConsole/TestConnection/TestConnection.story.tsx new file mode 100644 index 00000000000..3502bb6f37d --- /dev/null +++ b/web/packages/teleport/src/Discover/AwsMangementConsole/TestConnection/TestConnection.story.tsx @@ -0,0 +1,92 @@ +/** + * 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 React from 'react'; +import { MemoryRouter } from 'react-router'; + +import { ContextProvider } from 'teleport'; +import cfg from 'teleport/config'; +import { createTeleportContext } from 'teleport/mocks/contexts'; +import { + DiscoverProvider, + DiscoverContextState, +} from 'teleport/Discover/useDiscover'; + +import { app } from '../fixtures'; + +import { TestConnection as Comp } from './TestConnection'; + +export default { + title: 'Teleport/Discover/Application/AwsConsole', +}; + +export const TestConnection = () => ( + + + +); + +const Provider = ({ children }) => { + const ctx = createTeleportContext(); + const discoverCtx: DiscoverContextState = { + prevStep: () => {}, + nextStep: () => {}, + agentMeta: { + app: { + ...app, + awsRoles: [ + { + name: 'static-arn1', + arn: 'arn:aws:iam::123456789012:role/static-arn1', + display: 'static-arn1', + accountId: '123456789012', + }, + { + name: 'static-arn2', + arn: 'arn:aws:iam::123456789012:role/static-arn2', + display: 'static-arn2', + accountId: '123456789012', + }, + ], + }, + }, + currentStep: 0, + onSelectResource: () => null, + resourceSpec: undefined, + exitFlow: () => null, + viewConfig: null, + indexedViews: [], + setResourceSpec: () => null, + updateAgentMeta: () => null, + emitErrorEvent: () => null, + emitEvent: () => null, + eventState: null, + }; + + return ( + + + {children} + + + ); +}; diff --git a/web/packages/teleport/src/Discover/AwsMangementConsole/TestConnection/TestConnection.tsx b/web/packages/teleport/src/Discover/AwsMangementConsole/TestConnection/TestConnection.tsx new file mode 100644 index 00000000000..acef47ce0ab --- /dev/null +++ b/web/packages/teleport/src/Discover/AwsMangementConsole/TestConnection/TestConnection.tsx @@ -0,0 +1,137 @@ +/** + * 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 React, { useState } from 'react'; +import { Text, Box, Link } from 'design'; +import Select from 'shared/components/Select'; +import { OutlineInfo } from 'design/Alert/Alert'; +import { TextSelectCopy } from 'shared/components/TextSelectCopy'; + +import cfg from 'teleport/config'; +import { generateTshLoginCommand, openNewTab } from 'teleport/lib/util'; +import { + Header, + ActionButtons, + HeaderSubtitle, + StyledBox, + Mark, +} from 'teleport/Discover/Shared'; +import useTeleport from 'teleport/useTeleport'; +import { splitAwsIamArn } from 'teleport/services/integrations/aws'; +import { AWS_TAG_INFO_LINK } from 'teleport/Discover/Shared/const'; + +import { AppMeta, useDiscover } from '../../useDiscover'; + +import type { Option } from 'shared/components/Select'; + +export function TestConnection() { + const ctx = useTeleport(); + const { username, authType } = ctx.storeUser.state; + const clusterId = ctx.storeUser.getClusterId(); + + const { nextStep, prevStep, agentMeta } = useDiscover(); + const { app } = agentMeta as AppMeta; + + const arnOpts = app.awsRoles.map(({ arn }) => ({ value: arn, label: arn })); + const [selectedOpt, setSelectedOpt] = useState