Skip to content

Commit

Permalink
[v14] Show alert about insufficient permissions in Connect My Compute…
Browse files Browse the repository at this point in the history
…r setup tab (#34064)

* Rename ConnectMyComputer components

* Retain more details about Connect My Computer access

This will be needed in to determine why access was not granted and explain
that to the user in the UI.

* Show alert about no access in Setup step

* Trigger loading state on doc until access is settled

* Make access error messages more actionable, link to docs
  • Loading branch information
ravicious committed Nov 1, 2023
1 parent 3941a71 commit 10dcf83
Show file tree
Hide file tree
Showing 15 changed files with 331 additions and 120 deletions.
16 changes: 16 additions & 0 deletions web/packages/teleterm/src/services/tshd/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,27 @@ export interface Cluster extends apiCluster.Cluster.AsObject {
* `leafClusterId` is equal to the `name` property of the cluster.
*/
uri: uri.ClusterUri;
/**
* loggedInUser is present if the user has logged in to the cluster at least once. This
* includes a situation in which the cert has expired. If the cluster was added to the app but the
* user is yet to log in, loggedInUser is not present.
*/
loggedInUser?: LoggedInUser;
}

/**
* LoggedInUser describes loggedInUser field available on root clusters.
*
* loggedInUser is present if the user has logged in to the cluster at least once. This
* includes a situation in which the cert has expired. If the cluster was added to the app but the
* user is yet to log in, loggedInUser is not present.
*/
export type LoggedInUser = apiCluster.LoggedInUser.AsObject & {
assumedRequests?: Record<string, AssumedRequest>;
/**
* acl is available only after the cluster details are fetched, as acl is not stored on disk.
*/
acl?: apiCluster.ACL.AsObject;
};
export type AuthProvider = apiAuthSettings.AuthProvider.AsObject;
export type AuthSettings = apiAuthSettings.AuthSettings.AsObject;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,46 +14,50 @@
* limitations under the License.
*/

import React from 'react';
import React, { useCallback } from 'react';

import Indicator from 'design/Indicator';

import * as types from 'teleterm/ui/services/workspacesService';
import Document from 'teleterm/ui/Document';
import { useWorkspaceContext } from 'teleterm/ui/Documents';

import { useConnectMyComputerContext } from './connectMyComputerContext';
import { DocumentConnectMyComputerStatus } from './DocumentConnectMyComputerStatus/DocumentConnectMyComputerStatus';
import { DocumentConnectMyComputerSetup } from './DocumentConnectMyComputerSetup/DocumentConnectMyComputerSetup';
import { useConnectMyComputerContext } from '../connectMyComputerContext';

interface DocumentConnectMyComputerProps {
import { Status } from './Status';
import { Setup } from './Setup';

export function DocumentConnectMyComputer(props: {
visible: boolean;
doc: types.DocumentConnectMyComputer;
}

export function DocumentConnectMyComputer(
props: DocumentConnectMyComputerProps
) {
}) {
const { documentsService } = useWorkspaceContext();
const { isAgentConfiguredAttempt } = useConnectMyComputerContext();
const shouldShowSetup =
isAgentConfiguredAttempt.status === 'success' &&
!isAgentConfiguredAttempt.data;

const closeDocument = useCallback(() => {
documentsService.close(props.doc.uri);
}, [documentsService, props.doc.uri]);

const updateDocumentStatus = useCallback(
(status: types.DocumentConnectMyComputer['status']) => {
documentsService.update(props.doc.uri, { status });
},
[documentsService, props.doc.uri]
);

if (isAgentConfiguredAttempt.status === 'processing') {
return <Indicator m="auto" />;
}

function closeDocument(): void {
documentsService.close(props.doc.uri);
}

return (
<Document visible={props.visible}>
{shouldShowSetup ? (
<DocumentConnectMyComputerSetup />
<Setup updateDocumentStatus={updateDocumentStatus} />
) : (
<DocumentConnectMyComputerStatus closeDocument={closeDocument} />
<Status closeDocument={closeDocument} />
)}
</Document>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ import {
makeServer,
} from 'teleterm/services/tshd/testHelpers';
import { IAppContext } from 'teleterm/ui/types';
import { Cluster } from 'teleterm/services/tshd/types';
import { Cluster, UserType } from 'teleterm/services/tshd/types';
import { ResourcesContextProvider } from 'teleterm/ui/DocumentCluster/resourcesContext';

import { ConnectMyComputerContextProvider } from '../connectMyComputerContext';

import { DocumentConnectMyComputerSetup } from './DocumentConnectMyComputerSetup';
import { Setup } from './Setup';

export default {
title: 'Teleterm/ConnectMyComputer/Setup',
Expand Down Expand Up @@ -118,6 +118,34 @@ export function AgentVersionTooOld() {
);
}

export function NoAccess() {
const cluster = makeRootCluster();
cluster.loggedInUser.acl.tokens.create = false;
const appContext = new MockAppContext({});

return (
<ShowState
cluster={cluster}
appContext={appContext}
clickStartSetup={false}
/>
);
}

export function AccessUnknown() {
const cluster = makeRootCluster();
cluster.loggedInUser.userType = UserType.USER_TYPE_UNSPECIFIED;
const appContext = new MockAppContext({});

return (
<ShowState
cluster={cluster}
appContext={appContext}
clickStartSetup={false}
/>
);
}

function ShowState({
cluster,
appContext,
Expand Down Expand Up @@ -153,7 +181,7 @@ function ShowState({
<MockWorkspaceContextProvider rootClusterUri={cluster.uri}>
<ResourcesContextProvider>
<ConnectMyComputerContextProvider rootClusterUri={cluster.uri}>
<DocumentConnectMyComputerSetup />
<Setup updateDocumentStatus={() => {}} />
</ConnectMyComputerContextProvider>
</ResourcesContextProvider>
</MockWorkspaceContextProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import * as useResourcesContext from 'teleterm/ui/DocumentCluster/resourcesConte

import * as connectMyComputerContext from '../connectMyComputerContext';

import { DocumentConnectMyComputerSetup } from './DocumentConnectMyComputerSetup';
import { Setup } from './Setup';

beforeAll(() => {
Logger.init(new NullService());
Expand Down Expand Up @@ -170,7 +170,7 @@ function setupAppContext(): {
<connectMyComputerContext.ConnectMyComputerContextProvider
rootClusterUri={cluster.uri}
>
<DocumentConnectMyComputerSetup />
<Setup updateDocumentStatus={() => {}} />
</connectMyComputerContext.ConnectMyComputerContextProvider>
</useResourcesContext.ResourcesContextProvider>
</MockWorkspaceContextProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,66 +16,99 @@ limitations under the License.

import React, { useCallback, useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import { Box, ButtonPrimary, Flex, Text } from 'design';
import { Box, ButtonPrimary, Flex, Text, Alert } from 'design';
import { makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync';
import { wait } from 'shared/utils/wait';
import * as Alerts from 'design/Alert';

import { useAppContext } from 'teleterm/ui/appContextProvider';
import { useWorkspaceContext } from 'teleterm/ui/Documents';
import { retryWithRelogin } from 'teleterm/ui/utils';
import { assertUnreachable, retryWithRelogin } from 'teleterm/ui/utils';
import {
AgentProcessError,
NodeWaitJoinTimeout,
useConnectMyComputerContext,
} from 'teleterm/ui/ConnectMyComputer';
import Logger from 'teleterm/logger';
import { codeOrSignal } from 'teleterm/ui/utils/process';
import { RootClusterUri } from 'teleterm/ui/uri';
import { isAccessDeniedError } from 'teleterm/services/tshd/errors';
import { useResourcesContext } from 'teleterm/ui/DocumentCluster/resourcesContext';
import { useLogger } from 'teleterm/ui/hooks/useLogger';
import { DocumentConnectMyComputer } from 'teleterm/ui/services/workspacesService';

import { useAgentProperties } from '../useAgentProperties';
import { Logs } from '../Logs';
import { CompatibilityError } from '../CompatibilityPromise';
import { ConnectMyComputerAccessNoAccess } from '../access';

import { ProgressBar } from './ProgressBar';

const logger = new Logger('DocumentConnectMyComputerSetup');

// TODO(gzdunek): Rename to `Setup`
export function DocumentConnectMyComputerSetup() {
export function Setup(props: {
updateDocumentStatus: (status: DocumentConnectMyComputer['status']) => void;
}) {
const [step, setStep] = useState<'information' | 'agent-setup'>(
'information'
);
const { rootClusterUri } = useWorkspaceContext();

return (
<Box maxWidth="680px" mx="auto" mt="4" px="5" width="100%">
<Text typography="h3" mb="4">
Connect My Computer
</Text>
{step === 'information' && (
<Information onSetUpAgentClick={() => setStep('agent-setup')} />
<Information
onSetUpAgentClick={() => setStep('agent-setup')}
updateDocumentStatus={props.updateDocumentStatus}
/>
)}
{step === 'agent-setup' && <AgentSetup rootClusterUri={rootClusterUri} />}
{step === 'agent-setup' && <AgentSetup />}
</Box>
);
}

function Information(props: { onSetUpAgentClick(): void }) {
function Information(props: {
onSetUpAgentClick(): void;
updateDocumentStatus(status: DocumentConnectMyComputer['status']): void;
}) {
const { updateDocumentStatus } = props;
const { systemUsername, hostname, roleName, clusterName } =
useAgentProperties();
const { agentCompatibility } = useConnectMyComputerContext();
const isAgentIncompatible = agentCompatibility === 'incompatible';
const isAgentIncompatibleOrUnknown =
agentCompatibility === 'incompatible' || agentCompatibility === 'unknown';
const { agentCompatibility, access } = useConnectMyComputerContext();

let disabledButtonReason: string;
if (access.status === 'unknown') {
disabledButtonReason = 'Checking access…';
} else if (access.status === 'no-access') {
disabledButtonReason = "You don't have access to use Connect My Computer.";
} else if (agentCompatibility === 'unknown') {
disabledButtonReason = 'Checking agent compatibility…';
} else if (agentCompatibility === 'incompatible') {
disabledButtonReason =
'The agent version is not compatible with the cluster version.';
}

const isWaiting =
access.status === 'unknown' || agentCompatibility === 'unknown';

useEffect(() => {
if (isWaiting) {
updateDocumentStatus('connecting');
} else {
updateDocumentStatus('connected');
}
}, [isWaiting, updateDocumentStatus]);

let $alert: JSX.Element;
if (access.status === 'no-access') {
$alert = <AccessError access={access} />;
} else if (agentCompatibility === 'incompatible') {
$alert = <CompatibilityError />;
}

return (
<>
{isAgentIncompatible && (
{$alert && (
<>
<CompatibilityError />
{$alert}
<Separator mt={3} mb={2} />
</>
)}
Expand Down Expand Up @@ -107,7 +140,8 @@ function Information(props: { onSetUpAgentClick(): void }) {
css={`
display: block;
`}
disabled={isAgentIncompatibleOrUnknown}
title={disabledButtonReason}
disabled={!!disabledButtonReason}
onClick={props.onSetUpAgentClick}
data-testid="start-setup"
>
Expand All @@ -117,8 +151,68 @@ function Information(props: { onSetUpAgentClick(): void }) {
);
}

function AgentSetup({ rootClusterUri }: { rootClusterUri: RootClusterUri }) {
function AccessError(props: { access: ConnectMyComputerAccessNoAccess }) {
const $documentation = (
<>
See{' '}
<a
href="https://goteleport.com/docs/connect-your-client/teleport-connect/#prerequisites"
target="_blank"
>
the documentation
</a>{' '}
for more details.
</>
);

switch (props.access.reason) {
case 'unsupported-platform': {
return (
<Alert mb={0}>
<Text>
Connect My Computer is not supported on your operating system.
<br />
{$documentation}
</Text>
</Alert>
);
}
case 'insufficient-permissions': {
return (
<Alert mb={0}>
<Text>
You have insufficient permissions to use Connect My Computer. Reach
out to your Teleport administrator to request{' '}
<a
href="https://goteleport.com/docs/connect-your-client/teleport-connect/#prerequisites"
target="_blank"
>
additional permissions
</a>
.
</Text>
</Alert>
);
}
case 'sso-user': {
return (
<Alert mb={0}>
<Text>
Connect My Computer does not work with SSO users. {$documentation}
</Text>
</Alert>
);
}
default: {
return assertUnreachable(props.access);
}
}
}

function AgentSetup() {
const logger = useLogger('AgentSetup');
const ctx = useAppContext();
const { rootClusterUri } = useWorkspaceContext();
const {
startAgent,
markAgentAsConfigured,
Expand Down Expand Up @@ -206,6 +300,7 @@ function AgentSetup({ rootClusterUri }: { rootClusterUri: RootClusterUri }) {
ctx.connectMyComputerService,
cluster.uri,
requestResourcesRefresh,
logger,
])
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
NodeWaitJoinTimeout,
} from '../connectMyComputerContext';

import { DocumentConnectMyComputerStatus } from './DocumentConnectMyComputerStatus';
import { Status } from './Status';

export default {
title: 'Teleterm/ConnectMyComputer/Status',
Expand Down Expand Up @@ -271,7 +271,7 @@ function ShowState(props: {
<MockAppContextProvider appContext={appContext}>
<MockWorkspaceContextProvider rootClusterUri={cluster.uri}>
<ConnectMyComputerContextProvider rootClusterUri={cluster.uri}>
<DocumentConnectMyComputerStatus />
<Status />
</ConnectMyComputerContextProvider>
</MockWorkspaceContextProvider>
</MockAppContextProvider>
Expand Down

0 comments on commit 10dcf83

Please sign in to comment.