+ Please{' '}
+
+ view our documentation
+ {' '}
+ on how to configure a GitHub connector.
+
+
)}
{(resources.status === 'creating' || resources.status === 'editing') && (
diff --git a/web/packages/teleport/src/AuthConnectors/ConnectorList/CTAConnectors.tsx b/web/packages/teleport/src/AuthConnectors/ConnectorList/CTAConnectors.tsx
new file mode 100644
index 0000000000000..117b02e4236d6
--- /dev/null
+++ b/web/packages/teleport/src/AuthConnectors/ConnectorList/CTAConnectors.tsx
@@ -0,0 +1,120 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import styled from 'styled-components';
+
+import Box from 'design/Box';
+import { ResourceIcon } from 'design/ResourceIcon';
+import { H2, Subtitle2 } from 'design/Text';
+import Flex from 'design/Flex';
+import { RocketLaunch } from 'design/Icon';
+
+import { ButtonLockedFeature } from 'teleport/components/ButtonLockedFeature';
+import { CtaEvent } from 'teleport/services/userEvent';
+
+export default function CTAConnectors() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Unlock OIDC & SAML Single Sign-On with Teleport Enterprise
+
+ Connect your identity provider to streamline employee onboarding,
+ simplify compliance, and monitor access patterns against a single
+ source of truth.
+
+
+
+ Upgrade to Enterprise
+
+
+
+ );
+}
+
+const AuthConnectorsCTABox = styled(Box)`
+ width: 100%;
+ padding: ${p => p.theme.space[5]}px;
+
+ border: ${props => props.theme.borders[1]};
+ border-color: ${props => props.theme.colors.interactive.tonal.neutral[1]};
+ border-radius: ${props => props.theme.radii[2]}px;
+ background: transparent;
+
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: ${p => p.theme.space[3]}px;
+ align-self: stretch;
+`;
+
+const CTALogosContainer = styled(Box)`
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ // We have to manually set a height here since the CTALogo's are 'position: absolute' and won't automatically size the container.
+ //padding (top and bottom) + height of the icon
+ height: ${p => p.theme.space[4] * 2 + 36}px;
+
+ position: relative;
+`;
+
+const CTALogo = styled(Box)`
+ background-color: ${props => props.theme.colors.levels.sunken};
+ padding: ${p => p.theme.space[4]}px;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ border: ${props => props.theme.borders[1]};
+ border-color: ${props => props.theme.colors.interactive.tonal.neutral[1]};
+ border-radius: 50%;
+
+ position: absolute;
+`;
diff --git a/web/packages/teleport/src/AuthConnectors/ConnectorList/ConnectorList.tsx b/web/packages/teleport/src/AuthConnectors/ConnectorList/ConnectorList.tsx
index 39544ef9bda3b..8f3605b4044a1 100644
--- a/web/packages/teleport/src/AuthConnectors/ConnectorList/ConnectorList.tsx
+++ b/web/packages/teleport/src/AuthConnectors/ConnectorList/ConnectorList.tsx
@@ -16,23 +16,31 @@
* along with this program. If not, see .
*/
-import { Box, ButtonPrimary, Flex, ResourceIcon, Text } from 'design';
-import { MenuIcon, MenuItem } from 'shared/components/MenuAction';
+import styled from 'styled-components';
-import { State as ResourceState } from 'teleport/components/useResources';
+import { Box } from 'design';
-import { ResponsiveConnector } from 'teleport/AuthConnectors/styles/ConnectorBox.styles';
+import { State as ResourceState } from 'teleport/components/useResources';
import { State as AuthConnectorState } from '../useAuthConnectors';
+import { AuthConnectorTile, LocalConnectorTile } from '../AuthConnectorTile';
+import getSsoIcon from '../ssoIcons/getSsoIcon';
export default function ConnectorList({ items, onEdit, onDelete }: Props) {
items = items || [];
const $items = items.map(item => {
- const { id, name } = item;
+ const { id, name, kind } = item;
+
+ const Icon = getSsoIcon(kind, name);
+
return (
-
+
+
{$items}
-
- );
-}
-
-function ConnectorListItem({ name, id, onEdit, onDelete }) {
- const onClickEdit = () => onEdit(id);
- const onClickDelete = () => onDelete(id);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- {name}
-
-
-
- Edit Connector
-
-
+
);
}
-const menuActionProps = {
- style: {
- right: '10px',
- position: 'absolute',
- top: '10px',
- },
-};
-
type Props = {
items: AuthConnectorState['items'];
onEdit: ResourceState['edit'];
onDelete: ResourceState['remove'];
};
+
+export const AuthConnectorsGrid = styled(Box)`
+ width: 100%;
+ display: grid;
+ gap: ${p => p.theme.space[3]}px;
+ grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
+`;
diff --git a/web/packages/teleport/src/AuthConnectors/EmptyList/EmptyList.tsx b/web/packages/teleport/src/AuthConnectors/EmptyList/EmptyList.tsx
index b3dd5ea561647..0751eb31347f8 100644
--- a/web/packages/teleport/src/AuthConnectors/EmptyList/EmptyList.tsx
+++ b/web/packages/teleport/src/AuthConnectors/EmptyList/EmptyList.tsx
@@ -16,78 +16,27 @@
* along with this program. If not, see .
*/
-import { Card, Flex, H1, ResourceIcon, Text } from 'design';
-import { AuthProviderType } from 'shared/services';
-
-import { H2 } from 'design';
-
-import { ConnectorBox } from 'teleport/AuthConnectors/styles/ConnectorBox.styles';
-
-import {
- LockedFeatureButton,
- LockedFeatureContainer,
-} from 'teleport/AuthConnectors/styles/LockedFeatureContainer.styles';
-
import getSsoIcon from 'teleport/AuthConnectors/ssoIcons/getSsoIcon';
import { State as ResourceState } from 'teleport/components/useResources';
-import { CtaEvent } from 'teleport/services/userEvent';
-
-export default function EmptyList({ onCreate }: Props) {
- return (
-
-
- {
-
- Sign in using your GitHub account
-
- }
-
- );
-}
-
-function renderLockedItem(kind: AuthProviderType) {
- const { desc, SsoIcon, info } = getSsoIcon(kind);
+export default function EmptyList({ onCreate }: Props) {
return (
-
-
-
-
-
{desc}
- {info && (
-
- {info}
-
- )}
-
+
+
+ onCreate('github')}
+ />
+
);
}
diff --git a/web/packages/teleport/src/AuthConnectors/ssoIcons/getSsoIcon.tsx b/web/packages/teleport/src/AuthConnectors/ssoIcons/getSsoIcon.tsx
index 27fae0e56fd9a..fbb116b59f3f2 100644
--- a/web/packages/teleport/src/AuthConnectors/ssoIcons/getSsoIcon.tsx
+++ b/web/packages/teleport/src/AuthConnectors/ssoIcons/getSsoIcon.tsx
@@ -20,93 +20,132 @@ import styled from 'styled-components';
import { AuthProviderType } from 'shared/services';
import { Box, Flex, ResourceIcon } from 'design';
-export default function getSsoIcon(kind: AuthProviderType) {
- const desc = formatConnectorTypeDesc(kind);
+export default function getSsoIcon(
+ kind: AuthProviderType,
+ name?: string
+): () => JSX.Element {
+ const guessedIcon = guessIconFromName(name || '');
+ if (guessedIcon) {
+ return guessedIcon;
+ }
switch (kind) {
case 'github':
- return {
- SsoIcon: () => (
-
-
-
- ),
- desc,
- info: 'Sign in using your GitHub account',
- };
+ return () => (
+
+
+
+ );
case 'saml':
- return {
- SsoIcon: () => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ),
- desc,
- info: 'Okta, OneLogin, Microsoft Entra ID, etc.',
- };
+ return () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
case 'oidc':
default:
- return {
- SsoIcon: () => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ),
- desc,
- info: 'Google, GitLab, Amazon and more',
- };
+ return () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
}
}
-function formatConnectorTypeDesc(kind) {
- kind = kind || '';
- if (kind == 'github') {
- return `GitHub`;
+function guessIconFromName(connectorName: string) {
+ const name = connectorName.toLocaleLowerCase();
+
+ if (name.includes('okta')) {
+ return () => (
+
+
+
+ );
+ }
+ if (
+ name.includes('entra') ||
+ name.includes('active directory') ||
+ name.includes('microsoft') ||
+ name.includes('azure')
+ ) {
+ return () => (
+
+
+
+ );
+ }
+ if (name.includes('google')) {
+ return () => (
+
+
+
+ );
+ }
+ if (name.includes('gitlab')) {
+ return () => (
+
+
+
+ );
+ }
+ if (name.includes('onelogin')) {
+ return () => (
+
+
+
+ );
+ }
+ if (name.includes('auth0') || name.includes('authzero')) {
+ return () => (
+
+
+
+ );
}
- return kind.toUpperCase();
}
const MultiIconContainer = styled(Flex)`
- width: 83px;
+ width: 61px;
+ height: 61px;
flex-wrap: wrap;
gap: 3px;
- padding: 7px;
- border: 1px solid rgba(255, 255, 255, 0.07);
+ padding: ${p => p.theme.space[2]}px;
+ border: 1px solid ${p => p.theme.colors.interactive.tonal.neutral[2]};
border-radius: 8px;
`;
const SmIcon = styled(Box)`
- width: ${p => p.theme.space[5]}px;
- height: ${p => p.theme.space[5]}px;
- line-height: ${p => p.theme.space[5]}px;
- background: ${p => p.theme.colors.levels.popout};
- border-radius: 50%;
+ width: 20px;
+ height: 20px;
display: flex;
justify-content: center;
+ align-items: center;
`;
const StyledResourceIcon = styled(ResourceIcon).attrs({
width: '20px',
-})``;
+})`
+ line-height: 0px !important;
+`;
diff --git a/web/packages/teleport/src/AuthConnectors/styles/ConnectorBox.styles.ts b/web/packages/teleport/src/AuthConnectors/styles/ConnectorBox.styles.ts
deleted file mode 100644
index f88e5e7ad5646..0000000000000
--- a/web/packages/teleport/src/AuthConnectors/styles/ConnectorBox.styles.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * Teleport
- * Copyright (C) 2023 Gravitational, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-
-import { Box, Flex } from 'design';
-import styled from 'styled-components';
-
-export const ConnectorBox = styled(Box)`
- display: flex;
- flex-direction: column;
- font-family: ${props => props.theme.font};
- width: 320px;
- padding: ${p => p.theme.space[4]}px;
- margin: ${p => p.theme.space[3]}px ${p => p.theme.space[2]}px;
- background: transparent;
- transition: all 0.3s;
- border-radius: ${props => props.theme.radii[2]}px;
- min-height: 190px;
- border: ${props => props.theme.borders[2]}
- ${p => p.theme.colors.spotBackground[0]};
-
- &:hover,
- &:focus {
- border: ${props => props.theme.borders[2]}
- ${p => p.theme.colors.spotBackground[2]};
- background: ${p => p.theme.colors.spotBackground[0]};
- box-shadow: ${p => p.theme.boxShadow[3]};
- cursor: pointer;
- }
-
- &:disabled {
- cursor: not-allowed;
- color: inherit;
- font-family: inherit;
- outline: none;
- position: relative;
- text-align: center;
- text-decoration: none;
- opacity: 0.24;
- box-shadow: none;
- }
-`;
-
-export const ResponsiveConnector = styled(Flex)`
- position: relative;
- box-shadow: ${p => p.theme.boxShadow[5]};
- width: 240px;
- height: 240px;
- border-radius: ${props => props.theme.radii[2]}px;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- background-color: ${props => props.theme.colors.levels.surface};
- padding: ${props => props.theme.space[5]}px;
- @media screen and (max-width: ${props => props.theme.breakpoints.tablet}px) {
- width: 100%;
- }
-`;
diff --git a/web/packages/teleport/src/Discover/Shared/ResourceLabelTooltip/ResourceLabelTooltip.story.tsx b/web/packages/teleport/src/Discover/Shared/ResourceLabelTooltip/ResourceLabelTooltip.story.tsx
new file mode 100644
index 0000000000000..67aa0e695d529
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Shared/ResourceLabelTooltip/ResourceLabelTooltip.story.tsx
@@ -0,0 +1,29 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { ResourceLabelTooltip } from './ResourceLabelTooltip';
+
+export default {
+ title: 'Teleport/Discover/Shared/ResourceLabelTooltip',
+};
+
+export const RDS = () => ;
+export const EKS = () => ;
+export const Server = () => ;
+export const Database = () => ;
+export const Kube = () => ;
diff --git a/web/packages/teleport/src/Discover/Shared/ResourceLabelTooltip/ResourceLabelTooltip.tsx b/web/packages/teleport/src/Discover/Shared/ResourceLabelTooltip/ResourceLabelTooltip.tsx
new file mode 100644
index 0000000000000..2e7ad2d1980aa
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Shared/ResourceLabelTooltip/ResourceLabelTooltip.tsx
@@ -0,0 +1,147 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import Link from 'design/Link';
+import { MarkInverse } from 'design/Mark';
+import { Position } from 'design/Popover/Popover';
+import { IconTooltip } from 'design/Tooltip';
+import styled from 'styled-components';
+
+/**
+ * Returns a IconTooltip component with its tip contents
+ * set to the requested resource kind.
+ *
+ * @param resourceKind - the tip contents differ slightly depending
+ * on the resource kind
+ * @param toolTipPosition (opt) - the position which the tooltip should
+ * render (see type Position)
+ * @returns JSX Element
+ */
+export function ResourceLabelTooltip({
+ resourceKind,
+ toolTipPosition,
+}: {
+ resourceKind: 'server' | 'eks' | 'rds' | 'kube' | 'db';
+ toolTipPosition?: Position;
+}) {
+ let tip;
+
+ switch (resourceKind) {
+ case 'server': {
+ tip = (
+ <>
+ Labels allow you to do the following:
+
+
+ Filter servers by labels when using tsh, tctl, or the web UI.
+
+
+ Restrict access to this server with{' '}
+
+ Teleport RBAC
+
+ . Only roles with node_labels that
+ match these labels will be allowed to access this server.
+
+
+ >
+ );
+ break;
+ }
+ case 'kube':
+ case 'eks': {
+ tip = (
+ <>
+ Labels allow you to do the following:
+
+
+ Filter Kubernetes clusters by labels when using tsh, tctl, or the
+ web UI.
+
+
+ Restrict access to this Kubernetes cluster with{' '}
+
+ Teleport RBAC
+
+ . Only roles with kubernetes_labels{' '}
+ that match these labels will be allowed to access this Kubernetes
+ cluster.
+
+ {resourceKind === 'eks' && (
+
+ All the AWS tags from the selected EKS will be included upon
+ enrollment.
+
+ )}
+
+ >
+ );
+ break;
+ }
+ case 'rds':
+ case 'db': {
+ tip = (
+ <>
+ Labels allow you to do the following:
+
+
+ Filter databases by labels when using tsh, tctl, or the web UI.
+
+
+ Restrict access to this database with{' '}
+
+ Teleport RBAC
+
+ . Only roles with db_labels that match
+ these labels will be allowed to access this database.
+
+ {resourceKind === 'rds' && (
+
+ All the AWS tags from the selected RDS will be included upon
+ enrollment.
+
+ )}
+
+ >
+ );
+ break;
+ }
+ default:
+ resourceKind satisfies never;
+ }
+
+ return (
+
+ {tip}
+
+ );
+}
+
+const Ul = styled.ul`
+ margin: 0;
+ padding-left: ${p => p.theme.space[4]}px;
+`;
diff --git a/web/packages/teleport/src/Discover/Shared/ResourceLabelTooltip/index.ts b/web/packages/teleport/src/Discover/Shared/ResourceLabelTooltip/index.ts
new file mode 100644
index 0000000000000..fd093b71e1080
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Shared/ResourceLabelTooltip/index.ts
@@ -0,0 +1,19 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+export { ResourceLabelTooltip } from './ResourceLabelTooltip';
diff --git a/web/packages/teleport/src/JoinTokens/JoinTokens.tsx b/web/packages/teleport/src/JoinTokens/JoinTokens.tsx
index 862085cfd0157..1dbcc8b725ad2 100644
--- a/web/packages/teleport/src/JoinTokens/JoinTokens.tsx
+++ b/web/packages/teleport/src/JoinTokens/JoinTokens.tsx
@@ -446,7 +446,7 @@ const ActionCell = ({
function Directions() {
return (
<>
- WARNING Roles are defined using{' '}
+ WARNING Tokens are defined using{' '}
.
*/
+import { useCallback, useEffect } from 'react';
import styled from 'styled-components';
-import { Flex, Box } from 'design';
-
+import { Flex, Box, Indicator } from 'design';
import { Danger } from 'design/Alert';
-import { useParams, useLocation } from 'teleport/components/Router';
+import { makeSuccessAttempt, useAsync } from 'shared/hooks/useAsync';
+import { useParams, useLocation } from 'teleport/components/Router';
import session from 'teleport/services/websession';
import { UrlPlayerParams } from 'teleport/config';
import { getUrlParameter } from 'teleport/services/history';
-
import { RecordingType } from 'teleport/services/recordings';
+import useTeleport from 'teleport/useTeleport';
+
import ActionBar from './ActionBar';
import { DesktopPlayer } from './DesktopPlayer';
import SshPlayer from './SshPlayer';
@@ -38,19 +40,44 @@ import Tabs, { TabItem } from './PlayerTabs';
const validRecordingTypes = ['ssh', 'k8s', 'desktop', 'database'];
export function Player() {
+ const ctx = useTeleport();
const { sid, clusterId } = useParams();
const { search } = useLocation();
+ useEffect(() => {
+ document.title = `Play ${sid} • ${clusterId}`;
+ }, [sid, clusterId]);
+
const recordingType = getUrlParameter(
'recordingType',
search
) as RecordingType;
- const durationMs = Number(getUrlParameter('durationMs', search));
+
+ // In order to render the progress bar, we need to know the length of the session.
+ // All in-product links to the session player should include the session duration in the URL.
+ // Some users manually build the URL based on the session ID and don't specify the session duration.
+ // For those cases, we make a separate API call to get the duration.
+ const [fetchDurationAttempt, fetchDuration] = useAsync(
+ useCallback(
+ () => ctx.recordingsService.fetchRecordingDuration(clusterId, sid),
+ [ctx.recordingsService, clusterId, sid]
+ )
+ );
const validRecordingType = validRecordingTypes.includes(recordingType);
- const validDurationMs = Number.isInteger(durationMs) && durationMs > 0;
+ const durationMs = Number(getUrlParameter('durationMs', search));
+ const shouldFetchSessionDuration =
+ validRecordingType && (!Number.isInteger(durationMs) || durationMs <= 0);
+
+ useEffect(() => {
+ if (shouldFetchSessionDuration) {
+ fetchDuration();
+ }
+ }, [fetchDuration, shouldFetchSessionDuration]);
- document.title = `Play ${sid} • ${clusterId}`;
+ const combinedAttempt = shouldFetchSessionDuration
+ ? fetchDurationAttempt
+ : makeSuccessAttempt({ durationMs });
function onLogout() {
session.logout();
@@ -69,13 +96,25 @@ export function Player() {
);
}
- if (!validDurationMs) {
+ if (
+ combinedAttempt.status === '' ||
+ combinedAttempt.status === 'processing'
+ ) {
+ return (
+
+
+
+
+
+ );
+ }
+ if (combinedAttempt.status === 'error') {
return (
- Invalid query parameter durationMs:{' '}
- {getUrlParameter('durationMs', search)}, should be an integer.
+ Unable to determine the length of this session. The session
+ recording may be incomplete or corrupted.
@@ -101,15 +140,20 @@ export function Player() {
) : (
-
+
)}
);
}
+
const StyledPlayer = styled.div`
display: flex;
height: 100%;
diff --git a/web/packages/teleport/src/Support/Support.tsx b/web/packages/teleport/src/Support/Support.tsx
index 59012984cb941..7bff9ae9e7ec9 100644
--- a/web/packages/teleport/src/Support/Support.tsx
+++ b/web/packages/teleport/src/Support/Support.tsx
@@ -96,6 +96,27 @@ export const Support = ({
{isEnterprise && !cfg.isCloud && licenseExpiryDateText && (
)}
+ {isCloud && (
+
+
+
+ Looking for{' '}
+
+ Scheduled Upgrades?
+ {' '}
+ It is now in{' '}
+
+ Cluster Management
+ {' '}
+ page.
+
+
+ )}
diff --git a/web/packages/teleport/src/Users/UserReset/UserReset.story.tsx b/web/packages/teleport/src/Users/UserReset/UserReset.story.tsx
index fd74a04b68ded..3a68f43ac6a8f 100644
--- a/web/packages/teleport/src/Users/UserReset/UserReset.story.tsx
+++ b/web/packages/teleport/src/Users/UserReset/UserReset.story.tsx
@@ -16,40 +16,69 @@
* along with this program. If not, see .
*/
+import { Meta } from '@storybook/react';
+
+import { Attempt } from 'shared/hooks/useAttemptNext';
+import { useEffect, useState } from 'react';
+
+import cfg from 'teleport/config';
+
import { UserReset } from './UserReset';
-export default {
- title: 'Teleport/Users/UserReset',
+type StoryProps = {
+ status: 'processing' | 'success' | 'error';
+ isMfaEnabled: boolean;
+ allowPasswordless: boolean;
};
-export const Processing = () => {
- return ;
+const meta: Meta = {
+ title: 'Teleport/Users/UserReset',
+ component: Story,
+ argTypes: {
+ status: {
+ control: { type: 'radio' },
+ options: ['processing', 'success', 'error'],
+ },
+ },
+ args: {
+ status: 'processing',
+ isMfaEnabled: true,
+ allowPasswordless: true,
+ },
};
-export const Success = () => {
- return ;
-};
+export default meta;
+
+export function Story(props: StoryProps) {
+ const statusToAttempt: Record = {
+ processing: { status: 'processing' },
+ success: { status: 'success' },
+ error: { status: 'failed', statusText: 'some server error' },
+ };
+ const [, setState] = useState({});
+
+ useEffect(() => {
+ const defaultAuth = structuredClone(cfg.auth);
+ cfg.auth.second_factor = props.isMfaEnabled ? 'on' : 'off';
+ cfg.auth.allowPasswordless = props.allowPasswordless;
+ setState({}); // Force re-render of the component with new cfg.
+
+ return () => {
+ cfg.auth = defaultAuth;
+ };
+ }, [props.isMfaEnabled, props.allowPasswordless]);
-export const Failed = () => {
return (
{}}
+ onClose={() => {}}
+ attempt={statusToAttempt[props.status]}
/>
);
-};
-
-const props = {
- username: 'smith',
- token: {
- value: '0c536179038b386728dfee6602ca297f',
- expires: new Date('2021-04-08T07:30:00Z'),
- username: 'Lester',
- },
- onReset() {},
- onClose() {},
- attempt: {
- status: 'processing',
- statusText: '',
- },
-};
+}
diff --git a/web/packages/teleport/src/Users/UserReset/UserReset.tsx b/web/packages/teleport/src/Users/UserReset/UserReset.tsx
index 65e15b1a2f2c2..0eb11518a0fb5 100644
--- a/web/packages/teleport/src/Users/UserReset/UserReset.tsx
+++ b/web/packages/teleport/src/Users/UserReset/UserReset.tsx
@@ -17,7 +17,7 @@
*/
import { useState } from 'react';
-import { ButtonPrimary, ButtonSecondary, Text, Alert } from 'design';
+import { ButtonPrimary, ButtonSecondary, Text, Alert, P2 } from 'design';
import Dialog, {
DialogHeader,
DialogTitle,
@@ -26,6 +26,7 @@ import Dialog, {
} from 'design/Dialog';
import { useAttemptNext } from 'shared/hooks';
+import cfg from 'teleport/config';
import { ResetToken } from 'teleport/services/user';
import UserTokenLink from './../UserTokenLink';
@@ -58,16 +59,31 @@ export function UserReset({
{attempt.status === 'failed' && (
-
+ {attempt.statusText}
)}
-
- You are about to reset authentication for user
-
- {` ${username} `}
+
+ You are about to reset authentication for user{' '}
+
+ {username}
- . This will generate a temporary URL which can be used to set up new
- authentication.
-
+ . This will generate a temporary URL which can be used to set up
+ new authentication.
+
+ {cfg.isMfaEnabled() && (
+
+ All{' '}
+ {cfg.isPasswordlessEnabled()
+ ? 'passkeys and MFA methods'
+ : 'MFA methods'}{' '}
+ of this user will be removed. The user will be able to set up{' '}
+ {cfg.isPasswordlessEnabled() ? (
+ <>a new passkey or an MFA method>
+ ) : (
+ <>a new method>
+ )}{' '}
+ after following the URL.
+
+ )}
- Generate reset url
+ Generate Reset URL
Cancel
diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts
index 6980b827784cd..c8883b598eebf 100644
--- a/web/packages/teleport/src/config.ts
+++ b/web/packages/teleport/src/config.ts
@@ -17,21 +17,12 @@
*/
import { generatePath } from 'react-router';
-import { mergeDeep } from 'shared/utils/highbar';
-import { IncludedResourceMode } from 'shared/components/UnifiedResources';
-import generateResourcePath from './generateResourcePath';
+import { IncludedResourceMode } from 'shared/components/UnifiedResources';
-import { defaultEntitlements } from './entitlement';
+import { mergeDeep } from 'shared/utils/highbar';
import {
- AwsOidcPolicyPreset,
- IntegrationKind,
- PluginKind,
- Regions,
-} from './services/integrations';
-
-import type {
Auth2faType,
AuthProvider,
AuthType,
@@ -39,12 +30,23 @@ import type {
PrimaryAuthType,
} from 'shared/services';
+import {
+ AwsOidcPolicyPreset,
+ IntegrationKind,
+ PluginKind,
+ Regions,
+} from 'teleport/services/integrations';
+import { KubeResourceKind } from 'teleport/services/kube/types';
+
+import { defaultEntitlements } from './entitlement';
+
+import generateResourcePath from './generateResourcePath';
+
import type { SortType } from 'teleport/services/agents';
import type { RecordingType } from 'teleport/services/recordings';
-import type { WebauthnAssertionResponse } from './services/mfa';
+import type { WebauthnAssertionResponse } from 'teleport/services/mfa';
import type { ParticipantMode } from 'teleport/services/session';
-import type { YamlSupportedResourceKind } from './services/yaml/types';
-import type { KubeResourceKind } from './services/kube/types';
+import type { YamlSupportedResourceKind } from 'teleport/services/yaml/types';
const cfg = {
/** @deprecated Use cfg.edition instead. */
@@ -268,6 +270,7 @@ const cfg = {
ttyPlaybackWsAddr:
'wss://:fqdn/v1/webapi/sites/:clusterId/ttyplayback/:sid?access_token=:token', // TODO(zmb3): get token out of URL
activeAndPendingSessionsPath: '/v1/webapi/sites/:clusterId/sessions',
+ sessionDurationPath: '/v1/webapi/sites/:clusterId/sessionlength/:sid',
kubernetesPath:
'/v1/webapi/sites/:clusterId/kubernetes?searchAsRoles=:searchAsRoles?&limit=:limit?&startKey=:startKey?&query=:query?&search=:search?&sort=:sort?',
@@ -778,6 +781,10 @@ const cfg = {
return generatePath(cfg.api.activeAndPendingSessionsPath, { clusterId });
},
+ getSessionDurationUrl(clusterId: string, sid: string) {
+ return generatePath(cfg.api.sessionDurationPath, { clusterId, sid });
+ },
+
getUnifiedResourcesUrl(clusterId: string, params: UrlResourcesParams) {
return generateResourcePath(cfg.api.unifiedResourcesPath, {
clusterId,
diff --git a/web/packages/teleport/src/features.tsx b/web/packages/teleport/src/features.tsx
index 7c26132fcf600..5f8f4d41eadef 100644
--- a/web/packages/teleport/src/features.tsx
+++ b/web/packages/teleport/src/features.tsx
@@ -569,7 +569,7 @@ export class FeatureClusters implements TeleportFeature {
};
hasAccess(flags: FeatureFlags) {
- return flags.trustedClusters;
+ return cfg.isDashboard || flags.trustedClusters;
}
navigationItem = {
diff --git a/web/packages/teleport/src/lib/term/tty.ts b/web/packages/teleport/src/lib/term/tty.ts
index 3e924ff466f3f..7c50a38ee9a38 100644
--- a/web/packages/teleport/src/lib/term/tty.ts
+++ b/web/packages/teleport/src/lib/term/tty.ts
@@ -82,7 +82,7 @@ class Tty extends EventEmitterMfaSender {
this.socket.send(bytearray.buffer);
}
- sendChallengeResponse(data: MfaChallengeResponse) {
+ sendChallengeResponse(resp: MfaChallengeResponse) {
// we want to have the backend listen on a single message type
// for any responses. so our data will look like data.webauthn, data.sso, etc
// but to be backward compatible, we need to still spread the existing webauthn only fields
@@ -90,8 +90,8 @@ class Tty extends EventEmitterMfaSender {
// in 19, we can just pass "data" without this extra step
// TODO (avatus): DELETE IN 18
const backwardCompatibleData = {
- ...data.webauthn_response,
- ...data,
+ ...resp?.webauthn_response,
+ ...resp,
};
const encoded = this._proto.encodeChallengeResponse(
JSON.stringify(backwardCompatibleData)
diff --git a/web/packages/teleport/src/services/audit/makeEvent.ts b/web/packages/teleport/src/services/audit/makeEvent.ts
index 4095ee10fa604..ff1ae2a166e56 100644
--- a/web/packages/teleport/src/services/audit/makeEvent.ts
+++ b/web/packages/teleport/src/services/audit/makeEvent.ts
@@ -1900,6 +1900,20 @@ export const formatters: Formatters = {
return `User [${user}] deleted a plugin [${name}]`;
},
},
+ [eventCodes.CONTACT_CREATE]: {
+ type: 'contact.create',
+ desc: 'Contact Created',
+ format: ({ user, email, contact_type }) => {
+ return `User [${user}] created a [${contactTypeStr(contact_type)}] contact [${email}]`;
+ },
+ },
+ [eventCodes.CONTACT_DELETE]: {
+ type: 'contact.delete',
+ desc: 'Contact Deleted',
+ format: ({ user, email, contact_type }) => {
+ return `User [${user}] deleted a [${contactTypeStr(contact_type)}] contact [${email}]`;
+ },
+ },
[eventCodes.UNKNOWN]: {
type: 'unknown',
desc: 'Unknown Event',
@@ -1951,3 +1965,14 @@ function formatMembers(members: { member_name: string }[]) {
return `${pluralize(memberNames.length, 'member')} [${memberNamesJoined}]`;
}
+
+function contactTypeStr(type: number): string {
+ switch (type) {
+ case 1:
+ return 'Business';
+ case 2:
+ return 'Security';
+ default:
+ return `Unknown type: ${type}`;
+ }
+}
diff --git a/web/packages/teleport/src/services/audit/types.ts b/web/packages/teleport/src/services/audit/types.ts
index 066c4116b72c8..c50b27a782df2 100644
--- a/web/packages/teleport/src/services/audit/types.ts
+++ b/web/packages/teleport/src/services/audit/types.ts
@@ -308,6 +308,8 @@ export const eventCodes = {
PLUGIN_CREATE: 'PG001I',
PLUGIN_UPDATE: 'PG002I',
PLUGIN_DELETE: 'PG003I',
+ CONTACT_CREATE: 'TCTC001I',
+ CONTACT_DELETE: 'TCTC002I',
} as const;
/**
@@ -1746,6 +1748,20 @@ export type RawEvents = {
server_hostname: string;
}
>;
+ [eventCodes.CONTACT_CREATE]: RawEvent<
+ typeof eventCodes.CONTACT_CREATE,
+ {
+ email: string;
+ contact_type: number;
+ }
+ >;
+ [eventCodes.CONTACT_DELETE]: RawEvent<
+ typeof eventCodes.CONTACT_DELETE,
+ {
+ email: string;
+ contact_type: number;
+ }
+ >;
};
/**
diff --git a/web/packages/teleport/src/services/auth/auth.ts b/web/packages/teleport/src/services/auth/auth.ts
index 3724f1dc8b056..e1533ee4d3297 100644
--- a/web/packages/teleport/src/services/auth/auth.ts
+++ b/web/packages/teleport/src/services/auth/auth.ts
@@ -239,7 +239,7 @@ const auth = {
.then(res => {
const request = {
action: 'accept',
- webauthnAssertionResponse: res.webauthn_response,
+ webauthnAssertionResponse: res?.webauthn_response,
};
return api.put(cfg.getHeadlessSsoPath(transactionId), request);
@@ -280,7 +280,9 @@ const auth = {
challenge: MfaAuthenticateChallenge,
mfaType?: DeviceType,
totpCode?: string
- ): Promise {
+ ): Promise {
+ if (!challenge) return;
+
// TODO(Joerger): If mfaType is not provided by a parent component, use some global context
// to display a component, similar to the one used in useMfa. For now we just default to
// whichever method we can succeed with first.
@@ -303,7 +305,7 @@ const auth = {
}
// No viable challenge, return empty response.
- return null;
+ return;
},
async getWebAuthnChallengeResponse(
@@ -387,7 +389,7 @@ const auth = {
return auth
.getMfaChallenge({ scope, allowReuse, isMfaRequiredRequest }, abortSignal)
.then(challenge => auth.getMfaChallengeResponse(challenge, 'webauthn'))
- .then(res => res.webauthn_response);
+ .then(res => res?.webauthn_response);
},
getMfaChallengeResponseForAdminAction(allowReuse?: boolean) {
diff --git a/web/packages/teleport/src/services/mfa/makeMfa.ts b/web/packages/teleport/src/services/mfa/makeMfa.ts
index 505a972fe33e5..4d98503dafa87 100644
--- a/web/packages/teleport/src/services/mfa/makeMfa.ts
+++ b/web/packages/teleport/src/services/mfa/makeMfa.ts
@@ -63,13 +63,13 @@ export function parseMfaRegistrationChallengeJson(
// parseMfaChallengeJson formats fetched authenticate challenge JSON.
export function parseMfaChallengeJson(
challenge: MfaAuthenticateChallengeJson
-): MfaAuthenticateChallenge {
+): MfaAuthenticateChallenge | undefined {
if (
!challenge.sso_challenge &&
!challenge.webauthn_challenge &&
!challenge.totp_challenge
) {
- return null;
+ return;
}
// WebAuthn challenge contains Base64URL(byte) fields that needs to
diff --git a/web/packages/teleport/src/services/mfa/types.ts b/web/packages/teleport/src/services/mfa/types.ts
index f1292c50c99cd..382d7831f82fe 100644
--- a/web/packages/teleport/src/services/mfa/types.ts
+++ b/web/packages/teleport/src/services/mfa/types.ts
@@ -18,8 +18,7 @@
import { AuthProviderType } from 'shared/services';
-import { Base64urlString } from '../auth/types';
-import { CreateNewHardwareDeviceRequest } from '../auth/types';
+import { Base64urlString, CreateNewHardwareDeviceRequest } from '../auth/types';
export type DeviceType = 'totp' | 'webauthn' | 'sso';
diff --git a/web/packages/teleport/src/services/recordings/recordings.ts b/web/packages/teleport/src/services/recordings/recordings.ts
index e27ca67beea03..ba71160aa1795 100644
--- a/web/packages/teleport/src/services/recordings/recordings.ts
+++ b/web/packages/teleport/src/services/recordings/recordings.ts
@@ -45,4 +45,11 @@ export default class RecordingsService {
return { recordings: events.map(makeRecording), startKey: json.startKey };
});
}
+
+ fetchRecordingDuration(
+ clusterId: string,
+ sessionId: string
+ ): Promise<{ durationMs: number }> {
+ return api.get(cfg.getSessionDurationUrl(clusterId, sessionId));
+ }
}