WebDiscover: Enroll New AWS Integration Flow (#24459)

* Enroll new AWS integration flow

* Move IntegrationEnroll/* into Integration/Enroll/*

* Rename AWS into AwsOidc, story expects pascal case
but ran into non-story issues where casing was a problem
even renaming AWS to Aws wasn't enough, i don't know
why

* Capitalize browser directory to Browser

* Remove passing in clusterId for integration requests

Also define IntegrationKind as enum string values
to avoid hard coding it all over.

* Add sub routing for lazy loading AwsOidc comp

* Fix some styling, remove unused props, and add story

* Add a success dialog after creating integration

* Remove first in wording for now

* Replace teleport.lol with root cluster public URI

* revert renaming browser to Browser

* Fix lint: add license headers

---------

Co-authored-by: Ryan Clark <ryan.clark@goteleport.com>
This commit is contained in:
Lisa Kim
2023-04-13 19:40:21 +00:00
committed by GitHub
co-authored by Ryan Clark
parent e5128278d0
commit 49aa9db73e
45 changed files with 4170 additions and 25 deletions
+48
View File
@@ -0,0 +1,48 @@
/*
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 from 'react';
import type { SVGIconProps } from './common';
export function AWSIcon({ size = 24, fill = 'white' }: SVGIconProps) {
return (
<svg
viewBox="0 0 304 182"
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
fill={fill}
>
<path
fill="white"
d="M86.4 66.4c0 3.7.4 6.7 1.1 8.9.8 2.2 1.8 4.6 3.2 7.2.5.8.7 1.6.7 2.3 0 1-.6 2-1.9 3L83.2 92c-.9.6-1.8.9-2.6.9-1 0-2-.5-3-1.4-1.4-1.5-2.6-3.1-3.6-4.7-1-1.7-2-3.6-3.1-5.9-7.8 9.2-17.6 13.8-29.4 13.8-8.4 0-15.1-2.4-20-7.2-4.9-4.8-7.4-11.2-7.4-19.2 0-8.5 3-15.4 9.1-20.6 6.1-5.2 14.2-7.8 24.5-7.8 3.4 0 6.9.3 10.6.8 3.7.5 7.5 1.3 11.5 2.2v-7.3c0-7.6-1.6-12.9-4.7-16-3.2-3.1-8.6-4.6-16.3-4.6-3.5 0-7.1.4-10.8 1.3-3.7.9-7.3 2-10.8 3.4-1.6.7-2.8 1.1-3.5 1.3-.7.2-1.2.3-1.6.3-1.4 0-2.1-1-2.1-3.1v-4.9c0-1.6.2-2.8.7-3.5.5-.7 1.4-1.4 2.8-2.1 3.5-1.8 7.7-3.3 12.6-4.5C41 1.9 46.2 1.3 51.7 1.3c11.9 0 20.6 2.7 26.2 8.1 5.5 5.4 8.3 13.6 8.3 24.6v32.4zM45.8 81.6c3.3 0 6.7-.6 10.3-1.8 3.6-1.2 6.8-3.4 9.5-6.4 1.6-1.9 2.8-4 3.4-6.4.6-2.4 1-5.3 1-8.7v-4.2c-2.9-.7-6-1.3-9.2-1.7-3.2-.4-6.3-.6-9.4-.6-6.7 0-11.6 1.3-14.9 4-3.3 2.7-4.9 6.5-4.9 11.5 0 4.7 1.2 8.2 3.7 10.6 2.4 2.5 5.9 3.7 10.5 3.7zm80.3 10.8c-1.8 0-3-.3-3.8-1-.8-.6-1.5-2-2.1-3.9L96.7 10.2c-.6-2-.9-3.3-.9-4 0-1.6.8-2.5 2.4-2.5h9.8c1.9 0 3.2.3 3.9 1 .8.6 1.4 2 2 3.9l16.8 66.2 15.6-66.2c.5-2 1.1-3.3 1.9-3.9.8-.6 2.2-1 4-1h8c1.9 0 3.2.3 4 1 .8.6 1.5 2 1.9 3.9l15.8 67 17.3-67c.6-2 1.3-3.3 2-3.9.8-.6 2.1-1 3.9-1h9.3c1.6 0 2.5.8 2.5 2.5 0 .5-.1 1-.2 1.6-.1.6-.3 1.4-.7 2.5l-24.1 77.3c-.6 2-1.3 3.3-2.1 3.9-.8.6-2.1 1-3.8 1h-8.6c-1.9 0-3.2-.3-4-1-.8-.7-1.5-2-1.9-4L156 23l-15.4 64.4c-.5 2-1.1 3.3-1.9 4-.8.7-2.2 1-4 1h-8.6zm128.5 2.7c-5.2 0-10.4-.6-15.4-1.8-5-1.2-8.9-2.5-11.5-4-1.6-.9-2.7-1.9-3.1-2.8-.4-.9-.6-1.9-.6-2.8v-5.1c0-2.1.8-3.1 2.3-3.1.6 0 1.2.1 1.8.3.6.2 1.5.6 2.5 1 3.4 1.5 7.1 2.7 11 3.5 4 .8 7.9 1.2 11.9 1.2 6.3 0 11.2-1.1 14.6-3.3 3.4-2.2 5.2-5.4 5.2-9.5 0-2.8-.9-5.1-2.7-7-1.8-1.9-5.2-3.6-10.1-5.2L246 52c-7.3-2.3-12.7-5.7-16-10.2-3.3-4.4-5-9.3-5-14.5 0-4.2.9-7.9 2.7-11.1 1.8-3.2 4.2-6 7.2-8.2 3-2.3 6.4-4 10.4-5.2 4-1.2 8.2-1.7 12.6-1.7 2.2 0 4.5.1 6.7.4 2.3.3 4.4.7 6.5 1.1 2 .5 3.9 1 5.7 1.6 1.8.6 3.2 1.2 4.2 1.8 1.4.8 2.4 1.6 3 2.5.6.8.9 1.9.9 3.3v4.7c0 2.1-.8 3.2-2.3 3.2-.8 0-2.1-.4-3.8-1.2-5.7-2.6-12.1-3.9-19.2-3.9-5.7 0-10.2.9-13.3 2.8-3.1 1.9-4.7 4.8-4.7 8.9 0 2.8 1 5.2 3 7.1 2 1.9 5.7 3.8 11 5.5l14.2 4.5c7.2 2.3 12.4 5.5 15.5 9.6 3.1 4.1 4.6 8.8 4.6 14 0 4.3-.9 8.2-2.6 11.6-1.8 3.4-4.2 6.4-7.3 8.8-3.1 2.5-6.8 4.3-11.1 5.6-4.5 1.4-9.2 2.1-14.3 2.1z"
/>
<path
d="M273.5 143.7c-32.9 24.3-80.7 37.2-121.8 37.2-57.6 0-109.5-21.3-148.7-56.7-3.1-2.8-.3-6.6 3.4-4.4 42.4 24.6 94.7 39.5 148.8 39.5 36.5 0 76.6-7.6 113.5-23.2 5.5-2.5 10.2 3.6 4.8 7.6z"
fillRule="evenodd"
clipRule="evenodd"
fill="#f90"
/>
<path
d="M287.2 128.1c-4.2-5.4-27.8-2.6-38.5-1.3-3.2.4-3.7-2.4-.8-4.5 18.8-13.2 49.7-9.4 53.3-5 3.6 4.5-1 35.4-18.6 50.2-2.7 2.3-5.3 1.1-4.1-1.9 4-9.9 12.9-32.2 8.7-37.5z"
fillRule="evenodd"
clipRule="evenodd"
fill="#f90"
/>
</svg>
);
}
+1
View File
@@ -20,6 +20,7 @@ export { AddIcon } from './Add';
export { ApplicationsIcon } from './Applications';
export { AuditLogIcon } from './AuditLog';
export { AuthConnectorsIcon } from './AuthConnectors';
export { AWSIcon } from './AWS';
export { LockIcon } from './Lock';
export { ChevronRightIcon } from './ChevronRight';
export { DatabasesIcon } from './Databases';
@@ -0,0 +1,43 @@
/**
* 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 from 'react';
import { MemoryRouter } from 'react-router';
import { createTeleportContext } from 'teleport/mocks/contexts';
import { ContextProvider } from 'teleport';
import { AwsOidc } from './AwsOidc';
export default {
title: 'Teleport/Integrations/Enroll/AwsOidc',
};
export const Flow = () => (
<Provider>
<AwsOidc />
</Provider>
);
const Provider = props => {
const ctx = createTeleportContext({ customAcl: props.customAcl });
return (
<MemoryRouter>
<ContextProvider ctx={ctx}>{props.children}</ContextProvider>
</MemoryRouter>
);
};
@@ -0,0 +1,361 @@
/**
* 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, { useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import { SwitchTransition, Transition } from 'react-transition-group';
import { Header, HeaderSubtitle } from 'teleport/Discover/Shared';
import { Browser } from 'teleport/Integrations/Enroll/AwsOidc/browser/Browser';
import { IAMHomeScreen } from 'teleport/Integrations/Enroll/AwsOidc/IAM/IAMHomeScreen';
import { Cursor } from 'teleport/Integrations/Enroll/AwsOidc/browser/Cursor';
import { IAMIdentityProvidersScreen } from 'teleport/Integrations/Enroll/AwsOidc/IAM/IAMIdentityProvidersScreen';
import { IAMNewProviderScreen } from 'teleport/Integrations/Enroll/AwsOidc/IAM/IAMNewProviderScreen';
import { FirstStageInstructions } from 'teleport/Integrations/Enroll/AwsOidc/instructions/FirstStageInstructions';
import { SecondStageInstructions } from 'teleport/Integrations/Enroll/AwsOidc/instructions/SecondStageInstructions';
import { ThirdStageInstructions } from 'teleport/Integrations/Enroll/AwsOidc/instructions/ThirdStageInstructions';
import { IAMProvider } from 'teleport/Integrations/Enroll/AwsOidc/IAM/IAMProvider';
import { IAMCreateNewRole } from 'teleport/Integrations/Enroll/AwsOidc/IAM/IAMCreateNewRole';
import { FourthStageInstructions } from 'teleport/Integrations/Enroll/AwsOidc/instructions/FourthStageInstructions';
import { IAMCreateNewRolePermissions } from 'teleport/Integrations/Enroll/AwsOidc/IAM/IAMCreateNewRolePermissions';
import { FifthStageInstructions } from 'teleport/Integrations/Enroll/AwsOidc/instructions/FifthStageInstructions';
import { IAMCreateNewPolicy } from 'teleport/Integrations/Enroll/AwsOidc/IAM/IAMCreateNewPolicy';
import { SixthStageInstructions } from 'teleport/Integrations/Enroll/AwsOidc/instructions/SixthStageInstructions';
import { SeventhStageInstructions } from 'teleport/Integrations/Enroll/AwsOidc/instructions/SeventhStageInstructions';
import { IAMRoles } from 'teleport/Integrations/Enroll/AwsOidc/IAM/IAMRoles';
import useTeleport from 'teleport/useTeleport';
import { Stage, STAGES } from './stages';
const Container = styled.div`
padding-right: 40px;
padding-top: 16px;
`;
const InstructionsContainer = styled.div`
display: flex;
margin-top: 50px;
`;
const BrowserContainer = styled.div`
position: relative;
`;
const RestartAnimation = styled.div`
z-index: 100;
display: flex;
align-items: center;
opacity: ${p => (p.visible ? 1 : 0)};
transition: 0.2s ease-in-out opacity;
justify-content: center;
position: absolute;
bottom: 10px;
background: rgba(0, 0, 0, 0.8);
border-radius: 5px;
padding: 5px 10px;
cursor: pointer;
left: 50%;
transform: translate(-50%, 0);
box-shadow: 0 0 15px rgba(0, 0, 0, 0.3);
&:hover {
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
}
`;
const defaultStyle = {
transition: 'opacity 250ms, transform 250ms',
opacity: 0,
width: '100%',
};
const horizontalTransitionStyles = {
entering: { opacity: 0, transform: 'translateX(50px)' },
entered: { opacity: 1, transform: 'translateX(0%)' },
exiting: { opacity: 0, transform: 'translateX(-50px)' },
exited: { opacity: 0, transform: 'translateX(-50px)' },
};
enum InstructionStep {
First,
Second,
Third,
Fourth,
Fifth,
Sixth,
Seventh,
}
export function AwsOidc() {
const ctx = useTeleport();
let clusterPublicUri = ctx.storeUser.state.cluster.publicURL;
const [stage, setStage] = useState(Stage.Initial);
const [showRestartAnimation, setShowRestartAnimation] = useState(false);
const currentStageIndex = STAGES.findIndex(s => s.kind === stage);
const currentStage = STAGES[currentStageIndex];
const currentStageConfig = getStageConfig(stage);
const restartAnimation = useCallback(() => {
setStage(currentStageConfig.restartStage);
setShowRestartAnimation(false);
}, [currentStageConfig]);
useEffect(() => {
if (currentStage.end) {
setShowRestartAnimation(true);
return;
}
if (showRestartAnimation) {
setShowRestartAnimation(false);
}
if (currentStage.duration && STAGES[currentStageIndex + 1]) {
const id = window.setTimeout(
() => setStage(STAGES[currentStageIndex + 1].kind),
currentStage.duration
);
return () => window.clearTimeout(id);
}
}, [currentStage, currentStageIndex, showRestartAnimation]);
return (
<Container>
<Header>Set up your AWS account</Header>
<HeaderSubtitle>
Instead of storing long-lived static credentials, Teleport will become a
trusted OIDC provider with AWS to be able to request short lived
credentials when performing operations automatically.
</HeaderSubtitle>
<InstructionsContainer>
<SwitchTransition mode="out-in">
<Transition
key={currentStageConfig.instructionStep}
timeout={250}
mountOnEnter
unmountOnExit
>
{state => (
<div
style={{
...defaultStyle,
...horizontalTransitionStyles[state],
}}
>
{currentStageConfig.instructionStep ===
InstructionStep.First && (
<FirstStageInstructions
onNext={() => {
setStage(Stage.NewProviderFullScreen);
}}
clusterPublicUri={clusterPublicUri}
/>
)}
{currentStageConfig.instructionStep ===
InstructionStep.Second && (
<SecondStageInstructions
onNext={() => {
setStage(Stage.AddProvider);
}}
clusterPublicUri={clusterPublicUri}
/>
)}
{currentStageConfig.instructionStep ===
InstructionStep.Third && (
<ThirdStageInstructions
onNext={() => {
setStage(Stage.CreateNewRole);
}}
clusterPublicUri={clusterPublicUri}
/>
)}
{currentStageConfig.instructionStep ===
InstructionStep.Fourth && (
<FourthStageInstructions
onNext={() => {
setStage(Stage.CreatePolicy);
}}
clusterPublicUri={clusterPublicUri}
/>
)}
{currentStageConfig.instructionStep ===
InstructionStep.Fifth && (
<FifthStageInstructions
onNext={() => {
setStage(Stage.AssignPolicyToRole);
}}
clusterPublicUri={clusterPublicUri}
/>
)}
{currentStageConfig.instructionStep ===
InstructionStep.Sixth && (
<SixthStageInstructions
onNext={() => {
setStage(Stage.ListRoles);
}}
clusterPublicUri={clusterPublicUri}
/>
)}
{currentStageConfig.instructionStep ===
InstructionStep.Seventh && <SeventhStageInstructions />}
</div>
)}
</Transition>
</SwitchTransition>
<BrowserContainer>
<Browser stage={stage}>
<Cursor
top={currentStage.cursor.top}
left={currentStage.cursor.left}
click={currentStage.cursor.click}
/>
{getStageComponent(stage, clusterPublicUri)}
</Browser>
<RestartAnimation
visible={showRestartAnimation}
onClick={() => restartAnimation()}
>
Restart animation
</RestartAnimation>
</BrowserContainer>
</InstructionsContainer>
</Container>
);
}
function getStageComponent(stage: Stage, uri: string) {
let clusterPublicUri = uri;
// Truncate long URI's so it doesn't mess up the animation screens.
if (clusterPublicUri.length > 30) {
clusterPublicUri = `${clusterPublicUri.substring(0, 30)}...`;
}
const props = { stage, clusterPublicUri };
if (stage >= Stage.Initial && stage <= Stage.ClickIdentityProviders) {
return <IAMHomeScreen />;
}
if (stage >= Stage.IdentityProviders && stage <= Stage.ClickAddProvider) {
return <IAMIdentityProvidersScreen {...props} />;
}
if (stage >= Stage.NewProvider && stage <= Stage.AddProvider) {
return <IAMNewProviderScreen {...props} />;
}
if (stage >= Stage.ProviderAdded && stage <= Stage.SelectProvider) {
return <IAMIdentityProvidersScreen {...props} />;
}
if (stage >= Stage.ProviderView && stage <= Stage.ClickCreateNewRole) {
return <IAMProvider {...props} />;
}
if (stage >= Stage.CreateNewRole && stage <= Stage.ClickNextPermissions) {
return <IAMCreateNewRole {...props} />;
}
if (
stage >= Stage.ConfigureRolePermissions &&
stage <= Stage.ClickCreatePolicy
) {
return <IAMCreateNewRolePermissions {...props} />;
}
if (stage >= Stage.CreatePolicy && stage <= Stage.ClickCreatePolicyButton) {
return <IAMCreateNewPolicy {...props} />;
}
if (
stage >= Stage.AssignPolicyToRole &&
stage <= Stage.ClickCreateRoleButton
) {
return <IAMCreateNewRolePermissions {...props} />;
}
if (stage >= Stage.ListRoles) {
return <IAMRoles {...props} />;
}
}
function getStageConfig(stage: Stage) {
if (stage >= Stage.Initial && stage <= Stage.NewProvider) {
return {
instructionStep: InstructionStep.First,
restartStage: Stage.Initial,
};
}
if (
stage >= Stage.NewProviderFullScreen &&
stage <= Stage.ThumbprintSelected
) {
return {
instructionStep: InstructionStep.Second,
restartStage: Stage.NewProviderFullScreen,
};
}
if (stage >= Stage.AddProvider && stage <= Stage.ClickCreateNewRole) {
return {
instructionStep: InstructionStep.Third,
restartStage: Stage.AddProvider,
};
}
if (stage >= Stage.CreateNewRole && stage <= Stage.ClickCreatePolicy) {
return {
instructionStep: InstructionStep.Fourth,
restartStage: Stage.CreateNewRole,
};
}
if (stage >= Stage.CreatePolicy && stage <= Stage.ClickCreatePolicyButton) {
return {
instructionStep: InstructionStep.Fifth,
restartStage: Stage.CreatePolicy,
};
}
if (
stage >= Stage.AssignPolicyToRole &&
stage <= Stage.ClickCreateRoleButton
) {
return {
instructionStep: InstructionStep.Sixth,
restartStage: Stage.AssignPolicyToRole,
};
}
if (stage >= Stage.ListRoles) {
return {
instructionStep: InstructionStep.Seventh,
restartStage: Stage.ListRoles,
};
}
}
@@ -0,0 +1,171 @@
/**
* 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 from 'react';
import styled from 'styled-components';
import { AddIcon } from 'design/SVGIcon';
import { NextButton } from './common';
const Modal = styled.div`
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
z-index: 9;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
`;
const AssignRole = styled.div<{ active: boolean }>`
border-radius: 4px;
border: 1px solid #bdbdbd;
overflow: hidden;
width: 500px;
`;
const AssignRoleClose = styled.div`
transform: rotate(45deg);
position: relative;
top: 2px;
`;
const AssignRoleHeader = styled.div`
background: #eee;
display: flex;
justify-content: space-between;
font-size: 16px;
padding: 10px 20px;
border-bottom: 1px solid #dddddd;
`;
const AssignRoleFooter = styled.div`
background: #eee;
display: flex;
justify-content: flex-end;
font-size: 16px;
padding: 10px 20px;
border-top: 1px solid #dddddd;
`;
const AssignRoleContent = styled.div`
background: white;
padding: 10px 20px;
display: flex;
justify-content: space-between;
`;
const AssignRoleOption = styled.div<{ active: boolean }>`
display: flex;
background: ${p => (p.active ? '#f0f9ff' : 'white')};
border: 1px solid ${p => (p.active ? '#99c2ee' : '#e6e6e6')};
border-radius: 4px;
flex: 0 0 200px;
padding: 8px 12px 10px;
`;
const AssignRoleOptionBulletContainer = styled.div`
flex: 0 0 25px;
padding: 4px 0;
`;
const AssignRoleOptionBullet = styled.div<{ active: boolean }>`
width: 14px;
height: 14px;
background: ${p => (p.active ? '#1066bb' : '#cccccc')};
border-radius: 50%;
position: relative;
&:after {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: ${p => (p.active ? 6 : 11)}px;
height: ${p => (p.active ? 6 : 11)}px;
content: '';
background: white;
border-radius: 50%;
}
`;
const CancelButton = styled.div`
color: #1869bb;
padding: 5px 15px;
font-size: 14px;
font-weight: 700;
margin-right: 10px;
`;
const AssignRoleOptionDescription = styled.div`
font-size: 12px;
line-height: 1.2;
color: #999;
`;
export function AssignRoleModal({
clusterPublicUri,
}: {
clusterPublicUri: string;
}) {
return (
<Modal>
<div>
<AssignRole>
<AssignRoleHeader>
Assign role for {clusterPublicUri}
<AssignRoleClose>
<AddIcon fill="#444444" size={16} />
</AssignRoleClose>
</AssignRoleHeader>
<AssignRoleContent>
<AssignRoleOption active={true}>
<AssignRoleOptionBulletContainer>
<AssignRoleOptionBullet active={true} />
</AssignRoleOptionBulletContainer>
<div>
<div>Create a new role</div>
<AssignRoleOptionDescription>
Create a new role for this identity provider and add
permissions to the new role.
</AssignRoleOptionDescription>
</div>
</AssignRoleOption>
<AssignRoleOption>
<AssignRoleOptionBulletContainer>
<AssignRoleOptionBullet active={false} />
</AssignRoleOptionBulletContainer>
<div>
<div>Use an existing role</div>
<AssignRoleOptionDescription>
Associate an existing role by adding this IdP to the trusted
entities of the role.
</AssignRoleOptionDescription>
</div>
</AssignRoleOption>
</AssignRoleContent>
<AssignRoleFooter>
<CancelButton>Cancel</CancelButton>
<NextButton>Next</NextButton>
</AssignRoleFooter>
</AssignRole>
</div>
</Modal>
);
}
@@ -0,0 +1,206 @@
/**
* 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 from 'react';
import styled from 'styled-components';
import Flex from 'design/Flex';
import Text from 'design/Text';
import { Stage } from '../stages';
import { AWSWrapper } from './SharedComponents';
import {
Content,
Footer,
Header,
NextButton,
Page,
RoleButton,
Section,
SectionContent,
SectionDropdown,
SectionDropdownSelected,
SectionTitle,
} from './common';
import type { CommonIAMProps } from './common';
const Tabs = styled.div`
display: flex;
border-bottom: 1px solid #ccc;
`;
const Tab = styled.div<{ active: boolean }>`
background: ${p => (p.active ? 'white' : '#eeeeee')};
font-weight: bold;
border: 1px solid #cccccc;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
padding: 5px 15px;
margin-right: 10px;
position: relative;
overflow: hidden;
border-top-width: ${p => (p.active ? 0 : '1px')};
border-bottom-color: ${p => (p.active ? 'white' : '#cccccc')};
margin-bottom: -1px;
&:after {
height: 3px;
position: absolute;
left: 0;
top: 0;
right: 0;
background: #e07701;
content: '';
display: ${p => (p.active ? 'block' : 'none')};
}
`;
const JSONEditor = styled.div<{ selected: boolean }>`
border: 1px solid #ccc;
margin-top: 20px;
padding: 0 25px;
font-size: 14px;
position: relative;
&:after {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
content: '';
background: #b5d5ff;
z-index: 0;
display: ${p => (p.selected ? 'block' : 'none')};
}
pre {
position: relative;
z-index: 1;
}
`;
export function IAMCreateNewPolicy(props: CommonIAMProps) {
let jsonEditor;
if (props.stage >= Stage.ShowJSONEditor) {
let content = `{
"Version": "2012-10-17",
"Statement": []
}`;
if (props.stage >= Stage.PolicyJSONPasted) {
content = `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "rds:DescribeDBInstances",
"Resource": "*"
}
]
}`;
}
jsonEditor = (
<JSONEditor selected={props.stage === Stage.JSONContentsSelected}>
<pre>{content}</pre>
</JSONEditor>
);
}
let content;
if (props.stage <= Stage.PolicyClickNextTags) {
content = (
<>
<Tabs>
<Tab active={props.stage <= Stage.ClickJSONTab}>Visual Editor</Tab>
<Tab active={props.stage >= Stage.ShowJSONEditor}>JSON</Tab>
</Tabs>
{jsonEditor}
<Footer>
<NextButton>Next: Tags</NextButton>
</Footer>
</>
);
}
if (
props.stage >= Stage.PolicyTags &&
props.stage <= Stage.PolicyClickNextReview
) {
content = (
<>
<Text fontSize={4} bold>
Add tags - <em>optional</em>
</Text>
<Flex mt={4}>
<RoleButton>Add tag</RoleButton>
</Flex>
<Footer>
<NextButton>Next: Review</NextButton>
</Footer>
</>
);
}
if (props.stage >= Stage.PolicyReview) {
content = (
<>
<Text fontSize={4} mb={4}>
Review policy
</Text>
<Section>
<SectionTitle>Name*</SectionTitle>
<SectionContent>
<SectionDropdown>
<SectionDropdownSelected>
{props.stage >= Stage.PolicyHasName ? (
'SomePolicyName'
) : (
<>&nbsp;</>
)}
</SectionDropdownSelected>
</SectionDropdown>
</SectionContent>
</Section>
<Footer>
<NextButton>Create policy</NextButton>
</Footer>
</>
);
}
return (
<AWSWrapper>
<Page>
<Content>
<Header>Create policy</Header>
{content}
</Content>
</Page>
</AWSWrapper>
);
}
@@ -0,0 +1,168 @@
/**
* 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 from 'react';
import styled from 'styled-components';
import { Stage } from '../stages';
import { AWSWrapper } from './SharedComponents';
import {
Content,
Footer,
Header,
NextButton,
Page,
Section,
SectionContent,
SectionDropdown,
SectionDropdownSelected,
SectionTitle,
SubHeader,
} from './common';
import type { CommonIAMProps } from './common';
const TrustedEntities = styled.div`
display: flex;
margin-bottom: 30px;
`;
const TrustedEntity = styled.div<{ active: boolean }>`
background: ${p => (p.active ? '#e6f3ff' : 'white')};
border: 2px solid ${p => (p.active ? '#1066bb' : '#cccccc')};
flex: 0 0 180px;
margin-right: 10px;
display: flex;
`;
const TrustedEntityContent = styled.div`
padding: 10px 15px;
`;
const TrustedEntityTitle = styled.div`
font-weight: bold;
`;
const TrustedEntityDescription = styled.div`
font-size: 12px;
color: #666;
line-height: 1.2;
`;
const SectionDropdownItems = styled.div`
background: #222222;
position: absolute;
top: 34px;
color: white;
width: 300px;
border-radius: 4px;
`;
const SectionDropdownItem = styled.div<{ hovered: boolean }>`
padding: 5px 15px;
color: ${p => (p.hovered ? '#d27106' : 'white')};
`;
export function IAMCreateNewRole(props: CommonIAMProps) {
return (
<AWSWrapper>
<Page>
<Content>
<Header>Create role</Header>
<SubHeader>Select type of trusted entity</SubHeader>
<TrustedEntities>
<TrustedEntity>
<TrustedEntityContent>
<TrustedEntityTitle>AWS service</TrustedEntityTitle>
<TrustedEntityDescription>
EC2, Lambda and others
</TrustedEntityDescription>
</TrustedEntityContent>
</TrustedEntity>
<TrustedEntity active={true}>
<TrustedEntityContent>
<TrustedEntityTitle>Web identity</TrustedEntityTitle>
<TrustedEntityDescription>
Cognito or any OpenID provider
</TrustedEntityDescription>
</TrustedEntityContent>
</TrustedEntity>
<TrustedEntity>
<TrustedEntityContent>
<TrustedEntityTitle>SAML 2.0 federation</TrustedEntityTitle>
<TrustedEntityDescription>
Your corporate directory
</TrustedEntityDescription>
</TrustedEntityContent>
</TrustedEntity>
</TrustedEntities>
<SubHeader>Choose a web identity provider</SubHeader>
<Section>
<SectionTitle>Identity Provider</SectionTitle>
<SectionContent>
<SectionDropdown>
<SectionDropdownSelected>
{props.clusterPublicUri}:aud
</SectionDropdownSelected>
</SectionDropdown>
</SectionContent>
</Section>
<Section>
<SectionTitle>Audience*</SectionTitle>
<SectionContent>
<SectionDropdown>
<SectionDropdownSelected>
{props.stage >= Stage.DiscoverAudienceSelected ? (
'discover.teleport'
) : (
<>&nbsp;</>
)}
</SectionDropdownSelected>
{props.stage >= Stage.ShowAudienceDropdown &&
props.stage < Stage.DiscoverAudienceSelected && (
<SectionDropdownItems>
<SectionDropdownItem>
discover.teleport
</SectionDropdownItem>
</SectionDropdownItems>
)}
</SectionDropdown>
</SectionContent>
</Section>
<Footer>
<NextButton>Next: Permissions</NextButton>
</Footer>
</Content>
</Page>
</AWSWrapper>
);
}
@@ -0,0 +1,195 @@
/**
* 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 from 'react';
import styled from 'styled-components';
import Text from 'design/Text';
import Flex from 'design/Flex';
import { Stage } from '../stages';
import { AWSWrapper } from './SharedComponents';
import {
Content,
Footer,
Header,
NextButton,
Page,
RoleButton,
Section,
SectionContent,
SectionDropdown,
SectionDropdownSelected,
SectionTitle,
SubHeader,
TableCheckBox,
TableHeader,
TableItem,
TableSearch,
TableTitle,
} from './common';
import type { CommonIAMProps } from './common';
const CreatePolicyButton = styled.div`
display: flex;
justify-content: space-between;
`;
const Policies = styled.div`
margin-top: 20px;
`;
export function IAMCreateNewRolePermissions(props: CommonIAMProps) {
let content = (
<>
<SubHeader>Attach permissions policies</SubHeader>
<CreatePolicyButton>
<RoleButton>Create policy</RoleButton>
</CreatePolicyButton>
<Footer>
<NextButton>Next: Tags</NextButton>
</Footer>
</>
);
if (props.stage >= Stage.RoleReview) {
content = (
<>
<Text fontSize={4} mb={4}>
Review
</Text>
<Section>
<SectionTitle>Role Name*</SectionTitle>
<SectionContent>
<SectionDropdown>
<SectionDropdownSelected>
{props.stage >= Stage.RoleHasName ? (
'SomeRoleName'
) : (
<>&nbsp;</>
)}
</SectionDropdownSelected>
</SectionDropdown>
</SectionContent>
</Section>
<Footer>
<NextButton>Create role</NextButton>
</Footer>
</>
);
}
if (
props.stage >= Stage.RoleTags &&
props.stage <= Stage.RoleClickNextReview
) {
content = (
<>
<Text fontSize={4} bold>
Add tags - <em>optional</em>
</Text>
<Flex mt={4}>
<RoleButton>Add tag</RoleButton>
</Flex>
<Footer>
<NextButton>Next: Review</NextButton>
</Footer>
</>
);
}
if (
props.stage >= Stage.AssignPolicyToRole &&
props.stage <= Stage.RoleClickNextTags
) {
content = (
<>
<SubHeader>Attach permissions policies</SubHeader>
<CreatePolicyButton>
<RoleButton>Create policy</RoleButton>
<RoleButton>Refresh</RoleButton>
</CreatePolicyButton>
<Policies>
<TableTitle>
<TableSearch>
{props.stage >= Stage.SearchForPolicy
? 'SomePolicyName'
: 'Search'}
</TableSearch>
</TableTitle>
{props.stage >= Stage.SearchForPolicy ? (
<div>
<TableHeader>Policy Name</TableHeader>
<TableItem selected={props.stage >= Stage.PolicySelected}>
<TableCheckBox />
SomePolicyName
</TableItem>
</div>
) : (
<div>
<TableHeader>Policy Name</TableHeader>
<TableItem>
<TableCheckBox />
AWSDefaultPolicy
</TableItem>
<TableItem>
<TableCheckBox />
AWSDefaultPolicy2
</TableItem>
<TableItem>
<TableCheckBox />
AWSDefaultPolicy3
</TableItem>
<TableItem>
<TableCheckBox />
AWSDefaultPolicy4
</TableItem>
</div>
)}
</Policies>
<Footer>
<NextButton>Next: Tags</NextButton>
</Footer>
</>
);
}
return (
<AWSWrapper>
<Page>
<Content>
<Header>Create role</Header>
{content}
</Content>
</Page>
</AWSWrapper>
);
}
@@ -0,0 +1,94 @@
/**
* 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 from 'react';
import Text from 'design/Text';
import Flex from 'design/Flex';
import Box from 'design/Box';
import { AWSWrapper } from './SharedComponents';
import {
Blur,
Content,
Page,
Sidebar,
SidebarLink,
SidebarLinkActive,
SidebarSectionTitle,
SidebarTitle,
Title,
} from './common';
export function IAMHomeScreen() {
return (
<AWSWrapper>
<Page>
<Sidebar>
<SidebarTitle>Identity and Access Management (IAM)</SidebarTitle>
<SidebarLinkActive>Dashboard</SidebarLinkActive>
<SidebarSectionTitle>Access Management</SidebarSectionTitle>
<SidebarLink>User groups</SidebarLink>
<SidebarLink>Users</SidebarLink>
<SidebarLink>Roles</SidebarLink>
<SidebarLink style={{ background: 'white' }}>
Identity providers
</SidebarLink>
<SidebarLink>Account settings</SidebarLink>
<SidebarSectionTitle>Access reports</SidebarSectionTitle>
<SidebarLink>Access analyzer</SidebarLink>
<SidebarLink>Credential report</SidebarLink>
<SidebarLink>Organization activity</SidebarLink>
</Sidebar>
<Content>
<Title>IAM Dashboard</Title>
<Blur>
<Text mb={3}>Some blurred text here. Some other text here.</Text>
<Text mb={5}>Some blurred text here</Text>
<Text>IAM Resources</Text>
<Flex justifyContent="space-between" mt={3}>
<Box>
User groups
<div style={{ marginTop: 10, fontSize: 28, color: '#1166bb' }}>
10
</div>
</Box>
<Box>
Users
<div style={{ marginTop: 10, fontSize: 28, color: '#1166bb' }}>
145
</div>
</Box>
<Box>
Identity providers
<div style={{ marginTop: 10, fontSize: 28, color: '#1166bb' }}>
0
</div>
</Box>
</Flex>
</Blur>
</Content>
</Page>
</AWSWrapper>
);
}
@@ -0,0 +1,145 @@
/**
* 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 from 'react';
import styled from 'styled-components';
import Flex from 'design/Flex';
import { Stage } from '../stages';
import { AWSWrapper, BreadcrumbArrow } from './SharedComponents';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbItemActive,
Content,
NextButton,
Page,
Sidebar,
SidebarLink,
SidebarLinkActive,
SidebarSectionTitle,
SidebarTitle,
Title,
} from './common';
import type { CommonIAMProps } from './common';
const Providers = styled.div`
display: flex;
margin-top: 20px;
width: calc(100% - 60px);
`;
const ProviderSection = styled.div`
flex: 1;
`;
const ProviderSectionTitle = styled.div`
background: linear-gradient(#eee, #e0e0e0);
padding: 5px 10px;
font-weight: 700;
`;
const ProviderTitle = styled.div`
padding: 5px 10px;
color: #16b;
`;
const ProviderSectionContent = styled.div`
padding: 5px 10px;
`;
export function IAMIdentityProvidersScreen(props: CommonIAMProps) {
if (props.stage >= Stage.ProviderAdded) {
return (
<AWSWrapper>
<Page>
<Content>
<Breadcrumb>
<BreadcrumbItem>IAM</BreadcrumbItem>
<BreadcrumbArrow />
<BreadcrumbItemActive>Identity providers</BreadcrumbItemActive>
</Breadcrumb>
<Flex mt={5} alignItems="center">
<Title style={{ marginBottom: 0 }}>Identity Providers (1)</Title>
</Flex>
<Providers>
<ProviderSection>
<ProviderSectionTitle>Provider</ProviderSectionTitle>
<ProviderTitle>{props.clusterPublicUri}</ProviderTitle>
</ProviderSection>
<ProviderSection>
<ProviderSectionTitle>Type</ProviderSectionTitle>
<ProviderSectionContent>OpenID Connect</ProviderSectionContent>
</ProviderSection>
<ProviderSection>
<ProviderSectionTitle>Creation time</ProviderSectionTitle>
<ProviderSectionContent>1 minute ago</ProviderSectionContent>
</ProviderSection>
</Providers>
</Content>
</Page>
</AWSWrapper>
);
}
return (
<AWSWrapper>
<Page>
<Sidebar>
<SidebarTitle>Identity and Access Management (IAM)</SidebarTitle>
<SidebarLink>Dashboard</SidebarLink>
<SidebarSectionTitle>Access Management</SidebarSectionTitle>
<SidebarLink>User groups</SidebarLink>
<SidebarLink>Users</SidebarLink>
<SidebarLink>Roles</SidebarLink>
<SidebarLinkActive>Identity providers</SidebarLinkActive>
<SidebarLink>Account settings</SidebarLink>
<SidebarSectionTitle>Access reports</SidebarSectionTitle>
<SidebarLink>Access analyzer</SidebarLink>
<SidebarLink>Credential report</SidebarLink>
<SidebarLink>Organization activity</SidebarLink>
</Sidebar>
<Content>
<Breadcrumb>
<BreadcrumbItem>IAM</BreadcrumbItem>
<BreadcrumbArrow />
<BreadcrumbItemActive>Identity providers</BreadcrumbItemActive>
</Breadcrumb>
<Flex mt={5} alignItems="center">
<Title style={{ marginBottom: 0 }}>Identity Providers (0)</Title>
<NextButton>Add provider</NextButton>
</Flex>
</Content>
</Page>
</AWSWrapper>
);
}
@@ -0,0 +1,178 @@
/**
* 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 from 'react';
import styled from 'styled-components';
import Box from 'design/Box';
import Flex from 'design/Flex';
import { Stage } from '../stages';
import { AWSWrapper, BreadcrumbArrow } from './SharedComponents';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbItemActive,
Content,
Page,
Sidebar,
SidebarLink,
SidebarLinkActive,
SidebarSectionTitle,
SidebarTitle,
Title,
} from './common';
import { OpenIDForm } from './OpenIDForm';
import type { CommonIAMProps } from './common';
const ProviderType = styled.div<{ active: boolean }>`
flex: 0 0 220px;
border: 1px solid ${p => (p.active ? '#3388dd' : '#cccccc')};
background: ${p => (p.active ? '#f0f9ff' : 'white')};
border-radius: 5px;
display: flex;
margin-right: 10px;
`;
const ProviderTypeIconContainer = styled.div`
flex: 0 0 30px;
display: flex;
justify-content: center;
padding-top: 12px;
`;
const ProviderTypeIcon = styled.div`
width: 10px;
height: 10px;
background: ${p => (p.active ? '#1066bb' : 'white')};
border: 1px solid ${p => (p.active ? '#1066bb' : '#cccccc')};
border-radius: 50%;
position: relative;
&:after {
content: '';
visibility: ${p => (p.active ? 'visible' : 'hidden')};
position: absolute;
width: 5px;
height: 5px;
background: white;
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
`;
const ProviderTypeInfo = styled.div`
padding: 10px 0;
font-size: 10px;
color: #999;
line-height: 12px;
`;
const ProviderTypeTitle = styled.div`
font-size: 14px;
line-height: 16px;
margin-bottom: 10px;
color: #444;
`;
const SlideOutPage = styled(Page)`
transition: 1s transform cubic-bezier(0.4, 0, 0.2, 1);
transform: translate3d(${p => (p.hidden ? -251 : 0)}px, 0, 0);
`;
export function IAMNewProviderScreen(props: CommonIAMProps) {
const sidebarHidden = props.stage !== Stage.NewProvider;
const openIDSelected = props.stage >= Stage.OpenIDConnectSelected;
return (
<AWSWrapper>
<SlideOutPage hidden={sidebarHidden}>
<Sidebar>
<SidebarTitle>Identity and Access Management (IAM)</SidebarTitle>
<SidebarLink>Dashboard</SidebarLink>
<SidebarSectionTitle>Access Management</SidebarSectionTitle>
<SidebarLink>User groups</SidebarLink>
<SidebarLink>Users</SidebarLink>
<SidebarLink>Roles</SidebarLink>
<SidebarLinkActive>Identity providers</SidebarLinkActive>
<SidebarLink>Account settings</SidebarLink>
<SidebarSectionTitle>Access reports</SidebarSectionTitle>
<SidebarLink>Access analyzer</SidebarLink>
<SidebarLink>Credential report</SidebarLink>
<SidebarLink>Organization activity</SidebarLink>
</Sidebar>
<Content>
<Breadcrumb>
<BreadcrumbItem>IAM</BreadcrumbItem>
<BreadcrumbArrow />
<BreadcrumbItem>Identity providers</BreadcrumbItem>
<BreadcrumbArrow />
<BreadcrumbItemActive>Create</BreadcrumbItemActive>
</Breadcrumb>
<Box mt={5}>
<Title>Add an Identity Provider </Title>
</Box>
<Flex>
<ProviderType active={!openIDSelected}>
<ProviderTypeIconContainer>
<ProviderTypeIcon active={!openIDSelected} />
</ProviderTypeIconContainer>
<ProviderTypeInfo>
<ProviderTypeTitle>SAML</ProviderTypeTitle>
Establish trust between your AWS account and a SAML 2.0
compatible Identity Provider such as Shibboleth or Active
Directory Federation Services.
</ProviderTypeInfo>
</ProviderType>
<ProviderType active={openIDSelected}>
<ProviderTypeIconContainer>
<ProviderTypeIcon active={openIDSelected} />
</ProviderTypeIconContainer>
<ProviderTypeInfo>
<ProviderTypeTitle>OpenID Connect</ProviderTypeTitle>
Establish trust between your AWS account and Identity Provider
services, such as Google or Salesforce.
</ProviderTypeInfo>
</ProviderType>
</Flex>
{openIDSelected && (
<OpenIDForm
stage={props.stage}
clusterPublicUri={props.clusterPublicUri}
/>
)}
</Content>
</SlideOutPage>
</AWSWrapper>
);
}
@@ -0,0 +1,143 @@
/**
* 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 from 'react';
import styled from 'styled-components';
import Flex from 'design/Flex';
import Box from 'design/Box';
import { Stage } from '../stages';
import { AWSWrapper, BreadcrumbArrow } from './SharedComponents';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbItemActive,
Content,
Page,
RoleButton,
Title,
} from './common';
import { AssignRoleModal } from './AssignRoleModal';
import type { CommonIAMProps } from './common';
const AWSHeader = styled.div`
font-weight: bold;
font-size: 18px;
margin-bottom: 10px;
display: flex;
`;
const AWSHeaderCount = styled.div`
font-weight: 400;
opacity: 0.5;
margin-left: 10px;
`;
const SummaryContent = styled.div`
display: flex;
border-top: 1px solid #cccccc;
padding-top: 10px;
margin-bottom: 40px;
`;
const SummaryItem = styled.div`
display: flex;
flex-direction: column;
min-width: 200px;
border-right: 1px solid #ccc;
padding-left: 10px;
&:first-of-type {
padding: 0;
}
&:last-of-type {
border-right: none;
}
`;
export function IAMProvider(props: CommonIAMProps) {
const showAssignRoleModal = props.stage >= Stage.ShowAssignRoleModal;
return (
<AWSWrapper>
<Page>
{showAssignRoleModal && (
<AssignRoleModal clusterPublicUri={props.clusterPublicUri} />
)}
<Content>
<Breadcrumb>
<BreadcrumbItem>IAM</BreadcrumbItem>
<BreadcrumbArrow />
<BreadcrumbItem>Identity providers</BreadcrumbItem>
<BreadcrumbArrow />
<BreadcrumbItemActive>
{props.clusterPublicUri}
</BreadcrumbItemActive>
</Breadcrumb>
<Flex
mt={5}
alignItems="center"
justifyContent="space-between"
mb={5}
>
<Title style={{ marginBottom: 0 }}>{props.clusterPublicUri}</Title>
<RoleButton>Assign role</RoleButton>
</Flex>
<AWSHeader>Summary</AWSHeader>
<SummaryContent>
<SummaryItem>
Provider
<div style={{ marginTop: 10 }}>{props.clusterPublicUri}</div>
</SummaryItem>
<SummaryItem>
Provider Type
<div style={{ marginTop: 10 }}>OpenID Connect</div>
</SummaryItem>
<SummaryItem>
Creation Time
<div style={{ marginTop: 10 }}>1 minute ago</div>
</SummaryItem>
</SummaryContent>
<Flex justifyContent="space-between">
<Box width="48%">
<AWSHeader>
Audiences <AWSHeaderCount>(1)</AWSHeaderCount>
</AWSHeader>
</Box>
<Box width="48%">
<AWSHeader>
Thumbprints <AWSHeaderCount>(1)</AWSHeaderCount>
</AWSHeader>
</Box>
</Flex>
</Content>
</Page>
</AWSWrapper>
);
}
@@ -0,0 +1,176 @@
/**
* 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 from 'react';
import styled from 'styled-components';
import { Stage } from '../stages';
import { AWSWrapper } from './SharedComponents';
import { Content, Page } from './common';
import type { CommonIAMProps } from './common';
const RolesSuccess = styled.div`
background: #019934;
color: white;
padding: 10px 15px;
font-size: 16px;
`;
const RolesSuccessLink = styled.span`
text-decoration: underline;
`;
const RolesHeader = styled.div`
background-image: linear-gradient(#fff, #eee);
padding: 20px 20px;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
font-weight: bold;
font-size: 22px;
`;
const PoliciesTableHeader = styled.div`
background-image: linear-gradient(#eee, #e0e0e0);
padding: 10px 45px;
font-weight: bold;
border: 1px solid #ccc;
border-top: none;
`;
const PoliciesCheckBox = styled.div`
width: 10px;
height: 10px;
margin-right: 20px;
border-radius: 3px;
`;
const PoliciesTableItem = styled.div<{ selected?: boolean }>`
display: flex;
align-items: center;
padding: 10px 15px;
border-bottom: 1px solid #cccccc;
background: ${p => (p.selected ? '#e6f3ff' : 'white')};
${PoliciesCheckBox} {
background: ${p => (p.selected ? '#1066bb' : 'white')};
border: 1px solid ${p => (p.selected ? '#1066bb' : '#cccccc')};
}
`;
const RoleName = styled.div`
font-size: 24px;
`;
const SummaryTitle = styled.div`
font-weight: bold;
font-size: 18px;
margin-top: 30px;
`;
const Summary = styled.div`
margin-top: 20px;
border-top: 1px solid #ccc;
display: flex;
`;
const SummarySection = styled.div`
margin-top: 5px;
flex: 1;
padding-top: 10px;
`;
const SectionLabel = styled.div`
font-size: 14px;
color: rgba(0, 0, 0, 0.6);
`;
const SectionValue = styled.div`
font-size: 16px;
`;
export function IAMRoles(props: CommonIAMProps) {
let content;
if (props.stage >= Stage.ListRoles && props.stage <= Stage.ClickRole) {
content = (
<div>
<RolesSuccess>
The role <RolesSuccessLink>SomeRoleName</RolesSuccessLink> has been
created
</RolesSuccess>
<Content>
<div>
<RolesHeader>Roles</RolesHeader>
<div>
<PoliciesTableHeader>Role Name</PoliciesTableHeader>
<PoliciesTableItem>
<PoliciesCheckBox />
AWSDefaultRole
</PoliciesTableItem>
<PoliciesTableItem>
<PoliciesCheckBox />
AWSDefaultRole2
</PoliciesTableItem>
<PoliciesTableItem>
<PoliciesCheckBox />
AWSDefaultRole3
</PoliciesTableItem>
<PoliciesTableItem>
<PoliciesCheckBox />
AWSDefaultRole4
</PoliciesTableItem>
</div>
</div>
</Content>
</div>
);
}
if (props.stage >= Stage.ViewRole) {
content = (
<Content>
<RoleName>SomeRoleName</RoleName>
<SummaryTitle>Summary</SummaryTitle>
<Summary>
<SummarySection style={{ borderRight: '1px solid #ccc' }}>
<SectionLabel>Creation Date</SectionLabel>
<SectionValue>Just now</SectionValue>
<SectionLabel>Lst activity</SectionLabel>
<SectionValue>None</SectionValue>
</SummarySection>
<SummarySection style={{ paddingLeft: '20px' }}>
<SectionLabel>ARN</SectionLabel>
<SectionValue>
arn:aws:iam::123456789:role/SomeRoleName
</SectionValue>
</SummarySection>
</Summary>
</Content>
);
}
return (
<AWSWrapper>
<Page>{content}</Page>
</AWSWrapper>
);
}
@@ -0,0 +1,148 @@
/**
* 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 from 'react';
import styled from 'styled-components';
import { Stage } from '../stages';
import { NextButton } from './common';
import type { CommonIAMProps } from './common';
const Form = styled.div`
margin-top: 20px;
display: flex;
flex-direction: column;
`;
const InputContainer = styled.div`
display: flex;
margin-bottom: 20px;
`;
const Button = styled.div`
background: linear-gradient(#fff, #dedede);
border: 1px solid #b8b8b8;
color: #444;
padding: 3px 7px;
border-radius: 5px;
font-weight: 700;
height: 32px;
box-sizing: border-box;
`;
const Input = styled.div`
font-size: 14px;
border-radius: 5px;
border: 1px solid #ccc;
padding: 3px 7px;
height: 32px;
box-sizing: border-box;
width: 300px;
margin-right: 15px;
background: ${p => (p.disabled ? '#eeeeee' : 'white')};
`;
const ThumbprintContainer = styled.div`
background: #eeeeee;
border: 1px solid #cccccc;
padding: 7px 10px;
display: flex;
width: 400px;
margin-bottom: 20px;
`;
const ThumbprintSection = styled.div`
margin-right: 60px;
`;
const Thumbprint = styled.span`
background: ${p => (p.selected ? '#add0f7' : 'none')};
`;
const Buttons = styled.div`
display: flex;
justify-content: flex-end;
width: calc(100% - 120px);
margin-top: 20px;
`;
export function OpenIDForm(props: CommonIAMProps) {
const providerURL =
props.stage >= Stage.PastedProviderURL
? `https://${props.clusterPublicUri}`
: 'https://';
const audience =
props.stage >= Stage.PastedAudience ? 'discover.teleport' : '';
let providerDisabled = false;
let buttonText = 'Get thumbprint';
if (props.stage >= Stage.ThumbprintLoading) {
buttonText = 'Loading...';
}
if (props.stage >= Stage.ThumbprintResult) {
buttonText = 'Edit URL';
providerDisabled = true;
}
let fingerprintResult;
if (props.stage >= Stage.ThumbprintResult) {
fingerprintResult = (
<div>
<strong>Verify thumbprint</strong>
<ThumbprintContainer>
<ThumbprintSection>
<div>Thumbprint</div>
<Thumbprint selected={props.stage === Stage.ThumbprintSelected}>
examplevaluehere
</Thumbprint>
</ThumbprintSection>
<ThumbprintSection>
<div>Issued By</div>
Example Inc
</ThumbprintSection>
</ThumbprintContainer>
</div>
);
}
return (
<Form>
<div>Provider URL</div>
<InputContainer>
<Input disabled={providerDisabled}>{providerURL}</Input>
<Button>{buttonText}</Button>
</InputContainer>
{fingerprintResult}
<div>Audience</div>
<InputContainer>
<Input>{audience}</Input>
</InputContainer>
<Buttons>
<NextButton>Add provider</NextButton>
</Buttons>
</Form>
);
}
@@ -0,0 +1,76 @@
/**
* 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 from 'react';
import styled from 'styled-components';
import { AWSIcon, ChevronRightIcon } from 'design/SVGIcon';
import useTeleport from 'teleport/useTeleport';
import { BreadcrumbIconContainer } from './common';
const Container = styled.div`
display: flex;
flex-direction: column;
height: 100%;
width: 630px;
overflow: hidden;
`;
const Header = styled.div`
background: #232e3e;
height: 32px;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
`;
const HeaderLogo = styled.div`
height: 25px;
`;
const HeaderUsername = styled.div`
color: white;
font-size: 12px;
`;
export function BreadcrumbArrow() {
return (
<BreadcrumbIconContainer>
<ChevronRightIcon fill={'rgba(0, 0, 0, 0.8)'} size={10} />
</BreadcrumbIconContainer>
);
}
export function AWSWrapper(props: React.PropsWithChildren<unknown>) {
const ctx = useTeleport();
return (
<Container>
<Header>
<HeaderLogo>
<AWSIcon size={24} />
</HeaderLogo>
<HeaderUsername>{ctx.storeUser.state.username}</HeaderUsername>
</Header>
{props.children}
</Container>
);
}
@@ -0,0 +1,222 @@
/**
* 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 styled from 'styled-components';
import { Stage } from '../stages';
export interface CommonIAMProps {
stage: Stage;
clusterPublicUri: string;
}
export const Header = styled.div`
font-size: 22px;
margin-bottom: 20px;
display: flex;
`;
export const Footer = styled.div`
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 46px;
border-top: 1px solid #dcdcdc;
display: flex;
justify-content: flex-end;
align-items: center;
padding-right: 30px;
`;
export const Page = styled.div`
display: flex;
flex: 1;
`;
export const Sidebar = styled.div`
display: flex;
flex-direction: column;
border-right: 1px solid rgba(0, 0, 0, 0.1);
flex: 0 0 250px;
width: 250px;
height: inherit;
`;
export const SidebarSectionTitle = styled.div`
font-weight: 700;
color: rgba(0, 0, 0, 0.5);
padding: 0 20px;
margin-top: 15px;
margin-bottom: 5px;
`;
export const SidebarTitle = styled.div`
font-size: 18px;
padding: 20px 20px;
font-weight: bold;
border-bottom: 1px solid rgba(0, 0, 0, 0.07);
margin-bottom: 20px;
`;
export const SidebarLink = styled.div`
padding: 0 20px;
margin-bottom: 5px;
position: relative;
z-index: 2;
`;
export const SidebarLinkActive = styled(SidebarLink)`
color: #0073bb;
font-weight: 500;
`;
export const Content = styled.div`
padding: 30px;
box-sizing: border-box;
flex: 0 0 630px;
width: 630px;
`;
export const Title = styled.div`
font-size: 18px;
margin-bottom: 30px;
`;
export const Blur = styled.div`
filter: blur(5px);
width: 100%;
`;
export const Breadcrumb = styled.div`
display: flex;
`;
export const BreadcrumbItem = styled.div`
color: #0073bb;
`;
export const BreadcrumbItemActive = styled(BreadcrumbItem)`
font-weight: 700;
color: #687078;
`;
export const BreadcrumbIconContainer = styled.div`
margin: 0 10px;
`;
export const NextButton = styled.div`
background: linear-gradient(#2c8bea, #1267bc);
color: white;
padding: 5px 15px;
font-size: 14px;
font-weight: 700;
border-radius: 4px;
border: 1px solid #1d67b3;
margin-left: 12px;
`;
export const RoleButton = styled.div`
background: linear-gradient(#fff, #dedede);
color: #444;
padding: 5px 10px;
font-weight: 700;
border-radius: 4px;
border: 1px solid #b8b8b8;
width: 100px;
text-align: center;
`;
export const Section = styled.div`
display: flex;
align-items: center;
margin-bottom: 20px;
`;
export const SectionTitle = styled.div`
font-weight: bold;
width: 100px;
text-align: right;
padding-right: 15px;
`;
export const SectionContent = styled.div`
display: flex;
align-items: center;
width: 300px;
position: relative;
`;
export const SectionDropdown = styled.div`
position: relative;
width: 300px;
`;
export const SectionDropdownSelected = styled.div`
border: 1px solid #ccc;
padding: 5px 10px;
border-radius: 4px;
`;
export const SubHeader = styled.div`
font-size: 20px;
margin-bottom: 15px;
display: flex;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
`;
export const TableTitle = styled.div`
background: #e3e3e3;
border: 1px solid #cccccc;
padding: 13px 10px;
`;
export const TableSearch = styled.div`
background: white;
border: 1px solid #cccccc;
border-radius: 3px;
width: 200px;
padding: 3px 10px;
`;
export const TableHeader = styled.div`
background-image: linear-gradient(#eee, #e0e0e0);
padding: 10px 45px;
font-weight: bold;
border: 1px solid #ccc;
border-top: none;
`;
export const TableCheckBox = styled.div`
width: 10px;
height: 10px;
margin-right: 20px;
border-radius: 3px;
`;
export const TableItem = styled.div<{ selected?: boolean }>`
display: flex;
align-items: center;
padding: 10px 15px;
border-bottom: 1px solid #cccccc;
background: ${p => (p.selected ? '#e6f3ff' : 'white')};
${TableCheckBox} {
background: ${p => (p.selected ? '#1066bb' : 'white')};
border: 1px solid ${p => (p.selected ? '#1066bb' : '#ccc')};
}
`;
@@ -0,0 +1,121 @@
/**
* 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 from 'react';
import { AddIcon, LockIcon } from 'design/SVGIcon';
import {
BrowserContainer,
BrowserContentContainer,
BrowserTab,
BrowserTabClose,
BrowserTabFavicon,
BrowserTabs,
BrowserTabTitle,
BrowserTitleBarButton,
BrowserTitleBarButtons,
BrowserTitleBarContainer,
BrowserURL,
BrowserURLContainer,
BrowserURLIcon,
} from 'teleport/Integrations/Enroll/AwsOidc/browser/BrowserComponents';
import { Stage } from 'teleport/Integrations/Enroll/AwsOidc/stages';
interface BrowserProps {
stage: Stage;
}
export function Browser(props: React.PropsWithChildren<BrowserProps>) {
const tabs = [];
if (props.stage >= Stage.CreateNewRole) {
tabs.push(
<BrowserTab
active={
props.stage <= Stage.ClickCreatePolicy ||
props.stage >= Stage.AssignPolicyToRole
}
key="create-role"
>
<BrowserTabFavicon />
<BrowserTabTitle>
{props.stage >= Stage.ListRoles ? 'Roles' : 'Create role'}
</BrowserTabTitle>
<BrowserTabClose>
<AddIcon size={14} />
</BrowserTabClose>
</BrowserTab>
);
}
if (
props.stage >= Stage.CreatePolicy &&
props.stage <= Stage.ClickCreatePolicyButton
) {
tabs.push(
<BrowserTab active={true} key="create-policy">
<BrowserTabFavicon />
<BrowserTabTitle>Create policy</BrowserTabTitle>
<BrowserTabClose>
<AddIcon size={14} />
</BrowserTabClose>
</BrowserTab>
);
}
//<BrowserTab active={true}>
// <BrowserTabFavicon />
// <BrowserTabTitle>Create policy</BrowserTabTitle>
// <BrowserTabClose>
// <AddIcon size={14} />
// </BrowserTabClose>
// </BrowserTab>
return (
<BrowserContainer>
<BrowserTitleBarContainer>
<BrowserTitleBarButtons>
<BrowserTitleBarButton style={{ backgroundColor: '#f95e57' }} />
<BrowserTitleBarButton style={{ backgroundColor: '#fbbe2e' }} />
<BrowserTitleBarButton style={{ backgroundColor: '#31c842' }} />
</BrowserTitleBarButtons>
<BrowserURLContainer>
<BrowserURL>
<BrowserURLIcon>
<LockIcon size={12} />
</BrowserURLIcon>
console.aws.amazon.com
</BrowserURL>
</BrowserURLContainer>
</BrowserTitleBarContainer>
<BrowserTabs>
<BrowserTab>
<BrowserTabFavicon />
<BrowserTabTitle>IAM Management Console</BrowserTabTitle>
<BrowserTabClose>
<AddIcon size={14} />
</BrowserTabClose>
</BrowserTab>
{tabs}
</BrowserTabs>
<BrowserContentContainer>{props.children}</BrowserContentContainer>
</BrowserContainer>
);
}
@@ -0,0 +1,129 @@
/**
* 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 styled from 'styled-components';
import favicon from './favicon.png';
export const BrowserContainer = styled.div`
position: relative;
border-radius: 5px;
width: 100%;
box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.43);
`;
export const BrowserTitleBarContainer = styled.div`
background: #040b1d;
height: 60px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
`;
export const BrowserTitleBarButtons = styled.div`
display: flex;
position: absolute;
top: 50%;
left: 10px;
transform: translate(0, -50%);
`;
export const BrowserTitleBarButton = styled.div`
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 5px;
`;
export const BrowserContentContainer = styled.div`
background: white;
color: black;
height: var(--content-height, 660px);
overflow-y: auto;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
`;
export const BrowserCode = styled.div`
font-size: 12px;
font-family: Menlo, DejaVu Sans Mono, Consolas, Lucida Console, monospace;
line-height: 20px;
white-space: pre-wrap;
`;
export const BrowserURLContainer = styled.div`
padding: 4px 10px;
width: 300px;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
`;
export const BrowserURL = styled.div`
display: flex;
align-items: center;
`;
export const BrowserURLIcon = styled.div`
margin-right: 5px;
top: 2px;
position: relative;
`;
export const BrowserTabs = styled.div`
display: flex;
background: #060b1d;
height: 36px;
`;
export const BrowserTabFavicon = styled.div`
margin-right: 10px;
width: 16px;
height: 16px;
background: url(${favicon}) no-repeat;
`;
export const BrowserTabClose = styled.div`
margin-left: 10px;
svg {
position: relative;
top: 2px;
transform: rotate(45deg);
}
`;
export const BrowserTab = styled.div<{ active: boolean }>`
display: flex;
align-items: center;
padding: 0 10px;
background: ${p => (p.active ? 'rgba(255, 255, 255, 0.1)' : 'none')};
border-top-left-radius: ${p => (p.active ? '7px' : '0')};
border-top-right-radius: ${p => (p.active ? '7px' : '0')};
margin-right: 10px;
${BrowserTabClose} {
display: ${p => (p.active ? 'block' : 'none')};
}
`;
export const BrowserTabTitle = styled.div`
font-size: 13px;
`;
@@ -0,0 +1,146 @@
/**
* 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, { useEffect, useState } from 'react';
import styled, { keyframes } from 'styled-components';
import { SVGIconProps } from 'design/SVGIcon/common';
const Container = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10;
pointer-events: none;
`;
const CursorContainer = styled.div`
position: absolute;
transition: 0.3s linear;
`;
interface CursorProps {
top: number;
left: number;
click: boolean;
}
const pulse = keyframes`
0% {
transform: scale(0.1, 0.1);
opacity: 0;
}
50% {
opacity: 1;
}
100% {
transform: scale(1.6, 1.6);
opacity: 0;
}
`;
const fadeIn = keyframes`
0% {
opacity: 0;
}
20% {
opacity: 1;
}
80% {
opacity: 1;
}
100% {
opacity: 0;
}
`;
export const Pulse = styled.div`
background: #abc6e4;
opacity: 0.5;
border-radius: 50%;
height: 14px;
width: 14px;
position: absolute;
left: 8px;
top: 3px;
z-index: 2;
animation: ${fadeIn} 2s ease-in forwards;
&:after {
content: '';
border-radius: 50%;
height: 40px;
width: 40px;
opacity: 0;
box-shadow: 0 0 1px 2px #abc6e4;
position: absolute;
margin: -13px 0 0 -13px;
animation: ${pulse} 2s ease-out infinite;
}
`;
export function Cursor(props: CursorProps) {
const [showPulse, setShowPulse] = useState(false);
useEffect(() => {
if (!props.click) {
return;
}
const id = window.setTimeout(() => setShowPulse(true), 1000);
const id2 = window.setTimeout(() => setShowPulse(false), 3000);
return () => {
clearTimeout(id);
clearTimeout(id2);
};
}, [props.top, props.left, props.click]);
return (
<Container>
<CursorContainer style={{ top: props.top, left: props.left }}>
<div style={{ position: 'relative', zIndex: 3 }}>
<CursorIcon />
</div>
{showPulse && props.click && <Pulse />}
</CursorContainer>
</Container>
);
}
function CursorIcon({ size = 40, fill = 'white' }: SVGIconProps) {
return (
<svg
viewBox="0 0 48 48"
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
fill={fill}
>
<path d="M27.8,39.7c-0.1,0-0.2,0-0.4-0.1c-0.2-0.1-0.4-0.3-0.6-0.5l-3.7-8.6l-4.5,4.2C18.5,34.9,18.3,35,18,35c-0.1,0-0.3,0-0.4-0.1C17.3,34.8,17,34.4,17,34l0-22c0-0.4,0.2-0.8,0.6-0.9C17.7,11,17.9,11,18,11c0.2,0,0.5,0.1,0.7,0.3l16,15c0.3,0.3,0.4,0.7,0.3,1.1c-0.1,0.4-0.5,0.6-0.9,0.7l-6.3,0.6l3.9,8.5c0.1,0.2,0.1,0.5,0,0.8c-0.1,0.2-0.3,0.5-0.5,0.6l-2.9,1.3C28.1,39.7,27.9,39.7,27.8,39.7z" />
<path
fill="#212121"
d="M18,12l16,15l-7.7,0.7l4.5,9.8l-2.9,1.3l-4.3-9.9L18,34L18,12 M18,10c-0.3,0-0.5,0.1-0.8,0.2c-0.7,0.3-1.2,1-1.2,1.8l0,22c0,0.8,0.5,1.5,1.2,1.8C17.5,36,17.8,36,18,36c0.5,0,1-0.2,1.4-0.5l3.4-3.2l3.1,7.3c0.2,0.5,0.6,0.9,1.1,1.1c0.2,0.1,0.5,0.1,0.7,0.1c0.3,0,0.5-0.1,0.8-0.2l2.9-1.3c0.5-0.2,0.9-0.6,1.1-1.1c0.2-0.5,0.2-1.1,0-1.5l-3.3-7.2l4.9-0.4c0.8-0.1,1.5-0.6,1.7-1.3c0.3-0.7,0.1-1.6-0.5-2.1l-16-15C19,10.2,18.5,10,18,10L18,10z"
/>
</svg>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,18 @@
/**
* 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.
*/
// export as default for use with React.lazy
export { AwsOidc as default } from './AwsOidc';
@@ -0,0 +1,65 @@
/**
* 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 from 'react';
import Text from 'design/Text';
import { ButtonPrimary } from 'design';
import Box from 'design/Box';
import { TextSelectCopyMulti } from 'teleport/components/TextSelectCopy';
import { InstructionsContainer } from './common';
import type { CommonInstructionsProps } from './common';
export function FifthStageInstructions(props: CommonInstructionsProps) {
const policy = `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "rds:DescribeDBInstances",
"Resource": "*"
}
]
}`;
return (
<InstructionsContainer>
<Text>
Select the <strong>JSON</strong> tab
</Text>
<Text mt={5}>
Replace the JSON with the following
<TextSelectCopyMulti lines={[{ text: policy }]} bash={false} />
</Text>
<Text mt={5}>
Click <strong>Next: Tags</strong> and then <strong>Next: Review</strong>
</Text>
<Text mt={5}>
Give the policy a name and then click <strong>Create policy</strong>
</Text>
<Box mt={5}>
<ButtonPrimary onClick={() => props.onNext()}>Next</ButtonPrimary>
</Box>
</InstructionsContainer>
);
}
@@ -0,0 +1,52 @@
/**
* 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 from 'react';
import styled from 'styled-components';
import Box from 'design/Box';
import { ButtonPrimary } from 'design';
import { InstructionsContainer } from './common';
import type { CommonInstructionsProps } from './common';
const InstructionBlock = styled.div`
margin-bottom: 30px;
`;
export function FirstStageInstructions(props: CommonInstructionsProps) {
return (
<InstructionsContainer>
<InstructionBlock>
To connect Teleport to AWS as an identity provider, go to the{' '}
<strong>AWS Management Console</strong>
</InstructionBlock>
<InstructionBlock>
Search for <strong>IAM</strong>, and then click on{' '}
<strong>Identity providers</strong>
</InstructionBlock>
<InstructionBlock>
After that, click on <strong>Add Provider</strong>
</InstructionBlock>
<Box mt={5}>
<ButtonPrimary onClick={() => props.onNext()}>Next</ButtonPrimary>
</Box>
</InstructionsContainer>
);
}
@@ -0,0 +1,48 @@
/**
* 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 from 'react';
import Text from 'design/Text';
import Box from 'design/Box';
import { ButtonPrimary } from 'design';
import { InstructionsContainer } from './common';
import type { CommonInstructionsProps } from './common';
export function FourthStageInstructions(props: CommonInstructionsProps) {
return (
<InstructionsContainer>
<Text>
Select <strong>discover.teleport</strong> as the audience for the role
</Text>
<Text mt={5}>
Then click on <strong>Next: Permissions</strong>
</Text>
<Text mt={5}>
From the permissions page, click on <strong>Create policy</strong>
</Text>
<Box mt={5}>
<ButtonPrimary onClick={() => props.onNext()}>Next</ButtonPrimary>
</Box>
</InstructionsContainer>
);
}
@@ -0,0 +1,68 @@
/**
* 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 from 'react';
import { MemoryRouter } from 'react-router';
import { FirstStageInstructions } from './FirstStageInstructions';
import { SecondStageInstructions } from './SecondStageInstructions';
import { ThirdStageInstructions } from './ThirdStageInstructions';
import { FourthStageInstructions } from './FourthStageInstructions';
import { FifthStageInstructions } from './FifthStageInstructions';
import { SixthStageInstructions } from './SixthStageInstructions';
import {
SeventhStageInstructions,
SuccessfullyAddedIntegrationDialog,
} from './SeventhStageInstructions';
export default {
title: 'Teleport/Integrations/Enroll/AwsOidc/Instructions',
};
export const Step1 = () => <FirstStageInstructions {...props} />;
export const Step2 = () => <SecondStageInstructions {...props} />;
export const Step3 = () => <ThirdStageInstructions {...props} />;
export const Step4 = () => <FourthStageInstructions {...props} />;
export const Step5 = () => <FifthStageInstructions {...props} />;
export const Step6 = () => <SixthStageInstructions {...props} />;
export const Step7 = () => (
<MemoryRouter>
<SeventhStageInstructions />
</MemoryRouter>
);
export const ConfirmDialog = () => (
<MemoryRouter>
<SuccessfullyAddedIntegrationDialog
discoverEventId=""
integrationName="some-integration-name"
/>
</MemoryRouter>
);
export const ConfirmDialogFromDiscover = () => (
<MemoryRouter>
<SuccessfullyAddedIntegrationDialog
discoverEventId="some-event-id"
integrationName="some-integration-name"
/>
</MemoryRouter>
);
const props = {
onNext: () => null,
clusterPublicUri: 'gravitationalwashington.cloud.gravitional.io:4444',
};
@@ -0,0 +1,112 @@
/**
* 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 Text from 'design/Text';
import Box from 'design/Box';
import { ButtonPrimary } from 'design';
import FieldInput from 'shared/components/FieldInput';
import Validation, { Validator } from 'shared/components/Validation';
import { requiredField } from 'shared/components/Validation/rules';
import { TextSelectCopyMulti } from 'teleport/components/TextSelectCopy';
import { InstructionsContainer } from './common';
import type { CommonInstructionsProps } from './common';
export function SecondStageInstructions(props: CommonInstructionsProps) {
const [thumbprint, setThumbprint] = useState('');
function handleSubmit(validator: Validator) {
if (!validator.validate()) {
return;
}
// TODO(lisa): validate thumbprint with the back.
// This is a nice to have, so not a blocker.
props.onNext();
}
return (
<InstructionsContainer>
<Text>
Now select <strong>OpenID Connect</strong>
</Text>
<Text mt={5}>
Copy the following into <strong>Provider URL</strong>
</Text>
<Box mt={5}>
<TextSelectCopyMulti
bash={false}
lines={[
{
text: `https://${props.clusterPublicUri}`,
},
]}
/>
</Box>
<Text mt={5}>
Copy the following into <strong>Audience</strong>
</Text>
<Box mt={5}>
<TextSelectCopyMulti
bash={false}
lines={[
{
text: 'discover.teleport',
},
]}
/>
</Box>
<Text mt={5}>
Then, click <strong>Get thumbprint</strong>
</Text>
<Text mt={5}>Paste the thumbprint below for verification</Text>
<Validation>
{({ validator }) => (
<>
<Box mt={2}>
<FieldInput
label="thumbprint"
onChange={e => setThumbprint(e.target.value)}
value={thumbprint}
placeholder="Paste the thumbprint here"
rule={requiredField('Thumbprint is required')}
/>
</Box>
<Box mt={5}>
<ButtonPrimary onClick={() => handleSubmit(validator)}>
Next
</ButtonPrimary>
</Box>
</>
)}
</Validation>
</InstructionsContainer>
);
}
@@ -0,0 +1,175 @@
/**
* 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 { useLocation } from 'react-router';
import { Link } from 'react-router-dom';
import { Danger } from 'design/Alert';
import { CircleCheck } from 'design/Icon';
import { ButtonPrimary, ButtonSecondary, Text, Flex, Box } from 'design';
import Dialog, {
DialogHeader,
DialogContent,
DialogFooter,
} from 'design/Dialog';
import FieldInput from 'shared/components/FieldInput';
import Validation, { Validator } from 'shared/components/Validation';
import useAttempt from 'shared/hooks/useAttemptNext';
import { requiredField } from 'shared/components/Validation/rules';
import {
IntegrationKind,
integrationService,
} from 'teleport/services/integrations';
import cfg from 'teleport/config';
import { InstructionsContainer } from './common';
export function SeventhStageInstructions() {
const location = useLocation<{ discoverEventId: string }>();
const { attempt, setAttempt } = useAttempt('');
const [showConfirmBox, setShowConfirmBox] = useState(false);
const [roleArn, setRoleArn] = useState('');
const [name, setName] = useState('');
function handleSubmit(validator: Validator) {
if (!validator.validate()) {
return;
}
setAttempt({ status: 'processing' });
integrationService
.createIntegration({
name,
subKind: IntegrationKind.AwsOidc,
awsoidc: { roleArn },
})
.then(() => setShowConfirmBox(true))
.catch((err: Error) =>
setAttempt({ status: 'failed', statusText: err.message })
);
}
return (
<InstructionsContainer>
{attempt.status === 'failed' && (
<Danger mb={5}>{attempt.statusText}</Danger>
)}
<Text>From the list of roles, select the role you just created</Text>
<Validation>
{({ validator }) => (
<>
<Text mt={5}>Copy the role ARN and paste it below</Text>
<Box mt={2}>
<FieldInput
label="Role ARN"
onChange={e => setRoleArn(e.target.value)}
value={roleArn}
placeholder="Role ARN"
rule={requiredField('Role ARN is required')}
/>
</Box>
<Text mt={5}>Give this AWS integration a name</Text>
<Box mt={2}>
<FieldInput
label="Name"
onChange={e => setName(e.target.value)}
value={name}
placeholder="Integration name"
rule={requiredField('Name is required')}
/>
</Box>
<Box mt={5}>
<ButtonPrimary
onClick={() => handleSubmit(validator)}
disabled={attempt.status === 'processing'}
>
Next
</ButtonPrimary>
</Box>
</>
)}
</Validation>
{showConfirmBox && (
<SuccessfullyAddedIntegrationDialog
discoverEventId={location.state?.discoverEventId}
integrationName={name}
/>
)}
</InstructionsContainer>
);
}
export function SuccessfullyAddedIntegrationDialog({
discoverEventId,
integrationName,
}: {
discoverEventId: string;
integrationName: string;
}) {
return (
<Dialog
dialogCss={() => ({ maxWidth: '500px', width: '100%' })}
disableEscapeKeyDown={true}
onClose={close}
open={true}
>
<DialogHeader css={{ margin: '0 auto' }}>
<CircleCheck mb={4} fontSize={60} color="success" />
</DialogHeader>
<DialogContent>
<Text textAlign="center">
AWS integration "{integrationName}" successfully added
</Text>
</DialogContent>
<DialogFooter css={{ margin: '0 auto' }}>
{discoverEventId ? (
<ButtonPrimary
size="large"
as={Link}
to={{
pathname: cfg.routes.discover,
state: {
integrationSubKind: IntegrationKind.AwsOidc,
integrationName,
discoverEventId,
},
}}
>
Begin RDS Enrollment
</ButtonPrimary>
) : (
<Flex gap="3">
<ButtonPrimary as={Link} to={cfg.routes.integrations} size="large">
Go to Integration List
</ButtonPrimary>
<ButtonSecondary
as={Link}
to={cfg.getIntegrationEnrollRoute(null)}
size="large"
>
Add Another Integration
</ButtonSecondary>
</Flex>
)}
</DialogFooter>
</Dialog>
);
}
@@ -0,0 +1,52 @@
/**
* 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 from 'react';
import Box from 'design/Box';
import Text from 'design/Text';
import { ButtonPrimary } from 'design';
import { InstructionsContainer } from './common';
import type { CommonInstructionsProps } from './common';
export function SixthStageInstructions(props: CommonInstructionsProps) {
return (
<InstructionsContainer>
<Text>Close the "Create policy tab"</Text>
<Text mt={5}>
Refresh the list of policies and select the policy you just created
</Text>
<Text mt={5}>Search for the policy you just created and select it</Text>
<Text mt={5}>
Click <strong>Next: Tags</strong> and then <strong>Next: Review</strong>
</Text>
<Text mt={5}>
Give the role a name and then click <strong>Create role</strong>
</Text>
<Box mt={5}>
<ButtonPrimary onClick={() => props.onNext()}>Next</ButtonPrimary>
</Box>
</InstructionsContainer>
);
}
@@ -0,0 +1,49 @@
/**
* 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 from 'react';
import Text from 'design/Text';
import Box from 'design/Box';
import { ButtonPrimary } from 'design';
import { InstructionsContainer } from './common';
import type { CommonInstructionsProps } from './common';
export function ThirdStageInstructions(props: CommonInstructionsProps) {
return (
<InstructionsContainer>
<Text>
Now click <strong>Add Provider</strong>
</Text>
<Text mt={5}>
Select the Identity provider that you just created{' '}
<strong>({props.clusterPublicUri})</strong>
</Text>
<Text mt={5}>
Select <strong>Assign role</strong> and create a new role
</Text>
<Box mt={5}>
<ButtonPrimary onClick={() => props.onNext()}>Next</ButtonPrimary>
</Box>
</InstructionsContainer>
);
}
@@ -0,0 +1,27 @@
/**
* 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 styled from 'styled-components';
export interface CommonInstructionsProps {
onNext: () => void;
clusterPublicUri: string;
}
export const InstructionsContainer = styled.div`
flex: 0 0 600px;
padding-right: 100px;
`;
@@ -0,0 +1,621 @@
/**
* 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.
*/
export enum Stage {
Initial,
ClickIdentityProviders,
IdentityProviders,
ClickAddProvider,
NewProvider,
NewProviderFullScreen,
SelectOpenIDConnect,
OpenIDConnectSelected,
ClickProviderURL,
PastedProviderURL,
ClickAudience,
PastedAudience,
ClickGetThumbprint,
ThumbprintLoading,
ThumbprintResult,
SelectThumbprint,
ThumbprintSelected,
AddProvider,
ProviderAdded,
SelectProvider,
ProviderView,
ClickAssignRole,
ShowAssignRoleModal,
ClickCreateNewRole,
CreateNewRole,
SelectAudienceDropdown,
ShowAudienceDropdown,
ClickDiscoverAudience,
DiscoverAudienceSelected,
ClickNextPermissions,
ConfigureRolePermissions,
ClickCreatePolicy,
CreatePolicy,
ClickJSONTab,
ShowJSONEditor,
SelectJSONContents,
JSONContentsSelected,
PolicyJSONPasted,
PolicyClickNextTags,
PolicyTags,
PolicyClickNextReview,
PolicyReview,
ClickPolicyName,
PolicyHasName,
ClickCreatePolicyButton,
AssignPolicyToRole,
ClickRefreshButton,
PoliciesLoaded,
ClickSearchBox,
SearchForPolicy,
SelectPolicy,
PolicySelected,
RoleClickNextTags,
RoleTags,
RoleClickNextReview,
RoleReview,
ClickRoleName,
RoleHasName,
ClickCreateRoleButton,
ListRoles,
ClickRole,
ViewRole,
}
interface StageItem {
kind: Stage;
cursor: {
top: number;
left: number;
click?: boolean;
};
duration?: number;
end?: boolean;
}
export const STAGES: StageItem[] = [
{
kind: Stage.Initial,
cursor: {
top: 236,
left: 200,
click: false,
},
duration: 2000,
},
{
kind: Stage.ClickIdentityProviders,
cursor: {
top: 401,
left: 60,
click: true,
},
duration: 2500,
},
{
kind: Stage.IdentityProviders,
cursor: {
top: 401,
left: 200,
},
duration: 2000,
},
{
kind: Stage.ClickAddProvider,
cursor: {
top: 221,
left: 530,
click: true,
},
duration: 1500,
},
{
kind: Stage.NewProvider,
cursor: {
top: 221,
left: 530,
},
end: true,
},
{
kind: Stage.NewProviderFullScreen,
cursor: {
top: 221,
left: 530,
},
duration: 1500,
},
{
kind: Stage.SelectOpenIDConnect,
cursor: {
top: 281,
left: 350,
click: true,
},
duration: 2000,
},
{
kind: Stage.OpenIDConnectSelected,
cursor: {
top: 281,
left: 350,
},
duration: 2000,
},
{
kind: Stage.ClickProviderURL,
cursor: {
top: 416,
left: 160,
click: true,
},
duration: 1500,
},
{
kind: Stage.PastedProviderURL,
cursor: {
top: 416,
left: 160,
},
duration: 1500,
},
{
kind: Stage.ClickAudience,
cursor: {
top: 491,
left: 160,
click: true,
},
duration: 1500,
},
{
kind: Stage.PastedAudience,
cursor: {
top: 491,
left: 160,
click: true,
},
duration: 2500,
},
{
kind: Stage.ClickGetThumbprint,
cursor: {
top: 416,
left: 400,
click: true,
},
duration: 1000,
},
{
kind: Stage.ThumbprintLoading,
cursor: {
top: 416,
left: 400,
},
duration: 1000,
},
{
kind: Stage.ThumbprintResult,
cursor: {
top: 416,
left: 400,
},
duration: 1500,
},
{
kind: Stage.SelectThumbprint,
cursor: {
top: 516,
left: 100,
click: true,
},
duration: 1500,
},
{
kind: Stage.ThumbprintSelected,
cursor: {
top: 516,
left: 100,
},
end: true,
},
{
kind: Stage.AddProvider,
cursor: {
top: 666,
left: 420,
click: true,
},
duration: 2000,
},
{
kind: Stage.ProviderAdded,
cursor: {
top: 516,
left: 300,
},
duration: 2000,
},
{
kind: Stage.SelectProvider,
cursor: {
top: 301,
left: 70,
click: true,
},
duration: 1500,
},
{
kind: Stage.ProviderView,
cursor: {
top: 301,
left: 70,
},
duration: 1500,
},
{
kind: Stage.ClickAssignRole,
cursor: {
top: 221,
left: 535,
click: true,
},
duration: 2500,
},
{
kind: Stage.ShowAssignRoleModal,
cursor: {
top: 221,
left: 535,
},
duration: 2500,
},
{
kind: Stage.ClickCreateNewRole,
cursor: {
top: 446,
left: 500,
click: true,
},
end: true,
},
{
kind: Stage.CreateNewRole,
cursor: {
top: 446,
left: 500,
},
duration: 2000,
},
{
kind: Stage.SelectAudienceDropdown,
cursor: {
top: 477,
left: 300,
click: true,
},
duration: 2000,
},
{
kind: Stage.ShowAudienceDropdown,
cursor: {
top: 477,
left: 300,
},
duration: 1000,
},
{
kind: Stage.ClickDiscoverAudience,
cursor: {
top: 512,
left: 300,
click: true,
},
duration: 2000,
},
{
kind: Stage.DiscoverAudienceSelected,
cursor: {
top: 512,
left: 300,
},
duration: 1000,
},
{
kind: Stage.ClickNextPermissions,
cursor: {
top: 722,
left: 500,
click: true,
},
duration: 2500,
},
{
kind: Stage.ConfigureRolePermissions,
cursor: {
top: 422,
left: 500,
},
duration: 2500,
},
{
kind: Stage.ClickCreatePolicy,
cursor: {
top: 256,
left: 75,
click: true,
},
end: true,
},
{
kind: Stage.CreatePolicy,
cursor: {
top: 256,
left: 75,
},
duration: 2500,
},
{
kind: Stage.ClickJSONTab,
cursor: {
top: 210,
left: 180,
click: true,
},
duration: 2500,
},
{
kind: Stage.ShowJSONEditor,
cursor: {
top: 210,
left: 180,
},
duration: 2500,
},
{
kind: Stage.SelectJSONContents,
cursor: {
top: 320,
left: 140,
click: true,
},
duration: 2500,
},
{
kind: Stage.JSONContentsSelected,
cursor: {
top: 320,
left: 140,
},
duration: 2500,
},
{
kind: Stage.PolicyJSONPasted,
cursor: {
top: 320,
left: 140,
},
duration: 2500,
},
{
kind: Stage.PolicyClickNextTags,
cursor: {
top: 722,
left: 530,
click: true,
},
duration: 2500,
},
{
kind: Stage.PolicyTags,
cursor: {
top: 722,
left: 530,
},
duration: 2500,
},
{
kind: Stage.PolicyClickNextReview,
cursor: {
top: 722,
left: 530,
click: true,
},
duration: 2500,
},
{
kind: Stage.PolicyReview,
cursor: {
top: 722,
left: 530,
},
duration: 2500,
},
{
kind: Stage.ClickPolicyName,
cursor: {
top: 262,
left: 230,
click: true,
},
duration: 2500,
},
{
kind: Stage.PolicyHasName,
cursor: {
top: 262,
left: 230,
},
duration: 2500,
},
{
kind: Stage.ClickCreatePolicyButton,
cursor: {
top: 722,
left: 530,
click: true,
},
end: true,
},
{
kind: Stage.AssignPolicyToRole,
cursor: {
top: 260,
left: 550,
},
duration: 1000,
},
{
kind: Stage.ClickRefreshButton,
cursor: {
top: 260,
left: 550,
click: true,
},
duration: 2500,
},
{
kind: Stage.PoliciesLoaded,
cursor: {
top: 260,
left: 550,
},
duration: 1500,
},
{
kind: Stage.ClickSearchBox,
cursor: {
top: 325,
left: 120,
click: true,
},
duration: 2500,
},
{
kind: Stage.SearchForPolicy,
cursor: {
top: 325,
left: 120,
},
duration: 1500,
},
{
kind: Stage.SelectPolicy,
cursor: {
top: 420,
left: 36,
click: true,
},
duration: 2500,
},
{
kind: Stage.PolicySelected,
cursor: {
top: 440,
left: 50,
},
duration: 1500,
},
{
kind: Stage.RoleClickNextTags,
cursor: {
top: 722,
left: 530,
click: true,
},
duration: 2500,
},
{
kind: Stage.RoleTags,
cursor: {
top: 722,
left: 530,
},
duration: 2500,
},
{
kind: Stage.RoleClickNextReview,
cursor: {
top: 722,
left: 530,
click: true,
},
duration: 2500,
},
{
kind: Stage.RoleReview,
cursor: {
top: 722,
left: 530,
},
duration: 2500,
},
{
kind: Stage.ClickRoleName,
cursor: {
top: 262,
left: 230,
click: true,
},
duration: 2500,
},
{
kind: Stage.RoleHasName,
cursor: {
top: 262,
left: 230,
},
duration: 2500,
},
{
kind: Stage.ClickCreateRoleButton,
cursor: {
top: 722,
left: 530,
click: true,
},
end: true,
},
{
kind: Stage.ListRoles,
cursor: {
top: 722,
left: 530,
},
duration: 2000,
},
{
kind: Stage.ClickRole,
cursor: {
top: 142,
left: 120,
click: true,
},
duration: 2500,
},
{
kind: Stage.ViewRole,
cursor: {
top: 182,
left: 150,
},
end: true,
},
];
@@ -17,14 +17,16 @@
import React from 'react';
import { MemoryRouter } from 'react-router';
import cfg from 'teleport/config';
import { IntegrationEnroll } from './IntegrationEnroll';
export default {
title: 'Teleport/Integrations',
title: 'Teleport/Integrations/Enroll',
};
export const Enroll = () => (
<MemoryRouter>
export const Picker = () => (
<MemoryRouter initialEntries={[cfg.routes.integrationEnroll]}>
<IntegrationEnroll />
</MemoryRouter>
);
@@ -22,13 +22,30 @@ import {
FeatureHeader,
FeatureHeaderTitle,
} from 'teleport/components/Layout';
import cfg from 'teleport/config';
import { Route, Switch } from 'teleport/components/Router';
import { IntegrationTiles } from './IntegrationTiles';
import { NoCodeIntegrationDescription } from './common';
import { getRoutesToEnrollIntegrations } from './IntegrationRoute';
export function IntegrationEnroll() {
return (
<FeatureBox>
<Switch>
{getRoutesToEnrollIntegrations()}
<Route
path={cfg.routes.integrationEnroll}
component={IntegrationPicker}
/>
</Switch>
</FeatureBox>
);
}
export function IntegrationPicker() {
return (
<>
<FeatureHeader>
<FeatureHeaderTitle>Select Integration Type</FeatureHeaderTitle>
</FeatureHeader>
@@ -36,6 +53,6 @@ export function IntegrationEnroll() {
<NoCodeIntegrationDescription />
<IntegrationTiles />
</Box>
</FeatureBox>
</>
);
}
@@ -0,0 +1,36 @@
/**
* 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, { lazy } from 'react';
import cfg from 'teleport/config';
import { Route } from 'teleport/components/Router';
import { IntegrationKind } from 'teleport/services/integrations';
const EnrollAwsOidc = lazy(
() => import(/* webpackChunkName: "enroll-aws-oidc" */ './AwsOidc')
);
export function getRoutesToEnrollIntegrations() {
return [
<Route
key={IntegrationKind.AwsOidc}
exact
path={cfg.getIntegrationEnrollRoute(IntegrationKind.AwsOidc)}
component={EnrollAwsOidc}
/>,
];
}
@@ -21,6 +21,7 @@ import awsIcon from 'design/assets/images/icons/aws.svg';
import cfg from 'teleport/config';
import { ToolTipNoPermBadge } from 'teleport/components/ToolTipNoPermBadge';
import { IntegrationKind } from 'teleport/services/integrations';
import { IntegrationTile } from './common';
@@ -35,7 +36,11 @@ export function IntegrationTiles({
<IntegrationTile
disabled={!hasAccess}
as={hasAccess ? Link : null}
to={hasAccess ? cfg.getIntegrationEnrollRoute('aws-oidc') : null}
to={
hasAccess
? cfg.getIntegrationEnrollRoute(IntegrationKind.AwsOidc)
: null
}
>
<Image mt={3} mb={2} src={awsIcon} width="80px" height="80px" />
<Text>
@@ -18,8 +18,6 @@ import React from 'react';
import { Flex, Text, Box } from 'design';
import styled from 'styled-components';
export type IntegrationTypes = 'aws-oidc';
export const IntegrationTile = styled(Flex).attrs({
'data-testid': 'tile',
})`
@@ -16,6 +16,7 @@
// export as default for use with React.lazy
export { IntegrationEnroll as default } from './IntegrationEnroll';
export { IntegrationTiles } from './IntegrationTiles';
export { IntegrationTiles } from './IntegrationTiles';
export { getRoutesToEnrollIntegrations } from './IntegrationRoute';
export * from './common';
@@ -29,6 +29,7 @@ import {
getStatusCodeTitle,
Integration,
IntegrationStatusCode,
IntegrationKind,
Plugin,
} from 'teleport/services/integrations';
@@ -168,7 +169,7 @@ const IconCell = ({ item }: { item: IntegrationLike }) => {
} else {
// Default is integration.
switch (item.kind) {
case 'aws-oidc':
case IntegrationKind.AwsOidc:
formattedText = 'Amazon Web Services (OIDC)';
icon = <IconContainer src={awsIcon} />;
break;
@@ -25,7 +25,6 @@ import {
} from 'teleport/components/Layout';
import useTeleport from 'teleport/useTeleport';
import { integrationService } from 'teleport/services/integrations';
import cfg from 'teleport/config';
import { IntegrationsAddButton } from './IntegrationsAddButton';
import { IntegrationList } from './IntegrationList';
@@ -40,9 +39,7 @@ export function Integrations() {
const canCreateIntegrations = ctx.storeUser.getIntegrationsAccess().create;
useEffect(() => {
run(() =>
integrationService.fetchIntegrations(cfg.proxyCluster).then(setItems)
);
run(() => integrationService.fetchIntegrations().then(setItems));
}, []);
return (
+4 -2
View File
@@ -615,10 +615,12 @@ const cfg = {
});
},
getIntegrationsUrl(clusterId: string, name?: string) {
getIntegrationsUrl(integrationName?: string) {
// Currently you can only create integrations at the root cluster.
const clusterId = cfg.proxyCluster;
return generateResourcePath(cfg.api.integrationsPath, {
clusterId,
name,
name: integrationName,
});
},
+3 -1
View File
@@ -105,7 +105,9 @@ const Integrations = React.lazy(
);
const IntegrationEnroll = React.lazy(
() =>
import(/* webpackChunkName: "integration-enroll" */ './IntegrationEnroll')
import(
/* webpackChunkName: "integration-enroll" */ '@gravitational/teleport/src/Integrations/Enroll'
)
);
// ****************************
+2 -1
View File
@@ -84,7 +84,8 @@ const baseContext = {
lastConnected: '2020-09-26T17:30:23.512876876Z',
status: 'online',
nodeCount: 1,
publicURL: 'localhost',
publicURL:
'some-long-cluster-public-url-name.cloud.teleport.gravitational.io:1234',
authVersion: '4.4.0-dev',
proxyVersion: '4.4.0-dev',
},
@@ -17,17 +17,31 @@
import api from 'teleport/services/api';
import cfg from 'teleport/config';
import { Integration, IntegrationStatusCode } from './types';
import {
Integration,
IntegrationCreateRequest,
IntegrationStatusCode,
} from './types';
export const integrationService = {
fetchIntegration(clusterId: string, name: string): Promise<Integration> {
return api
.get(cfg.getIntegrationsUrl(clusterId, name))
.then(makeIntegration);
fetchIntegration(name: string): Promise<Integration> {
return api.get(cfg.getIntegrationsUrl(name)).then(makeIntegration);
},
fetchIntegrations(clusterId: string): Promise<Integration[]> {
return api.get(cfg.getIntegrationsUrl(clusterId)).then(makeIntegrations);
fetchIntegrations(): Promise<Integration[]> {
return api.get(cfg.getIntegrationsUrl()).then(makeIntegrations);
},
createIntegration(req: IntegrationCreateRequest): Promise<void> {
return api.post(cfg.getIntegrationsUrl(), req);
},
updateIntegration(name: string): Promise<void> {
return api.put(cfg.getIntegrationsUrl(name));
},
deleteIntegration(name: string): Promise<void> {
return api.delete(cfg.getIntegrationsUrl(name));
},
};
@@ -40,7 +40,12 @@ export type Integration<
details?: string;
statusCode: IntegrationStatusCode;
};
export type IntegrationKind = 'aws-oidc';
// IntegrationKind string values should be in sync
// with the backend value for defining the integration
// resource's subKind field.
export enum IntegrationKind {
AwsOidc = 'aws-oidc',
}
export type IntegrationSpecAwsOidc = {
roleArn: string;
};
@@ -86,3 +91,9 @@ export function getStatusCodeDescription(
export type Plugin = Integration<'plugin', PluginKind, PluginSpec>;
export type PluginSpec = Record<string, never>; // currently no 'spec' fields exposed to the frontend
export type PluginKind = 'slack';
export type IntegrationCreateRequest = {
name: string;
subKind: IntegrationKind;
awsoidc?: IntegrationSpecAwsOidc;
};