diff --git a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/AutoDeploy.test.tsx b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/AutoDeploy.test.tsx
index c239c9914c97c..2d30d48045077 100644
--- a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/AutoDeploy.test.tsx
+++ b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/AutoDeploy.test.tsx
@@ -75,6 +75,8 @@ const mocKIntegration: Integration = {
resourceType: 'integration',
spec: {
roleArn: `doncare/${awsoidcRoleArn}`,
+ issuerS3Bucket: '',
+ issuerS3Prefix: '',
},
statusCode: IntegrationStatusCode.Running,
};
diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabaseEnroll.story.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabaseEnroll.story.tsx
index bbabeccea0e4f..727ccf647463c 100644
--- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabaseEnroll.story.tsx
+++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/EnrollRdsDatabaseEnroll.story.tsx
@@ -172,6 +172,8 @@ const Component = () => {
resourceType: 'integration',
spec: {
roleArn: 'arn:aws:iam::123456789012:role/test-role-arn',
+ issuerS3Bucket: '',
+ issuerS3Prefix: '',
},
statusCode: IntegrationStatusCode.Running,
},
diff --git a/web/packages/teleport/src/Discover/Fixtures/databases.tsx b/web/packages/teleport/src/Discover/Fixtures/databases.tsx
index 4a1d735ed6b9d..04acbe8685539 100644
--- a/web/packages/teleport/src/Discover/Fixtures/databases.tsx
+++ b/web/packages/teleport/src/Discover/Fixtures/databases.tsx
@@ -88,6 +88,8 @@ export function getDbMeta(): DbMeta {
resourceType: 'integration',
spec: {
roleArn: 'arn:aws:iam::123456789012:role/test-role-arn',
+ issuerS3Bucket: '',
+ issuerS3Prefix: '',
},
statusCode: IntegrationStatusCode.Running,
},
diff --git a/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2Ice.story.tsx b/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2Ice.story.tsx
index 2259eefd09fe4..b62b26911cad9 100644
--- a/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2Ice.story.tsx
+++ b/web/packages/teleport/src/Discover/Server/CreateEc2Ice/CreateEc2Ice.story.tsx
@@ -279,6 +279,8 @@ const Component = () => {
resourceType: 'integration',
spec: {
roleArn: 'arn-123',
+ issuerS3Bucket: '',
+ issuerS3Prefix: '',
},
statusCode: IntegrationStatusCode.Running,
},
diff --git a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.story.tsx b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.story.tsx
index 87afc3e4d0ed5..0f6a46186a992 100644
--- a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.story.tsx
+++ b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.story.tsx
@@ -110,6 +110,8 @@ const Component = () => {
resourceType: 'integration',
spec: {
roleArn: 'arn-123',
+ issuerS3Bucket: '',
+ issuerS3Prefix: '',
},
statusCode: IntegrationStatusCode.Running,
},
diff --git a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.test.tsx b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.test.tsx
index 4b2817dc38e97..e844affe3a12c 100644
--- a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.test.tsx
+++ b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.test.tsx
@@ -51,6 +51,8 @@ describe('test EnrollEc2Instance.tsx', () => {
resourceType: 'integration',
spec: {
roleArn: 'arn-123',
+ issuerS3Bucket: '',
+ issuerS3Prefix: '',
},
statusCode: IntegrationStatusCode.Running,
},
diff --git a/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.test.tsx b/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.test.tsx
new file mode 100644
index 0000000000000..42e376d9ed322
--- /dev/null
+++ b/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.test.tsx
@@ -0,0 +1,194 @@
+/**
+ * Teleport
+ * Copyright (C) 2023 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, fireEvent } from 'design/utils/testing';
+
+import {
+ Integration,
+ IntegrationKind,
+ IntegrationStatusCode,
+} from 'teleport/services/integrations';
+
+import { EditAwsOidcIntegrationDialog } from './EditAwsOidcIntegrationDialog';
+
+test('edit without s3 fields', async () => {
+ render(
+ null}
+ edit={() => null}
+ integration={{
+ resourceType: 'integration',
+ kind: IntegrationKind.AwsOidc,
+ name: 'some-integration-name',
+ spec: {
+ roleArn: 'arn:aws:iam::123456789012:role/johndoe',
+ issuerS3Bucket: '',
+ issuerS3Prefix: '',
+ },
+ statusCode: IntegrationStatusCode.Running,
+ }}
+ />
+ );
+
+ // Initial state.
+ expect(screen.getByText(/required/i)).toBeInTheDocument();
+ expect(screen.queryByTestId('scriptbox')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('checkbox')).not.toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
+
+ // Click on generate command:
+ // - script rendered
+ // - checkbox to confirm user has ran command
+ // - edit button replaces generate command button
+ // - save button still disabled
+ fireEvent.click(screen.getByRole('button', { name: /generate command/i }));
+ screen.getByRole('button', { name: /edit/i });
+ expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
+ expect(
+ screen.queryByRole('button', { name: /generate command/i })
+ ).not.toBeInTheDocument();
+ expect(screen.getByTestId('checkbox')).toBeInTheDocument();
+ expect(screen.getByTestId('scriptbox')).toBeInTheDocument();
+
+ // Click on checkbox should enable save button and disable edit button.
+ fireEvent.click(screen.getByRole('checkbox'));
+ expect(screen.getByRole('button', { name: /save/i })).toBeEnabled();
+ expect(screen.getByRole('button', { name: /edit/i })).toBeDisabled();
+
+ // Unchecking the checkbox should disable save button.
+ fireEvent.click(screen.getByRole('checkbox'));
+ expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
+
+ // Click on edit, should replace it with generate command
+ fireEvent.click(screen.getByRole('button', { name: /edit/i }));
+ expect(
+ screen.getByRole('button', { name: /generate command/i })
+ ).toBeEnabled();
+});
+
+test('edit with s3 fields', async () => {
+ render(
+ null}
+ edit={() => null}
+ integration={integration}
+ />
+ );
+
+ // Initial state.
+ expect(screen.queryByText(/required/i)).not.toBeInTheDocument();
+ expect(screen.queryByTestId('scriptbox')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('checkbox')).not.toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
+ expect(
+ screen.queryByRole('button', { name: /generate command/i })
+ ).not.toBeInTheDocument();
+
+ // Changing role arn should not render generate command.
+ fireEvent.change(screen.getByPlaceholderText(/arn:aws:iam:/i), {
+ target: { value: 'something else' },
+ });
+ expect(screen.getByRole('button', { name: /save/i })).toBeEnabled();
+ expect(
+ screen.queryByRole('button', { name: /generate command/i })
+ ).not.toBeInTheDocument();
+
+ // Changing the s3 fields should render generate command.
+ fireEvent.change(screen.getByPlaceholderText(/bucket/i), {
+ target: { value: 's3-bucket-something' },
+ });
+ fireEvent.click(screen.getByRole('button', { name: /generate command/i }));
+ expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
+});
+
+test('edit invalid fields', async () => {
+ render(
+ null}
+ edit={() => null}
+ integration={integration}
+ />
+ );
+
+ expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
+
+ // invalid role arn
+ fireEvent.change(screen.getByPlaceholderText(/arn:aws:iam:/i), {
+ target: { value: 'role something else' },
+ });
+
+ fireEvent.click(screen.getByRole('button', { name: /save/i }));
+ expect(screen.getByText(/invalid role ARN format/i)).toBeInTheDocument();
+
+ // invalid s3 fields
+ fireEvent.change(screen.getByPlaceholderText(/bucket/i), {
+ target: { value: '' },
+ });
+ fireEvent.change(screen.getByPlaceholderText(/prefix/i), {
+ target: { value: '' },
+ });
+ fireEvent.click(screen.getByRole('button', { name: /generate command/i }));
+ expect(screen.queryAllByText(/required/i)).toHaveLength(2);
+});
+
+test('edit submit', async () => {
+ const mockEditFn = jest.fn();
+ render(
+ null}
+ edit={mockEditFn}
+ integration={integration}
+ />
+ );
+
+ expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
+
+ // change role arn
+ fireEvent.change(screen.getByPlaceholderText(/arn:aws:iam:/i), {
+ target: { value: 'arn:aws:iam::123456789011:role/other' },
+ });
+
+ // change s3 fields
+ fireEvent.change(screen.getByPlaceholderText(/bucket/i), {
+ target: { value: 'other-bucket' },
+ });
+ fireEvent.change(screen.getByPlaceholderText(/prefix/i), {
+ target: { value: 'other-prefix' },
+ });
+
+ fireEvent.click(screen.getByRole('button', { name: /generate command/i }));
+ fireEvent.click(screen.getByRole('checkbox'));
+ fireEvent.click(screen.getByRole('button', { name: /save/i }));
+
+ expect(mockEditFn).toHaveBeenCalledWith({
+ roleArn: 'arn:aws:iam::123456789011:role/other',
+ s3Bucket: 'other-bucket',
+ s3Prefix: 'other-prefix',
+ });
+});
+
+const integration: Integration = {
+ resourceType: 'integration',
+ kind: IntegrationKind.AwsOidc,
+ name: 'some-integration-name',
+ spec: {
+ roleArn: 'arn:aws:iam::123456789012:role/johndoe',
+ issuerS3Bucket: 's3-bucket',
+ issuerS3Prefix: 's3-prefix',
+ },
+ statusCode: IntegrationStatusCode.Running,
+};
diff --git a/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx b/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx
new file mode 100644
index 0000000000000..46adec5ca21d4
--- /dev/null
+++ b/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx
@@ -0,0 +1,258 @@
+/**
+ * Teleport
+ * Copyright (C) 2023 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 styled from 'styled-components';
+import {
+ ButtonSecondary,
+ ButtonPrimary,
+ ButtonBorder,
+ Alert,
+ Text,
+ Box,
+ Flex,
+ Link,
+} from 'design';
+import Dialog, {
+ DialogHeader,
+ DialogTitle,
+ DialogContent,
+ DialogFooter,
+} from 'design/DialogConfirmation';
+import useAttempt from 'shared/hooks/useAttemptNext';
+import FieldInput from 'shared/components/FieldInput';
+import Validation, { Validator } from 'shared/components/Validation';
+import { requiredRoleArn } from 'shared/components/Validation/rules';
+import { ToolTipInfo } from 'shared/components/ToolTip';
+import { CheckboxInput } from 'design/Checkbox';
+
+import { TextSelectCopyMulti } from 'teleport/components/TextSelectCopy';
+import { Integration } from 'teleport/services/integrations';
+import cfg from 'teleport/config';
+
+import { EditableIntegrationFields } from './Operations/useIntegrationOperation';
+import { S3BucketConfiguration } from './Enroll/AwsOidc/S3BucketConfiguration';
+import {
+ getDefaultS3BucketName,
+ getDefaultS3PrefixName,
+} from './Enroll/AwsOidc/Shared/utils';
+
+type Props = {
+ close(): void;
+ edit(req: EditableIntegrationFields): Promise;
+ integration: Integration;
+};
+
+export function EditAwsOidcIntegrationDialog(props: Props) {
+ const { close, edit, integration } = props;
+ const { attempt, run } = useAttempt();
+
+ const [roleArn, setRoleArn] = useState(integration.spec.roleArn);
+ const [s3Bucket, setS3Bucket] = useState(
+ () => integration.spec.issuerS3Bucket || getDefaultS3BucketName()
+ );
+ const [s3Prefix, setS3Prefix] = useState(
+ () =>
+ integration.spec.issuerS3Prefix ||
+ getDefaultS3PrefixName(integration.spec.roleArn.split(':role/')[1])
+ );
+
+ const [scriptUrl, setScriptUrl] = useState('');
+ const [confirmed, setConfirmed] = useState(false);
+
+ function handleEdit(validator: Validator) {
+ if (!validator.validate()) {
+ return;
+ }
+
+ run(() => edit({ roleArn, s3Bucket, s3Prefix }));
+ }
+
+ function generateAwsOidcConfigIdpScript(validator: Validator) {
+ if (!validator.validate()) {
+ return;
+ }
+
+ validator.reset();
+
+ const roleName = roleArn.split(':role/')[1];
+ const newScriptUrl = cfg.getAwsOidcConfigureIdpScriptUrl({
+ integrationName: integration.name,
+ roleName,
+ s3Bucket: s3Bucket,
+ s3Prefix: s3Prefix,
+ });
+
+ setScriptUrl(newScriptUrl);
+ }
+
+ const isProcessing = attempt.status === 'processing';
+ const requiresS3 =
+ !integration.spec.issuerS3Bucket || !integration.spec.issuerS3Prefix;
+ const showGenerateCommand =
+ requiresS3 ||
+ integration.spec.issuerS3Bucket !== s3Bucket ||
+ integration.spec.issuerS3Prefix !== s3Prefix;
+
+ return (
+
+ {({ validator }) => (
+
+ )}
+
+ );
+}
+
+const S3BucketBox = styled(Box)`
+ border-radius: ${p => p.theme.space[1]}px;
+ border: 2px solid
+ ${p => {
+ if (p.requiresS3) {
+ return p.theme.colors.warning.main;
+ }
+ return p.theme.colors.spotBackground[1];
+ }};
+ background-color: ${p => {
+ if (p.requiresS3) {
+ return p.theme.colors.interactive.tonal.alert[0];
+ }
+ return p.theme.colors.spotBackground[0];
+ }};
+`;
diff --git a/web/packages/teleport/src/Integrations/EditIntegrationDialog.tsx b/web/packages/teleport/src/Integrations/EditIntegrationDialog.tsx
deleted file mode 100644
index f470315e2d690..0000000000000
--- a/web/packages/teleport/src/Integrations/EditIntegrationDialog.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-/**
- * Copyright 2023 Gravitational, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import React, { useState } from 'react';
-import { ButtonSecondary, ButtonPrimary, Alert, Text } from 'design';
-import Dialog, {
- DialogHeader,
- DialogTitle,
- DialogContent,
- DialogFooter,
-} from 'design/DialogConfirmation';
-import useAttempt from 'shared/hooks/useAttemptNext';
-import FieldInput from 'shared/components/FieldInput';
-import Validation, { Validator } from 'shared/components/Validation';
-import { requiredRoleArn } from 'shared/components/Validation/rules';
-
-import { Integration } from 'teleport/services/integrations';
-
-import { EditableIntegrationFields } from './Operations/useIntegrationOperation';
-
-type Props = {
- close(): void;
- edit(req: EditableIntegrationFields): Promise;
- integration: Integration;
-};
-
-export function EditIntegrationDialog(props: Props) {
- const { close, edit, integration } = props;
- const { attempt, run } = useAttempt();
-
- const [roleArn, setRoleArn] = useState(integration.spec.roleArn);
-
- const isProcessing = attempt.status === 'processing';
-
- function handleEdit(validator: Validator) {
- if (!validator.validate()) {
- return;
- }
-
- run(() => edit({ roleArn }));
- }
-
- return (
-
- {({ validator }) => (
-
- )}
-
- );
-}
diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.tsx
index 41fd9828b2bec..a5f7cc11001b9 100644
--- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.tsx
+++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/AwsOidc.tsx
@@ -21,10 +21,7 @@ import styled from 'styled-components';
import { Box, ButtonSecondary, Text, Link, Flex, ButtonPrimary } from 'design';
import * as Icons from 'design/Icon';
import FieldInput from 'shared/components/FieldInput';
-import {
- requiredField,
- requiredIamRoleName,
-} from 'shared/components/Validation/rules';
+import { requiredIamRoleName } from 'shared/components/Validation/rules';
import Validation, { Validator } from 'shared/components/Validation';
import useAttempt from 'shared/hooks/useAttemptNext';
@@ -46,12 +43,20 @@ import {
import cfg from 'teleport/config';
import { FinishDialog } from './FinishDialog';
+import { S3BucketConfiguration } from './S3BucketConfiguration';
+import {
+ getDefaultS3BucketName,
+ requiredPrefixName,
+ validPrefixNameToolTipContent,
+} from './Shared/utils';
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 [createdIntegration, setCreatedIntegration] = useState();
const { attempt, run } = useAttempt('');
@@ -86,6 +91,8 @@ export function AwsOidc() {
subKind: IntegrationKind.AwsOidc,
awsoidc: {
roleArn,
+ issuerS3Bucket: s3Bucket,
+ issuerS3Prefix: s3Prefix,
},
})
.then(res => {
@@ -116,6 +123,8 @@ export function AwsOidc() {
const newScriptUrl = cfg.getAwsOidcConfigureIdpScriptUrl({
integrationName,
roleName,
+ s3Bucket,
+ s3Prefix,
});
setScriptUrl(newScriptUrl);
@@ -156,25 +165,39 @@ export function AwsOidc() {
Step 1
- setIntegrationName(e.target.value)}
- disabled={!!scriptUrl}
- />
- setRoleName(e.target.value)}
- disabled={!!scriptUrl}
- />
+
+ setIntegrationName(e.target.value)}
+ disabled={!!scriptUrl}
+ onBlur={() => {
+ // 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}
+ />
+
+
{scriptUrl ? (
setScriptUrl('')}>
Edit
diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/FinishDialog.story.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/FinishDialog.story.tsx
index 2f3feeb636395..53d2b0a62520c 100644
--- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/FinishDialog.story.tsx
+++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/FinishDialog.story.tsx
@@ -36,7 +36,11 @@ export const Story = () => (
kind: IntegrationKind.AwsOidc,
name: 'some-integration-name',
statusCode: IntegrationStatusCode.Running,
- spec: { roleArn: 'some-role-arn' },
+ spec: {
+ roleArn: 'some-role-arn',
+ issuerS3Bucket: '',
+ issuerS3Prefix: '',
+ },
}}
/>
@@ -51,7 +55,11 @@ export const FinishDialogueDiscover = () => (
kind: IntegrationKind.AwsOidc,
name: 'some-integration-name',
statusCode: IntegrationStatusCode.Running,
- spec: { roleArn: 'some-role-arn' },
+ spec: {
+ roleArn: 'some-role-arn',
+ issuerS3Bucket: '',
+ issuerS3Prefix: '',
+ },
}}
/>
diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx
new file mode 100644
index 0000000000000..0a8164a248d0f
--- /dev/null
+++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx
@@ -0,0 +1,82 @@
+/**
+ * Teleport
+ * Copyright (C) 2023 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 } 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.
+
+
+
+ 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.
+
+ }
+ />
+ setS3Prefix(e.target.value.trim())}
+ disabled={disabled}
+ toolTipContent={validPrefixNameToolTipContent('Prefix')}
+ />
+
+ >
+ );
+}
diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/Shared/Shared.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/Shared/Shared.tsx
new file mode 100644
index 0000000000000..82f059ddbc15b
--- /dev/null
+++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/Shared/Shared.tsx
@@ -0,0 +1,37 @@
+/**
+ * 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
new file mode 100644
index 0000000000000..8dc382b0b0e60
--- /dev/null
+++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/Shared/utils.test.ts
@@ -0,0 +1,89 @@
+/**
+ * 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', () => {
+ 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(input)();
+ expect(result.valid).toEqual(valid);
+ });
+});
+
+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 result = requiredBucketName(input)();
+ expect(result.valid).toEqual(valid);
+ });
+});
diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/Shared/utils.ts b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/Shared/utils.ts
new file mode 100644
index 0000000000000..915a32b6b7a96
--- /dev/null
+++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/Shared/utils.ts
@@ -0,0 +1,139 @@
+/**
+ * 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: Rule = inputVal => () => {
+ if (!inputVal) {
+ return {
+ valid: false,
+ message: '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: Rule = inputVal => () => {
+ if (!inputVal) {
+ return {
+ valid: false,
+ message: '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.tsx b/web/packages/teleport/src/Integrations/IntegrationList.tsx
index 93a3894d996a5..b00e01c6035e1 100644
--- a/web/packages/teleport/src/Integrations/IntegrationList.tsx
+++ b/web/packages/teleport/src/Integrations/IntegrationList.tsx
@@ -103,7 +103,11 @@ export function IntegrationList(props: Props) {
);
}
- if (item.resourceType === 'integration') {
+ if (
+ item.resourceType === 'integration' &&
+ // Currently, only AWSOIDC supports editing.
+ item.kind === IntegrationKind.AwsOidc
+ ) {
return (
@@ -177,6 +181,26 @@ export function IntegrationList(props: Props) {
}
const StatusCell = ({ item }: { item: IntegrationLike }) => {
+ if (
+ item.resourceType === 'integration' &&
+ item.kind === IntegrationKind.AwsOidc &&
+ (!item.spec.issuerS3Bucket || !item.spec.issuerS3Prefix)
+ ) {
+ return (
+ |
+
+
+ Integration needs updating
+
+
+ Requires setting up a Amazon S3 Bucket. Click on 'OPTIONS' and
+ 'Edit...' and fill out the 'S3 Location' input fields.
+
+
+
+ |
+ );
+ }
const status = getStatus(item);
const statusDescription = getStatusCodeDescription(item.statusCode);
diff --git a/web/packages/teleport/src/Integrations/Integrations.story.tsx b/web/packages/teleport/src/Integrations/Integrations.story.tsx
index ed79f552eca61..1e579aa51187b 100644
--- a/web/packages/teleport/src/Integrations/Integrations.story.tsx
+++ b/web/packages/teleport/src/Integrations/Integrations.story.tsx
@@ -23,7 +23,7 @@ import {
import { IntegrationList } from './IntegrationList';
import { DeleteIntegrationDialog } from './RemoveIntegrationDialog';
-import { EditIntegrationDialog } from './EditIntegrationDialog';
+import { EditAwsOidcIntegrationDialog } from './EditAwsOidcIntegrationDialog';
import { plugins, integrations } from './fixtures';
export default {
@@ -44,16 +44,40 @@ export function DeleteDialog() {
);
}
-export function EditDialog() {
+export function EditDialogWithoutS3() {
return (
- null}
edit={() => null}
integration={{
resourceType: 'integration',
kind: IntegrationKind.AwsOidc,
name: 'some-integration-name',
- spec: { roleArn: 'arn:aws:iam::123456789012:roles/johndoe' },
+ spec: {
+ roleArn: 'arn:aws:iam::123456789012:role/johndoe',
+ issuerS3Bucket: '',
+ issuerS3Prefix: '',
+ },
+ statusCode: IntegrationStatusCode.Running,
+ }}
+ />
+ );
+}
+
+export function EditDialogWithS3() {
+ return (
+ null}
+ edit={() => null}
+ integration={{
+ resourceType: 'integration',
+ kind: IntegrationKind.AwsOidc,
+ name: 'some-integration-name',
+ spec: {
+ roleArn: 'arn:aws:iam::123456789012:role/johndoe',
+ issuerS3Bucket: 'named-bucket',
+ issuerS3Prefix: 'named-prefix',
+ },
statusCode: IntegrationStatusCode.Running,
}}
/>
diff --git a/web/packages/teleport/src/Integrations/Operations/IntegrationOperations.tsx b/web/packages/teleport/src/Integrations/Operations/IntegrationOperations.tsx
index 75a1e07a98f3d..de49a8aba4008 100644
--- a/web/packages/teleport/src/Integrations/Operations/IntegrationOperations.tsx
+++ b/web/packages/teleport/src/Integrations/Operations/IntegrationOperations.tsx
@@ -19,7 +19,7 @@ import React from 'react';
import { Integration } from 'teleport/services/integrations';
import { DeleteIntegrationDialog } from '../RemoveIntegrationDialog';
-import { EditIntegrationDialog } from '../EditIntegrationDialog';
+import { EditAwsOidcIntegrationDialog } from '../EditAwsOidcIntegrationDialog';
import {
OperationType,
@@ -53,7 +53,7 @@ export function IntegrationOperations({
if (operation === 'edit') {
return (
- {
const params: UrlAwsOidcConfigureIdp = {
integrationName: 'int-name',
roleName: 'role-arn',
+ s3Bucket: 's3-bucket',
+ s3Prefix: 's3-prefix',
};
const base =
'http://localhost/webapi/scripts/integrations/configure/awsoidc-idp.sh?';
- const expected = `integrationName=${'int-name'}&role=${'role-arn'}`;
+ const expected = `integrationName=int-name&role=role-arn&s3Bucket=s3-bucket&s3Prefix=s3-prefix`;
expect(cfg.getAwsOidcConfigureIdpScriptUrl(params)).toBe(
`${base}${expected}`
);
diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts
index ff541c1d52b10..260da1dec2d23 100644
--- a/web/packages/teleport/src/config.ts
+++ b/web/packages/teleport/src/config.ts
@@ -263,7 +263,7 @@ const cfg = {
thumbprintPath: '/v1/webapi/thumbprint',
awsConfigureIamScriptOidcIdpPath:
- '/webapi/scripts/integrations/configure/awsoidc-idp.sh?integrationName=:integrationName&role=:roleName',
+ '/webapi/scripts/integrations/configure/awsoidc-idp.sh?integrationName=:integrationName&role=:roleName&s3Bucket=:s3Bucket&s3Prefix=:s3Prefix',
awsConfigureIamScriptDeployServicePath:
'/webapi/scripts/integrations/configure/deployservice-iam.sh?integrationName=:integrationName&awsRegion=:region&role=:awsOidcRoleArn&taskRole=:taskRoleArn',
awsConfigureIamScriptListDatabasesPath:
@@ -1075,6 +1075,8 @@ export interface UrlDeployServiceIamConfigureScriptParams {
export interface UrlAwsOidcConfigureIdp {
integrationName: string;
roleName: string;
+ s3Bucket: string;
+ s3Prefix: string;
}
export interface UrlAwsConfigureIamScriptParams {
diff --git a/web/packages/teleport/src/services/integrations/integrations.ts b/web/packages/teleport/src/services/integrations/integrations.ts
index 41ed78deba2e7..25fa7134a453e 100644
--- a/web/packages/teleport/src/services/integrations/integrations.ts
+++ b/web/packages/teleport/src/services/integrations/integrations.ts
@@ -230,6 +230,8 @@ function makeIntegration(json: any): Integration {
kind: subKind,
spec: {
roleArn: awsoidc?.roleArn,
+ issuerS3Bucket: awsoidc?.issuerS3Bucket,
+ issuerS3Prefix: awsoidc?.issuerS3Prefix,
},
// The integration resource does not have a "status" field, but is
// a required field for the table that lists both plugin and
diff --git a/web/packages/teleport/src/services/integrations/types.ts b/web/packages/teleport/src/services/integrations/types.ts
index 9b70129e9f051..91ec7f79fd65e 100644
--- a/web/packages/teleport/src/services/integrations/types.ts
+++ b/web/packages/teleport/src/services/integrations/types.ts
@@ -53,6 +53,8 @@ export enum IntegrationKind {
}
export type IntegrationSpecAwsOidc = {
roleArn: string;
+ issuerS3Prefix: string;
+ issuerS3Bucket: string;
};
export enum IntegrationStatusCode {
@@ -268,6 +270,8 @@ export type ListAwsRdsDatabaseResponse = {
export type IntegrationUpdateRequest = {
awsoidc: {
roleArn: string;
+ issuerS3Bucket: string;
+ issuerS3Prefix: string;
};
};
|