From 4bf8c02c24cf07997a09397a6ec3f638d81bd014 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Tue, 26 Mar 2024 12:30:15 +0100 Subject: [PATCH 1/8] Make `CloneableClient` type work better with code completion When we used `ReplaceRpcOptions`, the editor was not able to provide autocompletion for methods' parameters. --- .../src/services/tshd/cloneableClient.test.ts | 6 ++--- .../src/services/tshd/cloneableClient.ts | 24 ++++++++++++------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/web/packages/teleterm/src/services/tshd/cloneableClient.test.ts b/web/packages/teleterm/src/services/tshd/cloneableClient.test.ts index 23992dc546593..02e266b490e9e 100644 --- a/web/packages/teleterm/src/services/tshd/cloneableClient.test.ts +++ b/web/packages/teleterm/src/services/tshd/cloneableClient.test.ts @@ -106,7 +106,7 @@ test('response error is cloned as an object for a unary call', async () => { try { // Normally we would simply await `client.fakeMethod()`, but jest doesn't support // thenables https://github.com/jestjs/jest/issues/10501. - await client.fakeMethod().then(); + await client.fakeMethod({}).then(); } catch (e) { error = e; } @@ -180,7 +180,7 @@ test('response error is cloned as an object in a server streaming call', async ( fakeCall ) ); - const res = client.fakeMethod(); + const res = client.fakeMethod({}); const onNext = jest.fn(); const onError = jest.fn(); res.responses.onNext(onNext); @@ -232,7 +232,7 @@ test('response error is cloned as an object in a duplex call', async () => { fakeCall ) ); - const res = client.fakeMethod(); + const res = client.fakeMethod({}); const onNext = jest.fn(); const onError = jest.fn(); res.responses.onNext(onNext); diff --git a/web/packages/teleterm/src/services/tshd/cloneableClient.ts b/web/packages/teleterm/src/services/tshd/cloneableClient.ts index b4666400b2599..286f72b3b69c6 100644 --- a/web/packages/teleterm/src/services/tshd/cloneableClient.ts +++ b/web/packages/teleterm/src/services/tshd/cloneableClient.ts @@ -149,25 +149,31 @@ export type CloneableClient = { [Method in keyof Client]: Client[Method] extends ( ...args: infer Args ) => infer ReturnType - ? ( - ...args: { [K in keyof Args]: ReplaceRpcOptions } - ) => CloneableCallTypes + ? CloneableCallTypes : never; }; type CloneableCallTypes = T extends UnaryCall - ? CloneableUnaryCall + ? ( + input: Req, + options?: CloneableRpcOptions + ) => CloneableUnaryCall : T extends ClientStreamingCall - ? CloneableClientStreamingCall + ? ( + options?: CloneableRpcOptions + ) => CloneableClientStreamingCall : T extends ServerStreamingCall - ? CloneableServerStreamingCall + ? ( + input: Req, + options?: CloneableRpcOptions + ) => CloneableServerStreamingCall : T extends DuplexStreamingCall - ? CloneableDuplexStreamingCall + ? ( + options?: CloneableRpcOptions + ) => CloneableDuplexStreamingCall : never; -type ReplaceRpcOptions = T extends RpcOptions ? CloneableRpcOptions : T; - type CloneableUnaryCall = Pick< UnaryCall, 'then' From a4b10657f2456308320f4b0f151cd57800246357 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Tue, 26 Mar 2024 12:31:58 +0100 Subject: [PATCH 2/8] Replace typed URIs with strings --- web/packages/teleterm/src/ui/uri.ts | 35 +++++++++-------------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/web/packages/teleterm/src/ui/uri.ts b/web/packages/teleterm/src/ui/uri.ts index acad96bbd1ae9..a4c3c8ba6e29f 100644 --- a/web/packages/teleterm/src/ui/uri.ts +++ b/web/packages/teleterm/src/ui/uri.ts @@ -25,34 +25,22 @@ import type { RouteProps } from 'react-router'; * These are for identifying a specific resource within a root cluster. */ -type RootClusterId = string; -type LeafClusterId = string; -type ServerId = string; -type KubeId = string; -type DbId = string; -type AppId = string; -export type RootClusterUri = `/clusters/${RootClusterId}`; -export type RootClusterServerUri = - `/clusters/${RootClusterId}/servers/${ServerId}`; -export type RootClusterKubeUri = `/clusters/${RootClusterId}/kubes/${KubeId}`; -export type RootClusterDatabaseUri = `/clusters/${RootClusterId}/dbs/${DbId}`; -export type RootClusterAppUri = `/clusters/${RootClusterId}/apps/${AppId}`; +export type RootClusterUri = string; +export type RootClusterServerUri = string; +export type RootClusterKubeUri = string; +export type RootClusterDatabaseUri = string; +export type RootClusterAppUri = string; export type RootClusterResourceUri = | RootClusterServerUri | RootClusterKubeUri | RootClusterDatabaseUri | RootClusterAppUri; export type RootClusterOrResourceUri = RootClusterUri | RootClusterResourceUri; -export type LeafClusterUri = - `/clusters/${RootClusterId}/leaves/${LeafClusterId}`; -export type LeafClusterServerUri = - `/clusters/${RootClusterId}/leaves/${LeafClusterId}/servers/${ServerId}`; -export type LeafClusterKubeUri = - `/clusters/${RootClusterId}/leaves/${LeafClusterId}/kubes/${KubeId}`; -export type LeafClusterDatabaseUri = - `/clusters/${RootClusterId}/leaves/${LeafClusterId}/dbs/${DbId}`; -export type LeafClusterAppUri = - `/clusters/${RootClusterId}/leaves/${LeafClusterId}/apps/${AppId}`; +export type LeafClusterUri = string; +export type LeafClusterServerUri = string; +export type LeafClusterKubeUri = string; +export type LeafClusterDatabaseUri = string; +export type LeafClusterAppUri = string; export type LeafClusterResourceUri = | LeafClusterServerUri | LeafClusterKubeUri @@ -82,8 +70,7 @@ export type DocumentUri = `/docs/${DocumentId}`; * These are for gateways (proxies) managed by the tsh daemon. */ -type GatewayId = string; -export type GatewayUri = `/gateways/${GatewayId}`; +export type GatewayUri = string; export const paths = { // Resources. From fb043f0df14b08183f062019614f6c56f472d1f7 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Tue, 26 Mar 2024 12:48:52 +0100 Subject: [PATCH 3/8] Add `MockedUnaryCall` helper --- .../src/services/tshd/cloneableClient.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/web/packages/teleterm/src/services/tshd/cloneableClient.ts b/web/packages/teleterm/src/services/tshd/cloneableClient.ts index 286f72b3b69c6..e931bc6315e83 100644 --- a/web/packages/teleterm/src/services/tshd/cloneableClient.ts +++ b/web/packages/teleterm/src/services/tshd/cloneableClient.ts @@ -26,6 +26,7 @@ import { RpcError, RpcOptions, ServiceInfo, + FinishedUnaryCall, } from '@protobuf-ts/runtime-rpc'; /** @@ -373,3 +374,46 @@ function cloneThenRejection( return clonePromiseRejection(then(onFulfilled)); }; } + +/** + * A helper for mocking unary calls. Creates a promise-like instance of a class which resolves to + * an object where only the response field contains something. + * + * The need for this helper stems from the fact that cloneableClient returns the whole then property + * of a unary call, so TypeScript expects the types to match. + * + * Alternatively, we could change cloneableClient to merely return the response property, plus maybe + * some other fields that we need. + */ +export class MockedUnaryCall + implements CloneableUnaryCall +{ + constructor( + public response: Response, + private error?: any + ) {} + + // The signature of then was autocompleted by TypeScript language server. + then, TResult2 = never>( + onfulfilled?: ( + value: FinishedUnaryCall + ) => TResult1 | PromiseLike, + onrejected?: (reason: any) => TResult2 | PromiseLike + ): Promise { + if (this.error) { + return Promise.reject(onrejected(this.error)); + } + + return Promise.resolve( + onfulfilled({ + response: this.response, + method: undefined, + requestHeaders: undefined, + request: undefined, + headers: undefined, + status: undefined, + trailers: undefined, + }) + ); + } +} From af7605b3cb64ac61de8216b19e78dd04e2be3960 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Tue, 26 Mar 2024 12:49:23 +0100 Subject: [PATCH 4/8] Add `assumedRequests` to the "local" type of cluster service state We will no longer be able to extend the tshd type, as it will be removed. --- .../teleterm/src/ui/services/clusters/types.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/web/packages/teleterm/src/ui/services/clusters/types.ts b/web/packages/teleterm/src/ui/services/clusters/types.ts index 3ec745612a611..60424d29e9fae 100644 --- a/web/packages/teleterm/src/ui/services/clusters/types.ts +++ b/web/packages/teleterm/src/ui/services/clusters/types.ts @@ -54,6 +54,16 @@ export interface AuthSettings extends tsh.AuthSettings { } export type ClustersServiceState = { - clusters: Map; + clusters: Map< + uri.ClusterUri, + tsh.Cluster & { + // TODO(gzdunek): Remove assumedRequests from loggedInUser. + // The AssumedRequest objects are needed only in AssumedRolesBar. + // We should be able to move fetching them there. + loggedInUser?: tsh.LoggedInUser & { + assumedRequests?: Record; + }; + } + >; gateways: Map; }; From eef43d65d2d7a3ae0e44dade268ef44587117bc0 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Tue, 26 Mar 2024 12:50:25 +0100 Subject: [PATCH 5/8] Remove custom tshd types and `createTshdClient` boilerplate --- .../rootClusterProxyHostAllowList.ts | 6 +- web/packages/teleterm/src/preload.ts | 4 +- .../src/services/tshd/createClient.ts | 591 +----------------- .../src/services/tshd/fixtures/mocks.ts | 177 +++--- .../teleterm/src/services/tshd/testHelpers.ts | 3 +- .../teleterm/src/services/tshd/types.ts | 337 +--------- .../DocumentConnectMyComputer/Setup.story.tsx | 4 +- .../src/ui/ConnectMyComputer/access.ts | 4 +- .../DocumentCluster/DocumentCluster.story.tsx | 6 +- .../DocumentCluster/DocumentCluster.test.tsx | 4 +- .../ui/DocumentCluster/UnifiedResources.tsx | 10 +- .../useUserPreferences.test.tsx | 72 ++- .../ui/DocumentCluster/useUserPreferences.ts | 24 +- .../useDocumentGateway.test.tsx | 6 +- .../ui/DocumentGateway/useDocumentGateway.ts | 6 +- .../DocumentGatewayApp.story.tsx | 5 +- .../DocumentGatewayKube.tsx | 4 +- .../services/clusters/clustersService.test.ts | 98 ++- .../ui/services/clusters/clustersService.ts | 230 +++++-- .../src/ui/services/clusters/types.ts | 40 +- .../connectMyComputerService.ts | 34 +- .../fileTransferClient/fileTransferService.ts | 25 +- .../headlessAuthn/headlessAuthnService.ts | 9 +- .../resources/resourcesService.test.ts | 79 ++- .../ui/services/resources/resourcesService.ts | 111 +++- 25 files changed, 679 insertions(+), 1210 deletions(-) diff --git a/web/packages/teleterm/src/mainProcess/rootClusterProxyHostAllowList.ts b/web/packages/teleterm/src/mainProcess/rootClusterProxyHostAllowList.ts index 42f9c30110c57..ba1635dae669b 100644 --- a/web/packages/teleterm/src/mainProcess/rootClusterProxyHostAllowList.ts +++ b/web/packages/teleterm/src/mainProcess/rootClusterProxyHostAllowList.ts @@ -58,9 +58,11 @@ export function manageRootClusterProxyHostAllowList({ let rootClusters: tshd.Cluster[]; try { - rootClusters = await tshdClient.listRootClusters( - cloneAbortSignal(abortController.signal) + const { response } = await tshdClient.listRootClusters( + {}, + { abort: cloneAbortSignal(abortController.signal) } ); + rootClusters = response.clusters; } catch (error) { if (isAbortError(error)) { // Ignore abort errors. They will be logged by the gRPC client middleware. diff --git a/web/packages/teleterm/src/preload.ts b/web/packages/teleterm/src/preload.ts index 7c0f3fdc5b3b6..56d1f5ad54621 100644 --- a/web/packages/teleterm/src/preload.ts +++ b/web/packages/teleterm/src/preload.ts @@ -83,7 +83,9 @@ async function getElectronGlobals(): Promise { // All uses of tshClient must wait before updateTshdEventsServerAddress finishes to ensure that // the client is ready. Otherwise we run into a risk of causing panics in tshd due to a missing // tshd events client. - await tshClient.updateTshdEventsServerAddress(tshdEventsServerAddress); + await tshClient.updateTshdEventsServerAddress({ + address: tshdEventsServerAddress, + }); return { mainProcessClient, diff --git a/web/packages/teleterm/src/services/tshd/createClient.ts b/web/packages/teleterm/src/services/tshd/createClient.ts index 46a0bba232298..231d67faf55e1 100644 --- a/web/packages/teleterm/src/services/tshd/createClient.ts +++ b/web/packages/teleterm/src/services/tshd/createClient.ts @@ -18,25 +18,12 @@ import grpc from '@grpc/grpc-js'; import { GrpcTransport } from '@protobuf-ts/grpc-transport'; -import * as api from 'gen-proto-ts/teleport/lib/teleterm/v1/service_pb'; import { TerminalServiceClient } from 'gen-proto-ts/teleport/lib/teleterm/v1/service_pb.client'; import Logger from 'teleterm/logger'; -import * as uri from 'teleterm/ui/uri'; -import { - resourceOneOfIsApp, - resourceOneOfIsDatabase, - resourceOneOfIsKube, - resourceOneOfIsServer, -} from 'teleterm/helpers'; - -import { CloneableAbortSignal, cloneClient } from './cloneableClient'; +import { cloneClient } from './cloneableClient'; import * as types from './types'; -import { - UpdateHeadlessAuthenticationStateParams, - UnifiedResourceResponse, -} from './types'; import { loggingInterceptor } from './interceptors'; export function createTshdClient( @@ -49,579 +36,5 @@ export function createTshdClient( channelCredentials: credentials, interceptors: [loggingInterceptor(logger)], }); - const tshd = cloneClient(new TerminalServiceClient(transport)); - - // Create a client instance that could be shared with the renderer (UI) via Electron contextBridge - const client = { - async logout(clusterUri: uri.RootClusterUri) { - await tshd.logout({ clusterUri }); - }, - - async getKubes({ - clusterUri, - search, - sort, - query, - searchAsRoles, - startKey, - limit, - }: types.GetResourcesParams) { - const { response } = await tshd.getKubes({ - clusterUri, - searchAsRoles, - startKey, - search, - query, - limit, - sortBy: sort ? `${sort.fieldName}:${sort.dir.toLowerCase()}` : '', - }); - - return response as types.GetKubesResponse; - }, - - async getApps({ - clusterUri, - search, - sort, - query, - searchAsRoles, - startKey, - limit, - }: types.GetResourcesParams) { - const { response } = await tshd.getApps({ - clusterUri, - searchAsRoles, - startKey, - search, - query, - limit, - sortBy: sort ? `${sort.fieldName}:${sort.dir.toLowerCase()}` : '', - }); - - return response as types.GetAppsResponse; - }, - - async listGateways() { - const { response } = await tshd.listGateways({}); - - return response.gateways as types.Gateway[]; - }, - - async listLeafClusters(clusterUri: uri.RootClusterUri) { - const { response } = await tshd.listLeafClusters({ clusterUri }); - - return response.clusters as types.Cluster[]; - }, - - async listRootClusters(abortSignal?: CloneableAbortSignal) { - const { response } = await tshd.listRootClusters( - {}, - { - abort: abortSignal, - } - ); - - return response.clusters as types.Cluster[]; - }, - - async getDatabases({ - clusterUri, - search, - sort, - query, - searchAsRoles, - startKey, - limit, - }: types.GetResourcesParams) { - const { response } = await tshd.getDatabases({ - clusterUri, - searchAsRoles, - startKey, - search, - query, - limit, - sortBy: sort ? `${sort.fieldName}:${sort.dir.toLowerCase()}` : '', - }); - - return response as types.GetDatabasesResponse; - }, - - async listDatabaseUsers(dbUri: uri.DatabaseUri) { - const { response } = await tshd.listDatabaseUsers({ dbUri }); - - return response.users; - }, - - async getAccessRequest(clusterUri: uri.RootClusterUri, requestId: string) { - const { response } = await tshd.getAccessRequest({ - clusterUri, - accessRequestId: requestId, - }); - - return response.request; - }, - - async getAccessRequests(clusterUri: uri.RootClusterUri) { - const { response } = await tshd.getAccessRequests({ clusterUri }); - - return response.requests; - }, - - async getServers({ - clusterUri, - search, - query, - sort, - searchAsRoles, - startKey, - limit, - }: types.GetResourcesParams) { - const { response } = await tshd.getServers({ - clusterUri, - searchAsRoles, - startKey, - search, - query, - limit, - sortBy: sort ? `${sort.fieldName}:${sort.dir.toLowerCase()}` : '', - }); - return response as types.GetServersResponse; - }, - - async createAccessRequest(params: types.CreateAccessRequestParams) { - const { response } = await tshd.createAccessRequest(params); - - return response.request; - }, - - async deleteAccessRequest( - clusterUri: uri.RootClusterUri, - requestId: string - ) { - await tshd.deleteAccessRequest({ - rootClusterUri: clusterUri, - accessRequestId: requestId, - }); - }, - - async assumeRole( - clusterUri: uri.RootClusterUri, - requestIds: string[], - dropIds: string[] - ) { - await tshd.assumeRole({ - rootClusterUri: clusterUri, - accessRequestIds: requestIds, - dropRequestIds: dropIds, - }); - }, - - async reviewAccessRequest( - clusterUri: uri.RootClusterUri, - params: types.ReviewAccessRequestParams - ) { - const { response } = await tshd.reviewAccessRequest({ - rootClusterUri: clusterUri, - accessRequestId: params.id, - state: params.state, - reason: params.reason, - roles: params.roles, - assumeStartTime: params.assumeStartTime, - }); - - return response.request; - }, - - async getRequestableRoles(params: types.GetRequestableRolesParams) { - const { response } = await tshd.getRequestableRoles({ - clusterUri: params.rootClusterUri, - resourceIds: params.resourceIds!.map(({ id, clusterName, kind }) => ({ - name: id, - clusterName, - kind, - subResourceName: '', - })), - }); - - return response; - }, - - async addRootCluster(addr: string) { - const { response } = await tshd.addCluster({ name: addr }); - return response as types.Cluster; - }, - - async getCluster(uri: uri.RootClusterUri) { - const { response } = await tshd.getCluster({ clusterUri: uri }); - return response as types.Cluster; - }, - - async loginLocal( - params: types.LoginLocalParams, - abortSignal?: CloneableAbortSignal - ) { - await tshd.login( - { - clusterUri: params.clusterUri, - params: { - oneofKind: 'local', - local: { - token: params.token, - user: params.username, - password: params.password, - }, - }, - }, - { - abort: abortSignal, - } - ); - }, - - async loginSso( - params: types.LoginSsoParams, - abortSignal?: CloneableAbortSignal - ) { - await tshd.login( - { - clusterUri: params.clusterUri, - params: { - oneofKind: 'sso', - sso: { - providerName: params.providerName, - providerType: params.providerType, - }, - }, - }, - { abort: abortSignal } - ); - }, - - async loginPasswordless( - params: types.LoginPasswordlessParams, - abortSignal?: CloneableAbortSignal - ) { - return new Promise((resolve, reject) => { - const stream = tshd.loginPasswordless({ - abort: abortSignal, - }); - - let hasDeviceBeenTapped = false; - - // Init the stream. - stream.requests.send({ - request: { - oneofKind: 'init', - init: { - clusterUri: params.clusterUri, - }, - }, - }); - - stream.responses.onMessage(function (response) { - switch (response.prompt) { - case api.PasswordlessPrompt.PIN: - const pinResponse = (pin: string) => { - stream.requests.send({ - request: { - oneofKind: 'pin', - pin: { pin }, - }, - }); - }; - - params.onPromptCallback({ - type: 'pin', - onUserResponse: pinResponse, - }); - return; - - case api.PasswordlessPrompt.CREDENTIAL: - const credResponse = (index: number) => { - stream.requests.send({ - request: { - oneofKind: 'credential', - credential: { index }, - }, - }); - }; - - params.onPromptCallback({ - type: 'credential', - onUserResponse: credResponse, - data: { credentials: response.credentials || [] }, - }); - return; - - case api.PasswordlessPrompt.TAP: - if (hasDeviceBeenTapped) { - params.onPromptCallback({ type: 'retap' }); - } else { - hasDeviceBeenTapped = true; - params.onPromptCallback({ type: 'tap' }); - } - return; - - // Following cases should never happen but just in case? - case api.PasswordlessPrompt.UNSPECIFIED: - stream.requests.complete(); - return reject(new Error('no passwordless prompt was specified')); - - default: - stream.requests.complete(); - return reject( - new Error( - `passwordless prompt '${response.prompt}' not supported` - ) - ); - } - }); - - stream.responses.onComplete(function () { - resolve(); - }); - - stream.responses.onError(function (err: Error) { - reject(err); - }); - }); - }, - - async getAuthSettings(clusterUri: uri.RootClusterUri) { - const { response } = await tshd.getAuthSettings({ clusterUri }); - return response; - }, - - async createGateway(params: types.CreateGatewayParams) { - const { response } = await tshd.createGateway({ - targetUri: params.targetUri, - targetUser: params.user, - localPort: params.port, - targetSubresourceName: params.subresource_name, - }); - - return response as types.Gateway; - }, - - async removeCluster(clusterUri: uri.RootClusterUri) { - await tshd.removeCluster({ clusterUri }); - }, - - async removeGateway(gatewayUri: uri.GatewayUri) { - await tshd.removeGateway({ gatewayUri }); - }, - - async setGatewayTargetSubresourceName( - gatewayUri: uri.GatewayUri, - targetSubresourceName = '' - ) { - const { response } = await tshd.setGatewayTargetSubresourceName({ - gatewayUri, - targetSubresourceName, - }); - - return response as types.Gateway; - }, - - async setGatewayLocalPort(gatewayUri: uri.GatewayUri, localPort: string) { - const { response } = await tshd.setGatewayLocalPort({ - gatewayUri, - localPort, - }); - - return response as types.Gateway; - }, - - transferFile( - req: types.FileTransferRequest, - abortSignal: CloneableAbortSignal - ) { - const stream = tshd.transferFile(req, { - abort: abortSignal, - }); - - return { - onProgress(callback: (progress: number) => void) { - stream.responses.onMessage(data => callback(data.percentage)); - }, - onComplete: stream.responses.onComplete, - onError: stream.responses.onError, - }; - }, - - async updateTshdEventsServerAddress(address: string) { - await tshd.updateTshdEventsServerAddress({ address }); - }, - - reportUsageEvent: tshd.reportUsageEvent, - - async createConnectMyComputerRole(rootClusterUri: uri.RootClusterUri) { - const { response } = await tshd.createConnectMyComputerRole({ - rootClusterUri, - }); - - return response; - }, - - async createConnectMyComputerNodeToken(uri: uri.RootClusterUri) { - const { response } = await tshd.createConnectMyComputerNodeToken({ - rootClusterUri: uri, - }); - - return response; - }, - - async waitForConnectMyComputerNodeJoin( - uri: uri.RootClusterUri, - abortSignal: CloneableAbortSignal - ) { - const { response } = await tshd.waitForConnectMyComputerNodeJoin( - { - rootClusterUri: uri, - }, - { - abort: abortSignal, - } - ); - - return response as types.WaitForConnectMyComputerNodeJoinResponse; - }, - - async deleteConnectMyComputerNode(uri: uri.RootClusterUri) { - await tshd.deleteConnectMyComputerNode({ - rootClusterUri: uri, - }); - }, - - async getConnectMyComputerNodeName(uri: uri.RootClusterUri) { - const { response } = await tshd.getConnectMyComputerNodeName({ - rootClusterUri: uri, - }); - - return response.name as uri.ServerUri; - }, - - async updateHeadlessAuthenticationState( - params: UpdateHeadlessAuthenticationStateParams, - abortSignal?: CloneableAbortSignal - ) { - await tshd.updateHeadlessAuthenticationState(params, { - abort: abortSignal, - }); - }, - - async listUnifiedResources( - params: types.ListUnifiedResourcesRequest, - abortSignal?: CloneableAbortSignal - ) { - const { response } = await tshd.listUnifiedResources(params, { - abort: abortSignal, - }); - return { - nextKey: response.nextKey, - resources: response.resources - .map(p => { - if (resourceOneOfIsServer(p.resource)) { - return { - kind: 'server', - resource: p.resource.server, - }; - } - - if (resourceOneOfIsDatabase(p.resource)) { - return { - kind: 'database', - resource: p.resource.database, - }; - } - - if (resourceOneOfIsApp(p.resource)) { - return { - kind: 'app', - resource: p.resource.app, - }; - } - - if (resourceOneOfIsKube(p.resource)) { - return { - kind: 'kube', - resource: p.resource.kube, - }; - } - - logger.info(`Ignoring unsupported resource ${JSON.stringify(p)}.`); - }) - .filter(Boolean) as UnifiedResourceResponse[], - }; - }, - async getUserPreferences( - params: api.GetUserPreferencesRequest, - abortSignal?: CloneableAbortSignal - ): Promise { - const { response } = await tshd.getUserPreferences(params, { - abort: abortSignal, - }); - - return response.userPreferences; - }, - async updateUserPreferences( - params: api.UpdateUserPreferencesRequest, - abortSignal?: CloneableAbortSignal - ): Promise { - const userPreferences: api.UserPreferences = {}; - if (params.userPreferences.clusterPreferences) { - userPreferences.clusterPreferences = { - pinnedResources: { - resourceIds: - params.userPreferences.clusterPreferences.pinnedResources - ?.resourceIds, - }, - }; - } - - if (params.userPreferences.unifiedResourcePreferences) { - userPreferences.unifiedResourcePreferences = { - defaultTab: - params.userPreferences.unifiedResourcePreferences.defaultTab, - viewMode: params.userPreferences.unifiedResourcePreferences.viewMode, - labelsViewMode: - params.userPreferences.unifiedResourcePreferences.labelsViewMode, - }; - } - - const { response } = await tshd.updateUserPreferences( - { - clusterUri: params.clusterUri, - userPreferences, - }, - { - abort: abortSignal, - } - ); - - return response.userPreferences; - }, - async promoteAccessRequest( - params: api.PromoteAccessRequestRequest, - abortSignal?: CloneableAbortSignal - ): Promise { - const { response } = await tshd.promoteAccessRequest(params, { - abort: abortSignal, - }); - return response.request; - }, - - async getSuggestedAccessLists( - params: api.GetSuggestedAccessListsRequest, - abortSignal?: CloneableAbortSignal - ): Promise { - const { response } = await tshd.getSuggestedAccessLists(params, { - abort: abortSignal, - }); - - return response.accessLists; - }, - }; - - return client; + return cloneClient(new TerminalServiceClient(transport)); } diff --git a/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts b/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts index 8da9c38ac2cf7..ca14d8a05e997 100644 --- a/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts +++ b/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts @@ -16,94 +16,101 @@ * along with this program. If not, see . */ -import { makeRootCluster } from 'teleterm/services/tshd/testHelpers'; +import { + makeRootCluster, + makeAppGateway, +} from 'teleterm/services/tshd/testHelpers'; +import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient'; import * as types from '../types'; export class MockTshClient implements types.TshdClient { - listRootClusters: () => Promise; - listLeafClusters = () => Promise.resolve([]); - getKubes: ( - params: types.GetResourcesParams - ) => Promise; - getDatabases: ( - params: types.GetResourcesParams - ) => Promise; - listDatabaseUsers: (dbUri: string) => Promise; - getRequestableRoles: ( - params: types.GetRequestableRolesParams - ) => Promise; - getServers: ( - params: types.GetResourcesParams - ) => Promise; - getApps: (params: types.GetResourcesParams) => Promise; - assumeRole: ( - clusterUri: string, - requestIds: string[], - dropIds: string[] - ) => Promise; - deleteAccessRequest: (clusterUri: string, requestId: string) => Promise; - getAccessRequests: (clusterUri: string) => Promise; - getAccessRequest: ( - clusterUri: string, - requestId: string - ) => Promise; - reviewAccessRequest: ( - clusterUri: string, - params: types.ReviewAccessRequestParams - ) => Promise; - createAccessRequest: ( - params: types.CreateAccessRequestParams - ) => Promise; - addRootCluster: (addr: string) => Promise; - - listGateways: () => Promise; - createGateway: (params: types.CreateGatewayParams) => Promise; - removeGateway: (gatewayUri: string) => Promise; - setGatewayTargetSubresourceName: ( - gatewayUri: string, - targetSubresourceName: string - ) => Promise; - setGatewayLocalPort: ( - gatewayUri: string, - localPort: string - ) => Promise; - - getCluster = () => Promise.resolve(makeRootCluster()); - getAuthSettings: (clusterUri: string) => Promise; - removeCluster = () => Promise.resolve(); - loginLocal: ( - params: types.LoginLocalParams, - abortSignal?: types.CloneableAbortSignal - ) => Promise; - loginSso: ( - params: types.LoginSsoParams, - abortSignal?: types.CloneableAbortSignal - ) => Promise; - loginPasswordless: ( - params: types.LoginPasswordlessParams, - abortSignal?: types.CloneableAbortSignal - ) => Promise; - logout = () => Promise.resolve(); - transferFile: () => undefined; - reportUsageEvent: () => undefined; - - createConnectMyComputerRole = () => Promise.resolve({ certsReloaded: true }); + listRootClusters = () => new MockedUnaryCall({ clusters: [] }); + listLeafClusters = () => new MockedUnaryCall({ clusters: [] }); + getKubes = () => + new MockedUnaryCall({ + agents: [], + totalCount: 0, + startKey: '', + }); + getDatabases = () => + new MockedUnaryCall({ + agents: [], + totalCount: 0, + startKey: '', + }); + listDatabaseUsers = () => + new MockedUnaryCall({ + users: [], + totalCount: 0, + startKey: '', + }); + getRequestableRoles = () => + new MockedUnaryCall({ + roles: [], + applicableRoles: [], + }); + getServers = () => + new MockedUnaryCall({ + agents: [], + totalCount: 0, + startKey: '', + }); + getApps = () => + new MockedUnaryCall({ + agents: [], + totalCount: 0, + startKey: '', + }); + assumeRole = () => new MockedUnaryCall({}); + deleteAccessRequest = () => new MockedUnaryCall({}); + getAccessRequests = () => + new MockedUnaryCall({ + requests: [], + totalCount: 0, + startKey: '', + }); + getAccessRequest = () => new MockedUnaryCall({}); + reviewAccessRequest = () => new MockedUnaryCall({}); + createAccessRequest = () => new MockedUnaryCall({}); + addCluster = () => new MockedUnaryCall(makeRootCluster()); + listGateways = () => new MockedUnaryCall({ gateways: [] }); + createGateway = () => new MockedUnaryCall(makeAppGateway()); + removeGateway = () => new MockedUnaryCall({}); + setGatewayTargetSubresourceName = () => new MockedUnaryCall(makeAppGateway()); + setGatewayLocalPort = () => new MockedUnaryCall(makeAppGateway()); + getCluster = () => new MockedUnaryCall(makeRootCluster()); + getAuthSettings = () => + new MockedUnaryCall({ + localAuthEnabled: true, + secondFactor: 'webauthn', + preferredMfa: 'webauthn', + authProviders: [], + hasMessageOfTheDay: false, + authType: 'local', + allowPasswordless: false, + localConnectorName: '', + }); + removeCluster = () => new MockedUnaryCall({}); + login = () => new MockedUnaryCall({}); + loginPasswordless = undefined; + logout = () => new MockedUnaryCall({}); + transferFile = undefined; + reportUsageEvent = () => new MockedUnaryCall({}); + createConnectMyComputerRole = () => + new MockedUnaryCall({ certsReloaded: true }); createConnectMyComputerNodeToken = () => - Promise.resolve({ token: 'abc', labelsList: [] }); - waitForConnectMyComputerNodeJoin: () => Promise; - - updateHeadlessAuthenticationState: ( - params: types.UpdateHeadlessAuthenticationStateParams - ) => Promise; - deleteConnectMyComputerNode = () => Promise.resolve(); - getConnectMyComputerNodeName = () => Promise.resolve(''); - - listUnifiedResources = async () => ({ resources: [], nextKey: '' }); - getUserPreferences = async () => ({}); - updateUserPreferences = async () => ({}); - getSuggestedAccessLists = async () => []; - promoteAccessRequest = async () => undefined; - - updateTshdEventsServerAddress: (address: string) => Promise; + new MockedUnaryCall({ token: 'abc', labelsList: [] }); + waitForConnectMyComputerNodeJoin = () => new MockedUnaryCall({}); + updateHeadlessAuthenticationState = () => new MockedUnaryCall({}); + deleteConnectMyComputerNode = () => new MockedUnaryCall({}); + getConnectMyComputerNodeName = () => new MockedUnaryCall({ name: '' }); + listUnifiedResources = () => + new MockedUnaryCall({ resources: [], nextKey: '' }); + getUserPreferences = () => new MockedUnaryCall({}); + updateUserPreferences = () => new MockedUnaryCall({}); + getSuggestedAccessLists = () => new MockedUnaryCall({ accessLists: [] }); + promoteAccessRequest = () => new MockedUnaryCall({}); + updateTshdEventsServerAddress = () => new MockedUnaryCall({}); + authenticateWebDevice = () => new MockedUnaryCall({}); } diff --git a/web/packages/teleterm/src/services/tshd/testHelpers.ts b/web/packages/teleterm/src/services/tshd/testHelpers.ts index 56e52ec84ad08..8be8bbf5b07c5 100644 --- a/web/packages/teleterm/src/services/tshd/testHelpers.ts +++ b/web/packages/teleterm/src/services/tshd/testHelpers.ts @@ -111,7 +111,6 @@ export const makeLoggedInUser = ( props: Partial = {} ): tsh.LoggedInUser => ({ activeRequests: [], - assumedRequests: {}, name: 'alice', acl: { recordedSessions: { @@ -223,7 +222,7 @@ export const makeLoggedInUser = ( roles: [], requestableRoles: [], suggestedReviewers: [], - userType: tsh.UserType.LOCAL, + userType: tsh.LoggedInUser_UserType.LOCAL, ...props, }); diff --git a/web/packages/teleterm/src/services/tshd/types.ts b/web/packages/teleterm/src/services/tshd/types.ts index bd11535e07021..6e977cf89b355 100644 --- a/web/packages/teleterm/src/services/tshd/types.ts +++ b/web/packages/teleterm/src/services/tshd/types.ts @@ -16,93 +16,33 @@ * along with this program. If not, see . */ -/* eslint-disable @typescript-eslint/ban-ts-comment*/ -// @ts-ignore -import { ResourceKind } from 'e-teleterm/ui/DocumentAccessRequests/NewRequest/useNewRequest'; -// @ts-ignore -import { RequestState } from 'e-teleport/services/workflow'; +import { ITerminalServiceClient } from 'gen-proto-ts/teleport/lib/teleterm/v1/service_pb.client'; + import { SortType } from 'design/DataTable/types'; -import { FileTransferListeners } from 'shared/components/FileTransfer'; -import { NodeSubKind } from 'shared/services'; -import { Timestamp } from 'gen-proto-ts/google/protobuf/timestamp_pb'; -import * as apiCluster from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb'; -import * as apiDb from 'gen-proto-ts/teleport/lib/teleterm/v1/database_pb'; -import * as apiGateway from 'gen-proto-ts/teleport/lib/teleterm/v1/gateway_pb'; -import * as apiServer from 'gen-proto-ts/teleport/lib/teleterm/v1/server_pb'; -import * as apiKube from 'gen-proto-ts/teleport/lib/teleterm/v1/kube_pb'; -import * as apiApp from 'gen-proto-ts/teleport/lib/teleterm/v1/app_pb'; -import * as apiLabel from 'gen-proto-ts/teleport/lib/teleterm/v1/label_pb'; -import * as apiService from 'gen-proto-ts/teleport/lib/teleterm/v1/service_pb'; -import * as apiAuthSettings from 'gen-proto-ts/teleport/lib/teleterm/v1/auth_settings_pb'; -import * as apiAccessRequest from 'gen-proto-ts/teleport/lib/teleterm/v1/access_request_pb'; -import * as apiUsageEvents from 'gen-proto-ts/teleport/lib/teleterm/v1/usage_events_pb'; -import * as apiAccessList from 'gen-proto-ts/teleport/accesslist/v1/accesslist_pb'; -import * as uri from 'teleterm/ui/uri'; +import { CloneableClient } from './cloneableClient'; + +import type * as uri from 'teleterm/ui/uri'; -import { +export * from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb'; +export * from 'gen-proto-ts/teleport/lib/teleterm/v1/database_pb'; +export * from 'gen-proto-ts/teleport/lib/teleterm/v1/gateway_pb'; +export * from 'gen-proto-ts/teleport/lib/teleterm/v1/server_pb'; +export * from 'gen-proto-ts/teleport/lib/teleterm/v1/kube_pb'; +export * from 'gen-proto-ts/teleport/lib/teleterm/v1/app_pb'; +export * from 'gen-proto-ts/teleport/lib/teleterm/v1/label_pb'; +export * from 'gen-proto-ts/teleport/lib/teleterm/v1/service_pb'; +export * from 'gen-proto-ts/teleport/lib/teleterm/v1/auth_settings_pb'; +export * from 'gen-proto-ts/teleport/lib/teleterm/v1/access_request_pb'; +export * from 'gen-proto-ts/teleport/lib/teleterm/v1/usage_events_pb'; +export * from 'gen-proto-ts/teleport/accesslist/v1/accesslist_pb'; + +export type { CloneableAbortSignal, CloneableRpcOptions, CloneableClient, } from './cloneableClient'; -// We want to reexport both the type and the value of UserType. Because it's in a namespace, we have -// to alias it first to do the reexport. -// https://www.typescriptlang.org/docs/handbook/namespaces.html#aliases -import UserType = apiCluster.LoggedInUser_UserType; - -import type { ITerminalServiceClient } from 'gen-proto-ts/teleport/lib/teleterm/v1/service_pb.client'; - -export { UserType }; -export type { CloneableAbortSignal, CloneableRpcOptions }; - -export interface Kube extends apiKube.Kube { - uri: uri.KubeUri; -} - -export interface Server extends apiServer.Server { - uri: uri.ServerUri; - subKind: NodeSubKind; -} - -export interface App extends apiApp.App { - uri: uri.AppUri; -} - -export interface Gateway extends apiGateway.Gateway { - uri: uri.GatewayUri; - targetUri: uri.GatewayTargetUri; - gatewayCliCommand: GatewayCLICommand; -} - -export type GatewayCLICommand = apiGateway.GatewayCLICommand; - -export type AccessRequest = apiAccessRequest.AccessRequest; -export type ResourceId = apiAccessRequest.ResourceID; -export type AccessRequestReview = apiAccessRequest.AccessRequestReview; -export type AccessList = apiAccessList.AccessList; - -export interface GetServersResponse extends apiService.GetServersResponse { - agents: Server[]; -} - -export interface GetDatabasesResponse extends apiService.GetDatabasesResponse { - agents: Database[]; -} - -export interface GetKubesResponse extends apiService.GetKubesResponse { - agents: Kube[]; -} - -export interface GetAppsResponse extends apiService.GetAppsResponse { - agents: App[]; -} - -export type GetRequestableRolesResponse = - apiService.GetRequestableRolesResponse; - -export type ReportUsageEventRequest = apiUsageEvents.ReportUsageEventRequest; - // Available types are listed here: // https://github.com/gravitational/teleport/blob/v9.0.3/lib/defaults/defaults.go#L513-L530 // @@ -115,184 +55,7 @@ export type GatewayProtocol = | 'redis' | 'sqlserver'; -export interface Database extends apiDb.Database { - uri: uri.DatabaseUri; -} - -export interface Cluster extends apiCluster.Cluster { - uri: uri.ClusterUri; - loggedInUser?: LoggedInUser; -} - -export type LoggedInUser = apiCluster.LoggedInUser & { - assumedRequests?: Record; -}; -export type AuthProvider = apiAuthSettings.AuthProvider; -export type AuthSettings = apiAuthSettings.AuthSettings; - -export interface FileTransferRequest extends apiService.FileTransferRequest { - serverUri: uri.ServerUri; -} - -export type WebauthnCredentialInfo = apiService.CredentialInfo; -export type WebauthnLoginPrompt = - | WebauthnLoginTapPrompt - | WebauthnLoginRetapPrompt - | WebauthnLoginPinPrompt - | WebauthnLoginCredentialPrompt; -export type WebauthnLoginTapPrompt = { type: 'tap' }; -export type WebauthnLoginRetapPrompt = { type: 'retap' }; -export type WebauthnLoginPinPrompt = { - type: 'pin'; - onUserResponse(pin: string): void; -}; -export type WebauthnLoginCredentialPrompt = { - type: 'credential'; - data: { credentials: WebauthnCredentialInfo[] }; - onUserResponse(index: number): void; -}; -export type LoginPasswordlessRequest = - Partial; - -export type TshdClient = { - listRootClusters: (abortSignal?: CloneableAbortSignal) => Promise; - listLeafClusters: (clusterUri: uri.RootClusterUri) => Promise; - getKubes: (params: GetResourcesParams) => Promise; - getApps: (params: GetResourcesParams) => Promise; - getDatabases: (params: GetResourcesParams) => Promise; - listDatabaseUsers: (dbUri: uri.DatabaseUri) => Promise; - assumeRole: ( - clusterUri: uri.RootClusterUri, - requestIds: string[], - dropIds: string[] - ) => Promise; - getRequestableRoles: ( - params: GetRequestableRolesParams - ) => Promise; - getServers: (params: GetResourcesParams) => Promise; - getAccessRequests: ( - clusterUri: uri.RootClusterUri - ) => Promise; - getAccessRequest: ( - clusterUri: uri.RootClusterUri, - requestId: string - ) => Promise; - reviewAccessRequest: ( - clusterUri: uri.RootClusterUri, - params: ReviewAccessRequestParams - ) => Promise; - createAccessRequest: ( - params: CreateAccessRequestParams - ) => Promise; - deleteAccessRequest: ( - clusterUri: uri.RootClusterUri, - requestId: string - ) => Promise; - addRootCluster: (addr: string) => Promise; - - listGateways: () => Promise; - createGateway: (params: CreateGatewayParams) => Promise; - removeGateway: (gatewayUri: uri.GatewayUri) => Promise; - setGatewayTargetSubresourceName: ( - gatewayUri: uri.GatewayUri, - targetSubresourceName: string - ) => Promise; - setGatewayLocalPort: ( - gatewayUri: uri.GatewayUri, - localPort: string - ) => Promise; - - getCluster: (clusterUri: uri.RootClusterUri) => Promise; - getAuthSettings: (clusterUri: uri.RootClusterUri) => Promise; - removeCluster: (clusterUri: uri.RootClusterUri) => Promise; - loginLocal: ( - params: LoginLocalParams, - abortSignal?: CloneableAbortSignal - ) => Promise; - loginSso: ( - params: LoginSsoParams, - abortSignal?: CloneableAbortSignal - ) => Promise; - loginPasswordless: ( - params: LoginPasswordlessParams, - abortSignal?: CloneableAbortSignal - ) => Promise; - logout: (clusterUri: uri.RootClusterUri) => Promise; - transferFile: ( - options: FileTransferRequest, - abortSignal?: CloneableAbortSignal - ) => FileTransferListeners; - reportUsageEvent: CloneableClient['reportUsageEvent']; - createConnectMyComputerRole: ( - rootClusterUri: uri.RootClusterUri - ) => Promise; - createConnectMyComputerNodeToken: ( - clusterUri: uri.RootClusterUri - ) => Promise; - waitForConnectMyComputerNodeJoin: ( - rootClusterUri: uri.RootClusterUri, - abortSignal: CloneableAbortSignal - ) => Promise; - deleteConnectMyComputerNode: ( - clusterUri: uri.RootClusterUri - ) => Promise; - getConnectMyComputerNodeName: (uri: uri.RootClusterUri) => Promise; - - updateHeadlessAuthenticationState: ( - params: UpdateHeadlessAuthenticationStateParams, - abortSignal?: CloneableAbortSignal - ) => Promise; - - listUnifiedResources: ( - params: apiService.ListUnifiedResourcesRequest, - abortSignal?: CloneableAbortSignal - ) => Promise; - - getUserPreferences: ( - params: apiService.GetUserPreferencesRequest, - abortSignal?: CloneableAbortSignal - ) => Promise; - updateUserPreferences: ( - params: apiService.UpdateUserPreferencesRequest, - abortSignal?: CloneableAbortSignal - ) => Promise; - getSuggestedAccessLists: ( - params: apiService.GetSuggestedAccessListsRequest, - abortSignal?: CloneableAbortSignal - ) => Promise; - promoteAccessRequest: ( - params: PromoteAccessRequestParams, - abortSignal?: CloneableAbortSignal - ) => Promise; - - updateTshdEventsServerAddress: (address: string) => Promise; -}; - -interface LoginParamsBase { - clusterUri: uri.RootClusterUri; -} - -export interface LoginLocalParams extends LoginParamsBase { - username: string; - password: string; - token?: string; -} - -export interface LoginSsoParams extends LoginParamsBase { - providerType: string; - providerName: string; -} - -export interface LoginPasswordlessParams extends LoginParamsBase { - onPromptCallback(res: WebauthnLoginPrompt): void; -} - -export type CreateGatewayParams = { - targetUri: uri.GatewayTargetUri; - port?: string; - user: string; - subresource_name?: string; -}; +export type TshdClient = CloneableClient; export type GetResourcesParams = { clusterUri: uri.ClusterUri; @@ -309,68 +72,8 @@ export type GetResourcesParams = { query?: string; }; -// Compatibility type to make sure teleport.e doesn't break. -// TODO(ravicious): Remove after teleterm.e is updated to use GetResourcesParams. -export type ServerSideParams = GetResourcesParams; - -export type ReviewAccessRequestParams = { - state: RequestState; - reason: string; - roles: string[]; - id: string; - assumeStartTime?: Timestamp; -}; - -export type CreateAccessRequestParams = - apiService.CreateAccessRequestRequest & { - rootClusterUri: uri.RootClusterUri; - }; - -export type GetRequestableRolesParams = { - rootClusterUri: uri.RootClusterUri; - resourceIds?: { kind: ResourceKind; clusterName: string; id: string }[]; -}; - export type AssumedRequest = { id: string; expires: Date; roles: string[]; }; - -export type Label = apiLabel.Label; - -export type CreateConnectMyComputerRoleResponse = - apiService.CreateConnectMyComputerRoleResponse; -export type CreateConnectMyComputerNodeTokenResponse = - apiService.CreateConnectMyComputerNodeTokenResponse; -export type WaitForConnectMyComputerNodeJoinResponse = - apiService.WaitForConnectMyComputerNodeJoinResponse & { - server: Server; - }; - -export type ListUnifiedResourcesRequest = - apiService.ListUnifiedResourcesRequest; -export type ListUnifiedResourcesResponse = { - resources: UnifiedResourceResponse[]; - nextKey: string; -}; -export type UnifiedResourceResponse = - | { kind: 'server'; resource: Server } - | { - kind: 'database'; - resource: Database; - } - | { kind: 'kube'; resource: Kube } - | { kind: 'app'; resource: App }; - -export type UserPreferences = apiService.UserPreferences; -export type PromoteAccessRequestParams = - apiService.PromoteAccessRequestRequest & { - rootClusterUri: uri.RootClusterUri; - }; - -export type UpdateHeadlessAuthenticationStateParams = { - rootClusterUri: uri.RootClusterUri; - headlessAuthenticationId: string; - state: apiService.HeadlessAuthenticationState; -}; diff --git a/web/packages/teleterm/src/ui/ConnectMyComputer/DocumentConnectMyComputer/Setup.story.tsx b/web/packages/teleterm/src/ui/ConnectMyComputer/DocumentConnectMyComputer/Setup.story.tsx index 6098d8152ddeb..2ae06420f2445 100644 --- a/web/packages/teleterm/src/ui/ConnectMyComputer/DocumentConnectMyComputer/Setup.story.tsx +++ b/web/packages/teleterm/src/ui/ConnectMyComputer/DocumentConnectMyComputer/Setup.story.tsx @@ -26,7 +26,7 @@ import { makeServer, } from 'teleterm/services/tshd/testHelpers'; import { IAppContext } from 'teleterm/ui/types'; -import { Cluster, UserType } from 'teleterm/services/tshd/types'; +import { Cluster, LoggedInUser_UserType } from 'teleterm/services/tshd/types'; import { ResourcesContextProvider } from 'teleterm/ui/DocumentCluster/resourcesContext'; import { ConnectMyComputerContextProvider } from '../connectMyComputerContext'; @@ -136,7 +136,7 @@ export function NoAccess() { export function AccessUnknown() { const cluster = makeRootCluster(); - cluster.loggedInUser.userType = UserType.UNSPECIFIED; + cluster.loggedInUser.userType = LoggedInUser_UserType.UNSPECIFIED; const appContext = new MockAppContext({}); return ( diff --git a/web/packages/teleterm/src/ui/ConnectMyComputer/access.ts b/web/packages/teleterm/src/ui/ConnectMyComputer/access.ts index 2729cba25192e..ee76143d2b3b8 100644 --- a/web/packages/teleterm/src/ui/ConnectMyComputer/access.ts +++ b/web/packages/teleterm/src/ui/ConnectMyComputer/access.ts @@ -52,13 +52,13 @@ export function getConnectMyComputerAccess( if ( !loggedInUser || - loggedInUser.userType === tsh.UserType.UNSPECIFIED || + loggedInUser.userType === tsh.LoggedInUser_UserType.UNSPECIFIED || !loggedInUser.acl ) { return { status: 'unknown' }; } - if (loggedInUser.userType === tsh.UserType.SSO) { + if (loggedInUser.userType === tsh.LoggedInUser_UserType.SSO) { return { status: 'no-access', reason: 'sso-user' }; } diff --git a/web/packages/teleterm/src/ui/DocumentCluster/DocumentCluster.story.tsx b/web/packages/teleterm/src/ui/DocumentCluster/DocumentCluster.story.tsx index e696fd32b71bd..eceb061e970ed 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/DocumentCluster.story.tsx +++ b/web/packages/teleterm/src/ui/DocumentCluster/DocumentCluster.story.tsx @@ -137,7 +137,7 @@ export const OnlineEmptyResourcesAndCanAddResourcesAndConnectComputer = () => { makeRootCluster({ uri: rootClusterDoc.clusterUri, loggedInUser: makeLoggedInUser({ - userType: tsh.UserType.LOCAL, + userType: tsh.LoggedInUser_UserType.LOCAL, acl: { tokens: { create: true, @@ -173,7 +173,7 @@ export const OnlineEmptyResourcesAndCanAddResourcesButCannotConnectComputer = makeRootCluster({ uri: rootClusterDoc.clusterUri, loggedInUser: makeLoggedInUser({ - userType: tsh.UserType.SSO, + userType: tsh.LoggedInUser_UserType.SSO, acl: { tokens: { create: true, @@ -312,7 +312,7 @@ function renderState({ doc: docTypes.DocumentCluster; listUnifiedResources?: ResourcesService['listUnifiedResources']; platform?: NodeJS.Platform; - userType?: tsh.UserType; + userType?: tsh.LoggedInUser_UserType; }) { const appContext = new MockAppContext({ platform }); appContext.clustersService.state = state; diff --git a/web/packages/teleterm/src/ui/DocumentCluster/DocumentCluster.test.tsx b/web/packages/teleterm/src/ui/DocumentCluster/DocumentCluster.test.tsx index 5d6802dfdce64..3c7d7abb082a4 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/DocumentCluster.test.tsx +++ b/web/packages/teleterm/src/ui/DocumentCluster/DocumentCluster.test.tsx @@ -48,7 +48,7 @@ it('displays a button for Connect My Computer in the empty state if the user can makeRootCluster({ uri: doc.clusterUri, loggedInUser: makeLoggedInUser({ - userType: tsh.UserType.LOCAL, + userType: tsh.LoggedInUser_UserType.LOCAL, acl: { tokens: { create: true, @@ -118,7 +118,7 @@ it('does not display a button for Connect My Computer in the empty state if the makeRootCluster({ uri: doc.clusterUri, loggedInUser: makeLoggedInUser({ - userType: tsh.UserType.LOCAL, + userType: tsh.LoggedInUser_UserType.LOCAL, acl: { tokens: { create: false, diff --git a/web/packages/teleterm/src/ui/DocumentCluster/UnifiedResources.tsx b/web/packages/teleterm/src/ui/DocumentCluster/UnifiedResources.tsx index 0437934111708..01ead0f04002c 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/UnifiedResources.tsx +++ b/web/packages/teleterm/src/ui/DocumentCluster/UnifiedResources.tsx @@ -41,10 +41,10 @@ import { Attempt } from 'shared/hooks/useAsync'; import { DefaultTab } from 'gen-proto-ts/teleport/userpreferences/v1/unified_resource_preferences_pb'; -import { - UnifiedResourceResponse, - UserPreferences, -} from 'teleterm/services/tshd/types'; +import { NodeSubKind } from 'shared/services'; + +import { UserPreferences } from 'teleterm/services/tshd/types'; +import { UnifiedResourceResponse } from 'teleterm/ui/services/resources'; import { useAppContext } from 'teleterm/ui/appContextProvider'; import * as uri from 'teleterm/ui/uri'; import { useWorkspaceContext } from 'teleterm/ui/Documents'; @@ -305,7 +305,7 @@ const mapToSharedResource = ( hostname: server.hostname, addr: server.addr, tunnel: server.tunnel, - subKind: server.subKind, + subKind: server.subKind as NodeSubKind, }, ui: { ActionButton: , diff --git a/web/packages/teleterm/src/ui/DocumentCluster/useUserPreferences.test.tsx b/web/packages/teleterm/src/ui/DocumentCluster/useUserPreferences.test.tsx index 02023bb2d6bba..5092c8bccee86 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/useUserPreferences.test.tsx +++ b/web/packages/teleterm/src/ui/DocumentCluster/useUserPreferences.test.tsx @@ -25,10 +25,15 @@ import { } from 'gen-proto-ts/teleport/userpreferences/v1/unified_resource_preferences_pb'; import { makeRootCluster } from 'teleterm/services/tshd/testHelpers'; +import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient'; import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; -import { UserPreferences } from 'teleterm/services/tshd/types'; +import { + UserPreferences, + GetUserPreferencesResponse, + UpdateUserPreferencesResponse, +} from 'teleterm/services/tshd/types'; import { useUserPreferences } from './useUserPreferences'; @@ -44,7 +49,9 @@ const preferences: UserPreferences = { test('user preferences are fetched', async () => { const appContext = new MockAppContext(); - const getUserPreferencesPromise = Promise.resolve(preferences); + const getUserPreferencesPromise = new MockedUnaryCall({ + userPreferences: preferences, + }); jest .spyOn(appContext.tshd, 'getUserPreferences') @@ -77,14 +84,19 @@ test('user preferences are fetched', async () => { test('unified resources fallback preferences are taken from a workspace', async () => { const appContext = new MockAppContext(); - let resolveGetUserPreferencesPromise: (u: UserPreferences) => void; - const getUserPreferencesPromise = new Promise(resolve => { - resolveGetUserPreferencesPromise = resolve; - }); + let resolveGetUserPreferencesPromise: (u: GetUserPreferencesResponse) => void; + const getUserPreferencesPromise = new Promise( + resolve => { + resolveGetUserPreferencesPromise = resolve; + } + ); jest .spyOn(appContext.tshd, 'getUserPreferences') - .mockImplementation(() => getUserPreferencesPromise); + .mockImplementation(async () => { + const response = await getUserPreferencesPromise; + return new MockedUnaryCall(response); + }); jest .spyOn(appContext.workspacesService, 'getUnifiedResourcePreferences') .mockReturnValue(preferences.unifiedResourcePreferences); @@ -119,14 +131,19 @@ describe('updating preferences', () => { }); it('works correctly when the initial preferences were fetched', async () => { - const getUserPreferencesPromise = Promise.resolve(preferences); + const getUserPreferencesPromise = new MockedUnaryCall({ + userPreferences: preferences, + }); jest .spyOn(appContext.tshd, 'getUserPreferences') .mockImplementation(() => getUserPreferencesPromise); jest .spyOn(appContext.tshd, 'updateUserPreferences') - .mockImplementation(async preferences => preferences.userPreferences); + .mockImplementation( + async preferences => + new MockedUnaryCall({ userPreferences: preferences.userPreferences }) + ); const { result } = renderHook(() => useUserPreferences(cluster.uri), { wrapper: ({ children }) => ( @@ -169,27 +186,34 @@ describe('updating preferences', () => { it('works correctly when the initial preferences have not been fetched yet', async () => { let rejectGetUserPreferencesPromise: (error: Error) => void; - const getUserPreferencesPromise = new Promise( + const getUserPreferencesPromise = new Promise( (resolve, reject) => { rejectGetUserPreferencesPromise = reject; } ); - let resolveUpdateUserPreferencesPromise: (u: UserPreferences) => void; - const updateUserPreferencesPromise = new Promise(resolve => { - resolveUpdateUserPreferencesPromise = resolve; - }); + let resolveUpdateUserPreferencesPromise: ( + u: UpdateUserPreferencesResponse + ) => void; + const updateUserPreferencesPromise = + new Promise(resolve => { + resolveUpdateUserPreferencesPromise = resolve; + }); jest .spyOn(appContext.tshd, 'getUserPreferences') - .mockImplementation((requestParams, abortSignal) => { - abortSignal.addEventListener('abort', () => + .mockImplementation(async (requestParams, { abort }) => { + abort.addEventListener('abort', () => rejectGetUserPreferencesPromise(new Error('Aborted')) ); - return getUserPreferencesPromise; + const response = await getUserPreferencesPromise; + return new MockedUnaryCall(response); }); jest .spyOn(appContext.tshd, 'updateUserPreferences') - .mockImplementation(() => updateUserPreferencesPromise); + .mockImplementation(async () => { + const response = await updateUserPreferencesPromise; + return new MockedUnaryCall(response); + }); const { result } = renderHook(() => useUserPreferences(cluster.uri), { wrapper: ({ children }) => ( @@ -233,11 +257,13 @@ describe('updating preferences', () => { // (e.g., because they were changed it in the browser in the meantime) act(() => resolveUpdateUserPreferencesPromise({ - clusterPreferences: { pinnedResources: { resourceIds: ['abc'] } }, - unifiedResourcePreferences: { - viewMode: ViewMode.CARD, - defaultTab: DefaultTab.PINNED, - labelsViewMode: LabelsViewMode.COLLAPSED, + userPreferences: { + clusterPreferences: { pinnedResources: { resourceIds: ['abc'] } }, + unifiedResourcePreferences: { + viewMode: ViewMode.CARD, + defaultTab: DefaultTab.PINNED, + labelsViewMode: LabelsViewMode.COLLAPSED, + }, }, }) ); diff --git a/web/packages/teleterm/src/ui/DocumentCluster/useUserPreferences.ts b/web/packages/teleterm/src/ui/DocumentCluster/useUserPreferences.ts index 09cb189f307ff..829317dfd6d0c 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/useUserPreferences.ts +++ b/web/packages/teleterm/src/ui/DocumentCluster/useUserPreferences.ts @@ -77,12 +77,17 @@ export function useUserPreferences(clusterUri: ClusterUri): { const [initialFetchAttempt, runInitialFetchAttempt] = useAsync( useCallback( async () => - retryWithRelogin(appContext, clusterUri, () => - appContext.tshd.getUserPreferences( + retryWithRelogin(appContext, clusterUri, async () => { + const { response } = await appContext.tshd.getUserPreferences( { clusterUri }, - cloneAbortSignal(initialFetchAttemptAbortController.current.signal) - ) - ), + { + abort: cloneAbortSignal( + initialFetchAttemptAbortController.current.signal + ), + } + ); + return response.userPreferences; + }), [appContext, clusterUri] ) ); @@ -97,12 +102,13 @@ export function useUserPreferences(clusterUri: ClusterUri): { const [, runUpdateAttempt] = useAsync( useCallback( async (newPreferences: UserPreferences) => - retryWithRelogin(appContext, clusterUri, () => - appContext.tshd.updateUserPreferences({ + retryWithRelogin(appContext, clusterUri, async () => { + const { response } = await appContext.tshd.updateUserPreferences({ clusterUri, userPreferences: newPreferences, - }) - ), + }); + return response.userPreferences; + }), [appContext, clusterUri] ) ); diff --git a/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.test.tsx b/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.test.tsx index 0b1fc23e5a846..f2c0ed0023bb4 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.test.tsx +++ b/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.test.tsx @@ -58,9 +58,9 @@ it('creates a gateway on mount if it does not exist already', async () => { expect(appContext.clustersService.createGateway).toHaveBeenCalledWith({ targetUri: doc.targetUri, - subresource_name: doc.targetSubresourceName, - user: doc.targetUser, - port: doc.port, + targetSubresourceName: doc.targetSubresourceName, + targetUser: doc.targetUser, + localPort: doc.port, }); expect(appContext.clustersService.createGateway).toHaveBeenCalledTimes(1); }); diff --git a/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.ts b/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.ts index 683e1625c1e3c..1177d86d3bf4b 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.ts +++ b/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.ts @@ -50,9 +50,9 @@ export function useGateway(doc: types.DocumentGateway) { gw = await retryWithRelogin(ctx, doc.targetUri, () => ctx.clustersService.createGateway({ targetUri: doc.targetUri, - port: port, - user: doc.targetUser, - subresource_name: doc.targetSubresourceName, + localPort: port, + targetUser: doc.targetUser, + targetSubresourceName: doc.targetSubresourceName, }) ); } catch (error) { diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx index 2ee9246be8fcf..99d07789e7047 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx @@ -26,6 +26,7 @@ import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; import { MockWorkspaceContextProvider } from 'teleterm/ui/fixtures/MockWorkspaceContextProvider'; import { makeAppGateway } from 'teleterm/services/tshd/testHelpers'; +import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient'; export default { title: 'Teleterm/DocumentGatewayApp', @@ -64,8 +65,8 @@ export function Online() { }; }); - appContext.tshd.setGatewayLocalPort = (uri, localPort) => - wait(800).then(() => ({ ...gateway, localPort })); + appContext.tshd.setGatewayLocalPort = ({ localPort }) => + wait(800).then(() => new MockedUnaryCall({ ...gateway, localPort })); return ( diff --git a/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx b/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx index 0574a5a4df54b..0ea71ffc33c93 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx @@ -67,7 +67,9 @@ export const DocumentGatewayKube = (props: { await retryWithRelogin(ctx, doc.targetUri, () => ctx.clustersService.createGateway({ targetUri: doc.targetUri, - user: '', + targetSubresourceName: '', + targetUser: '', + localPort: '', }) ); } catch (error) { diff --git a/web/packages/teleterm/src/ui/services/clusters/clustersService.test.ts b/web/packages/teleterm/src/ui/services/clusters/clustersService.test.ts index f9e132b484936..50dad09d43850 100644 --- a/web/packages/teleterm/src/ui/services/clusters/clustersService.test.ts +++ b/web/packages/teleterm/src/ui/services/clusters/clustersService.test.ts @@ -25,6 +25,7 @@ import { makeRootCluster, makeLeafCluster, } from 'teleterm/services/tshd/testHelpers'; +import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient'; import { ClustersService } from './clustersService'; @@ -70,27 +71,35 @@ function createService(client: Partial): ClustersService { function getClientMocks(): Partial { return { - loginLocal: jest.fn().mockResolvedValueOnce(undefined), - logout: jest.fn().mockResolvedValueOnce(undefined), - addRootCluster: jest.fn().mockResolvedValueOnce(clusterMock), - removeCluster: jest.fn().mockResolvedValueOnce(undefined), - getCluster: jest.fn().mockResolvedValueOnce(clusterMock), - listLeafClusters: jest.fn().mockResolvedValueOnce([leafClusterMock]), - listGateways: jest.fn().mockResolvedValueOnce([gatewayMock]), - createGateway: jest.fn().mockResolvedValueOnce(gatewayMock), - removeGateway: jest.fn().mockResolvedValueOnce(undefined), + login: jest.fn().mockReturnValueOnce(new MockedUnaryCall({})), + logout: jest.fn().mockReturnValueOnce(new MockedUnaryCall({})), + addCluster: jest.fn().mockReturnValueOnce(new MockedUnaryCall(clusterMock)), + removeCluster: jest.fn().mockReturnValueOnce(new MockedUnaryCall({})), + getCluster: jest.fn().mockReturnValueOnce(new MockedUnaryCall(clusterMock)), + listLeafClusters: jest + .fn() + .mockReturnValueOnce( + new MockedUnaryCall({ clusters: [leafClusterMock] }) + ), + listGateways: jest + .fn() + .mockReturnValueOnce(new MockedUnaryCall({ gateways: [gatewayMock] })), + createGateway: jest + .fn() + .mockReturnValueOnce(new MockedUnaryCall(gatewayMock)), + removeGateway: jest.fn().mockReturnValueOnce(new MockedUnaryCall({})), }; } test('add cluster', async () => { - const { addRootCluster } = getClientMocks(); + const { addCluster } = getClientMocks(); const service = createService({ - addRootCluster, + addCluster, }); await service.addRootCluster(clusterUri); - expect(addRootCluster).toHaveBeenCalledWith(clusterUri); + expect(addCluster).toHaveBeenCalledWith({ name: clusterUri }); expect(service.state.clusters).toStrictEqual( new Map([[clusterUri, clusterMock]]) ); @@ -132,9 +141,15 @@ test('remove cluster', async () => { new Map([[gatewayFromOtherCluster.uri, gatewayFromOtherCluster]]) ); - expect(removeGateway).toHaveBeenCalledWith(gatewayFromRootCluster.uri); - expect(removeGateway).toHaveBeenCalledWith(gatewayFromLeafCluster.uri); - expect(removeGateway).not.toHaveBeenCalledWith(gatewayFromOtherCluster.uri); + expect(removeGateway).toHaveBeenCalledWith({ + gatewayUri: gatewayFromRootCluster.uri, + }); + expect(removeGateway).toHaveBeenCalledWith({ + gatewayUri: gatewayFromLeafCluster.uri, + }); + expect(removeGateway).not.toHaveBeenCalledWith({ + gatewayUri: gatewayFromOtherCluster.uri, + }); }); test('sync root cluster', async () => { @@ -146,11 +161,17 @@ test('sync root cluster', async () => { await service.syncRootClusterAndCatchErrors(clusterUri); - expect(service.findCluster(clusterUri)).toStrictEqual(clusterMock); + const clusterMockWithRequests = { + ...clusterMock, + loggedInUser: { ...clusterMock.loggedInUser, assumedRequests: {} }, + }; + expect(service.findCluster(clusterUri)).toStrictEqual( + clusterMockWithRequests + ); expect(service.findCluster(leafClusterMock.uri)).toStrictEqual( leafClusterMock ); - expect(listLeafClusters).toHaveBeenCalledWith(clusterUri); + expect(listLeafClusters).toHaveBeenCalledWith({ clusterUri }); }); test('login into cluster and sync cluster', async () => { @@ -166,7 +187,20 @@ test('login into cluster and sync cluster', async () => { await service.loginLocal(loginParams, undefined); - expect(client.loginLocal).toHaveBeenCalledWith(loginParams, undefined); + expect(client.login).toHaveBeenCalledWith( + { + clusterUri: loginParams.clusterUri, + params: { + oneofKind: 'local', + local: { + password: loginParams.password, + user: loginParams.username, + token: loginParams.token, + }, + }, + }, + { abort: undefined } + ); expect(service.findCluster(clusterUri).connected).toBe(true); }); @@ -175,7 +209,7 @@ test('logout from cluster', async () => { const service = createService({ logout, removeCluster, - getCluster: () => Promise.resolve({ ...clusterMock, connected: false }), + getCluster: () => new MockedUnaryCall({ ...clusterMock, connected: false }), }); service.setState(draftState => { draftState.clusters = new Map([ @@ -186,8 +220,8 @@ test('logout from cluster', async () => { await service.logout(clusterUri); - expect(logout).toHaveBeenCalledWith(clusterUri); - expect(removeCluster).toHaveBeenCalledWith(clusterUri); + expect(logout).toHaveBeenCalledWith({ clusterUri }); + expect(removeCluster).toHaveBeenCalledWith({ clusterUri }); expect(service.findCluster(clusterMock.uri).connected).toBe(false); expect(service.findCluster(leafClusterMock.uri).connected).toBe(false); }); @@ -201,9 +235,19 @@ test('create a gateway', async () => { const port = '2000'; const user = 'alice'; - await service.createGateway({ targetUri, port, user }); + await service.createGateway({ + targetUri, + localPort: port, + targetUser: user, + targetSubresourceName: '', + }); - expect(createGateway).toHaveBeenCalledWith({ targetUri, port, user }); + expect(createGateway).toHaveBeenCalledWith({ + targetUri, + localPort: port, + targetUser: user, + targetSubresourceName: '', + }); expect(service.state.gateways).toStrictEqual( new Map([[gatewayMock.uri, gatewayMock]]) ); @@ -218,7 +262,7 @@ test('remove a gateway', async () => { await service.removeGateway(gatewayUri); - expect(removeGateway).toHaveBeenCalledWith(gatewayUri); + expect(removeGateway).toHaveBeenCalledWith({ gatewayUri }); expect(service.findGateway(gatewayUri)).toBeUndefined(); }); @@ -238,7 +282,9 @@ test('remove a kube gateway', async () => { await service.removeKubeGateway(kubeGatewayMock.targetUri as uri.KubeUri); expect(removeGateway).toHaveBeenCalledTimes(1); - expect(removeGateway).toHaveBeenCalledWith(kubeGatewayMock.uri); + expect(removeGateway).toHaveBeenCalledWith({ + gatewayUri: kubeGatewayMock.uri, + }); expect(service.findGateway(kubeGatewayMock.uri)).toBeUndefined(); // Calling it again should not increase mock calls. @@ -257,7 +303,7 @@ test('sync gateways', async () => { expect(service.state.gateways).toStrictEqual( new Map([[gatewayMock.uri, gatewayMock]]) ); - expect(listGateways).toHaveBeenCalledWith(); + expect(listGateways).toHaveBeenCalledWith({}); }); test('find root cluster by resource URI', () => { diff --git a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts index 5d99eaed50dd4..1dcaf2f0aaad5 100644 --- a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts +++ b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts @@ -29,10 +29,11 @@ import { NotificationsService } from 'teleterm/ui/services/notifications'; import { Cluster, Gateway, - CreateAccessRequestParams, - GetRequestableRolesParams, - ReviewAccessRequestParams, - PromoteAccessRequestParams, + CreateAccessRequestRequest, + GetRequestableRolesRequest, + ReviewAccessRequestRequest, + PromoteAccessRequestRequest, + PasswordlessPrompt, } from 'teleterm/services/tshd/types'; import { MainProcessClient } from 'teleterm/mainProcess/types'; import { UsageService } from 'teleterm/ui/services/usage'; @@ -64,10 +65,10 @@ export class ClustersService extends ImmutableStore } async addRootCluster(addr: string) { - const cluster = await this.client.addRootCluster(addr); + const { response: cluster } = await this.client.addCluster({ name: addr }); this.setState(draft => { draft.clusters.set( - cluster.uri, + cluster.uri as uri.RootClusterUri, this.removeInternalLoginsFromCluster(cluster) ); }); @@ -86,8 +87,8 @@ export class ClustersService extends ImmutableStore */ async logout(clusterUri: uri.RootClusterUri) { // TODO(gzdunek): logout and removeCluster should be combined into a single acton in tshd - await this.client.logout(clusterUri); - await this.client.removeCluster(clusterUri); + await this.client.logout({ clusterUri }); + await this.client.removeCluster({ clusterUri }); this.setState(draft => { draft.clusters.forEach(cluster => { @@ -102,7 +103,20 @@ export class ClustersService extends ImmutableStore params: types.LoginLocalParams, abortSignal: tsh.CloneableAbortSignal ) { - await this.client.loginLocal(params, abortSignal); + await this.client.login( + { + clusterUri: params.clusterUri, + params: { + oneofKind: 'local', + local: { + user: params.username, + password: params.password, + token: params.token, + }, + }, + }, + { abort: abortSignal } + ); // We explicitly use the `andCatchErrors` variant here. If loginLocal succeeds but syncing the // cluster fails, we don't want to stop the user on the failed modal – we want to open the // workspace and show an error state within the workspace. @@ -114,7 +128,19 @@ export class ClustersService extends ImmutableStore params: types.LoginSsoParams, abortSignal: tsh.CloneableAbortSignal ) { - await this.client.loginSso(params, abortSignal); + await this.client.login( + { + clusterUri: params.clusterUri, + params: { + oneofKind: 'sso', + sso: { + providerType: params.providerType, + providerName: params.providerName, + }, + }, + }, + { abort: abortSignal } + ); await this.syncRootClusterAndCatchErrors(params.clusterUri); this.usageService.captureUserLogin(params.clusterUri, params.providerType); } @@ -123,7 +149,91 @@ export class ClustersService extends ImmutableStore params: types.LoginPasswordlessParams, abortSignal: tsh.CloneableAbortSignal ) { - await this.client.loginPasswordless(params, abortSignal); + await new Promise((resolve, reject) => { + const stream = this.client.loginPasswordless({ + abort: abortSignal, + }); + + let hasDeviceBeenTapped = false; + + // Init the stream. + stream.requests.send({ + request: { + oneofKind: 'init', + init: { + clusterUri: params.clusterUri, + }, + }, + }); + + stream.responses.onMessage(function (response) { + switch (response.prompt) { + case PasswordlessPrompt.PIN: + const pinResponse = (pin: string) => { + stream.requests.send({ + request: { + oneofKind: 'pin', + pin: { pin }, + }, + }); + }; + + params.onPromptCallback({ + type: 'pin', + onUserResponse: pinResponse, + }); + return; + + case PasswordlessPrompt.CREDENTIAL: + const credResponse = (index: number) => { + stream.requests.send({ + request: { + oneofKind: 'credential', + credential: { index }, + }, + }); + }; + + params.onPromptCallback({ + type: 'credential', + onUserResponse: credResponse, + data: { credentials: response.credentials || [] }, + }); + return; + + case PasswordlessPrompt.TAP: + if (hasDeviceBeenTapped) { + params.onPromptCallback({ type: 'retap' }); + } else { + hasDeviceBeenTapped = true; + params.onPromptCallback({ type: 'tap' }); + } + return; + + // Following cases should never happen but just in case? + case PasswordlessPrompt.UNSPECIFIED: + stream.requests.complete(); + return reject(new Error('no passwordless prompt was specified')); + + default: + stream.requests.complete(); + return reject( + new Error( + `passwordless prompt '${response.prompt}' not supported` + ) + ); + } + }); + + stream.responses.onComplete(function () { + resolve(); + }); + + stream.responses.onError(function (err: Error) { + reject(err); + }); + }); + await this.syncRootClusterAndCatchErrors(params.clusterUri); this.usageService.captureUserLogin(params.clusterUri, 'passwordless'); } @@ -163,7 +273,8 @@ export class ClustersService extends ImmutableStore let clusters: Cluster[]; try { - clusters = await this.client.listRootClusters(); + const { response } = await this.client.listRootClusters({}); + clusters = response.clusters; } catch (error) { this.notificationsService.notifyError({ title: 'Could not fetch root clusters', @@ -185,9 +296,9 @@ export class ClustersService extends ImmutableStore async syncGatewaysAndCatchErrors() { try { - const gws = await this.client.listGateways(); + const { response } = await this.client.listGateways({}); this.setState(draft => { - draft.gateways = new Map(gws.map(g => [g.uri, g])); + draft.gateways = new Map(response.gateways.map(g => [g.uri, g])); }); } catch (error) { this.notificationsService.notifyError({ @@ -198,10 +309,12 @@ export class ClustersService extends ImmutableStore } private async syncLeafClustersList(clusterUri: uri.RootClusterUri) { - const leaves = await this.client.listLeafClusters(clusterUri); + const { response } = await this.client.listLeafClusters({ + clusterUri, + }); this.setState(draft => { - for (const leaf of leaves) { + for (const leaf of response.clusters) { draft.clusters.set( leaf.uri, this.removeInternalLoginsFromCluster(leaf) @@ -209,18 +322,20 @@ export class ClustersService extends ImmutableStore } }); - return leaves; + return response.clusters; } - async getRequestableRoles(params: GetRequestableRolesParams) { - const cluster = this.state.clusters.get(params.rootClusterUri); + async getRequestableRoles(params: GetRequestableRolesRequest) { + const cluster = this.state.clusters.get(params.clusterUri); // TODO(ravicious): Remove check for cluster.connected. This check should be done earlier in the // UI rather than be repeated in each ClustersService method. if (!cluster.connected) { return; } - return this.client.getRequestableRoles(params); + const { response } = await this.client.getRequestableRoles(params); + + return response; } getAssumedRequests(rootClusterUri: uri.RootClusterUri) { @@ -244,7 +359,10 @@ export class ClustersService extends ImmutableStore return; } - return this.client.getAccessRequests(rootClusterUri); + const { response } = await this.client.getAccessRequests({ + clusterUri: rootClusterUri, + }); + return response.requests; } async deleteAccessRequest( @@ -256,7 +374,10 @@ export class ClustersService extends ImmutableStore if (!cluster.connected) { return; } - return this.client.deleteAccessRequest(rootClusterUri, requestId); + await this.client.deleteAccessRequest({ + rootClusterUri, + accessRequestId: requestId, + }); } async assumeRole( @@ -269,7 +390,11 @@ export class ClustersService extends ImmutableStore if (!cluster.connected) { return; } - await this.client.assumeRole(rootClusterUri, requestIds, dropIds); + await this.client.assumeRole({ + rootClusterUri, + accessRequestIds: requestIds, + dropRequestIds: dropIds, + }); this.usageService.captureAccessRequestAssumeRole(rootClusterUri); return this.syncRootCluster(rootClusterUri); } @@ -284,34 +409,33 @@ export class ClustersService extends ImmutableStore return; } - return this.client.getAccessRequest(rootClusterUri, requestId); + const { response } = await this.client.getAccessRequest({ + clusterUri: rootClusterUri, + accessRequestId: requestId, + }); + + return response.request; } - async reviewAccessRequest( - rootClusterUri: uri.RootClusterUri, - params: ReviewAccessRequestParams - ) { - const cluster = this.state.clusters.get(rootClusterUri); + async reviewAccessRequest(params: ReviewAccessRequestRequest) { + const cluster = this.state.clusters.get(params.rootClusterUri); // TODO(ravicious): Remove check for cluster.connected. See the comment in getRequestableRoles. if (!cluster.connected) { return; } - const response = await this.client.reviewAccessRequest( - rootClusterUri, - params - ); - this.usageService.captureAccessRequestReview(rootClusterUri); - return response; + const { response } = await this.client.reviewAccessRequest(params); + this.usageService.captureAccessRequestReview(params.rootClusterUri); + return response.request; } - async promoteAccessRequest(params: PromoteAccessRequestParams) { - const response = await this.client.promoteAccessRequest(params); + async promoteAccessRequest(params: PromoteAccessRequestRequest) { + const { response } = await this.client.promoteAccessRequest(params); this.usageService.captureAccessRequestReview(params.rootClusterUri); - return response; + return response.request; } - async createAccessRequest(params: CreateAccessRequestParams) { + async createAccessRequest(params: CreateAccessRequestRequest) { const cluster = this.state.clusters.get(params.rootClusterUri); // TODO(ravicious): Remove check for cluster.connected. See the comment in getRequestableRoles. if (!cluster.connected) { @@ -357,13 +481,12 @@ export class ClustersService extends ImmutableStore } async getAuthSettings(clusterUri: uri.RootClusterUri) { - return (await this.client.getAuthSettings( - clusterUri - )) as types.AuthSettings; + const { response } = await this.client.getAuthSettings({ clusterUri }); + return response as types.AuthSettings; } - async createGateway(params: tsh.CreateGatewayParams) { - const gateway = await this.client.createGateway(params); + async createGateway(params: tsh.CreateGatewayRequest) { + const { response: gateway } = await this.client.createGateway(params); this.setState(draft => { draft.gateways.set(gateway.uri, gateway); }); @@ -372,7 +495,7 @@ export class ClustersService extends ImmutableStore async removeGateway(gatewayUri: uri.GatewayUri) { try { - await this.client.removeGateway(gatewayUri); + await this.client.removeGateway({ gatewayUri }); this.setState(draft => { draft.gateways.delete(gatewayUri); }); @@ -409,10 +532,11 @@ export class ClustersService extends ImmutableStore throw new Error(`Could not find gateway ${gatewayUri}`); } - const gateway = await this.client.setGatewayTargetSubresourceName( - gatewayUri, - targetSubresourceName - ); + const { response: gateway } = + await this.client.setGatewayTargetSubresourceName({ + gatewayUri, + targetSubresourceName, + }); this.setState(draft => { draft.gateways.set(gatewayUri, gateway); @@ -426,10 +550,10 @@ export class ClustersService extends ImmutableStore throw new Error(`Could not find gateway ${gatewayUri}`); } - const gateway = await this.client.setGatewayLocalPort( + const { response: gateway } = await this.client.setGatewayLocalPort({ gatewayUri, - localPort - ); + localPort, + }); this.setState(draft => { draft.gateways.set(gatewayUri, gateway); @@ -519,7 +643,7 @@ export class ClustersService extends ImmutableStore } private async syncClusterInfo(clusterUri: uri.RootClusterUri) { - const cluster = await this.client.getCluster(clusterUri); + const { response: cluster } = await this.client.getCluster({ clusterUri }); // TODO: this information should eventually be gathered by getCluster const assumedRequests = cluster.loggedInUser ? await this.fetchClusterAssumedRequests( diff --git a/web/packages/teleterm/src/ui/services/clusters/types.ts b/web/packages/teleterm/src/ui/services/clusters/types.ts index 60424d29e9fae..b8deb5905442e 100644 --- a/web/packages/teleterm/src/ui/services/clusters/types.ts +++ b/web/packages/teleterm/src/ui/services/clusters/types.ts @@ -28,13 +28,26 @@ export type AuthType = shared.AuthType; export type AuthProvider = tsh.AuthProvider; -export type LoginLocalParams = { kind: 'local' } & tsh.LoginLocalParams; +export interface LoginLocalParams { + kind: 'local'; + clusterUri: uri.RootClusterUri; + username: string; + password: string; + token?: string; +} -export type LoginPasswordlessParams = { - kind: 'passwordless'; -} & tsh.LoginPasswordlessParams; +export interface LoginSsoParams { + kind: 'sso'; + clusterUri: uri.RootClusterUri; + providerType: string; + providerName: string; +} -export type LoginSsoParams = { kind: 'sso' } & tsh.LoginSsoParams; +export interface LoginPasswordlessParams { + kind: 'passwordless'; + clusterUri: uri.RootClusterUri; + onPromptCallback(res: WebauthnLoginPrompt): void; +} export type LoginParams = | LoginLocalParams @@ -43,7 +56,22 @@ export type LoginParams = export type LoginPasswordlessRequest = tsh.LoginPasswordlessRequest; -export type WebauthnLoginPrompt = tsh.WebauthnLoginPrompt; +export type WebauthnLoginPrompt = + | WebauthnLoginTapPrompt + | WebauthnLoginRetapPrompt + | WebauthnLoginPinPrompt + | WebauthnLoginCredentialPrompt; +export type WebauthnLoginTapPrompt = { type: 'tap' }; +export type WebauthnLoginRetapPrompt = { type: 'retap' }; +export type WebauthnLoginPinPrompt = { + type: 'pin'; + onUserResponse(pin: string): void; +}; +export type WebauthnLoginCredentialPrompt = { + type: 'credential'; + data: { credentials: tsh.CredentialInfo[] }; + onUserResponse(index: number): void; +}; export interface AuthSettings extends tsh.AuthSettings { secondFactor: Auth2faType; diff --git a/web/packages/teleterm/src/ui/services/connectMyComputer/connectMyComputerService.ts b/web/packages/teleterm/src/ui/services/connectMyComputer/connectMyComputerService.ts index 84953c52a0b0c..0dcb5dcab7411 100644 --- a/web/packages/teleterm/src/ui/services/connectMyComputer/connectMyComputerService.ts +++ b/web/packages/teleterm/src/ui/services/connectMyComputer/connectMyComputerService.ts @@ -41,21 +41,24 @@ export class ConnectMyComputerService { await this.mainProcessClient.verifyAgent(); } - createRole( + async createRole( rootClusterUri: uri.RootClusterUri ): Promise { - return this.tshClient.createConnectMyComputerRole(rootClusterUri); + const { response } = await this.tshClient.createConnectMyComputerRole({ + rootClusterUri, + }); + return response; } async createAgentConfigFile(rootCluster: Cluster): Promise { - const { token } = await this.tshClient.createConnectMyComputerNodeToken( - rootCluster.uri - ); + const { response } = await this.tshClient.createConnectMyComputerNodeToken({ + rootClusterUri: rootCluster.uri, + }); await this.mainProcessClient.createAgentConfigFile({ rootClusterUri: rootCluster.uri, proxy: rootCluster.proxyHost, - token: token, + token: response.token, username: rootCluster.loggedInUser.name, }); } @@ -76,20 +79,23 @@ export class ConnectMyComputerService { return this.mainProcessClient.isAgentConfigFileCreated({ rootClusterUri }); } - removeConnectMyComputerNode( + async removeConnectMyComputerNode( rootClusterUri: uri.RootClusterUri ): Promise { - return this.tshClient.deleteConnectMyComputerNode(rootClusterUri); + await this.tshClient.deleteConnectMyComputerNode({ rootClusterUri }); } removeAgentDirectory(rootClusterUri: uri.RootClusterUri): Promise { return this.mainProcessClient.removeAgentDirectory({ rootClusterUri }); } - getConnectMyComputerNodeName( + async getConnectMyComputerNodeName( rootClusterUri: uri.RootClusterUri ): Promise { - return this.tshClient.getConnectMyComputerNodeName(rootClusterUri); + const { response } = await this.tshClient.getConnectMyComputerNodeName({ + rootClusterUri, + }); + return response.name; } async killAgentAndRemoveData( @@ -103,9 +109,11 @@ export class ConnectMyComputerService { rootClusterUri: uri.RootClusterUri, abortSignal: CloneableAbortSignal ): Promise { - const response = await this.tshClient.waitForConnectMyComputerNodeJoin( - rootClusterUri, - abortSignal + const { response } = await this.tshClient.waitForConnectMyComputerNodeJoin( + { + rootClusterUri, + }, + { abort: abortSignal } ); return response.server; diff --git a/web/packages/teleterm/src/ui/services/fileTransferClient/fileTransferService.ts b/web/packages/teleterm/src/ui/services/fileTransferClient/fileTransferService.ts index c064bb7cf555a..6582dfac33aed 100644 --- a/web/packages/teleterm/src/ui/services/fileTransferClient/fileTransferService.ts +++ b/web/packages/teleterm/src/ui/services/fileTransferClient/fileTransferService.ts @@ -31,23 +31,28 @@ export class FileTransferService { ) {} transferFile( - options: FileTransferRequest, + request: FileTransferRequest, abortController: AbortController ): FileTransferListeners { - const listeners = this.tshClient.transferFile( - options, - cloneAbortSignal(abortController.signal) - ); - if (options.direction === FileTransferDirection.DOWNLOAD) { - this.usageService.captureFileTransferRun(options.serverUri, { + const stream = this.tshClient.transferFile(request, { + abort: cloneAbortSignal(abortController.signal), + }); + if (request.direction === FileTransferDirection.DOWNLOAD) { + this.usageService.captureFileTransferRun(request.serverUri, { isUpload: false, }); } - if (options.direction === FileTransferDirection.UPLOAD) { - this.usageService.captureFileTransferRun(options.serverUri, { + if (request.direction === FileTransferDirection.UPLOAD) { + this.usageService.captureFileTransferRun(request.serverUri, { isUpload: true, }); } - return listeners; + return { + onProgress(callback: (progress: number) => void) { + stream.responses.onMessage(data => callback(data.percentage)); + }, + onComplete: stream.responses.onComplete, + onError: stream.responses.onError, + }; } } diff --git a/web/packages/teleterm/src/ui/services/headlessAuthn/headlessAuthnService.ts b/web/packages/teleterm/src/ui/services/headlessAuthn/headlessAuthnService.ts index 7d0b9b82c5f5f..de55e9688e1c4 100644 --- a/web/packages/teleterm/src/ui/services/headlessAuthn/headlessAuthnService.ts +++ b/web/packages/teleterm/src/ui/services/headlessAuthn/headlessAuthnService.ts @@ -59,12 +59,11 @@ export class HeadlessAuthenticationService { } async updateHeadlessAuthenticationState( - params: types.UpdateHeadlessAuthenticationStateParams, + params: types.UpdateHeadlessAuthenticationStateRequest, abortSignal: types.CloneableAbortSignal ): Promise { - return this.tshClient.updateHeadlessAuthenticationState( - params, - abortSignal - ); + await this.tshClient.updateHeadlessAuthenticationState(params, { + abort: abortSignal, + }); } } diff --git a/web/packages/teleterm/src/ui/services/resources/resourcesService.test.ts b/web/packages/teleterm/src/ui/services/resources/resourcesService.test.ts index 718c23aa739f1..9a9b42086c2c1 100644 --- a/web/packages/teleterm/src/ui/services/resources/resourcesService.test.ts +++ b/web/packages/teleterm/src/ui/services/resources/resourcesService.test.ts @@ -22,6 +22,7 @@ import { makeServer, makeApp, } from 'teleterm/services/tshd/testHelpers'; +import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient'; import { AmbiguousHostnameError, @@ -36,7 +37,7 @@ describe('getServerByHostname', () => { const getServerByHostnameTests: Array< { name: string; - getServersMockedValue: Awaited>; + getServersMockedValue: ReturnType; } & ( | { expectedServer: tsh.Server; expectedErr?: never } | { expectedErr: any; expectedServer?: never } @@ -44,29 +45,29 @@ describe('getServerByHostname', () => { > = [ { name: 'returns a server when the hostname matches a single server', - getServersMockedValue: { + getServersMockedValue: new MockedUnaryCall({ agents: [server], totalCount: 1, startKey: 'foo', - }, + }), expectedServer: server, }, { name: 'throws an error when the hostname matches multiple servers', - getServersMockedValue: { + getServersMockedValue: new MockedUnaryCall({ agents: [server, server], totalCount: 2, startKey: 'foo', - }, + }), expectedErr: AmbiguousHostnameError, }, { name: 'returns nothing if the hostname does not match any servers', - getServersMockedValue: { + getServersMockedValue: new MockedUnaryCall({ agents: [], totalCount: 0, startKey: 'foo', - }, + }), expectedServer: undefined, }, ]; @@ -93,6 +94,10 @@ describe('getServerByHostname', () => { query: 'name == "foo"', limit: 2, sort: null, + sortBy: '', + startKey: '', + search: '', + searchAsRoles: '', }); } ); @@ -106,26 +111,34 @@ describe('searchResources', () => { const app = makeApp(); const tshClient: Partial = { - getServers: jest.fn().mockResolvedValueOnce({ - agents: [server], - totalCount: 1, - startKey: '', - }), - getDatabases: jest.fn().mockResolvedValueOnce({ - agents: [db], - totalCount: 1, - startKey: '', - }), - getKubes: jest.fn().mockResolvedValueOnce({ - agents: [kube], - totalCount: 1, - startKey: '', - }), - getApps: jest.fn().mockResolvedValueOnce({ - agents: [app], - totalCount: 1, - startKey: '', - }), + getServers: jest.fn().mockResolvedValueOnce( + new MockedUnaryCall({ + agents: [server], + totalCount: 1, + startKey: '', + }) + ), + getDatabases: jest.fn().mockResolvedValueOnce( + new MockedUnaryCall({ + agents: [db], + totalCount: 1, + startKey: '', + }) + ), + getKubes: jest.fn().mockResolvedValueOnce( + new MockedUnaryCall({ + agents: [kube], + totalCount: 1, + startKey: '', + }) + ), + getApps: jest.fn().mockResolvedValueOnce( + new MockedUnaryCall({ + agents: [app], + totalCount: 1, + startKey: '', + }) + ), }; const service = new ResourcesService(tshClient as tsh.TshdClient); @@ -160,11 +173,13 @@ describe('searchResources', () => { it('returns a single item if a filter is supplied', async () => { const server = makeServer(); const tshClient: Partial = { - getServers: jest.fn().mockResolvedValueOnce({ - agents: [server], - totalCount: 1, - startKey: '', - }), + getServers: jest.fn().mockResolvedValueOnce( + new MockedUnaryCall({ + agents: [server], + totalCount: 1, + startKey: '', + }) + ), }; const service = new ResourcesService(tshClient as tsh.TshdClient); diff --git a/web/packages/teleterm/src/ui/services/resources/resourcesService.ts b/web/packages/teleterm/src/ui/services/resources/resourcesService.ts index a56a729f58a26..b7de3ba21390d 100644 --- a/web/packages/teleterm/src/ui/services/resources/resourcesService.ts +++ b/web/packages/teleterm/src/ui/services/resources/resourcesService.ts @@ -25,15 +25,29 @@ import { TshdRpcError, } from 'teleterm/services/tshd/cloneableClient'; +import { + resourceOneOfIsServer, + resourceOneOfIsDatabase, + resourceOneOfIsApp, + resourceOneOfIsKube, +} from 'teleterm/helpers'; + +import Logger from 'teleterm/logger'; + import type * as types from 'teleterm/services/tshd/types'; import type * as uri from 'teleterm/ui/uri'; import type { ResourceTypeFilter } from 'teleterm/ui/Search/searchResult'; export class ResourcesService { + private logger = new Logger('ResourcesService'); + constructor(private tshClient: types.TshdClient) {} - fetchServers(params: types.GetResourcesParams) { - return this.tshClient.getServers(params); + async fetchServers(params: types.GetResourcesParams) { + const { response } = await this.tshClient.getServers( + makeGetResourcesParamsRequest(params) + ); + return response; } // TODO(ravicious): Refactor it to use logic similar to that in the Web UI. @@ -57,20 +71,30 @@ export class ResourcesService { return servers[0]; } - fetchDatabases(params: types.GetResourcesParams) { - return this.tshClient.getDatabases(params); + async fetchDatabases(params: types.GetResourcesParams) { + const { response } = await this.tshClient.getDatabases( + makeGetResourcesParamsRequest(params) + ); + return response; } - fetchKubes(params: types.GetResourcesParams) { - return this.tshClient.getKubes(params); + async fetchKubes(params: types.GetResourcesParams) { + const { response } = await this.tshClient.getKubes( + makeGetResourcesParamsRequest(params) + ); + return response; } - fetchApps(params: types.GetResourcesParams) { - return this.tshClient.getApps(params); + async fetchApps(params: types.GetResourcesParams) { + const { response } = await this.tshClient.getApps( + makeGetResourcesParamsRequest(params) + ); + return response; } async getDbUsers(dbUri: uri.DatabaseUri): Promise { - return await this.tshClient.listDatabaseUsers(dbUri); + const { response } = await this.tshClient.listDatabaseUsers({ dbUri }); + return response.users; } /** @@ -147,14 +171,51 @@ export class ResourcesService { return Promise.allSettled(promises); } - listUnifiedResources( + async listUnifiedResources( params: types.ListUnifiedResourcesRequest, abortSignal: AbortSignal ) { - return this.tshClient.listUnifiedResources( - params, - cloneAbortSignal(abortSignal) - ); + const { response } = await this.tshClient.listUnifiedResources(params, { + abort: cloneAbortSignal(abortSignal), + }); + return { + nextKey: response.nextKey, + resources: response.resources + .map(p => { + if (resourceOneOfIsServer(p.resource)) { + return { + kind: 'server', + resource: p.resource.server, + }; + } + + if (resourceOneOfIsDatabase(p.resource)) { + return { + kind: 'database', + resource: p.resource.database, + }; + } + + if (resourceOneOfIsApp(p.resource)) { + return { + kind: 'app', + resource: p.resource.app, + }; + } + + if (resourceOneOfIsKube(p.resource)) { + return { + kind: 'kube', + resource: p.resource.kube, + }; + } + + this.logger.info( + `Ignoring unsupported resource ${JSON.stringify(p)}.` + ); + }) + .filter(Boolean) as UnifiedResourceResponse[], + }; } } @@ -228,3 +289,25 @@ export type SearchResultResource = : Kind extends 'kube' ? SearchResultKube['resource'] : never; + +function makeGetResourcesParamsRequest(params: types.GetResourcesParams) { + return { + ...params, + search: params.search || '', + query: params.query || '', + searchAsRoles: params.searchAsRoles || '', + startKey: params.startKey || '', + sortBy: params.sort + ? `${params.sort.fieldName}:${params.sort.dir.toLowerCase()}` + : '', + }; +} + +export type UnifiedResourceResponse = + | { kind: 'server'; resource: types.Server } + | { + kind: 'database'; + resource: types.Database; + } + | { kind: 'kube'; resource: types.Kube } + | { kind: 'app'; resource: types.App }; From 1a54d47e633c701b512d9e92e083deb7a04e7d88 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Wed, 27 Mar 2024 11:57:33 +0100 Subject: [PATCH 6/8] Add comments and deprecations --- .../teleterm/src/services/tshd/types.ts | 18 ++++++++++++------ web/packages/teleterm/src/ui/uri.ts | 10 ++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/web/packages/teleterm/src/services/tshd/types.ts b/web/packages/teleterm/src/services/tshd/types.ts index 6e977cf89b355..6d44f18825d03 100644 --- a/web/packages/teleterm/src/services/tshd/types.ts +++ b/web/packages/teleterm/src/services/tshd/types.ts @@ -43,10 +43,16 @@ export type { CloneableClient, } from './cloneableClient'; -// Available types are listed here: -// https://github.com/gravitational/teleport/blob/v9.0.3/lib/defaults/defaults.go#L513-L530 -// -// The list below can get out of sync with what tsh actually implements. +export type TshdClient = CloneableClient; + +/** + * Available types are listed here: + * https://github.com/gravitational/teleport/blob/v9.0.3/lib/defaults/defaults.go#L513-L530 + * + * The list below can get out of sync with what tsh actually implements. + * + * @depreacate Move to a better suited file. + */ export type GatewayProtocol = | 'postgres' | 'mysql' @@ -55,8 +61,7 @@ export type GatewayProtocol = | 'redis' | 'sqlserver'; -export type TshdClient = CloneableClient; - +/** @depreacate Move to a better suited file. */ export type GetResourcesParams = { clusterUri: uri.ClusterUri; // sort is a required field because it has direct implications on performance of ListResources. @@ -72,6 +77,7 @@ export type GetResourcesParams = { query?: string; }; +/** @depreacate Use `AccessRequest` instead. */ export type AssumedRequest = { id: string; expires: Date; diff --git a/web/packages/teleterm/src/ui/uri.ts b/web/packages/teleterm/src/ui/uri.ts index a4c3c8ba6e29f..5c58387c39796 100644 --- a/web/packages/teleterm/src/ui/uri.ts +++ b/web/packages/teleterm/src/ui/uri.ts @@ -25,6 +25,16 @@ import type { RouteProps } from 'react-router'; * These are for identifying a specific resource within a root cluster. */ +// TODO(gzdunek): These types used to be template literals +// (for example, RootClusterUri = `/clusters/${RootClusterId}`). +// They were replaced with strings here https://github.com/gravitational/teleport/pull/39828, +// because we started using the generated proto types directly +// (so it was not possible to assign these types to plain strings). +// However, I didn't remove the type aliases below, because: +// 1. Ripping them out is too much work. +// 2. They still carry some useful information. +// 3. We might be able to add them back in the future +// (maybe with some sort of TypeScript declaration merging). export type RootClusterUri = string; export type RootClusterServerUri = string; export type RootClusterKubeUri = string; From 9b1f82fb390e6c180793e748114bbb7f264594da Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Thu, 28 Mar 2024 11:41:37 +0100 Subject: [PATCH 7/8] @depreacate -> @deprecated --- web/packages/teleterm/src/services/tshd/types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/packages/teleterm/src/services/tshd/types.ts b/web/packages/teleterm/src/services/tshd/types.ts index 6d44f18825d03..828d09addd187 100644 --- a/web/packages/teleterm/src/services/tshd/types.ts +++ b/web/packages/teleterm/src/services/tshd/types.ts @@ -51,7 +51,7 @@ export type TshdClient = CloneableClient; * * The list below can get out of sync with what tsh actually implements. * - * @depreacate Move to a better suited file. + * @deprecated Move to a better suited file. */ export type GatewayProtocol = | 'postgres' @@ -61,7 +61,7 @@ export type GatewayProtocol = | 'redis' | 'sqlserver'; -/** @depreacate Move to a better suited file. */ +/** @deprecated Move to a better suited file. */ export type GetResourcesParams = { clusterUri: uri.ClusterUri; // sort is a required field because it has direct implications on performance of ListResources. @@ -77,7 +77,7 @@ export type GetResourcesParams = { query?: string; }; -/** @depreacate Use `AccessRequest` instead. */ +/** @deprecated Use `AccessRequest` instead. */ export type AssumedRequest = { id: string; expires: Date; From 83fe593179d89281fd70b9cd2f2a47593224f33e Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Fri, 29 Mar 2024 09:44:41 +0100 Subject: [PATCH 8/8] Remove type casting --- .../src/ui/services/resources/resourcesService.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/packages/teleterm/src/ui/services/resources/resourcesService.ts b/web/packages/teleterm/src/ui/services/resources/resourcesService.ts index b7de3ba21390d..60b3fbb3ccc5f 100644 --- a/web/packages/teleterm/src/ui/services/resources/resourcesService.ts +++ b/web/packages/teleterm/src/ui/services/resources/resourcesService.ts @@ -174,7 +174,7 @@ export class ResourcesService { async listUnifiedResources( params: types.ListUnifiedResourcesRequest, abortSignal: AbortSignal - ) { + ): Promise<{ nextKey: string; resources: UnifiedResourceResponse[] }> { const { response } = await this.tshClient.listUnifiedResources(params, { abort: cloneAbortSignal(abortSignal), }); @@ -184,28 +184,28 @@ export class ResourcesService { .map(p => { if (resourceOneOfIsServer(p.resource)) { return { - kind: 'server', + kind: 'server' as const, resource: p.resource.server, }; } if (resourceOneOfIsDatabase(p.resource)) { return { - kind: 'database', + kind: 'database' as const, resource: p.resource.database, }; } if (resourceOneOfIsApp(p.resource)) { return { - kind: 'app', + kind: 'app' as const, resource: p.resource.app, }; } if (resourceOneOfIsKube(p.resource)) { return { - kind: 'kube', + kind: 'kube' as const, resource: p.resource.kube, }; } @@ -214,7 +214,7 @@ export class ResourcesService { `Ignoring unsupported resource ${JSON.stringify(p)}.` ); }) - .filter(Boolean) as UnifiedResourceResponse[], + .filter(Boolean), }; } }