diff --git a/web/packages/shared/components/ToolTip/ToolTip.tsx b/web/packages/shared/components/ToolTip/ToolTip.tsx index 4b27754a06b9c..9b02ae9ab511f 100644 --- a/web/packages/shared/components/ToolTip/ToolTip.tsx +++ b/web/packages/shared/components/ToolTip/ToolTip.tsx @@ -28,6 +28,7 @@ export const ToolTipInfo: React.FC< muteIconColor?: boolean; sticky?: boolean; maxWidth?: number; + kind?: 'info' | 'warning'; }> > = ({ children, @@ -35,6 +36,7 @@ export const ToolTipInfo: React.FC< muteIconColor, sticky = false, maxWidth = 350, + kind = 'info', }) => { const [anchorEl, setAnchorEl] = useState(); const open = Boolean(anchorEl); @@ -71,7 +73,12 @@ export const ToolTipInfo: React.FC< height: 18px; `} > - + {kind === 'info' && ( + + )} + {kind === 'warning' && ( + + )} @@ -108,3 +115,12 @@ const InfoIcon = styled(Icons.Info)<{ $muteIconColor?: boolean }>` width: 18px; color: ${p => (p.$muteIconColor ? p.theme.colors.text.disabled : 'inherit')}; `; + +const WarningIcon = styled(Icons.Warning)<{ $muteIconColor?: boolean }>` + height: 18px; + width: 18px; + color: ${p => + p.$muteIconColor + ? p.theme.colors.text.disabled + : p.theme.colors.interactive.solid.alert.default.background}; +`; diff --git a/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.test.tsx b/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.test.tsx index ed6151770e7cb..0dc8135e747ef 100644 --- a/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.test.tsx +++ b/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.test.tsx @@ -26,7 +26,7 @@ import { import { EditAwsOidcIntegrationDialog } from './EditAwsOidcIntegrationDialog'; -test('user acknowledging script was ran when s3 bucket fields are edited', async () => { +test('user acknowledging script was ran when reconfiguring', async () => { render( null} @@ -37,8 +37,6 @@ test('user acknowledging script was ran when s3 bucket fields are edited', async name: 'some-integration-name', spec: { roleArn: 'arn:aws:iam::123456789012:role/johndoe', - issuerS3Bucket: 'test-value', - issuerS3Prefix: '', }, statusCode: IntegrationStatusCode.Running, }} @@ -49,31 +47,32 @@ test('user acknowledging script was ran when s3 bucket fields are edited', async expect(screen.queryByTestId('scriptbox')).not.toBeInTheDocument(); expect(screen.queryByLabelText(/I ran the command/i)).not.toBeInTheDocument(); expect( - screen.queryByRole('button', { name: /generate command/i }) + screen.queryByRole('button', { name: /reconfigure/i }) ).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: /save/i })).toBeDisabled(); - // Fill in the s3 prefix field. - fireEvent.change(screen.getByPlaceholderText(/prefix/i), { - target: { value: 'test-value' }, + // Check s3 related fields are not rendered. + expect(screen.queryByText(/not recommended/)).not.toBeInTheDocument(); + expect(screen.queryByText('Amazon S3')).not.toBeInTheDocument(); + + // change role arn + fireEvent.change(screen.getByPlaceholderText(/arn:aws:iam:/i), { + target: { value: 'arn:aws:iam::123456789011:role/other' }, }); + await waitFor(() => - expect( - screen.getByRole('button', { name: /generate command/i }) - ).toBeEnabled() + expect(screen.getByRole('button', { name: /reconfigure/i })).toBeEnabled() ); - // When clicking on generate command: + // When clicking on reconfigure: // - script rendered // - checkbox to confirm user has ran command - // - edit button replaces generate command button + // - edit button replaces reconfigure button // - save button still disabled - await userEvent.click( - screen.getByRole('button', { name: /generate command/i }) - ); + await userEvent.click(screen.getByRole('button', { name: /reconfigure/i })); await screen.findByRole('button', { name: /edit/i }); expect(screen.getByRole('button', { name: /save/i })).toBeDisabled(); expect( - screen.queryByRole('button', { name: /generate command/i }) + screen.queryByRole('button', { name: /reconfigure/i }) ).not.toBeInTheDocument(); expect(screen.getByLabelText(/I ran the command/i)).toBeInTheDocument(); expect(screen.getByTestId('scriptbox')).toBeInTheDocument(); @@ -91,16 +90,14 @@ test('user acknowledging script was ran when s3 bucket fields are edited', async expect(screen.getByRole('button', { name: /save/i })).toBeDisabled() ); - // Click on edit, should replace it with generate command + // Click on edit, should replace it with reconfigure await userEvent.click(screen.getByRole('button', { name: /edit/i })); await waitFor(() => - expect( - screen.getByRole('button', { name: /generate command/i }) - ).toBeEnabled() + expect(screen.getByRole('button', { name: /reconfigure/i })).toBeEnabled() ); }); -test('render warning on save when leaving s3 fields empty', async () => { +test('render warning when s3 buckets are present', async () => { const edit = jest.fn(() => Promise.resolve()); render( { name: 'some-integration-name', spec: { roleArn: 'arn:aws:iam::123456789012:role/johndoe', - issuerS3Bucket: '', - issuerS3Prefix: '', + issuerS3Bucket: 'some-bucket', + issuerS3Prefix: 'some-prefix', }, statusCode: IntegrationStatusCode.Running, }} @@ -124,112 +121,25 @@ test('render warning on save when leaving s3 fields empty', async () => { expect(screen.queryByTestId('scriptbox')).not.toBeInTheDocument(); expect(screen.queryByLabelText(/I ran the command/i)).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: /save/i })).toBeDisabled(); - expect( - screen.queryByRole('button', { name: /generate command/i }) - ).not.toBeInTheDocument(); - - // Enable the generate command button by changing a field. - fireEvent.change(screen.getByPlaceholderText(/arn:aws:iam:/i), { - target: { value: 'arn:aws:iam::123456789012:role/someonelse' }, - }); - await waitFor(() => - expect( - screen.getByRole('button', { name: /generate command/i }) - ).toBeEnabled() - ); - - expect(screen.queryByLabelText(/I ran the command/i)).not.toBeInTheDocument(); - expect(screen.getByRole('button', { name: /save/i })).toBeDisabled(); - - await userEvent.click( - screen.getByRole('button', { name: /generate command/i }) - ); - await screen.findByRole('button', { name: /edit/i }); - expect(screen.getByRole('button', { name: /save/i })).toBeDisabled(); - - await userEvent.click(screen.getByLabelText(/I ran the command/i)); - await waitFor(() => - expect(screen.getByRole('button', { name: /save/i })).toBeEnabled() - ); - - // Clicking on save without defining s3 fields, should render - // a warning. - await 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. - await userEvent.click(screen.getByRole('button', { name: /cancel/i })); - await screen.findByRole('button', { name: /save/i }); - - await userEvent.click(screen.getByRole('button', { name: /save/i })); - await screen.findByText(/recommended to use an S3 bucket/i); - - await 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, - }} - /> - ); + // Check s3 related fields/warnings are rendered. expect( - screen.queryByRole('button', { name: /generate command/i }) - ).not.toBeInTheDocument(); - - // Delete the s3 fields. - fireEvent.change(screen.getByPlaceholderText(/bucket/i), { - target: { value: '' }, - }); - fireEvent.change(screen.getByPlaceholderText(/prefix/i), { - target: { value: '' }, - }); - await waitFor(() => - expect( - screen.getByRole('button', { name: /generate command/i }) - ).toBeEnabled() - ); - - expect(screen.queryByLabelText(/I ran the command/i)).not.toBeInTheDocument(); - expect(screen.getByRole('button', { name: /save/i })).toBeDisabled(); + screen.getByRole('button', { name: /reconfigure/i }) + ).toBeInTheDocument(); + expect(screen.getByText(/not recommended/)).toBeInTheDocument(); + expect(screen.getByText(/Amazon S3 Location/)).toBeInTheDocument(); - await userEvent.click( - screen.getByRole('button', { name: /generate command/i }) - ); - await screen.findByRole('button', { name: /edit/i }); - expect(screen.getByRole('button', { name: /save/i })).toBeDisabled(); + // Clicking on reconfigure should hide s3 fields. + await userEvent.click(screen.getByRole('button', { name: /reconfigure/i })); + await screen.findByText(/AWS CloudShell/); + expect(screen.queryByText(/not recommended/)).not.toBeInTheDocument(); + expect(screen.queryByText('/Amazon S3 Location/')).not.toBeInTheDocument(); - await userEvent.click(screen.getByLabelText(/I ran the command/i)); - await waitFor(() => - expect(screen.getByRole('button', { name: /save/i })).toBeEnabled() - ); - - // Test for warning render. - await 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(); + // Clicking on edit, should render it back. + await userEvent.click(screen.getByRole('button', { name: /edit/i })); - await userEvent.click(screen.getByRole('button', { name: /continue/i })); - await waitFor(() => expect(edit).toHaveBeenCalledTimes(1)); + await screen.findByText(/not recommended/); + await screen.findByText(/Amazon S3 Location/); }); test('edit invalid fields', async () => { @@ -249,14 +159,10 @@ test('edit invalid fields', async () => { }); await waitFor(() => - expect( - screen.getByRole('button', { name: /generate command/i }) - ).toBeEnabled() + expect(screen.getByRole('button', { name: /reconfigure/i })).toBeEnabled() ); - await userEvent.click( - screen.getByRole('button', { name: /generate command/i }) - ); + await userEvent.click(screen.getByRole('button', { name: /reconfigure/i })); await screen.findByText(/invalid role ARN format/i); }); @@ -286,14 +192,10 @@ test('edit submit called with proper fields', async () => { }); await waitFor(() => - expect( - screen.getByRole('button', { name: /generate command/i }) - ).toBeEnabled() + expect(screen.getByRole('button', { name: /reconfigure/i })).toBeEnabled() ); - await userEvent.click( - screen.getByRole('button', { name: /generate command/i }) - ); + await userEvent.click(screen.getByRole('button', { name: /reconfigure/i })); await screen.findByRole('button', { name: /edit/i }); await userEvent.click(screen.getByLabelText(/I ran the command/i)); @@ -305,8 +207,6 @@ test('edit submit called with proper fields', async () => { expect(mockEditFn).toHaveBeenCalledWith({ roleArn: 'arn:aws:iam::123456789011:role/other', - s3Bucket: 'other-bucket', - s3Prefix: 'other-prefix', }); }); diff --git a/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx b/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx index 35a3dfd1c0dec..3813f7381ef49 100644 --- a/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx +++ b/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx @@ -33,6 +33,7 @@ import Dialog, { DialogContent, DialogFooter, } from 'design/DialogConfirmation'; +import { OutlineInfo, OutlineWarn } from 'design/Alert/Alert'; import useAttempt from 'shared/hooks/useAttemptNext'; import FieldInput from 'shared/components/FieldInput'; import Validation, { Validator } from 'shared/components/Validation'; @@ -47,7 +48,6 @@ import { splitAwsIamArn } from 'teleport/services/integrations/aws'; import { EditableIntegrationFields } from './Operations/useIntegrationOperation'; import { S3BucketConfiguration } from './Enroll/AwsOidc/S3BucketConfiguration'; -import { S3BucketWarningBanner } from './Enroll/AwsOidc/S3BucketWarningBanner'; type Props = { close(): void; @@ -59,15 +59,7 @@ 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 - ); - const [s3Prefix, setS3Prefix] = useState( - () => integration.spec.issuerS3Prefix - ); - const [scriptUrl, setScriptUrl] = useState(''); const [confirmed, setConfirmed] = useState(false); @@ -76,7 +68,7 @@ export function EditAwsOidcIntegrationDialog(props: Props) { return; } - run(() => edit({ roleArn, s3Bucket, s3Prefix })); + run(() => edit({ roleArn })); } function generateAwsOidcConfigIdpScript(validator: Validator) { @@ -92,24 +84,20 @@ export function EditAwsOidcIntegrationDialog(props: Props) { const newScriptUrl = cfg.getAwsOidcConfigureIdpScriptUrl({ integrationName: integration.name, roleName: arnResourceName, - s3Bucket: s3Bucket, - s3Prefix: s3Prefix, }); setScriptUrl(newScriptUrl); } + const s3Bucket = integration.spec.issuerS3Bucket; + const s3Prefix = integration.spec.issuerS3Prefix; + const showReadonlyS3Fields = s3Bucket || s3Prefix; + const isProcessing = attempt.status === 'processing'; - const requiresS3BucketWarning = !s3Bucket && !s3Prefix; const showGenerateCommand = - integration.spec.issuerS3Bucket !== s3Bucket || - integration.spec.issuerS3Prefix !== s3Prefix || - integration.spec.roleArn !== roleArn; + integration.spec.roleArn !== roleArn || showReadonlyS3Fields; - const changeDetected = - integration.spec.issuerS3Bucket !== s3Bucket || - integration.spec.issuerS3Prefix !== s3Prefix || - integration.spec.roleArn !== roleArn; + const changeDetected = integration.spec.roleArn !== roleArn; return ( @@ -135,7 +123,7 @@ export function EditAwsOidcIntegrationDialog(props: Props) { value={integration.name} readonly={true} /> - + - + {showReadonlyS3Fields && !scriptUrl && ( + <> + + + Using an S3 bucket to store the OpenID Configuration is not + recommended. Reconfiguring this integration is suggested + (this will not break existing features). + + + )} {scriptUrl && ( - Configure the required permission in your AWS account. Open{' '} + {showReadonlyS3Fields && ( + + + After running the command, delete the previous{' '} + + identity provider + {' '} + along with its{' '} + + S3 bucket + {' '} + from your AWS console. + + + )} )} @@ -192,16 +206,13 @@ export function EditAwsOidcIntegrationDialog(props: Props) { Edit )} - {showGenerateCommand && !scriptUrl && ( + {!scriptUrl && showGenerateCommand && ( generateAwsOidcConfigIdpScript(validator)} - disabled={ - (!requiresS3BucketWarning && (!s3Bucket || !s3Prefix)) || - !roleArn - } + disabled={!roleArn} > - Generate Command + Reconfigure )} @@ -217,40 +228,20 @@ export function EditAwsOidcIntegrationDialog(props: Props) { }} /> )} - - {requiresS3BucketWarning && showS3BucketWarning ? ( - setShowS3BucketWarning(false)} - onContinue={() => { - setShowS3BucketWarning(false); - handleEdit(validator); - }} - btnFlexWrap={true} - /> - ) : ( - <> - { - if (requiresS3BucketWarning) { - setShowS3BucketWarning(true); - } else { - handleEdit(validator); - } - }} - > - Save - - - Cancel - - - )} + handleEdit(validator)} + > + Save + + + Cancel + )} @@ -263,3 +254,17 @@ const EditableBox = styled(Box)` border: 2px solid ${p => p.theme.colors.spotBackground[1]}; background-color: ${p => p.theme.colors.spotBackground[0]}; `; + +function getIdpArn({ + s3Bucket, + s3Prefix, + roleArn, +}: { + s3Bucket: string; + s3Prefix: string; + roleArn: string; +}) { + const { awsAccountId } = splitAwsIamArn(roleArn); + const arn = `arn:aws:iam::${awsAccountId}:oidc-provider/${s3Bucket}.s3.amazonaws.com/${s3Prefix}`; + return encodeURIComponent(arn); +} diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.story.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.story.tsx index c7bf533f841b5..a00a102207ddd 100644 --- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.story.tsx +++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.story.tsx @@ -20,26 +20,9 @@ import React from 'react'; import { MemoryRouter } from 'react-router'; import { AwsOidc } from './AwsOidc'; -import { S3BucketWarningBanner } from './S3BucketWarningBanner'; - -export default { - title: 'Teleport/Integrations/Enroll/AwsOidc', -}; export const Flow = () => ( ); - -export const SBucketWarning = () => ( - null} onContinue={() => null} /> -); - -export const SBucketWarningWithReview = () => ( - null} - onContinue={() => null} - reviewing={true} - /> -); diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.tsx index 79d7dac8e0b45..4415fb646d5b2 100644 --- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.tsx +++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.tsx @@ -20,15 +20,7 @@ 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, - ButtonText, -} from 'design'; +import { Box, ButtonSecondary, Text, Link, Flex, ButtonPrimary } from 'design'; import * as Icons from 'design/Icon'; import FieldInput from 'shared/components/FieldInput'; import { requiredIamRoleName } from 'shared/components/Validation/rules'; @@ -53,24 +45,12 @@ import { import cfg from 'teleport/config'; import { FinishDialog } from './FinishDialog'; -import { S3BucketConfiguration } from './S3BucketConfiguration'; -import { - getDefaultS3BucketName, - requiredPrefixName, - validPrefixNameToolTipContent, -} from './Shared/utils'; -import { S3BucketWarningBanner } from './S3BucketWarningBanner'; export function AwsOidc() { const [integrationName, setIntegrationName] = useState(''); const [roleArn, setRoleArn] = useState(''); const [roleName, setRoleName] = useState(''); const [scriptUrl, setScriptUrl] = useState(''); - const [s3Bucket, setS3Bucket] = useState(() => getDefaultS3BucketName()); - const [s3Prefix, setS3Prefix] = useState(''); - const [showS3BucketWarning, setShowS3BucketWarning] = useState(false); - const [confirmedS3BucketWarning, setConfirmedS3BucketWarning] = - useState(false); const [createdIntegration, setCreatedIntegration] = useState(); const { attempt, run } = useAttempt(''); @@ -81,8 +61,6 @@ export function AwsOidc() { kind: IntegrationEnrollKind.AwsOidc, }); - const requiresS3BucketWarning = !s3Bucket && !s3Prefix; - useEffect(() => { // If a user came from the discover wizard, // discover wizard will send of appropriate events. @@ -107,8 +85,6 @@ export function AwsOidc() { subKind: IntegrationKind.AwsOidc, awsoidc: { roleArn, - issuerS3Bucket: s3Bucket, - issuerS3Prefix: s3Prefix, }, }) .then(res => { @@ -139,8 +115,6 @@ export function AwsOidc() { const newScriptUrl = cfg.getAwsOidcConfigureIdpScriptUrl({ integrationName, roleName, - s3Bucket, - s3Prefix, }); setScriptUrl(newScriptUrl); @@ -182,23 +156,12 @@ export function AwsOidc() { Step 1 setIntegrationName(e.target.value)} disabled={!!scriptUrl} - onBlur={() => { - // s3Bucket by default is defined. - // If empty user intentionally cleared it. - if (!integrationName || (!s3Bucket && !s3Prefix)) return; - // Help come up with a default prefix name for user. - if (!s3Prefix) { - setS3Prefix(`${integrationName}-oidc-idp`); - } - }} - toolTipContent={validPrefixNameToolTipContent('Integration')} /> setRoleName(e.target.value)} disabled={!!scriptUrl} /> - - {confirmedS3BucketWarning && ( - - setShowS3BucketWarning(true)} - alignItems="center" - > - - Click to view S3 Bucket Warning - - - )} - {showS3BucketWarning ? ( - setShowS3BucketWarning(false)} - onContinue={() => { - setShowS3BucketWarning(false); - setConfirmedS3BucketWarning(true); - generateAwsOidcConfigIdpScript(validator); - }} - reviewing={confirmedS3BucketWarning} - /> - ) : scriptUrl ? ( + {scriptUrl ? ( { setScriptUrl(''); - setConfirmedS3BucketWarning(false); }} > Edit @@ -252,13 +184,7 @@ export function AwsOidc() { ) : ( { - if (requiresS3BucketWarning) { - setShowS3BucketWarning(true); - } else { - generateAwsOidcConfigIdpScript(validator); - } - }} + onClick={() => generateAwsOidcConfigIdpScript(validator)} > Generate Command diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx index 874564aa1a4f6..a225196d65dfc 100644 --- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx +++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx @@ -21,62 +21,37 @@ import { Text, Flex } from 'design'; import FieldInput from 'shared/components/FieldInput'; import { ToolTipInfo } from 'shared/components/ToolTip'; -import { - requiredBucketName, - requiredPrefixName, - validPrefixNameToolTipContent, -} from './Shared/utils'; - export function S3BucketConfiguration({ s3Bucket, - setS3Bucket, s3Prefix, - setS3Prefix, - disabled, }: { s3Bucket: string; - setS3Bucket(s: string): void; s3Prefix: string; - setS3Prefix(s: string): void; - disabled: boolean; }) { return ( <> Amazon S3 Location - - Teleport will create and use Amazon S3 Bucket as this integration's - issuer and will publicly host two files: one for the OpenID - configuration and another one for the public key. + + Deprecated. Amazon is now validating the IdP certificate against a + list of root CAs. Storing the OpenID Configuration in S3 is no longer + required, and should be removed to improve security. setS3Bucket(e.target.value.trim())} - disabled={disabled} - toolTipContent={ - - Bucket name can consist only of lowercase letters and numbers. - Hyphens (-) are allowed in between letters and numbers. An - existing bucket might be used, but it must have Access Control - Lists (ACL) enabled. - - } + readonly={true} /> setS3Prefix(e.target.value.trim())} - disabled={disabled} - toolTipContent={validPrefixNameToolTipContent('Prefix')} + readonly={true} /> diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketWarningBanner.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketWarningBanner.tsx deleted file mode 100644 index e90ef71b4cf7d..0000000000000 --- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketWarningBanner.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/** - * 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 { ButtonText, Box, Text, Flex, ButtonBorder } from 'design'; -import { OutlineWarn } from 'design/Alert/Alert'; -import { Notification } from 'design/Icon'; -import styled from 'styled-components'; - -export const S3BucketWarningBanner = ({ - onClose, - onContinue, - reviewing = false, - btnFlexWrap = false, -}: { - onClose(): void; - onContinue(): void; - reviewing?: boolean; - btnFlexWrap?: boolean; -}) => { - return ( - - - - - - - - It is recommended to use an S3 bucket to host the public keys. - - - Without an S3 bucket, you will be required to append the new - certificate's thumbprint in the AWS IAM/Identity Provider section - after you have renewed and started using the new certificate. - - - - {reviewing ? ( - - Ok - - ) : ( - <> - Continue - Cancel - - )} - - - - ); -}; - -const BellIcon = styled(Notification)` - background-color: ${p => p.theme.colors.warning.hover}; - border-radius: 100px; - height: 32px; - width: 32px; - color: ${p => p.theme.colors.text.primaryInverse}; - margin-right: ${p => p.theme.space[3]}px; -`; diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/Shared/Shared.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/Shared/Shared.tsx deleted file mode 100644 index 82f059ddbc15b..0000000000000 --- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/Shared/Shared.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/** - * 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 { Validator } from 'shared/components/Validation'; - -import cfg, { UrlAwsOidcConfigureIdp } from 'teleport/config'; - -export function generateAwsOidcConfigIdpScript( - validator: Validator, - setScriptUrl: (s: string) => void, - params: UrlAwsOidcConfigureIdp -) { - if (!validator.validate()) { - return; - } - - validator.reset(); - - const newScriptUrl = cfg.getAwsOidcConfigureIdpScriptUrl(params); - - setScriptUrl(newScriptUrl); -} diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/Shared/utils.test.ts b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/Shared/utils.test.ts deleted file mode 100644 index f65e9294cc6bc..0000000000000 --- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/Shared/utils.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * 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 cfg from 'teleport/config'; - -import { - getDefaultS3BucketName, - getDefaultS3PrefixName, - requiredPrefixName, - requiredBucketName, -} from './utils'; - -const defaultProxyCluster = cfg.proxyCluster; - -afterEach(() => { - cfg.proxyCluster = defaultProxyCluster; -}); - -test('getDefaultS3BucketName', () => { - cfg.proxyCluster = 'llama#42.slkej'; - expect(getDefaultS3BucketName()).toBe(''); - - cfg.proxyCluster = 'llama.cloud.gravitational-io'; - expect(getDefaultS3BucketName()).toBe('llama-cloud-gravitational-io'); -}); - -test('getDefaultS3PrefixName', () => { - expect(getDefaultS3PrefixName('')).toBe(''); - expect(getDefaultS3PrefixName('sdf@$@#sdf')).toBe(''); - - cfg.proxyCluster = 'llama.cloud.gravitational-io'; - expect(getDefaultS3PrefixName('int-name')).toBe('int-name-oidc-idp'); -}); - -describe('requiredPrefixName', () => { - const requiredField = true; - test.each` - input | valid - ${''} | ${false} - ${Array.from('x'.repeat(64))} | ${false} - ${'-sdf'} | ${false} - ${'sdfs-'} | ${false} - ${'_sdf'} | ${false} - ${'sdfd_'} | ${false} - ${'..sdf'} | ${false} - ${'sdf.'} | ${false} - ${'sdlfkjs/dfsd'} | ${false} - ${'Asd09f-_.sdfDFs1'} | ${true} - `('validity of input($input) should be ($valid)', ({ input, valid }) => { - const result = requiredPrefixName(requiredField)(input)(); - expect(result.valid).toEqual(valid); - }); - - test('empty prefix name is valid if not a required field', () => { - const requiredField = false; - expect(requiredPrefixName(requiredField)('')().valid).toBeTruthy(); - }); -}); - -describe('requiredBucketName', () => { - test.each` - input | valid - ${''} | ${false} - ${Array.from('x'.repeat(64))} | ${false} - ${Array.from('x'.repeat(2))} | ${false} - ${'-sdf'} | ${false} - ${'sdfs-'} | ${false} - ${'sdfds_sdf'} | ${false} - ${'xn--sdf'} | ${false} - ${'sthree-sdf'} | ${false} - ${'sthree-configurator-dfs'} | ${false} - ${'sdf-s3alias'} | ${false} - ${'sdf--ol-s3'} | ${false} - ${'Asd09f-sdfDFs1'} | ${false} - ${'sdf0-dfs0'} | ${true} - `('validity of input($input) should be ($valid)', ({ input, valid }) => { - const requiredField = true; - const result = requiredBucketName(requiredField)(input)(); - expect(result.valid).toEqual(valid); - }); - - test('empty bucket name is valid if not a required field', () => { - const requiredField = false; - expect(requiredBucketName(requiredField)('')().valid).toBeTruthy(); - }); -}); diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/Shared/utils.ts b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/Shared/utils.ts deleted file mode 100644 index 28e8e20a0fac1..0000000000000 --- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/Shared/utils.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * 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 { Rule } from 'shared/components/Validation/rules'; - -import cfg from 'teleport/config'; - -// Must start and end with lowercase letters or numbers. -// Can have hyphens in between start and end. -const bucketNameRegex = new RegExp(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/); -export const requiredBucketName = - (required): Rule => - inputVal => - () => { - if (!inputVal) { - return { - valid: !required, - message: required ? 'required' : '', - }; - } - - if (inputVal.length < 3 || inputVal.length > 63) { - return { - valid: false, - message: 'name should be 3-63 characters', - }; - } - - if (!bucketNameRegex.test(inputVal)) { - return { - valid: false, - message: 'name is in a invalid format', - }; - } - - if (inputVal.startsWith('xn--')) { - return { - valid: false, - message: 'cannot start with "xn--"', - }; - } - - if (inputVal.startsWith('sthree-')) { - return { - valid: false, - message: 'cannot start with "sthree-"', - }; - } - - if (inputVal.startsWith('sthree-configurator')) { - return { - valid: false, - message: 'cannot start with "sthree-configurator"', - }; - } - - if (inputVal.endsWith('-s3alias')) { - return { - valid: false, - message: 'cannot end with "-s3alias"', - }; - } - - if (inputVal.endsWith('--ol-s3')) { - return { - valid: false, - message: 'cannot end with "--ol-s3"', - }; - } - - return { - valid: true, - }; - }; - -// Must start and end with letters or numbers. -// Can have hyphens, underscores, and periods in between start and end. -const prefixNameRegex = new RegExp(/^[a-zA-Z0-9][a-zA-Z0-9-_.]*[a-zA-Z0-9]$/); -export const requiredPrefixName = - (required): Rule => - inputVal => - () => { - if (!inputVal) { - return { - valid: !required, - message: required ? 'required' : '', - }; - } - - // Just a random hard cap. - if (inputVal.length > 63) { - return { - valid: false, - message: 'name can be max 63 characters long', - }; - } - - if (!prefixNameRegex.test(inputVal)) { - return { - valid: false, - message: 'name is in a invalid format', - }; - } - - return { - valid: true, - }; - }; - -export function getDefaultS3BucketName() { - const modifiedClusterName = cfg.proxyCluster.replaceAll('.', '-'); - if (bucketNameRegex.test(modifiedClusterName)) { - return modifiedClusterName; - } - - return ''; -} - -export function getDefaultS3PrefixName(integrationName: string) { - if (!integrationName || !prefixNameRegex.test(integrationName)) { - return ''; - } - - return `${integrationName}-oidc-idp`; -} - -export function validPrefixNameToolTipContent(fieldName: string) { - return `${fieldName} name can consist only of letters and numbers. \ - Hyphens (-), dots (.), and underscores (_) are allowed in between letters and numbers.`; -} diff --git a/web/packages/teleport/src/Integrations/IntegrationList.test.tsx b/web/packages/teleport/src/Integrations/IntegrationList.test.tsx deleted file mode 100644 index 5dce1329ae977..0000000000000 --- a/web/packages/teleport/src/Integrations/IntegrationList.test.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/** - * 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 { render, screen, userEvent } from 'design/utils/testing'; - -import { - IntegrationKind, - integrationService, - IntegrationStatusCode, -} from 'teleport/services/integrations'; - -import { IntegrationList } from './IntegrationList'; - -test('aws oidc row without s3 fields should render tooltip', async () => { - jest - .spyOn(integrationService, 'fetchThumbprint') - .mockResolvedValue('some-thumbprint'); - - render( - - ); - - expect(screen.getByText('aws')).toBeInTheDocument(); - expect(screen.getByText(/running/i)).toBeInTheDocument(); - await userEvent.hover(screen.getByRole('icon')); - expect(screen.queryByTestId('btn-copy')).not.toBeInTheDocument(); - - await userEvent.click(screen.getByText(/generate a new thumbprint/i)); - expect(screen.getByText(/some-thumbprint/i)).toBeInTheDocument(); -}); - -test('aws oidc row with s3 fields should NOT render tooltip', async () => { - jest - .spyOn(integrationService, 'fetchThumbprint') - .mockResolvedValue('some-thumbprint'); - - render( - - ); - - expect(screen.getByText('aws')).toBeInTheDocument(); - expect(screen.getByText(/running/i)).toBeInTheDocument(); - expect(screen.queryByRole('icon')).not.toBeInTheDocument(); -}); diff --git a/web/packages/teleport/src/Integrations/IntegrationList.tsx b/web/packages/teleport/src/Integrations/IntegrationList.tsx index cfe00ae717398..a7a41ccbbafa9 100644 --- a/web/packages/teleport/src/Integrations/IntegrationList.tsx +++ b/web/packages/teleport/src/Integrations/IntegrationList.tsx @@ -51,7 +51,6 @@ import { import cfg from 'teleport/config'; import { ExternalAuditStorageOpType } from './Operations/useIntegrationOperation'; -import { UpdateAwsOidcThumbprint } from './UpdateAwsOidcThumbprint'; type Props = { list: IntegrationLike[]; @@ -197,9 +196,6 @@ const StatusCell = ({ item }: { item: IntegrationLike }) => { {getStatusCodeTitle(item.statusCode)} - - - ); diff --git a/web/packages/teleport/src/Integrations/Integrations.story.tsx b/web/packages/teleport/src/Integrations/Integrations.story.tsx index c47261b869aef..aa6c697ec525c 100644 --- a/web/packages/teleport/src/Integrations/Integrations.story.tsx +++ b/web/packages/teleport/src/Integrations/Integrations.story.tsx @@ -26,7 +26,6 @@ import { import { IntegrationList } from './IntegrationList'; import { DeleteIntegrationDialog } from './RemoveIntegrationDialog'; import { EditAwsOidcIntegrationDialog } from './EditAwsOidcIntegrationDialog'; -import { UpdateAwsOidcThumbprint } from './UpdateAwsOidcThumbprint'; import { plugins, integrations } from './fixtures'; export default { @@ -37,20 +36,6 @@ export function List() { return ; } -export function UpdateAwsOidcThumbprintHoverTooltip() { - return ( - - ); -} - export function DeleteDialog() { return ( . - */ - -import { useState } from 'react'; -import styled from 'styled-components'; -import { - Text, - Link as ExternalLink, - Flex, - Box, - ButtonPrimary, - Mark, -} from 'design'; -import { TextSelectCopyMulti } from 'shared/components/TextSelectCopy'; -import { ToolTipInfo } from 'shared/components/ToolTip'; -import useAttempt from 'shared/hooks/useAttemptNext'; -import * as Icons from 'design/Icon'; - -import { - Integration, - integrationService, -} from 'teleport/services/integrations'; -import { splitAwsIamArn } from 'teleport/services/integrations/aws'; -import cfg from 'teleport/config'; - -export function UpdateAwsOidcThumbprint({ - integration, -}: { - integration: Integration; -}) { - const { attempt, run } = useAttempt(); - - const [thumbprint, setThumbprint] = useState(''); - - function getThumbprint() { - run(() => integrationService.fetchThumbprint().then(setThumbprint)); - } - - const { awsAccountId, arnStartingPart } = splitAwsIamArn( - integration.spec.roleArn - ); - - const encodedOidcProviderArn = encodeURIComponent( - `${arnStartingPart}${awsAccountId}:oidc-provider/${cfg.proxyCluster}` - ); - - return ( - - - - - This integration has no S3 bucket configured. When renewing your - HTTPS certificate, if it has a different CA, a manual update of this - integration's thumbprint is required. - - You may run into issues when the thumbprint is stale. - - - - Generate a New Thumbprint - - - {thumbprint && ( - - )} - {attempt.status === 'failed' && ( - - Error - fetching thumbprint: some kind of error - - )} - - - -
    - To update thumbprint: -
  • - - Go to your{' '} - - IAM Identity Provider - {' '} - dashboard -
  • -
  • - - On Thumbprints section click on Manage{' '} - then click on Add Thumbprint -
  • -
  • - Copy and paste the generated thumbprint
  • -
-
-
- ); -} - -const Ul = styled.ul` - margin-left: 0; - padding-left: 0; - list-style: none; -`; diff --git a/web/packages/teleport/src/Integrations/fixtures.ts b/web/packages/teleport/src/Integrations/fixtures.ts index 029fca37725d9..b4422fa01da7e 100644 --- a/web/packages/teleport/src/Integrations/fixtures.ts +++ b/web/packages/teleport/src/Integrations/fixtures.ts @@ -150,17 +150,6 @@ export const integrations: Integration[] = [ statusCode: IntegrationStatusCode.Running, spec: { roleArn: '', issuerS3Prefix: '', issuerS3Bucket: '' }, }, - { - resourceType: 'integration', - name: 'aws', - kind: IntegrationKind.AwsOidc, - statusCode: IntegrationStatusCode.Running, - spec: { - roleArn: 'some-role-arn', - issuerS3Prefix: 'some-prefix', - issuerS3Bucket: 'some-bucket', - }, - }, ]; export const externalAuditStorage: ExternalAuditStorage = { diff --git a/web/packages/teleport/src/config.test.ts b/web/packages/teleport/src/config.test.ts index ac1cacaefe3c2..462e1610cf1ea 100644 --- a/web/packages/teleport/src/config.test.ts +++ b/web/packages/teleport/src/config.test.ts @@ -37,21 +37,6 @@ test('getDeployServiceIamConfigureScriptPath formatting', async () => { ); }); -test('getAwsOidcConfigureIdpScriptUrl formatting with s3 fields', async () => { - const params: UrlAwsOidcConfigureIdp = { - integrationName: 'int-name', - roleName: 'role-arn', - s3Bucket: 's3-bucket', - s3Prefix: 's3-prefix', - }; - const base = - 'http://localhost/v1/webapi/scripts/integrations/configure/awsoidc-idp.sh?'; - const expected = `integrationName=int-name&role=role-arn&s3Bucket=s3-bucket&s3Prefix=s3-prefix`; - expect(cfg.getAwsOidcConfigureIdpScriptUrl(params)).toBe( - `${base}${expected}` - ); -}); - test('getAwsOidcConfigureIdpScriptUrl formatting, without s3 fields', async () => { const params: UrlAwsOidcConfigureIdp = { integrationName: 'int-name', diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index b84b154a46b8d..226e7faa619bd 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -532,10 +532,7 @@ const cfg = { }, getAwsOidcConfigureIdpScriptUrl(p: UrlAwsOidcConfigureIdp) { - let path = cfg.api.awsConfigureIamScriptOidcIdpPath; - if (p.s3Bucket && p.s3Prefix) { - path += '&s3Bucket=:s3Bucket&s3Prefix=:s3Prefix'; - } + const path = cfg.api.awsConfigureIamScriptOidcIdpPath; return cfg.baseUrl + generatePath(path, { ...p }); }, diff --git a/web/packages/teleport/src/services/integrations/types.ts b/web/packages/teleport/src/services/integrations/types.ts index 6d55ea28d279a..025472b22bc90 100644 --- a/web/packages/teleport/src/services/integrations/types.ts +++ b/web/packages/teleport/src/services/integrations/types.ts @@ -56,8 +56,8 @@ export enum IntegrationKind { } export type IntegrationSpecAwsOidc = { roleArn: string; - issuerS3Prefix: string; - issuerS3Bucket: string; + issuerS3Prefix?: string; + issuerS3Bucket?: string; }; export enum IntegrationStatusCode { @@ -293,8 +293,6 @@ export type ListAwsRdsDatabaseResponse = { export type IntegrationUpdateRequest = { awsoidc: { roleArn: string; - issuerS3Bucket: string; - issuerS3Prefix: string; }; };