From 14770044f1ab7990c2bd9da18436a94d4a676511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Mon, 13 Jan 2025 17:55:41 +0100 Subject: [PATCH 1/6] Add basic support for target port in gateways in Connect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update type for targetSubresourceName on DocumentGateway The way DocumentsService.createGatewayDocument is implemented means that the targetSubresourceName property is always present, but it can be undefined. * Use "local port" instead of "port" in DocumentGatewapApp * Rewrite gateway FieldInputs to use styled components * Update comments in protos * useGateway: Stabilize useAsync functions of ports * Add padding to menu label if it's first child * Add support for required prop to Input and FieldInput * Add UI for changing target port * ActionButtons: Show ports of multi-port apps when VNet is not supported Now that we have support for the target port in Connect's gateways, we can show the ports and then open a gateway for that specific port on click. * Add RWMutex to gateways * Clear app gateway cert on target port change * Remove gateways/app.LocalProxyURL It was used only in tests and it made sense only for web apps anyway. * TestTCP: Close connections when test ends * Create context with timeout in testGatewayCertRenewal …instead of in each function that uses it. * Add tests for changing the target port of a TCP gateway * Parallelize app gateway tests within MFA/non-MFA groups * Make testGatewayConnection take ctx as first arg This will be needed in tests that check target port validation. * Validate target port of app gateways * Increase timeouts in app gateway tests --- .../go/teleport/lib/teleterm/v1/gateway.pb.go | 5 +- .../ts/teleport/lib/teleterm/v1/gateway_pb.ts | 5 +- integration/appaccess/appaccess_test.go | 1 + integration/appaccess/pack.go | 56 ++++ integration/proxy/proxy_helpers.go | 53 +++- integration/proxy/proxy_test.go | 30 ++- integration/proxy/teleterm_test.go | 254 +++++++++++++++--- .../apiserver/handler/handler_gateways.go | 2 +- lib/teleterm/clusters/cluster_apps.go | 31 ++- lib/teleterm/clusters/cluster_gateways.go | 31 ++- lib/teleterm/daemon/daemon.go | 24 +- lib/teleterm/gateway/app.go | 11 - lib/teleterm/gateway/app_middleware.go | 6 +- lib/teleterm/gateway/base.go | 28 +- lib/teleterm/gateway/config.go | 5 + lib/teleterm/gateway/interfaces.go | 5 +- lib/teleterm/gateway/kube.go | 2 + proto/teleport/lib/teleterm/v1/gateway.proto | 5 +- web/packages/design/src/Input/Input.tsx | 3 + web/packages/design/src/Menu/Menu.story.tsx | 12 + web/packages/design/src/Menu/MenuItem.tsx | 30 ++- web/packages/design/src/keyframes.ts | 4 + .../components/FieldInput/FieldInput.tsx | 5 +- .../teleterm/src/services/tshd/testHelpers.ts | 2 +- .../src/ui/DocumentCluster/ActionButtons.tsx | 97 ++++--- .../src/ui/DocumentGateway/useGateway.ts | 63 +++-- .../src/ui/DocumentGatewayApp/AppGateway.tsx | 208 +++++++++++--- .../DocumentGatewayApp.story.tsx | 47 +++- .../DocumentGatewayApp/DocumentGatewayApp.tsx | 13 +- .../src/ui/TabHost/useTabShortcuts.test.tsx | 2 + .../src/ui/components/FieldInputs.tsx | 37 +-- .../src/ui/components/OfflineGateway.tsx | 4 +- .../documentsService/connectToApp.test.ts | 28 +- .../documentsService/connectToApp.ts | 20 +- .../documentsService/documentsService.test.ts | 2 + .../documentsService/types.ts | 6 +- 36 files changed, 904 insertions(+), 233 deletions(-) diff --git a/gen/proto/go/teleport/lib/teleterm/v1/gateway.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/gateway.pb.go index 7f5f4f8332bd0..fcfb0d077a532 100644 --- a/gen/proto/go/teleport/lib/teleterm/v1/gateway.pb.go +++ b/gen/proto/go/teleport/lib/teleterm/v1/gateway.pb.go @@ -62,10 +62,11 @@ type Gateway struct { LocalAddress string `protobuf:"bytes,5,opt,name=local_address,json=localAddress,proto3" json:"local_address,omitempty"` // local_port is the gateway address on localhost LocalPort string `protobuf:"bytes,6,opt,name=local_port,json=localPort,proto3" json:"local_port,omitempty"` - // protocol is the gateway protocol + // protocol is the protocol used by the gateway. For databases, it matches the type of the + // database that the gateway targets. For apps, it's either "HTTP" or "TCP". Protocol string `protobuf:"bytes,7,opt,name=protocol,proto3" json:"protocol,omitempty"` // target_subresource_name points at a subresource of the remote resource, for example a - // database name on a database server. + // database name on a database server or a target port of a multi-port TCP app. TargetSubresourceName string `protobuf:"bytes,9,opt,name=target_subresource_name,json=targetSubresourceName,proto3" json:"target_subresource_name,omitempty"` // gateway_cli_client represents a command that the user can execute to connect to the resource // through the gateway. diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/gateway_pb.ts b/gen/proto/ts/teleport/lib/teleterm/v1/gateway_pb.ts index f6523f7cc2210..194cc93867671 100644 --- a/gen/proto/ts/teleport/lib/teleterm/v1/gateway_pb.ts +++ b/gen/proto/ts/teleport/lib/teleterm/v1/gateway_pb.ts @@ -80,14 +80,15 @@ export interface Gateway { */ localPort: string; /** - * protocol is the gateway protocol + * protocol is the protocol used by the gateway. For databases, it matches the type of the + * database that the gateway targets. For apps, it's either "HTTP" or "TCP". * * @generated from protobuf field: string protocol = 7; */ protocol: string; /** * target_subresource_name points at a subresource of the remote resource, for example a - * database name on a database server. + * database name on a database server or a target port of a multi-port TCP app. * * @generated from protobuf field: string target_subresource_name = 9; */ diff --git a/integration/appaccess/appaccess_test.go b/integration/appaccess/appaccess_test.go index dffd5f8aa1912..8bb73e091754b 100644 --- a/integration/appaccess/appaccess_test.go +++ b/integration/appaccess/appaccess_test.go @@ -831,6 +831,7 @@ func TestTCP(t *testing.T) { conn, err := net.Dial("tcp", localProxyAddress) require.NoError(t, err) + defer conn.Close() buf := make([]byte, 1024) n, err := conn.Read(buf) diff --git a/integration/appaccess/pack.go b/integration/appaccess/pack.go index 609a60adca036..a3e19634c79e0 100644 --- a/integration/appaccess/pack.go +++ b/integration/appaccess/pack.go @@ -185,6 +185,34 @@ func (p *Pack) RootAppPublicAddr() string { return p.rootAppPublicAddr } +func (p *Pack) RootTCPAppName() string { + return p.rootTCPAppName +} + +func (p *Pack) RootTCPMessage() string { + return p.rootTCPMessage +} + +func (p *Pack) RootTCPMultiPortAppName() string { + return p.rootTCPMultiPortAppName +} + +func (p *Pack) RootTCPMultiPortAppPortAlpha() int { + return p.rootTCPMultiPortAppPortAlpha +} + +func (p *Pack) RootTCPMultiPortMessageAlpha() string { + return p.rootTCPMultiPortMessageAlpha +} + +func (p *Pack) RootTCPMultiPortAppPortBeta() int { + return p.rootTCPMultiPortAppPortBeta +} + +func (p *Pack) RootTCPMultiPortMessageBeta() string { + return p.rootTCPMultiPortMessageBeta +} + func (p *Pack) RootAuthServer() *auth.Server { return p.rootCluster.Process.GetAuthServer() } @@ -201,6 +229,34 @@ func (p *Pack) LeafAppPublicAddr() string { return p.leafAppPublicAddr } +func (p *Pack) LeafTCPAppName() string { + return p.leafTCPAppName +} + +func (p *Pack) LeafTCPMessage() string { + return p.leafTCPMessage +} + +func (p *Pack) LeafTCPMultiPortAppName() string { + return p.leafTCPMultiPortAppName +} + +func (p *Pack) LeafTCPMultiPortAppPortAlpha() int { + return p.leafTCPMultiPortAppPortAlpha +} + +func (p *Pack) LeafTCPMultiPortMessageAlpha() string { + return p.leafTCPMultiPortMessageAlpha +} + +func (p *Pack) LeafTCPMultiPortAppPortBeta() int { + return p.leafTCPMultiPortAppPortBeta +} + +func (p *Pack) LeafTCPMultiPortMessageBeta() string { + return p.leafTCPMultiPortMessageBeta +} + func (p *Pack) LeafAuthServer() *auth.Server { return p.leafCluster.Process.GetAuthServer() } diff --git a/integration/proxy/proxy_helpers.go b/integration/proxy/proxy_helpers.go index 9de514caf73e1..e5e717410e8b9 100644 --- a/integration/proxy/proxy_helpers.go +++ b/integration/proxy/proxy_helpers.go @@ -28,6 +28,7 @@ import ( "net/http" "net/url" "path/filepath" + "strconv" "strings" "testing" "time" @@ -684,7 +685,7 @@ func mustFindKubePod(t *testing.T, tc *client.TeleportClient) { require.Equal(t, types.KindKubePod, response.Resources[0].Kind) } -func mustConnectDatabaseGateway(t *testing.T, _ *daemon.Service, gw gateway.Gateway) { +func mustConnectDatabaseGateway(ctx context.Context, t *testing.T, _ *daemon.Service, gw gateway.Gateway) { t.Helper() dbGateway, err := gateway.AsDatabase(gw) @@ -705,15 +706,15 @@ func mustConnectDatabaseGateway(t *testing.T, _ *daemon.Service, gw gateway.Gate require.NoError(t, client.Close()) } -// mustConnectAppGateway verifies that the gateway acts as an unauthenticated proxy that forwards -// requests to the app behind it. -func mustConnectAppGateway(t *testing.T, _ *daemon.Service, gw gateway.Gateway) { +// mustConnectWebAppGateway verifies that the gateway acts as an unauthenticated proxy that forwards +// requests to the web app behind it. +func mustConnectWebAppGateway(ctx context.Context, t *testing.T, _ *daemon.Service, gw gateway.Gateway) { t.Helper() - appGw, err := gateway.AsApp(gw) - require.NoError(t, err) + gatewayAddress := net.JoinHostPort(gw.LocalAddress(), gw.LocalPort()) + gatewayURL := fmt.Sprintf("http://%s", gatewayAddress) - req, err := http.NewRequest(http.MethodGet, appGw.LocalProxyURL(), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, gatewayURL, nil) require.NoError(t, err) client := &http.Client{} @@ -724,6 +725,44 @@ func mustConnectAppGateway(t *testing.T, _ *daemon.Service, gw gateway.Gateway) require.Equal(t, http.StatusOK, resp.StatusCode) } +func makeMustConnectMultiPortTCPAppGateway(wantMessage string, otherTargetPort int, otherWantMessage string) testGatewayConnectionFunc { + return func(ctx context.Context, t *testing.T, d *daemon.Service, gw gateway.Gateway) { + t.Helper() + + gwURI := gw.URI().String() + originalTargetPort := gw.TargetSubresourceName() + makeMustConnectTCPAppGateway(wantMessage)(ctx, t, d, gw) + + _, err := d.SetGatewayTargetSubresourceName(ctx, gwURI, strconv.Itoa(otherTargetPort)) + require.NoError(t, err) + makeMustConnectTCPAppGateway(otherWantMessage)(ctx, t, d, gw) + + // Restore the original port, so that the next time the test calls this function after certs + // expire, wantMessage is going to match the port that the gateway points to. + _, err = d.SetGatewayTargetSubresourceName(ctx, gwURI, originalTargetPort) + require.NoError(t, err) + makeMustConnectTCPAppGateway(wantMessage)(ctx, t, d, gw) + } +} + +func makeMustConnectTCPAppGateway(wantMessage string) testGatewayConnectionFunc { + return func(ctx context.Context, t *testing.T, _ *daemon.Service, gw gateway.Gateway) { + t.Helper() + + gatewayAddress := net.JoinHostPort(gw.LocalAddress(), gw.LocalPort()) + conn, err := net.Dial("tcp", gatewayAddress) + require.NoError(t, err) + defer conn.Close() + + buf := make([]byte, 1024) + n, err := conn.Read(buf) + require.NoError(t, err) + + resp := strings.TrimSpace(string(buf[:n])) + require.Equal(t, wantMessage, resp) + } +} + func kubeClientForLocalProxy(t *testing.T, kubeconfigPath, teleportCluster, kubeCluster string) *kubernetes.Clientset { t.Helper() diff --git a/integration/proxy/proxy_test.go b/integration/proxy/proxy_test.go index 0dcf986d70109..7935c5aff06cb 100644 --- a/integration/proxy/proxy_test.go +++ b/integration/proxy/proxy_test.go @@ -54,6 +54,7 @@ import ( "github.com/gravitational/teleport/lib" "github.com/gravitational/teleport/lib/auth/testauthority" libclient "github.com/gravitational/teleport/lib/client" + "github.com/gravitational/teleport/lib/client/mfa" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/modules" "github.com/gravitational/teleport/lib/multiplexer" @@ -1315,18 +1316,29 @@ func TestALPNSNIProxyAppAccess(t *testing.T) { }) t.Run("teleterm app gateways cert renewal", func(t *testing.T) { - user, _ := pack.CreateUser(t) - tc := pack.MakeTeleportClient(t, user.GetName()) - - // test without per session MFA. - testTeletermAppGateway(t, pack, tc) + t.Run("without per-session MFA", func(t *testing.T) { + makeTC := func(t *testing.T) (*libclient.TeleportClient, mfa.WebauthnLoginFunc) { + user, _ := pack.CreateUser(t) + tc := pack.MakeTeleportClient(t, user.GetName()) + return tc, nil + } + testTeletermAppGateway(t, pack, makeTC) + testTeletermAppGatewayTargetPortValidation(t, pack, makeTC) + }) - t.Run("per session MFA", func(t *testing.T) { - // They update user's authentication to Webauthn so they must run after tests which do not use MFA. + t.Run("per-session MFA", func(t *testing.T) { + // They update clusters authentication to Webauthn so they must run after tests which do not use MFA. requireSessionMFAAuthPref(ctx, t, pack.RootAuthServer(), "127.0.0.1") requireSessionMFAAuthPref(ctx, t, pack.LeafAuthServer(), "127.0.0.1") - tc.WebauthnLogin = setupUserMFA(ctx, t, pack.RootAuthServer(), user.GetName(), "127.0.0.1") - testTeletermAppGateway(t, pack, tc) + makeTCAndWebauthnLogin := func(t *testing.T) (*libclient.TeleportClient, mfa.WebauthnLoginFunc) { + // Create a separate user for each tests to enable parallel tests that use per-session MFA. + // See the comment for webauthnLogin in setupUserMFA for more details. + user, _ := pack.CreateUser(t) + tc := pack.MakeTeleportClient(t, user.GetName()) + webauthnLogin := setupUserMFA(ctx, t, pack.RootAuthServer(), user.GetName(), "127.0.0.1") + return tc, webauthnLogin + } + testTeletermAppGateway(t, pack, makeTCAndWebauthnLogin) }) }) } diff --git a/integration/proxy/teleterm_test.go b/integration/proxy/teleterm_test.go index 67feeda87944c..18b0efd4884c7 100644 --- a/integration/proxy/teleterm_test.go +++ b/integration/proxy/teleterm_test.go @@ -19,9 +19,11 @@ package proxy import ( + "cmp" "context" "errors" "net" + "strconv" "sync" "sync/atomic" "testing" @@ -50,9 +52,9 @@ import ( "github.com/gravitational/teleport/lib/auth/mocku2f" wancli "github.com/gravitational/teleport/lib/auth/webauthncli" wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes" - "github.com/gravitational/teleport/lib/client" libclient "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/clientcache" + "github.com/gravitational/teleport/lib/client/mfa" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/service" "github.com/gravitational/teleport/lib/service/servicecfg" @@ -168,8 +170,8 @@ func testDBGatewayCertRenewal(ctx context.Context, t *testing.T, params dbGatewa TargetURI: params.databaseURI.String(), TargetUser: params.pack.Root.User.GetName(), }, - testGatewayConnectionFunc: mustConnectDatabaseGateway, - webauthnLogin: params.webauthnLogin, + testGatewayConnection: mustConnectDatabaseGateway, + webauthnLogin: params.webauthnLogin, generateAndSetupUserCreds: func(t *testing.T, tc *libclient.TeleportClient, ttl time.Duration) { creds, err := helpers.GenerateUserCreds(helpers.UserCredsRequest{ Process: params.pack.Root.Cluster.Process, @@ -184,7 +186,7 @@ func testDBGatewayCertRenewal(ctx context.Context, t *testing.T, params dbGatewa ) } -type testGatewayConnectionFunc func(*testing.T, *daemon.Service, gateway.Gateway) +type testGatewayConnectionFunc func(context.Context, *testing.T, *daemon.Service, gateway.Gateway) type generateAndSetupUserCredsFunc func(t *testing.T, tc *libclient.TeleportClient, ttl time.Duration) @@ -192,14 +194,19 @@ type gatewayCertRenewalParams struct { tc *libclient.TeleportClient albAddr string createGatewayParams daemon.CreateGatewayParams - testGatewayConnectionFunc testGatewayConnectionFunc + testGatewayConnection testGatewayConnectionFunc webauthnLogin libclient.WebauthnLoginFunc generateAndSetupUserCreds generateAndSetupUserCredsFunc + wantPromptMFACallCount int } func testGatewayCertRenewal(ctx context.Context, t *testing.T, params gatewayCertRenewalParams) { t.Helper() + // The test can potentially hang forever if something is wrong with the MFA prompt, add a timeout. + ctx, cancel := context.WithTimeout(ctx, time.Minute) + t.Cleanup(cancel) + tc := params.tc // Save the profile yaml file to disk as test helpers like helpers.NewClientWithCreds don't do @@ -273,7 +280,7 @@ func testGatewayCertRenewal(ctx context.Context, t *testing.T, params gatewayCer gateway, err := daemonService.CreateGateway(ctx, params.createGatewayParams) require.NoError(t, err, trace.DebugReport(err)) - params.testGatewayConnectionFunc(t, daemonService, gateway) + params.testGatewayConnection(ctx, t, daemonService, gateway) // Advance the fake clock to simulate the db cert expiry inside the middleware. fakeClock.Advance(time.Hour * 48) @@ -286,16 +293,17 @@ func testGatewayCertRenewal(ctx context.Context, t *testing.T, params gatewayCer // and then it will attempt to reissue the user cert using an expired user cert. // The mocked tshdEventsClient will issue a valid user cert, save it to disk, and the middleware // will let the connection through. - params.testGatewayConnectionFunc(t, daemonService, gateway) + params.testGatewayConnection(ctx, t, daemonService, gateway) require.Equal(t, uint32(1), tshdEventsService.reloginCallCount.Load(), "Unexpected number of calls to TSHDEventsClient.Relogin") require.Equal(t, uint32(0), tshdEventsService.sendNotificationCallCount.Load(), "Unexpected number of calls to TSHDEventsClient.SendNotification") if params.webauthnLogin != nil { - // There are two calls, one to issue the certs when creating the gateway and then another to - // reissue them after relogin. - require.Equal(t, uint32(2), tshdEventsService.promptMFACallCount.Load(), + // By default, there are two calls, one to issue the certs when creating the gateway and then + // another to reissue them after relogin. + wantCallCount := cmp.Or(params.wantPromptMFACallCount, 2) + require.Equal(t, uint32(wantCallCount), tshdEventsService.promptMFACallCount.Load(), "Unexpected number of calls to TSHDEventsClient.PromptMFA") } } @@ -474,9 +482,6 @@ func TestTeletermKubeGateway(t *testing.T) { t.Run("root with per-session MFA", func(t *testing.T) { profileName := mustGetProfileName(t, suite.root.Web) kubeURI := uri.NewClusterURI(profileName).AppendKube(kubeClusterName) - // The test can potentially hang forever if something is wrong with the MFA prompt, add a timeout. - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - t.Cleanup(cancel) testKubeGatewayCertRenewal(ctx, t, kubeGatewayCertRenewalParams{ suite: suite, kubeURI: kubeURI, @@ -486,9 +491,6 @@ func TestTeletermKubeGateway(t *testing.T) { t.Run("leaf with per-session MFA", func(t *testing.T) { profileName := mustGetProfileName(t, suite.root.Web) kubeURI := uri.NewClusterURI(profileName).AppendLeafCluster(suite.leaf.Secrets.SiteName).AppendKube(kubeClusterName) - // The test can potentially hang forever if something is wrong with the MFA prompt, add a timeout. - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - t.Cleanup(cancel) testKubeGatewayCertRenewal(ctx, t, kubeGatewayCertRenewalParams{ suite: suite, kubeURI: kubeURI, @@ -523,7 +525,7 @@ func testKubeGatewayCertRenewal(ctx context.Context, t *testing.T, params kubeGa }) require.NoError(t, err) - testKubeConnection := func(t *testing.T, daemonService *daemon.Service, gw gateway.Gateway) { + testKubeConnection := func(ctx context.Context, t *testing.T, daemonService *daemon.Service, gw gateway.Gateway) { t.Helper() clientOnce.Do(func() { @@ -548,8 +550,8 @@ func testKubeGatewayCertRenewal(ctx context.Context, t *testing.T, params kubeGa createGatewayParams: daemon.CreateGatewayParams{ TargetURI: params.kubeURI.String(), }, - testGatewayConnectionFunc: testKubeConnection, - webauthnLogin: params.webauthnLogin, + testGatewayConnection: testKubeConnection, + webauthnLogin: params.webauthnLogin, generateAndSetupUserCreds: func(t *testing.T, tc *libclient.TeleportClient, ttl time.Duration) { creds, err := helpers.GenerateUserCreds(helpers.UserCredsRequest{ Process: params.suite.root.Process, @@ -614,6 +616,10 @@ func setupUserMFA(ctx context.Context, t *testing.T, authServer *auth.Server, us }) require.NoError(t, err) + // webauthnLogin is not safe for concurrent use, partly due to the implementation of device, but + // mostly because Teleport itself doesn't allow for more than one in-flight MFA challenge. This is + // an arbitrary limitation which in theory we could change. But for now, parallel tests that use + // webauthnLogin must use a separate user for each test and not trigger parallel MFA prompts. webauthnLogin := func(ctx context.Context, origin string, assertion *wantypes.CredentialAssertion, prompt wancli.LoginPrompt, opts *wancli.LoginOpts) (*proto.MFAAuthenticateResponse, string, error) { car, err := device.SignAssertion(origin, assertion) if err != nil { @@ -676,34 +682,210 @@ func requireSessionMFARole(ctx context.Context, t *testing.T, authServer *auth.S require.NoError(t, err) } -func testTeletermAppGateway(t *testing.T, pack *appaccess.Pack, tc *client.TeleportClient) { +type makeTCAndWebauthnLoginFunc func(t *testing.T) (*libclient.TeleportClient, mfa.WebauthnLoginFunc) + +func testTeletermAppGateway(t *testing.T, pack *appaccess.Pack, makeTCAndWebauthnLogin makeTCAndWebauthnLoginFunc) { ctx := context.Background() t.Run("root cluster", func(t *testing.T) { - profileName := mustGetProfileName(t, pack.RootWebAddr()) - appURI := uri.NewClusterURI(profileName).AppendApp(pack.RootAppName()) + t.Parallel() - // The test can potentially hang forever if something is wrong with the MFA prompt, add a timeout. - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - t.Cleanup(cancel) - testAppGatewayCertRenewal(ctx, t, pack, tc, appURI) + t.Run("web app", func(t *testing.T) { + t.Parallel() + + profileName := mustGetProfileName(t, pack.RootWebAddr()) + appURI := uri.NewClusterURI(profileName).AppendApp(pack.RootAppName()) + + testAppGatewayCertRenewal(ctx, t, pack, makeTCAndWebauthnLogin, appURI) + }) + + t.Run("TCP app", func(t *testing.T) { + t.Parallel() + + profileName := mustGetProfileName(t, pack.RootWebAddr()) + appURI := uri.NewClusterURI(profileName).AppendApp(pack.RootTCPAppName()) + + tc, webauthnLogin := makeTCAndWebauthnLogin(t) + + testGatewayCertRenewal( + ctx, + t, + gatewayCertRenewalParams{ + tc: tc, + createGatewayParams: daemon.CreateGatewayParams{TargetURI: appURI.String()}, + testGatewayConnection: makeMustConnectTCPAppGateway(pack.RootTCPMessage()), + generateAndSetupUserCreds: pack.GenerateAndSetupUserCreds, + webauthnLogin: webauthnLogin, + }, + ) + }) + + t.Run("multi-port TCP app", func(t *testing.T) { + t.Parallel() + profileName := mustGetProfileName(t, pack.RootWebAddr()) + appURI := uri.NewClusterURI(profileName).AppendApp(pack.RootTCPMultiPortAppName()) + + tc, webauthnLogin := makeTCAndWebauthnLogin(t) + + testGatewayCertRenewal( + ctx, + t, + gatewayCertRenewalParams{ + tc: tc, + createGatewayParams: daemon.CreateGatewayParams{ + TargetURI: appURI.String(), + TargetSubresourceName: strconv.Itoa(pack.RootTCPMultiPortAppPortAlpha()), + }, + testGatewayConnection: makeMustConnectMultiPortTCPAppGateway( + pack.RootTCPMultiPortMessageAlpha(), pack.RootTCPMultiPortAppPortBeta(), pack.RootTCPMultiPortMessageBeta(), + ), + generateAndSetupUserCreds: pack.GenerateAndSetupUserCreds, + webauthnLogin: webauthnLogin, + // First MFA prompt is made when creating the gateway. Then makeMustConnectMultiPortTCPAppGateway + // changes the target port twice, which means two more prompts. + // + // Then testGatewayCertRenewal expires the certs and calls + // makeMustConnectMultiPortTCPAppGateway. The first connection refreshes the expired cert, + // then the function changes the target port twice again, resulting in two more prompts. + wantPromptMFACallCount: 3 + 3, + }, + ) + }) }) t.Run("leaf cluster", func(t *testing.T) { - profileName := mustGetProfileName(t, pack.RootWebAddr()) - appURI := uri.NewClusterURI(profileName). - AppendLeafCluster(pack.LeafAppClusterName()). - AppendApp(pack.LeafAppName()) + t.Parallel() + + t.Run("web app", func(t *testing.T) { + t.Parallel() + + profileName := mustGetProfileName(t, pack.RootWebAddr()) + appURI := uri.NewClusterURI(profileName). + AppendLeafCluster(pack.LeafAppClusterName()). + AppendApp(pack.LeafAppName()) + + testAppGatewayCertRenewal(ctx, t, pack, makeTCAndWebauthnLogin, appURI) + }) + + t.Run("TCP app", func(t *testing.T) { + t.Parallel() + + profileName := mustGetProfileName(t, pack.RootWebAddr()) + appURI := uri.NewClusterURI(profileName).AppendLeafCluster(pack.LeafAppClusterName()).AppendApp(pack.LeafTCPAppName()) - // The test can potentially hang forever if something is wrong with the MFA prompt, add a timeout. - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + tc, webauthnLogin := makeTCAndWebauthnLogin(t) + + testGatewayCertRenewal( + ctx, + t, + gatewayCertRenewalParams{ + tc: tc, + createGatewayParams: daemon.CreateGatewayParams{TargetURI: appURI.String()}, + testGatewayConnection: makeMustConnectTCPAppGateway(pack.LeafTCPMessage()), + generateAndSetupUserCreds: pack.GenerateAndSetupUserCreds, + webauthnLogin: webauthnLogin, + }, + ) + }) + + t.Run("multi-port TCP app", func(t *testing.T) { + t.Parallel() + + profileName := mustGetProfileName(t, pack.RootWebAddr()) + appURI := uri.NewClusterURI(profileName).AppendLeafCluster(pack.LeafAppClusterName()).AppendApp(pack.LeafTCPMultiPortAppName()) + + tc, webauthnLogin := makeTCAndWebauthnLogin(t) + + testGatewayCertRenewal( + ctx, + t, + gatewayCertRenewalParams{ + tc: tc, + createGatewayParams: daemon.CreateGatewayParams{ + TargetURI: appURI.String(), + TargetSubresourceName: strconv.Itoa(pack.LeafTCPMultiPortAppPortAlpha()), + }, + testGatewayConnection: makeMustConnectMultiPortTCPAppGateway( + pack.LeafTCPMultiPortMessageAlpha(), pack.LeafTCPMultiPortAppPortBeta(), pack.LeafTCPMultiPortMessageBeta(), + ), + generateAndSetupUserCreds: pack.GenerateAndSetupUserCreds, + webauthnLogin: webauthnLogin, + // First MFA prompt is made when creating the gateway. Then makeMustConnectMultiPortTCPAppGateway + // changes the target port twice, which means two more prompts. + // + // Then testGatewayCertRenewal expires the certs and calls + // makeMustConnectMultiPortTCPAppGateway. The first connection refreshes the expired cert, + // then the function changes the target port twice again, resulting in two more prompts. + wantPromptMFACallCount: 3 + 3, + }, + ) + }) + }) +} + +func testTeletermAppGatewayTargetPortValidation(t *testing.T, pack *appaccess.Pack, makeTCAndWebauthnLogin makeTCAndWebauthnLoginFunc) { + t.Run("target port validation", func(t *testing.T) { + t.Parallel() + + tc, _ := makeTCAndWebauthnLogin(t) + err := tc.SaveProfile(false /* makeCurrent */) + require.NoError(t, err) + + storage, err := clusters.NewStorage(clusters.Config{ + Dir: tc.KeysDir, + InsecureSkipVerify: tc.InsecureSkipVerify, + HardwareKeyPromptConstructor: func(rootClusterURI uri.ResourceURI) keys.HardwareKeyPrompt { + return nil + }, + }) + require.NoError(t, err) + daemonService, err := daemon.New(daemon.Config{ + Storage: storage, + CreateTshdEventsClientCredsFunc: func() (grpc.DialOption, error) { + return grpc.WithTransportCredentials(insecure.NewCredentials()), nil + }, + CreateClientCacheFunc: func(newClient clientcache.NewClientFunc) (daemon.ClientCache, error) { + return clientcache.NewNoCache(newClient), nil + }, + KubeconfigsDir: t.TempDir(), + AgentsDir: t.TempDir(), + }) + require.NoError(t, err) + t.Cleanup(func() { + daemonService.Stop() + }) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) t.Cleanup(cancel) - testAppGatewayCertRenewal(ctx, t, pack, tc, appURI) + + // Here the test setup ends and actual test code starts. + profileName := mustGetProfileName(t, pack.RootWebAddr()) + appURI := uri.NewClusterURI(profileName).AppendApp(pack.RootTCPMultiPortAppName()) + + _, err = daemonService.CreateGateway(ctx, daemon.CreateGatewayParams{ + TargetURI: appURI.String(), + // 42 shouldn't be handed out to a non-root user when creating a listener on port 0, so it's + // unlikely that 42 is going to end up in the app spec. + TargetSubresourceName: "42", + }) + require.True(t, trace.IsBadParameter(err), "Expected BadParameter, got %v", err) + require.ErrorContains(t, err, "not included in target ports") + + gateway, err := daemonService.CreateGateway(ctx, daemon.CreateGatewayParams{ + TargetURI: appURI.String(), + TargetSubresourceName: strconv.Itoa(pack.RootTCPMultiPortAppPortAlpha()), + }) + require.NoError(t, err) + + _, err = daemonService.SetGatewayTargetSubresourceName(ctx, gateway.URI().String(), "42") + require.True(t, trace.IsBadParameter(err), "Expected BadParameter, got %v", err) + require.ErrorContains(t, err, "not included in target ports") }) } -func testAppGatewayCertRenewal(ctx context.Context, t *testing.T, pack *appaccess.Pack, tc *libclient.TeleportClient, appURI uri.ResourceURI) { +func testAppGatewayCertRenewal(ctx context.Context, t *testing.T, pack *appaccess.Pack, makeTCAndWebauthnLogin makeTCAndWebauthnLoginFunc, appURI uri.ResourceURI) { t.Helper() + tc, webauthnLogin := makeTCAndWebauthnLogin(t) testGatewayCertRenewal( ctx, @@ -713,9 +895,9 @@ func testAppGatewayCertRenewal(ctx context.Context, t *testing.T, pack *appacces createGatewayParams: daemon.CreateGatewayParams{ TargetURI: appURI.String(), }, - testGatewayConnectionFunc: mustConnectAppGateway, + testGatewayConnection: mustConnectWebAppGateway, generateAndSetupUserCreds: pack.GenerateAndSetupUserCreds, - webauthnLogin: tc.WebauthnLogin, + webauthnLogin: webauthnLogin, }, ) } diff --git a/lib/teleterm/apiserver/handler/handler_gateways.go b/lib/teleterm/apiserver/handler/handler_gateways.go index 5a303e8e45c78..dbb0de52c9363 100644 --- a/lib/teleterm/apiserver/handler/handler_gateways.go +++ b/lib/teleterm/apiserver/handler/handler_gateways.go @@ -119,7 +119,7 @@ func makeGatewayCLICommand(cmds cmd.Cmds) *api.GatewayCLICommand { // // In Connect this is used to update the db name of a db connection along with the CLI command. func (s *Handler) SetGatewayTargetSubresourceName(ctx context.Context, req *api.SetGatewayTargetSubresourceNameRequest) (*api.Gateway, error) { - gateway, err := s.DaemonService.SetGatewayTargetSubresourceName(req.GatewayUri, req.TargetSubresourceName) + gateway, err := s.DaemonService.SetGatewayTargetSubresourceName(ctx, req.GatewayUri, req.TargetSubresourceName) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/teleterm/clusters/cluster_apps.go b/lib/teleterm/clusters/cluster_apps.go index 5b92788cb15b4..cfdecb8a62f66 100644 --- a/lib/teleterm/clusters/cluster_apps.go +++ b/lib/teleterm/clusters/cluster_apps.go @@ -25,6 +25,7 @@ import ( apiclient "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" + apidefaults "github.com/gravitational/teleport/api/defaults" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/client" @@ -55,11 +56,11 @@ type SAMLIdPServiceProvider struct { Provider types.SAMLIdPServiceProvider } -func (c *Cluster) getApp(ctx context.Context, authClient authclient.ClientI, appName string) (types.Application, error) { +func GetApp(ctx context.Context, authClient authclient.ClientI, appName string) (types.Application, error) { var app types.Application err := AddMetadataToRetryableError(ctx, func() error { apps, err := apiclient.GetAllResources[types.AppServer](ctx, authClient, &proto.ListResourcesRequest{ - Namespace: c.clusterClient.Namespace, + Namespace: apidefaults.Namespace, ResourceType: types.KindAppServer, PredicateExpression: fmt.Sprintf(`name == "%s"`, appName), }) @@ -143,3 +144,29 @@ func (c *Cluster) GetAWSRoles(app types.Application) aws.Roles { } return aws.Roles{} } + +// ValidateTargetPort parses rawTargetPort to uint32 and checks if it's included in TCP ports of app. +// It also returns an error if app doesn't have any TCP ports defined. +func ValidateTargetPort(app types.Application, rawTargetPort string) (uint32, error) { + if rawTargetPort == "" { + return 0, nil + } + + targetPort, err := parseTargetPort(rawTargetPort) + if err != nil { + return 0, trace.Wrap(err) + } + + tcpPorts := app.GetTCPPorts() + if len(tcpPorts) == 0 { + return 0, trace.BadParameter("cannot specify target port %d because app %s does not provide access to multiple ports", + targetPort, app.GetName()) + } + + if !tcpPorts.Contains(int(targetPort)) { + return 0, trace.BadParameter("port %d is not included in target ports of app %s", + targetPort, app.GetName()) + } + + return targetPort, nil +} diff --git a/lib/teleterm/clusters/cluster_gateways.go b/lib/teleterm/clusters/cluster_gateways.go index 590fa27611f21..5a08464919e11 100644 --- a/lib/teleterm/clusters/cluster_gateways.go +++ b/lib/teleterm/clusters/cluster_gateways.go @@ -21,6 +21,7 @@ package clusters import ( "context" "crypto/tls" + "strconv" "github.com/gravitational/trace" @@ -160,7 +161,7 @@ func (c *Cluster) createKubeGateway(ctx context.Context, params CreateGatewayPar func (c *Cluster) createAppGateway(ctx context.Context, params CreateGatewayParams) (gateway.Gateway, error) { appName := params.TargetURI.GetAppName() - app, err := c.getApp(ctx, params.ClusterClient.AuthClient, appName) + app, err := GetApp(ctx, params.ClusterClient.AuthClient, appName) if err != nil { return nil, trace.Wrap(err) } @@ -170,6 +171,13 @@ func (c *Cluster) createAppGateway(ctx context.Context, params CreateGatewayPara ClusterName: c.clusterClient.SiteName, URI: app.GetURI(), } + if params.TargetSubresourceName != "" { + targetPort, err := ValidateTargetPort(app, params.TargetSubresourceName) + if err != nil { + return nil, trace.Wrap(err) + } + routeToApp.TargetPort = targetPort + } var cert tls.Certificate if err := AddMetadataToRetryableError(ctx, func() error { @@ -182,6 +190,7 @@ func (c *Cluster) createAppGateway(ctx context.Context, params CreateGatewayPara gw, err := gateway.New(gateway.Config{ LocalPort: params.LocalPort, TargetURI: params.TargetURI, + TargetSubresourceName: params.TargetSubresourceName, TargetName: appName, Cert: cert, Protocol: app.GetProtocol(), @@ -195,6 +204,9 @@ func (c *Cluster) createAppGateway(ctx context.Context, params CreateGatewayPara RootClusterCACertPoolFunc: c.clusterClient.RootClusterCACertPool, ClusterName: c.Name, Username: c.status.Username, + // For multi-port TCP apps, the target port is stored in the target subresource name. Whenever + // that field is updated, the local proxy needs to generate a new cert which includes that port. + ClearCertsOnTargetSubresourceNameChange: true, }) return gw, trace.Wrap(err) } @@ -214,7 +226,7 @@ func (c *Cluster) ReissueGatewayCerts(ctx context.Context, clusterClient *client return cert, trace.Wrap(err) case g.TargetURI().IsApp(): appName := g.TargetURI().GetAppName() - app, err := c.getApp(ctx, clusterClient.AuthClient, appName) + app, err := GetApp(ctx, clusterClient.AuthClient, appName) if err != nil { return tls.Certificate{}, trace.Wrap(err) } @@ -224,6 +236,13 @@ func (c *Cluster) ReissueGatewayCerts(ctx context.Context, clusterClient *client ClusterName: c.clusterClient.SiteName, URI: app.GetURI(), } + if g.TargetSubresourceName() != "" { + targetPort, err := parseTargetPort(g.TargetSubresourceName()) + if err != nil { + return tls.Certificate{}, trace.BadParameter(err.Error()) + } + routeToApp.TargetPort = targetPort + } // The cert is returned from this function and finally set on LocalProxy by the middleware. cert, err := c.ReissueAppCert(ctx, clusterClient, routeToApp) @@ -232,3 +251,11 @@ func (c *Cluster) ReissueGatewayCerts(ctx context.Context, clusterClient *client return tls.Certificate{}, trace.NotImplemented("ReissueGatewayCerts does not support this gateway kind %v", g.TargetURI().String()) } } + +func parseTargetPort(rawTargetPort string) (uint32, error) { + targetPort, err := strconv.ParseUint(rawTargetPort, 10, 32) + if err != nil { + return 0, trace.BadParameter(err.Error()) + } + return uint32(targetPort), nil +} diff --git a/lib/teleterm/daemon/daemon.go b/lib/teleterm/daemon/daemon.go index 13f12f4dfa253..3848c43caa2a4 100644 --- a/lib/teleterm/daemon/daemon.go +++ b/lib/teleterm/daemon/daemon.go @@ -511,7 +511,7 @@ func (s *Service) GetGatewayCLICommand(ctx context.Context, gateway gateway.Gate // SetGatewayTargetSubresourceName updates the TargetSubresourceName field of a gateway stored in // s.gateways. -func (s *Service) SetGatewayTargetSubresourceName(gatewayURI, targetSubresourceName string) (gateway.Gateway, error) { +func (s *Service) SetGatewayTargetSubresourceName(ctx context.Context, gatewayURI, targetSubresourceName string) (gateway.Gateway, error) { s.mu.Lock() defer s.mu.Unlock() @@ -520,6 +520,28 @@ func (s *Service) SetGatewayTargetSubresourceName(gatewayURI, targetSubresourceN return nil, trace.Wrap(err) } + targetURI := gateway.TargetURI() + switch { + case targetURI.IsApp(): + clusterClient, err := s.GetCachedClient(ctx, targetURI) + if err != nil { + return nil, trace.Wrap(err) + } + + var app types.Application + if err := clusters.AddMetadataToRetryableError(ctx, func() error { + var err error + app, err = clusters.GetApp(ctx, clusterClient.CurrentCluster(), targetURI.GetAppName()) + return trace.Wrap(err) + }); err != nil { + return nil, trace.Wrap(err) + } + + if _, err := clusters.ValidateTargetPort(app, targetSubresourceName); err != nil { + return nil, trace.Wrap(err) + } + } + gateway.SetTargetSubresourceName(targetSubresourceName) return gateway, nil diff --git a/lib/teleterm/gateway/app.go b/lib/teleterm/gateway/app.go index 603d640a05a9c..248f48fe873a1 100644 --- a/lib/teleterm/gateway/app.go +++ b/lib/teleterm/gateway/app.go @@ -19,8 +19,6 @@ package gateway import ( "context" "crypto/tls" - "net/url" - "strings" "github.com/gravitational/trace" @@ -33,15 +31,6 @@ type app struct { *base } -// LocalProxyURL returns the URL of the local proxy. -func (a *app) LocalProxyURL() string { - proxyURL := url.URL{ - Scheme: strings.ToLower(a.Protocol()), - Host: a.LocalAddress() + ":" + a.LocalPort(), - } - return proxyURL.String() -} - func makeAppGateway(cfg Config) (Gateway, error) { base, err := newBase(cfg) if err != nil { diff --git a/lib/teleterm/gateway/app_middleware.go b/lib/teleterm/gateway/app_middleware.go index 2da5946018147..89a695640f446 100644 --- a/lib/teleterm/gateway/app_middleware.go +++ b/lib/teleterm/gateway/app_middleware.go @@ -43,12 +43,12 @@ func (m *appMiddleware) OnNewConnection(ctx context.Context, lp *alpn.LocalProxy return nil } - // Return early and don't fire onExpiredCert if certs are invalid but not due to expiry. - if !errors.As(err, &x509.CertificateInvalidError{}) { + // Return early and don't fire onExpiredCert if certs are invalid but not due to expiry or removal. + if !errors.As(err, &x509.CertificateInvalidError{}) && !trace.IsNotFound(err) { return trace.Wrap(err) } - m.log.WithError(err).Debug("Gateway certificates have expired") + m.log.WithError(err).Debug("Gateway certificates have expired or been removed") cert, err := m.onExpiredCert(ctx) if err != nil { diff --git a/lib/teleterm/gateway/base.go b/lib/teleterm/gateway/base.go index e0a9a33cc4d86..57657aec42424 100644 --- a/lib/teleterm/gateway/base.go +++ b/lib/teleterm/gateway/base.go @@ -20,9 +20,11 @@ package gateway import ( "context" + "crypto/tls" "fmt" "net" "strconv" + "sync" "github.com/gravitational/trace" "github.com/sirupsen/logrus" @@ -89,6 +91,9 @@ func newBase(cfg Config) (*base, error) { // Close terminates gateway connection. Fails if called on an already closed gateway. func (b *base) Close() error { + b.mu.Lock() + defer b.mu.Unlock() + b.closeCancel() var errs []error @@ -158,17 +163,29 @@ func (b *base) TargetUser() string { } func (b *base) TargetSubresourceName() string { + b.mu.RLock() + defer b.mu.RUnlock() + return b.cfg.TargetSubresourceName } func (b *base) SetTargetSubresourceName(value string) { + b.mu.Lock() + defer b.mu.Unlock() b.cfg.TargetSubresourceName = value + + if b.cfg.ClearCertsOnTargetSubresourceNameChange { + b.Log().Info("Clearing cert") + b.localProxy.SetCert(tls.Certificate{}) + } } func (b *base) Log() *logrus.Entry { return b.cfg.Log } +// LocalAddress returns the local host in the net package terms (localhost or 127.0.0.1, depending +// on the platform). func (b *base) LocalAddress() string { return b.cfg.LocalAddress } @@ -187,15 +204,13 @@ func (b *base) LocalPortInt() int { } func (b *base) cloneConfig() Config { + b.mu.RLock() + defer b.mu.RUnlock() + return *b.cfg } -// Gateway describes local proxy that creates a gateway to the remote Teleport resource. -// -// Gateway is not safe for concurrent use in itself. However, all access to gateways is gated by -// daemon.Service which obtains a lock for any operation pertaining to gateways. -// -// In the future if Gateway becomes more complex it might be worthwhile to add an RWMutex to it. +// Gateway is a local proxy to a remote Teleport resource. type base struct { cfg *Config localProxy *alpn.LocalProxy @@ -206,6 +221,7 @@ type base struct { // that the local proxy is now closed and to release any resources. closeContext context.Context closeCancel context.CancelFunc + mu sync.RWMutex } type TCPPortAllocator interface { diff --git a/lib/teleterm/gateway/config.go b/lib/teleterm/gateway/config.go index c870df9075728..978cbf58d5ae3 100644 --- a/lib/teleterm/gateway/config.go +++ b/lib/teleterm/gateway/config.go @@ -91,6 +91,11 @@ type Config struct { RootClusterCACertPoolFunc alpnproxy.GetClusterCACertPoolFunc // KubeconfigsDir is the directory containing kubeconfigs for kube gateways. KubeconfigsDir string + // ClearCertsOnTargetSubresourceNameChange is useful in situations where TargetSubresourceName is + // used to generate a cert. In that case, after TargetSubresourceName is changed, the gateway will + // clear the cert from the local proxy and the middleware is going to request a new cert on the + // next connection. + ClearCertsOnTargetSubresourceNameChange bool } // OnExpiredCertFunc is the type of a function that is called when a new downstream connection is diff --git a/lib/teleterm/gateway/interfaces.go b/lib/teleterm/gateway/interfaces.go index 4cedf02e7ffd7..b0bbefcb4b1a9 100644 --- a/lib/teleterm/gateway/interfaces.go +++ b/lib/teleterm/gateway/interfaces.go @@ -42,6 +42,8 @@ type Gateway interface { TargetSubresourceName() string SetTargetSubresourceName(value string) Log() *logrus.Entry + // LocalAddress returns the local host in the net package terms (localhost or 127.0.0.1, depending + // on the platform). LocalAddress() string LocalPort() string LocalPortInt() int @@ -94,7 +96,4 @@ type Kube interface { // App defines an app gateway. type App interface { Gateway - - // LocalProxyURL returns the URL of the local proxy. - LocalProxyURL() string } diff --git a/lib/teleterm/gateway/kube.go b/lib/teleterm/gateway/kube.go index 17a3b4a55241d..d03bdd08bd09b 100644 --- a/lib/teleterm/gateway/kube.go +++ b/lib/teleterm/gateway/kube.go @@ -185,6 +185,8 @@ func (k *kube) makeForwardProxyForKube() error { } func (k *kube) writeKubeconfig(key *keys.PrivateKey, cas map[string]tls.Certificate) error { + k.base.mu.RLock() + defer k.base.mu.RUnlock() ca, ok := cas[k.cfg.ClusterName] if !ok { return trace.BadParameter("CA for teleport cluster %q is missing", k.cfg.ClusterName) diff --git a/proto/teleport/lib/teleterm/v1/gateway.proto b/proto/teleport/lib/teleterm/v1/gateway.proto index 7661a6bf31f4a..4399fcc307e26 100644 --- a/proto/teleport/lib/teleterm/v1/gateway.proto +++ b/proto/teleport/lib/teleterm/v1/gateway.proto @@ -43,12 +43,13 @@ message Gateway { string local_address = 5; // local_port is the gateway address on localhost string local_port = 6; - // protocol is the gateway protocol + // protocol is the protocol used by the gateway. For databases, it matches the type of the + // database that the gateway targets. For apps, it's either "HTTP" or "TCP". string protocol = 7; reserved 8; reserved "cli_command"; // target_subresource_name points at a subresource of the remote resource, for example a - // database name on a database server. + // database name on a database server or a target port of a multi-port TCP app. string target_subresource_name = 9; // gateway_cli_client represents a command that the user can execute to connect to the resource // through the gateway. diff --git a/web/packages/design/src/Input/Input.tsx b/web/packages/design/src/Input/Input.tsx index 3cf50e9d1009b..fe3b7feca968c 100644 --- a/web/packages/design/src/Input/Input.tsx +++ b/web/packages/design/src/Input/Input.tsx @@ -70,6 +70,7 @@ interface InputProps extends ColorProps, SpaceProps, WidthProps, HeightProps { inputMode?: InputMode; spellCheck?: boolean; style?: React.CSSProperties; + required?: boolean; 'aria-invalid'?: HTMLAttributes<'input'>['aria-invalid']; 'aria-describedby'?: HTMLAttributes<'input'>['aria-describedby']; @@ -170,6 +171,7 @@ const Input = forwardRef((props, ref) => { inputMode, spellCheck, style, + required, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, @@ -222,6 +224,7 @@ const Input = forwardRef((props, ref) => { inputMode, spellCheck, style, + required, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, diff --git a/web/packages/design/src/Menu/Menu.story.tsx b/web/packages/design/src/Menu/Menu.story.tsx index c7b0726ea414b..c3ba4ae802762 100644 --- a/web/packages/design/src/Menu/Menu.story.tsx +++ b/web/packages/design/src/Menu/Menu.story.tsx @@ -107,6 +107,18 @@ export const MenuItems = () => ( Amet nisi tempor + +

Label as first child

+ + Tempus ut libero + Lorem ipsum + Dolor sit amet + + Leo vitae arcu + Donec volutpat + Mauris sit + +
); diff --git a/web/packages/design/src/Menu/MenuItem.tsx b/web/packages/design/src/Menu/MenuItem.tsx index 5ccbae227c835..a9b06373bd787 100644 --- a/web/packages/design/src/Menu/MenuItem.tsx +++ b/web/packages/design/src/Menu/MenuItem.tsx @@ -71,37 +71,39 @@ const MenuItemBase = styled(Flex)` ${fromThemeBase} `; -export const MenuItemSectionLabel = styled(MenuItemBase).attrs({ - px: 2, +export const MenuItemSectionSeparator = styled.hr.attrs({ onClick: event => { // Make sure that clicks on this element don't trigger onClick set on MenuList. event.stopPropagation(); }, })` - font-weight: bold; - min-height: 16px; + background: ${props => props.theme.colors.interactive.tonal.neutral[1]}; + height: 1px; + border: 0; + font-size: 0; `; -export const MenuItemSectionSeparator = styled.hr.attrs({ +export const MenuItemSectionLabel = styled(MenuItemBase).attrs({ + px: 2, onClick: event => { // Make sure that clicks on this element don't trigger onClick set on MenuList. event.stopPropagation(); }, })` - background: ${props => props.theme.colors.interactive.tonal.neutral[1]}; - height: 1px; - border: 0; - font-size: 0; + font-weight: bold; + min-height: 16px; - // Add padding to the label for extra visual space, but only when it follows a separator. - // If a separator follows a MenuItem, there's already enough visual space, so no extra space is - // needed. The hover state of MenuItem highlights everything right from the separator start to the - // end of MenuItem. + // Add padding to the label for extra visual space, but only when it follows a separator or is the + // first child. + // + // If a separator follows a MenuItem, there's already enough visual space between MenuItem and + // separator, so no extra space is needed. The hover state of MenuItem highlights everything right + // from the separator start to the end of MenuItem. // // Padding is used instead of margin here on purpose, so that there's no empty transparent space // between Separator and Label – otherwise clicking on that space would count as a click on // MenuList and not trigger onClick set on Separator or Label. - & + ${MenuItemSectionLabel} { + ${MenuItemSectionSeparator} + &, &:first-child { padding-top: ${props => props.theme.space[1]}px; } `; diff --git a/web/packages/design/src/keyframes.ts b/web/packages/design/src/keyframes.ts index c49799db9f67f..a3a7bf96f7245 100644 --- a/web/packages/design/src/keyframes.ts +++ b/web/packages/design/src/keyframes.ts @@ -46,3 +46,7 @@ export const blink = keyframes` opacity: 100%; } `; + +export const disappear = keyframes` +to { opacity: 0; } +`; diff --git a/web/packages/shared/components/FieldInput/FieldInput.tsx b/web/packages/shared/components/FieldInput/FieldInput.tsx index 2ac28f54e810c..2f3a3eb012550 100644 --- a/web/packages/shared/components/FieldInput/FieldInput.tsx +++ b/web/packages/shared/components/FieldInput/FieldInput.tsx @@ -59,6 +59,7 @@ const FieldInput = forwardRef( toolTipContent = null, disabled = false, markAsError = false, + required = false, ...styles }, ref @@ -94,6 +95,7 @@ const FieldInput = forwardRef( size={size} aria-invalid={hasError || markAsError} aria-describedby={helperTextId} + required={required} /> ); @@ -219,7 +221,7 @@ export type FieldInputProps = BoxProps & { id?: string; name?: string; value?: string; - label?: string; + label?: React.ReactNode; helperText?: React.ReactNode; icon?: React.ComponentType; size?: InputSize; @@ -245,4 +247,5 @@ export type FieldInputProps = BoxProps & { // input box as error color before validator // runs (which marks it as error) markAsError?: boolean; + required?: boolean; }; diff --git a/web/packages/teleterm/src/services/tshd/testHelpers.ts b/web/packages/teleterm/src/services/tshd/testHelpers.ts index b19fc95725192..8cb15ec3e3701 100644 --- a/web/packages/teleterm/src/services/tshd/testHelpers.ts +++ b/web/packages/teleterm/src/services/tshd/testHelpers.ts @@ -290,7 +290,7 @@ export const makeAppGateway = ( targetUri: appUri, localAddress: 'localhost', localPort: '1337', - targetSubresourceName: 'bar', + targetSubresourceName: undefined, gatewayCliCommand: { path: '', preview: 'curl http://localhost:1337', diff --git a/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.tsx b/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.tsx index c16dd1d5fa779..39147660e843e 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.tsx +++ b/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.tsx @@ -23,7 +23,7 @@ import { MenuItemSectionLabel, MenuItemSectionSeparator, } from 'design/Menu/MenuItem'; -import { App } from 'gen-proto-ts/teleport/lib/teleterm/v1/app_pb'; +import { App, PortRange } from 'gen-proto-ts/teleport/lib/teleterm/v1/app_pb'; import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb'; import { Database } from 'gen-proto-ts/teleport/lib/teleterm/v1/database_pb'; import { Kube } from 'gen-proto-ts/teleport/lib/teleterm/v1/kube_pb'; @@ -125,8 +125,11 @@ export function ConnectAppActionButton(props: { app: App }): React.JSX.Element { connectToAppWithVnet(appContext, launchVnet, props.app, targetPort); } - function setUpGateway(): void { - setUpAppGateway(appContext, props.app, { origin: 'resource_table' }); + function setUpGateway(targetPort?: number): void { + setUpAppGateway(appContext, props.app, { + telemetry: { origin: 'resource_table' }, + targetPort, + }); } const rootCluster = appContext.clustersService.findCluster( @@ -229,7 +232,7 @@ function AppButton(props: { cluster: Cluster; rootCluster: Cluster; connectWithVnet(targetPort?: number): void; - setUpGateway(): void; + setUpGateway(targetPort?: number): void; onLaunchUrl(): void; isVnetSupported: boolean; }) { @@ -285,37 +288,15 @@ function AppButton(props: { target="_blank" title="Launch the app in the browser" > - Set up connection + props.setUpGateway()}> + Set up connection + ); } // TCP app with VNet. if (props.isVnetSupported) { - let $targetPorts: JSX.Element; - if (props.app.tcpPorts.length) { - $targetPorts = ( - <> - - Available target ports - {props.app.tcpPorts.map((portRange, index) => ( - props.connectWithVnet(portRange.port)} - > - {formatPortRange(portRange)} - - ))} - - ); - } - return ( props.connectWithVnet()} > - Connect without VNet - {$targetPorts} + props.setUpGateway()}> + Connect without VNet + + {!!props.app.tcpPorts.length && ( + <> + + props.connectWithVnet(port)} + /> + + )} + + ); + } + + // Multi-port TCP app without VNet. + if (props.app.tcpPorts.length) { + return ( + props.setUpGateway()} + > + props.setUpGateway(port)} + /> ); } - // TCP app without VNet. + // Single-port TCP app without VNet. return ( props.setUpGateway()} textTransform="none" > Connect @@ -341,6 +349,29 @@ function AppButton(props: { ); } +const AvailableTargetPorts = (props: { + tcpPorts: PortRange[]; + onItemClick: (portRangePort: number) => void; +}) => ( + <> + Available target ports + {props.tcpPorts.map((portRange, index) => ( + props.onItemClick(portRange.port)} + > + {formatPortRange(portRange)} + + ))} + +); + export function AccessRequestButton(props: { isResourceAdded: boolean; requestStarted: boolean; diff --git a/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts b/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts index 1c08bae058742..743667ebfa662 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts +++ b/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts @@ -30,6 +30,7 @@ import { retryWithRelogin } from 'teleterm/ui/utils'; export function useGateway(doc: DocumentGateway) { const ctx = useAppContext(); + const { clustersService } = ctx; const { documentsService } = useWorkspaceContext(); // The port to show as default in the input field in case creating a gateway fails. // This is typically the case if someone reopens the app and the port of the gateway is already @@ -51,7 +52,7 @@ export function useGateway(doc: DocumentGateway) { try { gw = await retryWithRelogin(ctx, doc.targetUri, () => - ctx.clustersService.createGateway({ + clustersService.createGateway({ targetUri: doc.targetUri, localPort: port, targetUser: doc.targetUser, @@ -92,34 +93,52 @@ export function useGateway(doc: DocumentGateway) { }); const [disconnectAttempt, disconnect] = useAsync(async () => { - await ctx.clustersService.removeGateway(doc.gatewayUri); + await clustersService.removeGateway(doc.gatewayUri); documentsService.close(doc.uri); }); const [changeTargetSubresourceNameAttempt, changeTargetSubresourceName] = - useAsync(async (name: string) => { - const updatedGateway = - await ctx.clustersService.setGatewayTargetSubresourceName( - doc.gatewayUri, - name - ); + useAsync( + useCallback( + (name: string) => + retryWithRelogin(ctx, doc.targetUri, async () => { + const updatedGateway = + await clustersService.setGatewayTargetSubresourceName( + doc.gatewayUri, + name + ); - documentsService.update(doc.uri, { - targetSubresourceName: updatedGateway.targetSubresourceName, - }); - }); - - const [changePortAttempt, changePort] = useAsync(async (port: string) => { - const updatedGateway = await ctx.clustersService.setGatewayLocalPort( - doc.gatewayUri, - port + documentsService.update(doc.uri, { + targetSubresourceName: updatedGateway.targetSubresourceName, + }); + }), + [ + clustersService, + documentsService, + doc.uri, + doc.gatewayUri, + ctx, + doc.targetUri, + ] + ) ); - documentsService.update(doc.uri, { - targetSubresourceName: updatedGateway.targetSubresourceName, - port: updatedGateway.localPort, - }); - }); + const [changePortAttempt, changePort] = useAsync( + useCallback( + async (port: string) => { + const updatedGateway = await clustersService.setGatewayLocalPort( + doc.gatewayUri, + port + ); + + documentsService.update(doc.uri, { + targetSubresourceName: updatedGateway.targetSubresourceName, + port: updatedGateway.localPort, + }); + }, + [clustersService, documentsService, doc.uri, doc.gatewayUri] + ) + ); useEffect( function createGatewayOnMount() { diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx index 1c2981ce9f42b..bd31e84d80035 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx @@ -16,18 +16,27 @@ * along with this program. If not, see . */ -import { useMemo, useRef } from 'react'; +import { + ChangeEvent, + ChangeEventHandler, + PropsWithChildren, + useEffect, + useMemo, + useState, +} from 'react'; +import styled from 'styled-components'; import { Alert, - Box, ButtonSecondary, + disappear, Flex, H1, - Indicator, Link, + rotate360, Text, } from 'design'; +import { Check, Spinner } from 'design/Icon'; import { Gateway } from 'gen-proto-ts/teleport/lib/teleterm/v1/gateway_pb'; import { TextSelectCopy } from 'shared/components/TextSelectCopy'; import Validation from 'shared/components/Validation'; @@ -39,68 +48,110 @@ import { PortFieldInput } from '../components/FieldInputs'; export function AppGateway(props: { gateway: Gateway; disconnectAttempt: Attempt; - changePort(port: string): void; - changePortAttempt: Attempt; + changeLocalPort(port: string): void; + changeLocalPortAttempt: Attempt; + changeTargetPort(port: string): void; + changeTargetPortAttempt: Attempt; disconnect(): void; }) { const { gateway } = props; - const formRef = useRef(); - const { changePort } = props; - const handleChangePort = useMemo(() => { - return debounce((value: string) => { - if (formRef.current.reportValidity()) { - changePort(value); - } - }, 1000); - }, [changePort]); + const { + changeLocalPort, + changeLocalPortAttempt, + changeTargetPort, + changeTargetPortAttempt, + disconnectAttempt, + } = props; + // It must be possible to update local port while target port is invalid, hence why + // useDebouncedPortChangeHandler checks the validity of only one input at a time. Otherwise the UI + // would lose updates to the local port while the target port was invalid. + const handleLocalPortChange = useDebouncedPortChangeHandler(changeLocalPort); + const handleTargetPortChange = + useDebouncedPortChangeHandler(changeTargetPort); let address = `${gateway.localAddress}:${gateway.localPort}`; if (gateway.protocol === 'HTTP') { address = `http://${address}`; } + // AppGateway doesn't have access to the app resource itself, so it has to decide whether the + // app is multi-port or not in some other way. + // For multi-port apps, DocumentGateway comes with targetSubresourceName prefilled to the first + // port number found in TCP ports. Single-port apps have this field empty. + // So, if targetSubresourceName is present, then the app must be multi-port. In this case, the + // user is free to change it and can never provide an empty targetSubresourceName. + // When the app is not multi-port, targetSubresourceName is empty and the user cannot change it. + const isMultiPort = + gateway.protocol === 'TCP' && gateway.targetSubresourceName; + return ( - - + +

App Connection

Close Connection
- {props.disconnectAttempt.status === 'error' && ( - + {disconnectAttempt.status === 'error' && ( + Could not close the connection )} - + + } defaultValue={gateway.localPort} - onChange={e => handleChangePort(e.target.value)} - mb={2} + onChange={handleLocalPortChange} + mb={0} /> + {isMultiPort && ( + + } + required + defaultValue={gateway.targetSubresourceName} + onChange={handleTargetPortChange} + mb={0} + /> + )} - {props.changePortAttempt.status === 'processing' && ( - - )} - Access the app at: - +
+ Access the app at: + +
- {props.changePortAttempt.status === 'error' && ( - - Could not change the port number + {changeLocalPortAttempt.status === 'error' && ( + + Could not change the local port + + )} + + {changeTargetPortAttempt.status === 'error' && ( + + Could not change the target port )} @@ -115,6 +166,89 @@ export function AppGateway(props: { {' '} for more details. -
+ ); } + +const LabelWithAttemptStatus = (props: { + text: string; + attempt: Attempt; +}) => ( + + {props.text} + {props.attempt.status === 'processing' && ( + + )} + {props.attempt.status === 'success' && ( + // CSS animations are repeated whenever the parent goes from `display: none` to something + // else. As a result, we need to unmount the animated check so that the animation is not + // repeated when the user switches to this tab. + // https://www.w3.org/TR/css-animations-1/#example-4e34d7ba + + + + )} + +); + +/** + * useDebouncedPortChangeHandler returns a debounced change handler that calls the change function + * only if the input from which the event originated is valid. + */ +const useDebouncedPortChangeHandler = ( + changeFunc: (port: string) => void +): ChangeEventHandler => + useMemo( + () => + debounce((event: ChangeEvent) => { + if (event.target.reportValidity()) { + changeFunc(event.target.value); + } + }, 1000), + [changeFunc] + ); + +const AnimatedSpinner = styled(Spinner)` + animation: ${rotate360} 1.5s infinite linear; + // The spinner needs to be positioned absolutely so that the fact that it's spinning + // doesn't affect the size of the parent. + position: absolute; + right: 0; + top: 0; +`; + +const disappearanceDelayMs = 1000; +const disappearanceDurationMs = 200; + +const DisappearingCheck = styled(Check)` + opacity: 1; + animation: ${disappear}; + animation-delay: ${disappearanceDelayMs}ms; + animation-duration: ${disappearanceDurationMs}ms; + animation-fill-mode: forwards; +`; + +const UnmountAfter = ({ + timeoutMs, + children, +}: PropsWithChildren<{ timeoutMs: number }>) => { + const [isMounted, setIsMounted] = useState(true); + + useEffect(() => { + const timeout = setTimeout(() => { + setIsMounted(false); + }, timeoutMs); + + return () => { + clearTimeout(timeout); + }; + }, [timeoutMs]); + + return isMounted ? children : null; +}; diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx index c0b4ec802b28e..936f1c8a399b1 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx @@ -30,9 +30,10 @@ import { MockWorkspaceContextProvider } from 'teleterm/ui/fixtures/MockWorkspace import * as types from 'teleterm/ui/services/workspacesService'; type StoryProps = { - appType: 'web' | 'tcp'; + appType: 'web' | 'tcp' | 'tcp-multi-port'; online: boolean; - changePort: 'succeed' | 'throw-error'; + changeLocalPort: 'succeed' | 'throw-error'; + changeTargetPort: 'succeed' | 'throw-error'; disconnect: 'succeed' | 'throw-error'; }; @@ -42,9 +43,14 @@ const meta: Meta = { argTypes: { appType: { control: { type: 'radio' }, - options: ['web', 'tcp'], + options: ['web', 'tcp', 'tcp-multi-port'], }, - changePort: { + changeLocalPort: { + if: { arg: 'online' }, + control: { type: 'radio' }, + options: ['succeed', 'throw-error'], + }, + changeTargetPort: { if: { arg: 'online' }, control: { type: 'radio' }, options: ['succeed', 'throw-error'], @@ -58,7 +64,8 @@ const meta: Meta = { args: { appType: 'web', online: true, - changePort: 'succeed', + changeLocalPort: 'succeed', + changeTargetPort: 'succeed', disconnect: 'succeed', }, }; @@ -70,6 +77,10 @@ export function Story(props: StoryProps) { if (props.appType === 'tcp') { gateway.protocol = 'TCP'; } + if (props.appType === 'tcp-multi-port') { + gateway.protocol = 'TCP'; + gateway.targetSubresourceName = '4242'; + } const documentGateway: types.DocumentGateway = { kind: 'doc.gateway', targetUri: '/clusters/bar/apps/quux', @@ -80,10 +91,14 @@ export function Story(props: StoryProps) { targetUser: '', status: '', targetName: 'quux', + targetSubresourceName: undefined, }; if (!props.online) { documentGateway.gatewayUri = undefined; } + if (props.appType === 'tcp-multi-port') { + documentGateway.targetSubresourceName = '4242'; + } const appContext = new MockAppContext(); appContext.workspacesService.setState(draftState => { @@ -105,8 +120,26 @@ export function Story(props: StoryProps) { wait(1000).then( () => new MockedUnaryCall( - { ...gateway, localPort }, - props.changePort === 'throw-error' + { + ...appContext.clustersService.findGateway(gateway.uri), + localPort, + }, + props.changeLocalPort === 'throw-error' + ? new Error('something went wrong') + : undefined + ) + ); + appContext.tshd.setGatewayTargetSubresourceName = ({ + targetSubresourceName, + }) => + wait(1000).then( + () => + new MockedUnaryCall( + { + ...appContext.clustersService.findGateway(gateway.uri), + targetSubresourceName, + }, + props.changeTargetPort === 'throw-error' ? new Error('something went wrong') : undefined ) diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx index ba70a7dfbdbe3..24db9f673be64 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx @@ -29,13 +29,15 @@ export function DocumentGatewayApp(props: { const { doc } = props; const { gateway, - changePort, - changePortAttempt, + changePort: changeLocalPort, + changePortAttempt: changeLocalPortAttempt, connected, connectAttempt, disconnect, disconnectAttempt, reconnect, + changeTargetSubresourceName: changeTargetPort, + changeTargetSubresourceNameAttempt: changeTargetPortAttempt, } = useGateway(doc); return ( @@ -47,14 +49,17 @@ export function DocumentGatewayApp(props: { targetName={doc.targetName} gatewayPort={{ isSupported: true, defaultPort: doc.port }} reconnect={reconnect} + portFieldLabel="Local Port (optional)" /> ) : ( )} diff --git a/web/packages/teleterm/src/ui/TabHost/useTabShortcuts.test.tsx b/web/packages/teleterm/src/ui/TabHost/useTabShortcuts.test.tsx index ce65290c2eb1f..b8fe467178b54 100644 --- a/web/packages/teleterm/src/ui/TabHost/useTabShortcuts.test.tsx +++ b/web/packages/teleterm/src/ui/TabHost/useTabShortcuts.test.tsx @@ -55,6 +55,7 @@ function getMockDocuments(): Document[] { targetUri: '/clusters/bar/dbs/foobar', targetName: 'foobar', targetUser: 'foo', + targetSubresourceName: undefined, origin: 'resource_table', status: '', }, @@ -66,6 +67,7 @@ function getMockDocuments(): Document[] { targetUri: '/clusters/bar/dbs/foobar', targetName: 'foobar', targetUser: 'bar', + targetSubresourceName: undefined, origin: 'resource_table', status: '', }, diff --git a/web/packages/teleterm/src/ui/components/FieldInputs.tsx b/web/packages/teleterm/src/ui/components/FieldInputs.tsx index 21086d8f9bb23..7e7d57e4ec40f 100644 --- a/web/packages/teleterm/src/ui/components/FieldInputs.tsx +++ b/web/packages/teleterm/src/ui/components/FieldInputs.tsx @@ -16,23 +16,26 @@ * along with this program. If not, see . */ -import { forwardRef } from 'react'; +import styled from 'styled-components'; -import FieldInput, { FieldInputProps } from 'shared/components/FieldInput'; +import FieldInput from 'shared/components/FieldInput'; -export const ConfigFieldInput = forwardRef( - (props, ref) => -); +export const ConfigFieldInput = styled(FieldInput).attrs({ size: 'small' })` + input { + &:invalid, + &:invalid:hover { + border-color: ${props => + props.theme.colors.interactive.solid.danger.default}; + } + } +`; -export const PortFieldInput = forwardRef( - (props, ref) => ( - - ) -); +export const PortFieldInput = styled(ConfigFieldInput).attrs({ + type: 'number', + min: 1, + max: 65535, + // Without a min width, the stepper controls end up being to close to a long port number such + // as 65535. minWidth instead of width allows the field to grow with the label, so that e.g. + // a custom label of "Local Port (optional)" is displayed on a single line. + minWidth: '110px', +})``; diff --git a/web/packages/teleterm/src/ui/components/OfflineGateway.tsx b/web/packages/teleterm/src/ui/components/OfflineGateway.tsx index 500a85951ba9a..2dbc1027565ee 100644 --- a/web/packages/teleterm/src/ui/components/OfflineGateway.tsx +++ b/web/packages/teleterm/src/ui/components/OfflineGateway.tsx @@ -36,7 +36,9 @@ export function OfflineGateway(props: { targetName: string; /** Gateway kind displayed in the UI, for example, 'database'. */ gatewayKind: string; + portFieldLabel?: string; }) { + const portFieldLabel = props.portFieldLabel || 'Port (optional)'; const defaultPort = props.gatewayPort.isSupported ? props.gatewayPort.defaultPort : undefined; @@ -88,7 +90,7 @@ export function OfflineGateway(props: { {props.gatewayPort.isSupported && ( { describe('setUpAppGateway', () => { test.each([ { - name: 'creates tunnel for a tcp app', + name: 'creates tunnel for a single-port TCP app', app: makeApp({ endpointUri: 'tcp://localhost:3000', }), }, + { + name: 'creates tunnel for a multi-port TCP app', + app: makeApp({ + endpointUri: 'tcp://localhost', + tcpPorts: [{ port: 1234, endPort: 0 }], + }), + expectedTargetSubresourceName: '1234', + }, + { + name: 'creates tunnel for a multi-port TCP app with a preselected target port', + app: makeApp({ + endpointUri: 'tcp://localhost', + tcpPorts: [{ port: 1234, endPort: 0 }], + }), + targetPort: 1234, + }, { name: 'creates tunnel for a web app', app: makeApp({ endpointUri: 'http://localhost:3000', }), }, - ])('$name', async ({ app }) => { + ])('$name', async ({ app, targetPort, expectedTargetSubresourceName }) => { const appContext = new MockAppContext(); setTestCluster(appContext); - await setUpAppGateway(appContext, app, { origin: 'resource_table' }); + await setUpAppGateway(appContext, app, { + telemetry: { origin: 'resource_table' }, + targetPort, + }); const documents = appContext.workspacesService .getActiveWorkspaceDocumentService() .getGatewayDocuments(); @@ -147,7 +166,8 @@ describe('setUpAppGateway', () => { port: undefined, status: '', targetName: 'foo', - targetSubresourceName: undefined, + targetSubresourceName: + expectedTargetSubresourceName || targetPort?.toString() || undefined, targetUri: '/clusters/teleport-local/apps/foo', targetUser: '', title: 'foo', diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.ts index 93aee047a7341..2711bae403b0d 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.ts @@ -115,13 +115,21 @@ export async function connectToApp( return; } - await setUpAppGateway(ctx, target, telemetry); + await setUpAppGateway(ctx, target, { telemetry }); } export async function setUpAppGateway( ctx: IAppContext, target: App, - telemetry: { origin: DocumentOrigin } + options: { + telemetry: { origin: DocumentOrigin }; + /** + * targetPort allows the caller to preselect the target port for the gateway. Works only with + * multi-port TCP apps. If it's not specified and the app is multi-port, the first port from + * it's TCP ports is used instead. + */ + targetPort?: number; + } ) { const rootClusterUri = routing.ensureRootClusterUri(target.uri); @@ -129,16 +137,20 @@ export async function setUpAppGateway( ctx.workspacesService.getWorkspaceDocumentService(rootClusterUri); const doc = documentsService.createGatewayDocument({ targetUri: target.uri, - origin: telemetry.origin, + origin: options.telemetry.origin, targetName: routing.parseAppUri(target.uri).params.appId, targetUser: '', + targetSubresourceName: + target.tcpPorts.length > 0 + ? (options.targetPort || target.tcpPorts[0].port).toString() + : undefined, }); const connectionToReuse = ctx.connectionTracker.findConnectionByDocument(doc); if (connectionToReuse) { await ctx.connectionTracker.activateItem(connectionToReuse.id, { - origin: telemetry.origin, + origin: options.telemetry.origin, }); } else { await ctx.workspacesService.setActiveWorkspace(rootClusterUri); diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.test.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.test.ts index b50989a4273ff..96d1f3129ea24 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.test.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.test.ts @@ -79,6 +79,7 @@ describe('document should be added', () => { targetUri: '/clusters/bar/dbs/quux', targetName: 'quux', targetUser: 'foo', + targetSubresourceName: undefined, origin: 'resource_table', status: '', }; @@ -155,6 +156,7 @@ test('only gateway documents should be returned', () => { targetUri: '/clusters/bar/dbs/quux', targetName: 'quux', targetUser: 'foo', + targetSubresourceName: undefined, origin: 'resource_table', status: '', }; diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts index 970fb09d22ba7..e975d8e268ae7 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts @@ -109,7 +109,11 @@ export interface DocumentGateway extends DocumentBase { targetUri: uri.DatabaseUri | uri.AppUri; targetUser: string; targetName: string; - targetSubresourceName?: string; + /** + * targetSubresourceName contains database name for db gateways and target port for TCP app + * gateways. + */ + targetSubresourceName: string | undefined; port?: string; origin: DocumentOrigin; } From bcc116c6f80b538b846e704225d6b015b2f5a98d Mon Sep 17 00:00:00 2001 From: Alan Parra Date: Tue, 14 Jan 2025 11:12:14 -0300 Subject: [PATCH 2/6] fix: Move testGatewayConnectionFunc to proxy_helpers.go --- integration/proxy/proxy_helpers.go | 2 ++ integration/proxy/teleterm_test.go | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/proxy/proxy_helpers.go b/integration/proxy/proxy_helpers.go index e5e717410e8b9..2c1681a7b7a8c 100644 --- a/integration/proxy/proxy_helpers.go +++ b/integration/proxy/proxy_helpers.go @@ -725,6 +725,8 @@ func mustConnectWebAppGateway(ctx context.Context, t *testing.T, _ *daemon.Servi require.Equal(t, http.StatusOK, resp.StatusCode) } +type testGatewayConnectionFunc func(context.Context, *testing.T, *daemon.Service, gateway.Gateway) + func makeMustConnectMultiPortTCPAppGateway(wantMessage string, otherTargetPort int, otherWantMessage string) testGatewayConnectionFunc { return func(ctx context.Context, t *testing.T, d *daemon.Service, gw gateway.Gateway) { t.Helper() diff --git a/integration/proxy/teleterm_test.go b/integration/proxy/teleterm_test.go index 18b0efd4884c7..3cd4353c417f1 100644 --- a/integration/proxy/teleterm_test.go +++ b/integration/proxy/teleterm_test.go @@ -186,8 +186,6 @@ func testDBGatewayCertRenewal(ctx context.Context, t *testing.T, params dbGatewa ) } -type testGatewayConnectionFunc func(context.Context, *testing.T, *daemon.Service, gateway.Gateway) - type generateAndSetupUserCredsFunc func(t *testing.T, tc *libclient.TeleportClient, ttl time.Duration) type gatewayCertRenewalParams struct { From cdfc4fe22517704f304e94a554498fa8d02c333e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Wed, 15 Jan 2025 16:31:35 +0100 Subject: [PATCH 3/6] Add field for changing target port if app gateway fails to start * Make OfflineGateway use uncontrolled forms * Pass form fields to OfflineGateway from outside This will enable each callsite to establish it's own rules as to which fields are required. This will be useful once we add required target subresource name for TCP apps. * Add tests for submitting form through OfflineGateway * Make sure reconnect and formSchema operate on the same type * Add comment for Terminal input --- web/.storybook/preview.tsx | 3 + web/packages/teleterm/src/logger.ts | 16 ++ .../DocumentGateway/DocumentGateway.story.tsx | 7 +- .../DocumentGateway/DocumentGateway.test.tsx | 78 ++++++++ .../ui/DocumentGateway/DocumentGateway.tsx | 27 ++- .../src/ui/DocumentGateway/useGateway.ts | 104 ++++++----- .../DocumentGatewayApp.test.tsx | 170 ++++++++++++++++++ .../DocumentGatewayApp/DocumentGatewayApp.tsx | 49 ++++- .../DocumentGatewayKube.test.tsx | 84 +++++++++ .../DocumentGatewayKube.tsx | 4 +- .../src/ui/components/OfflineGateway.tsx | 97 +++++++--- .../teleterm/src/ui/fixtures/mocks.ts | 18 ++ .../documentsService/types.ts | 2 + 13 files changed, 576 insertions(+), 83 deletions(-) create mode 100644 web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.test.tsx create mode 100644 web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.test.tsx create mode 100644 web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.test.tsx diff --git a/web/.storybook/preview.tsx b/web/.storybook/preview.tsx index 405295e381006..da72200610f6d 100644 --- a/web/.storybook/preview.tsx +++ b/web/.storybook/preview.tsx @@ -26,6 +26,7 @@ import { Theme } from '../packages/design/src/theme/themes/types'; import { ConfiguredThemeProvider } from '../packages/design/src/ThemeProvider'; import history from '../packages/teleport/src/services/history/history'; import { UserContextProvider } from '../packages/teleport/src/User'; +import Logger, { ConsoleService } from '../packages/teleterm/src/logger'; import { StaticThemeProvider as TeletermThemeProvider } from '../packages/teleterm/src/ui/ThemeProvider'; import { darkTheme as teletermDarkTheme, @@ -36,6 +37,8 @@ initialize(); history.init(); +Logger.init(new ConsoleService()); + interface ThemeDecoratorProps { theme: string; title: string; diff --git a/web/packages/teleterm/src/logger.ts b/web/packages/teleterm/src/logger.ts index e215653fa1304..c4eae14b6d4d0 100644 --- a/web/packages/teleterm/src/logger.ts +++ b/web/packages/teleterm/src/logger.ts @@ -69,3 +69,19 @@ export class NullService implements LoggerService { } /* eslint-enable @typescript-eslint/no-unused-vars */ } + +export class ConsoleService implements LoggerService { + createLogger(loggerName: string): types.Logger { + return { + warn(...args: any[]) { + console.warn(loggerName, ...args); + }, + info(...args: any[]) { + console.info(loggerName, ...args); + }, + error(...args: any[]) { + console.error(loggerName, ...args); + }, + }; + } +} diff --git a/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx index 239e75dc51c45..6be35eb892d13 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx +++ b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx @@ -28,6 +28,10 @@ import { import { makeDatabaseGateway } from 'teleterm/services/tshd/testHelpers'; import { OfflineGateway } from '../components/OfflineGateway'; +import { + formSchema, + makeRenderFormControlsFromDefaultPort, +} from './DocumentGateway'; import { OnlineDocumentGateway } from './OnlineDocumentGateway'; type StoryProps = { @@ -99,9 +103,10 @@ export function Story(props: StoryProps) { const offlineGatewayProps: ComponentProps = { connectAttempt: makeEmptyAttempt(), reconnect: () => {}, - gatewayPort: { isSupported: true, defaultPort: '1337' }, targetName: gateway.targetName, gatewayKind: 'database', + formSchema, + renderFormControls: makeRenderFormControlsFromDefaultPort('1337'), }; if (props.connectAttempt === 'error') { diff --git a/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.test.tsx b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.test.tsx new file mode 100644 index 0000000000000..2ded4c2d9b620 --- /dev/null +++ b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.test.tsx @@ -0,0 +1,78 @@ +/** + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import userEvent from '@testing-library/user-event'; + +import { render, screen } from 'design/utils/testing'; + +import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient'; +import { + makeDatabaseGateway, + makeRootCluster, +} from 'teleterm/services/tshd/testHelpers'; +import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; +import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; +import type * as docs from 'teleterm/ui/services/workspacesService'; +import { DatabaseUri } from 'teleterm/ui/uri'; + +import { MockWorkspaceContextProvider } from '../fixtures/MockWorkspaceContextProvider'; +import { DocumentGateway } from './DocumentGateway'; + +test('it allows reconnecting when the gateway fails to be created', async () => { + const user = userEvent.setup(); + + const appContext = new MockAppContext(); + const cluster = makeRootCluster(); + const gateway = makeDatabaseGateway(); + const doc: docs.DocumentGateway = { + uri: '/docs/1', + kind: 'doc.gateway', + targetName: gateway.targetName, + targetUri: gateway.targetUri as DatabaseUri, + targetUser: gateway.targetUser, + targetSubresourceName: gateway.targetSubresourceName, + gatewayUri: gateway.uri, + origin: 'resource_table', + title: '', + status: '', + }; + appContext.addRootClusterWithDoc(cluster, doc); + + jest + .spyOn(appContext.tshd, 'createGateway') + .mockReturnValueOnce( + new MockedUnaryCall(undefined, new Error('Something went wrong')) + ) + .mockReturnValueOnce(new MockedUnaryCall(gateway)); + + render( + + + + + + ); + + expect( + await screen.findByText('Could not establish the connection') + ).toBeInTheDocument(); + + await user.click(screen.getByText('Reconnect')); + + expect(await screen.findByText('Close Connection')).toBeInTheDocument(); +}); diff --git a/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.tsx b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.tsx index 6c0a3585bb98a..cb884bff8a924 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.tsx +++ b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.tsx @@ -16,11 +16,15 @@ * along with this program. If not, see . */ +import { useMemo } from 'react'; +import { z } from 'zod'; + import { getCliCommandArgv0 } from 'teleterm/services/tshd/gateway'; import Document from 'teleterm/ui/Document'; import * as types from 'teleterm/ui/services/workspacesService'; -import { OfflineGateway } from '../components/OfflineGateway'; +import { PortFieldInput } from '../components/FieldInputs'; +import { FormFields, OfflineGateway } from '../components/OfflineGateway'; import { useWorkspaceContext } from '../Documents'; import { OnlineDocumentGateway } from './OnlineDocumentGateway'; import { useGateway } from './useGateway'; @@ -63,15 +67,21 @@ export function DocumentGateway(props: { documentsService.setLocation(cliDoc.uri); }; + const renderFormControls = useMemo( + () => makeRenderFormControlsFromDefaultPort(defaultPort), + [defaultPort] + ); + if (!connected) { return ( ); @@ -92,3 +102,16 @@ export function DocumentGateway(props: { ); } + +export const formSchema = z.object({ [FormFields.LocalPort]: z.string() }); + +export const makeRenderFormControlsFromDefaultPort = + (defaultPort: string) => (isProcessing: boolean) => ( + + ); diff --git a/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts b/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts index 743667ebfa662..8efac1231fa1a 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts +++ b/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts @@ -30,7 +30,7 @@ import { retryWithRelogin } from 'teleterm/ui/utils'; export function useGateway(doc: DocumentGateway) { const ctx = useAppContext(); - const { clustersService } = ctx; + const { clustersService, usageService } = ctx; const { documentsService } = useWorkspaceContext(); // The port to show as default in the input field in case creating a gateway fails. // This is typically the case if someone reopens the app and the port of the gateway is already @@ -46,51 +46,63 @@ export function useGateway(doc: DocumentGateway) { ); const connected = !!gateway; - const [connectAttempt, createGateway] = useAsync(async (port: string) => { - documentsService.update(doc.uri, { status: 'connecting' }); - let gw: Gateway; + const [connectAttempt, createGateway] = useAsync( + useCallback( + async (args: { localPort?: string; targetSubresourceName?: string }) => { + documentsService.update(doc.uri, { status: 'connecting' }); + let gw: Gateway; - try { - gw = await retryWithRelogin(ctx, doc.targetUri, () => - clustersService.createGateway({ - targetUri: doc.targetUri, - localPort: port, - targetUser: doc.targetUser, - targetSubresourceName: doc.targetSubresourceName, - }) - ); - } catch (error) { - documentsService.update(doc.uri, { status: 'error' }); - throw error; - } - documentsService.update(doc.uri, { - gatewayUri: gw.uri, - // Set the port on doc to match the one returned from the daemon. Teleterm doesn't let the - // user provide a port for the gateway, so instead we have to let the daemon use a random - // one. - // - // Setting it here makes it so that on app restart, Teleterm will restart the proxy with the - // same port number. - port: gw.localPort, - status: 'connected', - }); - if (isDatabaseUri(doc.targetUri)) { - ctx.usageService.captureProtocolUse({ - uri: doc.targetUri, - protocol: 'db', - origin: doc.origin, - accessThrough: 'local_proxy', - }); - } - if (isAppUri(doc.targetUri)) { - ctx.usageService.captureProtocolUse({ - uri: doc.targetUri, - protocol: 'app', - origin: doc.origin, - accessThrough: 'local_proxy', - }); - } - }); + try { + gw = await retryWithRelogin(ctx, doc.targetUri, () => + clustersService.createGateway({ + targetUri: doc.targetUri, + localPort: args.localPort, + targetUser: doc.targetUser, + targetSubresourceName: + args.targetSubresourceName || doc.targetSubresourceName, + }) + ); + } catch (error) { + documentsService.update(doc.uri, { status: 'error' }); + throw error; + } + documentsService.update(doc.uri, { + gatewayUri: gw.uri, + // Set the port on doc to match the one returned from the daemon. By default, + // createGateway is called with an empty localPort, so the daemon creates a listener on a + // random port. + // + // Setting it here makes it so that on app restart, Teleterm will restart the proxy with the + // same port number. + // + // Alternatively, if createGateway was called from OfflineGateway, this will persist in + // the doc the local port chosen by the user. + port: gw.localPort, + // targetSubresourceName needs to be updated here in case the createGateway function was + // called from OfflineGateway. + targetSubresourceName: gw.targetSubresourceName, + status: 'connected', + }); + if (isDatabaseUri(doc.targetUri)) { + usageService.captureProtocolUse({ + uri: doc.targetUri, + protocol: 'db', + origin: doc.origin, + accessThrough: 'local_proxy', + }); + } + if (isAppUri(doc.targetUri)) { + usageService.captureProtocolUse({ + uri: doc.targetUri, + protocol: 'app', + origin: doc.origin, + accessThrough: 'local_proxy', + }); + } + }, + [clustersService, ctx, doc, documentsService, usageService] + ) + ); const [disconnectAttempt, disconnect] = useAsync(async () => { await clustersService.removeGateway(doc.gatewayUri); @@ -146,7 +158,7 @@ export function useGateway(doc: DocumentGateway) { // to open DocumentGateway while the gateway is already running. In that scenario, we must // not attempt to create a gateway. if (!gateway && connectAttempt.status === '') { - createGateway(doc.port); + createGateway({ localPort: doc.port }); } }, // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.test.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.test.tsx new file mode 100644 index 0000000000000..91e060966233b --- /dev/null +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.test.tsx @@ -0,0 +1,170 @@ +/** + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import userEvent from '@testing-library/user-event'; + +import { render, screen } from 'design/utils/testing'; +import { Gateway } from 'gen-proto-ts/teleport/lib/teleterm/v1/gateway_pb'; + +import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient'; +import { + makeAppGateway, + makeRootCluster, +} from 'teleterm/services/tshd/testHelpers'; +import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; +import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; +import { MockWorkspaceContextProvider } from 'teleterm/ui/fixtures/MockWorkspaceContextProvider'; +import type * as docs from 'teleterm/ui/services/workspacesService'; +import { AppUri } from 'teleterm/ui/uri'; + +import { DocumentGatewayApp } from './DocumentGatewayApp'; + +beforeEach(() => { + jest.restoreAllMocks(); +}); + +describe('reconnecting when the gateway fails to be created', () => { + const tests: Array<{ + name: string; + gateway: Gateway; + }> = [ + { + name: 'web app', + gateway: makeAppGateway({ + protocol: 'HTTP', + targetSubresourceName: undefined, + }), + }, + { + name: 'single-port TCP app', + gateway: makeAppGateway({ + protocol: 'TCP', + targetSubresourceName: undefined, + }), + }, + { + name: 'multi-port TCP app', + gateway: makeAppGateway({ + protocol: 'TCP', + targetSubresourceName: '1337', + }), + }, + ]; + + test.each(tests)('$name', async ({ gateway }) => { + const user = userEvent.setup(); + + const appContext = new MockAppContext(); + const cluster = makeRootCluster(); + const doc: docs.DocumentGateway = { + uri: '/docs/1', + kind: 'doc.gateway', + targetName: gateway.targetName, + targetUri: gateway.targetUri as AppUri, + targetUser: gateway.targetUser, + targetSubresourceName: gateway.targetSubresourceName, + gatewayUri: gateway.uri, + origin: 'resource_table', + title: '', + status: '', + }; + appContext.addRootClusterWithDoc(cluster, doc); + + jest + .spyOn(appContext.tshd, 'createGateway') + .mockReturnValueOnce( + new MockedUnaryCall(undefined, new Error('Something went wrong')) + ) + .mockReturnValueOnce(new MockedUnaryCall(gateway)); + + render( + + + + + + ); + + expect( + await screen.findByText('Could not establish the connection') + ).toBeInTheDocument(); + + await user.click(screen.getByText('Reconnect')); + + expect(await screen.findByText('Close Connection')).toBeInTheDocument(); + }); + + it('allows changing the target port for multi-port TCP apps', async () => { + const user = userEvent.setup(); + + const appContext = new MockAppContext(); + const cluster = makeRootCluster(); + const gateway = makeAppGateway({ + protocol: 'TCP', + targetSubresourceName: '1337', + }); + const doc: docs.DocumentGateway = { + uri: '/docs/1', + kind: 'doc.gateway', + targetName: gateway.targetName, + targetUri: gateway.targetUri as AppUri, + targetUser: gateway.targetUser, + targetSubresourceName: '1337', + gatewayUri: gateway.uri, + origin: 'resource_table', + title: '', + status: '', + }; + appContext.addRootClusterWithDoc(cluster, doc); + + jest + .spyOn(appContext.tshd, 'createGateway') + .mockReturnValueOnce( + new MockedUnaryCall(undefined, new Error('Something went wrong')) + ) + .mockImplementationOnce( + async req => new MockedUnaryCall({ ...gateway, ...req }) + ); + + render( + + + + + + ); + + expect( + await screen.findByText('Could not establish the connection') + ).toBeInTheDocument(); + + const targetPortInput = screen.getByLabelText('Target Port'); + await user.clear(targetPortInput); + await user.type(targetPortInput, '4242'); + await user.click(screen.getByText('Reconnect')); + + expect(await screen.findByText('Close Connection')).toBeInTheDocument(); + expect(screen.getByLabelText('Target Port')).toHaveValue(4242); + + expect(appContext.tshd.createGateway).toHaveBeenLastCalledWith( + expect.objectContaining({ + targetSubresourceName: '4242', + }) + ); + }); +}); diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx index 24db9f673be64..32f22e90e2af8 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx @@ -15,10 +15,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import { ComponentProps } from 'react'; +import { z } from 'zod'; + import Document from 'teleterm/ui/Document'; import { DocumentGateway } from 'teleterm/ui/services/workspacesService'; -import { OfflineGateway } from '../components/OfflineGateway'; +import { PortFieldInput } from '../components/FieldInputs'; +import { FormFields, OfflineGateway } from '../components/OfflineGateway'; import { useGateway } from '../DocumentGateway/useGateway'; import { AppGateway } from './AppGateway'; @@ -29,6 +33,7 @@ export function DocumentGatewayApp(props: { const { doc } = props; const { gateway, + defaultPort, changePort: changeLocalPort, changePortAttempt: changeLocalPortAttempt, connected, @@ -40,6 +45,19 @@ export function DocumentGatewayApp(props: { changeTargetSubresourceNameAttempt: changeTargetPortAttempt, } = useGateway(doc); + const isMultiPort = !!doc.targetSubresourceName; + // TypeScript doesn't seem to be able to properly infer a simpler construct such as + // + // isMultiPort ? multiPortSchema : singlePortSchema + // + // This code would always infer formSchema to be that of the simpler type (singlePortSchema), so + // any errors in multiPortSchema would not be caught. + let formSchema: ComponentProps['formSchema'] = + singlePortSchema; + if (isMultiPort) { + formSchema = multiPortSchema; + } + return ( {!connected ? ( @@ -47,9 +65,29 @@ export function DocumentGatewayApp(props: { connectAttempt={connectAttempt} gatewayKind="app" targetName={doc.targetName} - gatewayPort={{ isSupported: true, defaultPort: doc.port }} reconnect={reconnect} - portFieldLabel="Local Port (optional)" + formSchema={formSchema} + renderFormControls={(isProcessing: boolean) => ( + <> + + {isMultiPort && ( + + )} + + )} /> ) : ( ); } + +const singlePortSchema = z.object({ [FormFields.LocalPort]: z.string() }); +const multiPortSchema = singlePortSchema.extend({ + [FormFields.TargetSubresourceName]: z.string(), +}); diff --git a/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.test.tsx b/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.test.tsx new file mode 100644 index 0000000000000..a673c316a3f00 --- /dev/null +++ b/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.test.tsx @@ -0,0 +1,84 @@ +/** + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'jest-canvas-mock'; + +import userEvent from '@testing-library/user-event'; + +import { render, screen } from 'design/utils/testing'; + +import Logger, { NullService } from 'teleterm/logger'; +import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient'; +import { + makeKubeGateway, + makeRootCluster, +} from 'teleterm/services/tshd/testHelpers'; +import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; +import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; +import type * as docs from 'teleterm/ui/services/workspacesService'; +import { KubeUri, routing } from 'teleterm/ui/uri'; + +import { MockWorkspaceContextProvider } from '../fixtures/MockWorkspaceContextProvider'; +import { DocumentGatewayKube } from './DocumentGatewayKube'; + +beforeAll(() => { + Logger.init(new NullService()); +}); + +test('it allows reconnecting when the gateway fails to be created', async () => { + const user = userEvent.setup(); + + const appContext = new MockAppContext(); + const cluster = makeRootCluster(); + const gateway = makeKubeGateway(); + const doc: docs.DocumentGatewayKube = { + uri: '/docs/1', + kind: 'doc.gateway_kube', + targetUri: gateway.targetUri as KubeUri, + rootClusterId: routing.parseClusterUri(cluster.uri).params['rootClusterId'], + leafClusterId: undefined, + origin: 'resource_table', + title: '', + status: '', + }; + appContext.addRootClusterWithDoc(cluster, doc); + + jest + .spyOn(appContext.tshd, 'createGateway') + .mockReturnValueOnce( + new MockedUnaryCall(undefined, new Error('Something went wrong')) + ) + .mockReturnValueOnce(new MockedUnaryCall(gateway)); + + render( + + + + + + ); + + expect( + await screen.findByText('Could not establish the connection') + ).toBeInTheDocument(); + + await user.click(screen.getByText('Reconnect')); + + // Verify that the gateway was created by checking if the terminal was rendered. + // "Terminal input" is an ARIA label added by xterm.js. + expect(await screen.findByLabelText('Terminal input')).toBeInTheDocument(); +}); diff --git a/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx b/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx index 8d75b876e389f..f6c98a145049e 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx @@ -28,7 +28,7 @@ import * as types from 'teleterm/ui/services/workspacesService'; import { routing } from 'teleterm/ui/uri'; import { retryWithRelogin } from 'teleterm/ui/utils'; -import { OfflineGateway } from '../components/OfflineGateway'; +import { emptyFormSchema, OfflineGateway } from '../components/OfflineGateway'; /** * DocumentGatewayKube creates a terminal session that presets KUBECONFIG env @@ -96,8 +96,8 @@ export const DocumentGatewayKube = (props: { connectAttempt={connectAttempt} targetName={params.kubeId} gatewayKind="kube" + formSchema={emptyFormSchema} reconnect={createGateway} - gatewayPort={{ isSupported: false }} /> ); diff --git a/web/packages/teleterm/src/ui/components/OfflineGateway.tsx b/web/packages/teleterm/src/ui/components/OfflineGateway.tsx index 2dbc1027565ee..1518154d5c239 100644 --- a/web/packages/teleterm/src/ui/components/OfflineGateway.tsx +++ b/web/packages/teleterm/src/ui/components/OfflineGateway.tsx @@ -16,41 +16,72 @@ * along with this program. If not, see . */ -import { useState } from 'react'; +import { FormEvent, ReactNode, useState } from 'react'; +import { z } from 'zod'; import { ButtonPrimary, Flex, H2, Text } from 'design'; import * as Alerts from 'design/Alert'; import Validation from 'shared/components/Validation'; import { Attempt } from 'shared/hooks/useAsync'; -import { PortFieldInput } from './FieldInputs'; +import { useLogger } from 'teleterm/ui/hooks/useLogger'; -export function OfflineGateway(props: { +export function OfflineGateway< + FormFieldsT extends Partial>, +>(props: { connectAttempt: Attempt; - /** Setting `isSupported` to false hides the port input. */ - gatewayPort: - | { isSupported: true; defaultPort: string } - | { isSupported: false }; - reconnect(port?: string): void; + reconnect(args: FormFieldsT): void; /** Gateway target displayed in the UI, for example, 'cockroachdb'. */ targetName: string; /** Gateway kind displayed in the UI, for example, 'database'. */ gatewayKind: string; - portFieldLabel?: string; + /** + * Each callsite is expected to pass its own formSchema that parses form data from controls passed + * through renderFormControls. If the callsite doesn't pass any form data, it's expected to use + * emptyFormSchema. We cannot do params.formSchema || emptyFormSchema, as that would mess with + * type inference. + */ + formSchema: z.ZodType; + /** + * renderFormControls allows each consumer to provide its own form fields with specific HTML form + * validation rules. The form fields are read through FormData – names on the inputs must match + * names available through the FormFields enum. + */ + renderFormControls?: (isProcessing: boolean) => ReactNode; }) { - const portFieldLabel = props.portFieldLabel || 'Port (optional)'; - const defaultPort = props.gatewayPort.isSupported - ? props.gatewayPort.defaultPort - : undefined; + const logger = useLogger('OfflineGateway'); + const { reconnect } = props; - const [port, setPort] = useState(defaultPort); const [reconnectRequested, setReconnectRequested] = useState(false); + const [parseError, setParseError] = useState(''); const isProcessing = props.connectAttempt.status === 'processing'; const statusDescription = isProcessing ? 'being set up…' : 'offline.'; const shouldShowReconnectControls = props.connectAttempt.status === 'error' || reconnectRequested; + const submitForm = (event: FormEvent) => { + event.preventDefault(); + setReconnectRequested(true); + setParseError(''); + + const formData = new FormData(event.currentTarget); + const parseResult = props.formSchema.safeParse( + Object.fromEntries(formData.entries()) + ); + + // Explicitly compare to false to make type inference work since strictNullChecks are off. + if (parseResult.success === false) { + // There's no need to show validation errors in the UI, since they come from a programmer + // error, not user inputting actually invalid data. + logger.error('Could not parse form', parseResult.error); + setParseError(`Could not submit form. See logs for more details.`); + return; + } + + reconnect(parseResult.data); + }; + return ( )} + {!!parseError && ( + + Form validation error + + )} { - e.preventDefault(); - setReconnectRequested(true); - props.reconnect(props.gatewayPort.isSupported ? port : undefined); - }} + onSubmit={submitForm} alignItems="flex-end" flexWrap="wrap" justifyContent="space-between" @@ -87,16 +119,11 @@ export function OfflineGateway(props: { > {shouldShowReconnectControls && ( <> - {props.gatewayPort.isSupported && ( - - setPort(e.target.value)} - /> - + {props.renderFormControls && ( + // Form controls are expected to use HTML validation instead of our Validation, but + // PortFieldInput is written in a way where it expects the context provided by + // Validation to be present, no matter whether it's used or not. + {props.renderFormControls(isProcessing)} )} Reconnect @@ -107,3 +134,15 @@ export function OfflineGateway(props: { ); } + +export enum FormFields { + LocalPort = 'localPort', + TargetSubresourceName = 'targetSubresourceName', +} +type FormFieldNames = `${FormFields}`; + +/** + * emptyFormSchema is useful in situations where the callsite that uses OfflineGateway has no form + * fields to show. + */ +export const emptyFormSchema = z.object({}); diff --git a/web/packages/teleterm/src/ui/fixtures/mocks.ts b/web/packages/teleterm/src/ui/fixtures/mocks.ts index e2c6d9d0a6985..fad0cd858d520 100644 --- a/web/packages/teleterm/src/ui/fixtures/mocks.ts +++ b/web/packages/teleterm/src/ui/fixtures/mocks.ts @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb'; + import { MockMainProcessClient } from 'teleterm/mainProcess/fixtures/mocks'; import { MockPtyServiceClient } from 'teleterm/services/pty/fixtures/mocks'; import { @@ -24,6 +26,7 @@ import { } from 'teleterm/services/tshd/fixtures/mocks'; import { RuntimeSettings } from 'teleterm/types'; import AppContext from 'teleterm/ui/appContext'; +import { Document } from 'teleterm/ui/services/workspacesService'; export class MockAppContext extends AppContext { constructor(runtimeSettings?: Partial) { @@ -41,4 +44,19 @@ export class MockAppContext extends AppContext { getPathForFile: () => '', }); } + + addRootClusterWithDoc(cluster: Cluster, doc: Document) { + this.clustersService.setState(draftState => { + draftState.clusters.set(cluster.uri, cluster); + }); + this.workspacesService.setState(draftState => { + draftState.rootClusterUri = cluster.uri; + draftState.workspaces[cluster.uri] = { + documents: [doc], + location: doc.uri, + localClusterUri: cluster.uri, + accessRequests: undefined, + }; + }); + } } diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts index e975d8e268ae7..c767a8efbddfe 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts @@ -112,6 +112,8 @@ export interface DocumentGateway extends DocumentBase { /** * targetSubresourceName contains database name for db gateways and target port for TCP app * gateways. + * A DocumentGateway created for a multi-port TCP app is expected to always have this field + * present. */ targetSubresourceName: string | undefined; port?: string; From 801aec3d32ba9d24fde279b29f1e12b93d8cce85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Thu, 16 Jan 2025 11:00:20 +0100 Subject: [PATCH 4/6] Show available target ports in gateways for multi-port apps --- .../go/teleport/lib/teleterm/v1/app.pb.go | 6 +- .../go/teleport/lib/teleterm/v1/service.pb.go | 1055 +++++++++-------- .../lib/teleterm/v1/service_grpc.pb.go | 42 + .../ts/teleport/lib/teleterm/v1/app_pb.ts | 6 +- .../lib/teleterm/v1/service_pb.client.ts | 19 + .../lib/teleterm/v1/service_pb.grpc-server.ts | 19 + .../ts/teleport/lib/teleterm/v1/service_pb.ts | 114 +- .../apiserver/handler/handler_apps.go | 35 + proto/teleport/lib/teleterm/v1/app.proto | 6 +- proto/teleport/lib/teleterm/v1/service.proto | 12 + .../src/services/tshd/fixtures/mocks.ts | 2 + .../src/ui/DocumentGatewayApp/AppGateway.tsx | 175 ++- .../DocumentGatewayApp.story.tsx | 42 +- .../DocumentGatewayApp/DocumentGatewayApp.tsx | 33 +- 14 files changed, 1018 insertions(+), 548 deletions(-) diff --git a/gen/proto/go/teleport/lib/teleterm/v1/app.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/app.pb.go index 1f0eef3891b2a..5c981aa3bd1d4 100644 --- a/gen/proto/go/teleport/lib/teleterm/v1/app.pb.go +++ b/gen/proto/go/teleport/lib/teleterm/v1/app.pb.go @@ -92,9 +92,11 @@ type App struct { // proxy hostname] if public_addr is not present. // If the app belongs to a leaf cluster, fqdn is equal to [name].[root cluster proxy hostname]. // - // fqdn is not present for SAML applications. + // fqdn is not present for SAML applications. Available only when the app was fetched through the + // ListUnifiedResources RPC. Fqdn string `protobuf:"bytes,10,opt,name=fqdn,proto3" json:"fqdn,omitempty"` - // aws_roles is a list of AWS IAM roles for the application representing AWS console. + // aws_roles is a list of AWS IAM roles for the application representing AWS console. Available + // only when the app wast fetched through the ListUnifiedResources RPC. AwsRoles []*AWSRole `protobuf:"bytes,11,rep,name=aws_roles,json=awsRoles,proto3" json:"aws_roles,omitempty"` // TCPPorts is a list of ports and port ranges that an app agent can forward connections to. // Only applicable to TCP App Access. diff --git a/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go index decb858b1d2ef..33bc0bb10b906 100644 --- a/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go +++ b/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go @@ -4035,6 +4035,94 @@ func (x *AuthenticateWebDeviceResponse) GetConfirmationToken() *v12.DeviceConfir return nil } +type GetAppRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + AppUri string `protobuf:"bytes,1,opt,name=app_uri,json=appUri,proto3" json:"app_uri,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAppRequest) Reset() { + *x = GetAppRequest{} + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[70] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAppRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAppRequest) ProtoMessage() {} + +func (x *GetAppRequest) ProtoReflect() protoreflect.Message { + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[70] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAppRequest.ProtoReflect.Descriptor instead. +func (*GetAppRequest) Descriptor() ([]byte, []int) { + return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{70} +} + +func (x *GetAppRequest) GetAppUri() string { + if x != nil { + return x.AppUri + } + return "" +} + +type GetAppResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + App *App `protobuf:"bytes,1,opt,name=app,proto3" json:"app,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAppResponse) Reset() { + *x = GetAppResponse{} + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[71] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAppResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAppResponse) ProtoMessage() {} + +func (x *GetAppResponse) ProtoReflect() protoreflect.Message { + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[71] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAppResponse.ProtoReflect.Descriptor instead. +func (*GetAppResponse) Descriptor() ([]byte, []int) { + return file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP(), []int{71} +} + +func (x *GetAppResponse) GetApp() *App { + if x != nil { + return x.App + } + return nil +} + // LoginPasswordlessRequestInit contains fields needed to init the stream request. type LoginPasswordlessRequest_LoginPasswordlessRequestInit struct { state protoimpl.MessageState @@ -4047,7 +4135,7 @@ type LoginPasswordlessRequest_LoginPasswordlessRequestInit struct { func (x *LoginPasswordlessRequest_LoginPasswordlessRequestInit) Reset() { *x = LoginPasswordlessRequest_LoginPasswordlessRequestInit{} - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[70] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4059,7 +4147,7 @@ func (x *LoginPasswordlessRequest_LoginPasswordlessRequestInit) String() string func (*LoginPasswordlessRequest_LoginPasswordlessRequestInit) ProtoMessage() {} func (x *LoginPasswordlessRequest_LoginPasswordlessRequestInit) ProtoReflect() protoreflect.Message { - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[70] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4094,7 +4182,7 @@ type LoginPasswordlessRequest_LoginPasswordlessPINResponse struct { func (x *LoginPasswordlessRequest_LoginPasswordlessPINResponse) Reset() { *x = LoginPasswordlessRequest_LoginPasswordlessPINResponse{} - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[71] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4106,7 +4194,7 @@ func (x *LoginPasswordlessRequest_LoginPasswordlessPINResponse) String() string func (*LoginPasswordlessRequest_LoginPasswordlessPINResponse) ProtoMessage() {} func (x *LoginPasswordlessRequest_LoginPasswordlessPINResponse) ProtoReflect() protoreflect.Message { - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[71] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4143,7 +4231,7 @@ type LoginPasswordlessRequest_LoginPasswordlessCredentialResponse struct { func (x *LoginPasswordlessRequest_LoginPasswordlessCredentialResponse) Reset() { *x = LoginPasswordlessRequest_LoginPasswordlessCredentialResponse{} - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[72] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4155,7 +4243,7 @@ func (x *LoginPasswordlessRequest_LoginPasswordlessCredentialResponse) String() func (*LoginPasswordlessRequest_LoginPasswordlessCredentialResponse) ProtoMessage() {} func (x *LoginPasswordlessRequest_LoginPasswordlessCredentialResponse) ProtoReflect() protoreflect.Message { - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[72] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4194,7 +4282,7 @@ type LoginRequest_LocalParams struct { func (x *LoginRequest_LocalParams) Reset() { *x = LoginRequest_LocalParams{} - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[73] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4206,7 +4294,7 @@ func (x *LoginRequest_LocalParams) String() string { func (*LoginRequest_LocalParams) ProtoMessage() {} func (x *LoginRequest_LocalParams) ProtoReflect() protoreflect.Message { - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[73] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4257,7 +4345,7 @@ type LoginRequest_SsoParams struct { func (x *LoginRequest_SsoParams) Reset() { *x = LoginRequest_SsoParams{} - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[74] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4269,7 +4357,7 @@ func (x *LoginRequest_SsoParams) String() string { func (*LoginRequest_SsoParams) ProtoMessage() {} func (x *LoginRequest_SsoParams) ProtoReflect() protoreflect.Message { - mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[74] + mi := &file_teleport_lib_teleterm_v1_service_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4897,365 +4985,377 @@ var file_teleport_lib_teleterm_v1_service_proto_rawDesc = []byte{ 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x2a, 0x97, 0x01, 0x0a, 0x12, 0x50, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x50, 0x72, 0x6f, 0x6d, 0x70, - 0x74, 0x12, 0x23, 0x0a, 0x1f, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x4c, 0x45, 0x53, - 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x4d, 0x50, 0x54, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, - 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, - 0x52, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x4d, 0x50, 0x54, 0x5f, 0x50, 0x49, - 0x4e, 0x10, 0x01, 0x12, 0x1b, 0x0a, 0x17, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x4c, - 0x45, 0x53, 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x4d, 0x50, 0x54, 0x5f, 0x54, 0x41, 0x50, 0x10, 0x02, - 0x12, 0x22, 0x0a, 0x1e, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x4c, 0x45, 0x53, 0x53, - 0x5f, 0x50, 0x52, 0x4f, 0x4d, 0x50, 0x54, 0x5f, 0x43, 0x52, 0x45, 0x44, 0x45, 0x4e, 0x54, 0x49, - 0x41, 0x4c, 0x10, 0x03, 0x2a, 0x8a, 0x01, 0x0a, 0x15, 0x46, 0x69, 0x6c, 0x65, 0x54, 0x72, 0x61, - 0x6e, 0x73, 0x66, 0x65, 0x72, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, - 0x0a, 0x23, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, 0x52, 0x5f, - 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x24, 0x0a, 0x20, 0x46, 0x49, 0x4c, 0x45, 0x5f, - 0x54, 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, 0x52, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, - 0x4f, 0x4e, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, 0x10, 0x01, 0x12, 0x22, 0x0a, - 0x1e, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, 0x52, 0x5f, 0x44, - 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x50, 0x4c, 0x4f, 0x41, 0x44, 0x10, - 0x02, 0x2a, 0xcd, 0x01, 0x0a, 0x1b, 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x41, 0x75, - 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x2d, 0x0a, 0x29, 0x48, 0x45, 0x41, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x41, 0x55, - 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, - 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x29, 0x0a, 0x25, 0x48, 0x45, 0x41, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x41, 0x55, 0x54, - 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, - 0x45, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x28, 0x0a, 0x24, 0x48, - 0x45, 0x41, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, - 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x45, 0x4e, - 0x49, 0x45, 0x44, 0x10, 0x02, 0x12, 0x2a, 0x0a, 0x26, 0x48, 0x45, 0x41, 0x44, 0x4c, 0x45, 0x53, - 0x53, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, - 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x41, 0x50, 0x50, 0x52, 0x4f, 0x56, 0x45, 0x44, 0x10, - 0x03, 0x32, 0x81, 0x28, 0x0a, 0x0f, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xa0, 0x01, 0x0a, 0x1d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x54, 0x73, 0x68, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x3e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, - 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x73, 0x68, 0x64, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3f, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, - 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x73, 0x68, 0x64, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x71, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, - 0x52, 0x6f, 0x6f, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x74, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x28, 0x0a, 0x0d, 0x47, 0x65, + 0x74, 0x41, 0x70, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x61, + 0x70, 0x70, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x70, + 0x70, 0x55, 0x72, 0x69, 0x22, 0x41, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x41, 0x70, 0x70, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x03, 0x61, 0x70, 0x70, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, + 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, + 0x70, 0x70, 0x52, 0x03, 0x61, 0x70, 0x70, 0x2a, 0x97, 0x01, 0x0a, 0x12, 0x50, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x23, + 0x0a, 0x1f, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x50, + 0x52, 0x4f, 0x4d, 0x50, 0x54, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x4c, + 0x45, 0x53, 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x4d, 0x50, 0x54, 0x5f, 0x50, 0x49, 0x4e, 0x10, 0x01, + 0x12, 0x1b, 0x0a, 0x17, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x4c, 0x45, 0x53, 0x53, + 0x5f, 0x50, 0x52, 0x4f, 0x4d, 0x50, 0x54, 0x5f, 0x54, 0x41, 0x50, 0x10, 0x02, 0x12, 0x22, 0x0a, + 0x1e, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x50, 0x52, + 0x4f, 0x4d, 0x50, 0x54, 0x5f, 0x43, 0x52, 0x45, 0x44, 0x45, 0x4e, 0x54, 0x49, 0x41, 0x4c, 0x10, + 0x03, 0x2a, 0x8a, 0x01, 0x0a, 0x15, 0x46, 0x69, 0x6c, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, + 0x65, 0x72, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x23, 0x46, + 0x49, 0x4c, 0x45, 0x5f, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, 0x52, 0x5f, 0x44, 0x49, 0x52, + 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, + 0x45, 0x44, 0x10, 0x00, 0x12, 0x24, 0x0a, 0x20, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x54, 0x52, 0x41, + 0x4e, 0x53, 0x46, 0x45, 0x52, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, + 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, 0x10, 0x01, 0x12, 0x22, 0x0a, 0x1e, 0x46, 0x49, + 0x4c, 0x45, 0x5f, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, 0x52, 0x5f, 0x44, 0x49, 0x52, 0x45, + 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x50, 0x4c, 0x4f, 0x41, 0x44, 0x10, 0x02, 0x2a, 0xcd, + 0x01, 0x0a, 0x1b, 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x41, 0x75, 0x74, 0x68, 0x65, + 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2d, + 0x0a, 0x29, 0x48, 0x45, 0x41, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x45, + 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x29, 0x0a, + 0x25, 0x48, 0x45, 0x41, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, + 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, + 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x28, 0x0a, 0x24, 0x48, 0x45, 0x41, 0x44, + 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, + 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x45, 0x4e, 0x49, 0x45, 0x44, + 0x10, 0x02, 0x12, 0x2a, 0x0a, 0x26, 0x48, 0x45, 0x41, 0x44, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x41, + 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, + 0x41, 0x54, 0x45, 0x5f, 0x41, 0x50, 0x50, 0x52, 0x4f, 0x56, 0x45, 0x44, 0x10, 0x03, 0x32, 0xde, + 0x28, 0x0a, 0x0f, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0xa0, 0x01, 0x0a, 0x1d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x73, 0x68, + 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x12, 0x3e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x73, 0x68, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3f, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x73, 0x68, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x71, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x6f, + 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, + 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, + 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, + 0x4c, 0x65, 0x61, 0x66, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x12, 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6c, 0x75, 0x73, - 0x74, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, - 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x10, 0x4c, - 0x69, 0x73, 0x74, 0x4c, 0x65, 0x61, 0x66, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x12, - 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, - 0x65, 0x61, 0x66, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, - 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x14, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x65, 0x61, 0x64, - 0x6c, 0x65, 0x73, 0x73, 0x57, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x12, 0x35, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x65, 0x61, 0x64, - 0x6c, 0x65, 0x73, 0x73, 0x57, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, - 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, - 0x61, 0x72, 0x74, 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x57, 0x61, 0x74, 0x63, 0x68, - 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7c, 0x0a, 0x11, 0x4c, 0x69, - 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, - 0x32, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x44, - 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, - 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x2b, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, - 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, + 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x65, 0x61, 0x66, + 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, + 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x85, 0x01, 0x0a, 0x14, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, + 0x73, 0x57, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x12, 0x35, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, + 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, + 0x73, 0x57, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, + 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x57, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7c, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x44, + 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x32, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, + 0x62, 0x61, 0x73, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, + 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x73, 0x12, 0x2b, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0x7c, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x32, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x79, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x2c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, + 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, + 0x88, 0x02, 0x01, 0x12, 0x7c, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x32, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x74, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, - 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, - 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, + 0x75, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, - 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x13, 0x52, - 0x65, 0x76, 0x69, 0x65, 0x77, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, - 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, - 0x76, 0x69, 0x65, 0x77, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, - 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x82, 0x01, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x61, 0x62, - 0x6c, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x61, 0x62, 0x6c, - 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0a, 0x41, 0x73, 0x73, 0x75, 0x6d, 0x65, 0x52, 0x6f, - 0x6c, 0x65, 0x12, 0x2b, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, - 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73, - 0x73, 0x75, 0x6d, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x14, 0x50, 0x72, 0x6f, - 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x35, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, - 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, - 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x79, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, - 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x8e, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x65, - 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x12, 0x38, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x67, 0x67, 0x65, - 0x73, 0x74, 0x65, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, - 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x65, 0x64, 0x41, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x8e, 0x01, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x75, 0x62, 0x65, 0x72, 0x6e, - 0x65, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x38, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x75, 0x62, - 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, - 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, - 0x73, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x5c, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, - 0x12, 0x2b, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x43, - 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, - 0x12, 0x68, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, - 0x72, 0x12, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, - 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, - 0x6f, 0x76, 0x65, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, - 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6d, 0x0a, 0x0c, 0x4c, 0x69, - 0x73, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x12, 0x2d, 0x2e, 0x74, 0x65, 0x6c, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x74, 0x0a, 0x13, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, + 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, + 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, - 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x35, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, + 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x13, 0x52, 0x65, 0x76, 0x69, + 0x65, 0x77, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, + 0x77, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, + 0x13, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, + 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, + 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x62, 0x0a, 0x0a, 0x41, 0x73, 0x73, 0x75, 0x6d, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x12, + 0x2b, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73, 0x73, 0x75, 0x6d, + 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x6d, 0x6f, 0x74, + 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, + 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6d, 0x6f, 0x74, + 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, + 0x2e, 0x50, 0x72, 0x6f, 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8e, 0x01, + 0x0a, 0x17, 0x47, 0x65, 0x74, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x65, 0x64, 0x41, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x12, 0x38, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, - 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0d, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x2e, 0x2e, 0x74, 0x65, 0x6c, + 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x65, + 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, + 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, + 0x65, 0x74, 0x53, 0x75, 0x67, 0x67, 0x65, 0x73, 0x74, 0x65, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8e, + 0x01, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, + 0x73, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x38, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x47, 0x61, 0x74, 0x65, - 0x77, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x74, 0x65, 0x6c, + 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x75, 0x62, 0x65, 0x72, 0x6e, + 0x65, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, 0x73, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x5c, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x2b, 0x2e, + 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x43, 0x6c, 0x75, 0x73, + 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x68, 0x0a, - 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x2e, + 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x68, 0x0a, + 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, - 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, + 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x86, 0x01, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x47, - 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x75, 0x62, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, - 0x79, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, - 0x12, 0x6e, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4c, 0x6f, - 0x63, 0x61, 0x6c, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6d, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x47, + 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x12, 0x2d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, - 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4c, 0x6f, 0x63, - 0x61, 0x6c, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, - 0x12, 0x6b, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x74, 0x74, 0x69, - 0x6e, 0x67, 0x73, 0x12, 0x30, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, - 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, - 0x2e, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x5c, 0x0a, - 0x0a, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x2b, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, - 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, - 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, - 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x58, 0x0a, 0x05, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x26, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, - 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x80, 0x01, 0x0a, 0x11, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x12, 0x32, 0x2e, 0x74, 0x65, + 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, + 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x68, 0x0a, 0x0d, 0x52, 0x65, + 0x6d, 0x6f, 0x76, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, - 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x12, 0x5a, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, - 0x75, 0x74, 0x12, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, - 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, - 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x65, + 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x47, 0x61, 0x74, + 0x65, 0x77, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x0c, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, - 0x46, 0x69, 0x6c, 0x65, 0x12, 0x2d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x86, 0x01, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x47, 0x61, 0x74, 0x65, + 0x77, 0x61, 0x79, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, + 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x53, 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4e, + 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x6e, 0x0a, + 0x13, 0x53, 0x65, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4c, 0x6f, 0x63, 0x61, 0x6c, + 0x50, 0x6f, 0x72, 0x74, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, - 0x46, 0x69, 0x6c, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, - 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x46, - 0x69, 0x6c, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x67, 0x72, - 0x65, 0x73, 0x73, 0x30, 0x01, 0x12, 0x6e, 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x55, - 0x73, 0x61, 0x67, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x53, 0x65, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, + 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x6b, 0x0a, + 0x0f, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, + 0x12, 0x30, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, + 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, + 0x75, 0x74, 0x68, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, + 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, + 0x74, 0x68, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x5c, 0x0a, 0x0a, 0x47, 0x65, + 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x2b, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x58, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x12, 0x26, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, + 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, - 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, + 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x80, 0x01, 0x0a, 0x11, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x12, 0x32, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, + 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x6c, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xac, 0x01, 0x0a, 0x21, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x42, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x65, 0x61, + 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x28, 0x01, 0x30, 0x01, 0x12, 0x5a, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, + 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, + 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x6f, 0x0a, 0x0c, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x46, 0x69, 0x6c, + 0x65, 0x12, 0x2d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, + 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6c, + 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, + 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6c, 0x65, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, + 0x30, 0x01, 0x12, 0x6e, 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x55, 0x73, 0x61, 0x67, + 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, + 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, + 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0xac, 0x01, 0x0a, 0x21, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x43, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9a, 0x01, 0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, - 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x3c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, - 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, - 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x3d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, - 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, - 0x70, 0x75, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0xa9, 0x01, 0x0a, 0x20, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, - 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x41, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x42, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, + 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x65, 0x61, 0x64, 0x6c, 0x65, + 0x73, 0x73, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x43, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x65, + 0x61, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x9a, 0x01, 0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x6c, + 0x65, 0x12, 0x3c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, + 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, + 0x75, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x3d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, + 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xa9, + 0x01, 0x0a, 0x20, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x41, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, + 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, + 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x42, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x42, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, - 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xa9, 0x01, - 0x0a, 0x20, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4a, 0x6f, - 0x69, 0x6e, 0x12, 0x41, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, + 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xa9, 0x01, 0x0a, 0x20, 0x57, + 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, + 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4a, 0x6f, 0x69, 0x6e, 0x12, + 0x41, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x46, + 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, + 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4a, 0x6f, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x42, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4a, 0x6f, 0x69, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x42, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, - 0x2e, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, - 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4a, 0x6f, 0x69, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9a, 0x01, 0x0a, 0x1b, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, - 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x3c, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, - 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9a, 0x01, 0x0a, 0x1b, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, + 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x3c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, + 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, + 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, + 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x9d, 0x01, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, - 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x9d, 0x01, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x43, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, - 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, - 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, + 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x3e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, + 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, + 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x75, + 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x6e, 0x69, 0x66, + 0x69, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x35, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x6e, 0x69, 0x66, + 0x69, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, + 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x55, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7f, 0x0a, 0x12, 0x47, + 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, + 0x73, 0x12, 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, + 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, + 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x4d, 0x79, 0x43, 0x6f, - 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x55, - 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, - 0x35, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, - 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, + 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, + 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, + 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, - 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7f, - 0x0a, 0x12, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, - 0x6e, 0x63, 0x65, 0x73, 0x12, 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, - 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, - 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, - 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, - 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, - 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x88, 0x01, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, - 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, + 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, + 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, + 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x12, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, + 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, + 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, - 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, - 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x37, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, - 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, - 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x15, 0x41, - 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, - 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, - 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x74, + 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x57, 0x65, 0x62, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x06, 0x47, 0x65, 0x74, 0x41, 0x70, 0x70, 0x12, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x54, 0x5a, 0x52, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, - 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x2f, 0x6c, 0x69, 0x62, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2f, 0x76, 0x31, - 0x3b, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x70, 0x70, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x41, 0x70, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, + 0x54, 0x5a, 0x52, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, + 0x61, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, + 0x6f, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x6c, 0x69, 0x62, 0x2f, 0x74, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2f, 0x76, 0x31, 0x3b, 0x74, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x72, 0x6d, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -5271,7 +5371,7 @@ func file_teleport_lib_teleterm_v1_service_proto_rawDescGZIP() []byte { } var file_teleport_lib_teleterm_v1_service_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_teleport_lib_teleterm_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 75) +var file_teleport_lib_teleterm_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 77) var file_teleport_lib_teleterm_v1_service_proto_goTypes = []any{ (PasswordlessPrompt)(0), // 0: teleport.lib.teleterm.v1.PasswordlessPrompt (FileTransferDirection)(0), // 1: teleport.lib.teleterm.v1.FileTransferDirection @@ -5346,154 +5446,159 @@ var file_teleport_lib_teleterm_v1_service_proto_goTypes = []any{ (*UserPreferences)(nil), // 70: teleport.lib.teleterm.v1.UserPreferences (*AuthenticateWebDeviceRequest)(nil), // 71: teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest (*AuthenticateWebDeviceResponse)(nil), // 72: teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse - (*LoginPasswordlessRequest_LoginPasswordlessRequestInit)(nil), // 73: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessRequestInit - (*LoginPasswordlessRequest_LoginPasswordlessPINResponse)(nil), // 74: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessPINResponse - (*LoginPasswordlessRequest_LoginPasswordlessCredentialResponse)(nil), // 75: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessCredentialResponse - (*LoginRequest_LocalParams)(nil), // 76: teleport.lib.teleterm.v1.LoginRequest.LocalParams - (*LoginRequest_SsoParams)(nil), // 77: teleport.lib.teleterm.v1.LoginRequest.SsoParams - (*AccessRequest)(nil), // 78: teleport.lib.teleterm.v1.AccessRequest - (*ResourceID)(nil), // 79: teleport.lib.teleterm.v1.ResourceID - (*timestamppb.Timestamp)(nil), // 80: google.protobuf.Timestamp - (*v1.AccessList)(nil), // 81: teleport.accesslist.v1.AccessList - (*KubeResource)(nil), // 82: teleport.lib.teleterm.v1.KubeResource - (*Cluster)(nil), // 83: teleport.lib.teleterm.v1.Cluster - (*Gateway)(nil), // 84: teleport.lib.teleterm.v1.Gateway - (*Server)(nil), // 85: teleport.lib.teleterm.v1.Server - (*Database)(nil), // 86: teleport.lib.teleterm.v1.Database - (*Kube)(nil), // 87: teleport.lib.teleterm.v1.Kube - (*App)(nil), // 88: teleport.lib.teleterm.v1.App - (*v11.ClusterUserPreferences)(nil), // 89: teleport.userpreferences.v1.ClusterUserPreferences - (*v11.UnifiedResourcePreferences)(nil), // 90: teleport.userpreferences.v1.UnifiedResourcePreferences - (*v12.DeviceWebToken)(nil), // 91: teleport.devicetrust.v1.DeviceWebToken - (*v12.DeviceConfirmationToken)(nil), // 92: teleport.devicetrust.v1.DeviceConfirmationToken - (*ReportUsageEventRequest)(nil), // 93: teleport.lib.teleterm.v1.ReportUsageEventRequest - (*AuthSettings)(nil), // 94: teleport.lib.teleterm.v1.AuthSettings + (*GetAppRequest)(nil), // 73: teleport.lib.teleterm.v1.GetAppRequest + (*GetAppResponse)(nil), // 74: teleport.lib.teleterm.v1.GetAppResponse + (*LoginPasswordlessRequest_LoginPasswordlessRequestInit)(nil), // 75: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessRequestInit + (*LoginPasswordlessRequest_LoginPasswordlessPINResponse)(nil), // 76: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessPINResponse + (*LoginPasswordlessRequest_LoginPasswordlessCredentialResponse)(nil), // 77: teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessCredentialResponse + (*LoginRequest_LocalParams)(nil), // 78: teleport.lib.teleterm.v1.LoginRequest.LocalParams + (*LoginRequest_SsoParams)(nil), // 79: teleport.lib.teleterm.v1.LoginRequest.SsoParams + (*AccessRequest)(nil), // 80: teleport.lib.teleterm.v1.AccessRequest + (*ResourceID)(nil), // 81: teleport.lib.teleterm.v1.ResourceID + (*timestamppb.Timestamp)(nil), // 82: google.protobuf.Timestamp + (*v1.AccessList)(nil), // 83: teleport.accesslist.v1.AccessList + (*KubeResource)(nil), // 84: teleport.lib.teleterm.v1.KubeResource + (*Cluster)(nil), // 85: teleport.lib.teleterm.v1.Cluster + (*Gateway)(nil), // 86: teleport.lib.teleterm.v1.Gateway + (*Server)(nil), // 87: teleport.lib.teleterm.v1.Server + (*Database)(nil), // 88: teleport.lib.teleterm.v1.Database + (*Kube)(nil), // 89: teleport.lib.teleterm.v1.Kube + (*App)(nil), // 90: teleport.lib.teleterm.v1.App + (*v11.ClusterUserPreferences)(nil), // 91: teleport.userpreferences.v1.ClusterUserPreferences + (*v11.UnifiedResourcePreferences)(nil), // 92: teleport.userpreferences.v1.UnifiedResourcePreferences + (*v12.DeviceWebToken)(nil), // 93: teleport.devicetrust.v1.DeviceWebToken + (*v12.DeviceConfirmationToken)(nil), // 94: teleport.devicetrust.v1.DeviceConfirmationToken + (*ReportUsageEventRequest)(nil), // 95: teleport.lib.teleterm.v1.ReportUsageEventRequest + (*AuthSettings)(nil), // 96: teleport.lib.teleterm.v1.AuthSettings } var file_teleport_lib_teleterm_v1_service_proto_depIdxs = []int32{ - 78, // 0: teleport.lib.teleterm.v1.GetAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest - 78, // 1: teleport.lib.teleterm.v1.GetAccessRequestsResponse.requests:type_name -> teleport.lib.teleterm.v1.AccessRequest - 79, // 2: teleport.lib.teleterm.v1.CreateAccessRequestRequest.resource_ids:type_name -> teleport.lib.teleterm.v1.ResourceID - 80, // 3: teleport.lib.teleterm.v1.CreateAccessRequestRequest.assume_start_time:type_name -> google.protobuf.Timestamp - 80, // 4: teleport.lib.teleterm.v1.CreateAccessRequestRequest.max_duration:type_name -> google.protobuf.Timestamp - 80, // 5: teleport.lib.teleterm.v1.CreateAccessRequestRequest.request_ttl:type_name -> google.protobuf.Timestamp - 78, // 6: teleport.lib.teleterm.v1.CreateAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest - 79, // 7: teleport.lib.teleterm.v1.GetRequestableRolesRequest.resource_ids:type_name -> teleport.lib.teleterm.v1.ResourceID - 80, // 8: teleport.lib.teleterm.v1.ReviewAccessRequestRequest.assume_start_time:type_name -> google.protobuf.Timestamp - 78, // 9: teleport.lib.teleterm.v1.ReviewAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest - 78, // 10: teleport.lib.teleterm.v1.PromoteAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest - 81, // 11: teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse.access_lists:type_name -> teleport.accesslist.v1.AccessList - 82, // 12: teleport.lib.teleterm.v1.ListKubernetesResourcesResponse.resources:type_name -> teleport.lib.teleterm.v1.KubeResource + 80, // 0: teleport.lib.teleterm.v1.GetAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest + 80, // 1: teleport.lib.teleterm.v1.GetAccessRequestsResponse.requests:type_name -> teleport.lib.teleterm.v1.AccessRequest + 81, // 2: teleport.lib.teleterm.v1.CreateAccessRequestRequest.resource_ids:type_name -> teleport.lib.teleterm.v1.ResourceID + 82, // 3: teleport.lib.teleterm.v1.CreateAccessRequestRequest.assume_start_time:type_name -> google.protobuf.Timestamp + 82, // 4: teleport.lib.teleterm.v1.CreateAccessRequestRequest.max_duration:type_name -> google.protobuf.Timestamp + 82, // 5: teleport.lib.teleterm.v1.CreateAccessRequestRequest.request_ttl:type_name -> google.protobuf.Timestamp + 80, // 6: teleport.lib.teleterm.v1.CreateAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest + 81, // 7: teleport.lib.teleterm.v1.GetRequestableRolesRequest.resource_ids:type_name -> teleport.lib.teleterm.v1.ResourceID + 82, // 8: teleport.lib.teleterm.v1.ReviewAccessRequestRequest.assume_start_time:type_name -> google.protobuf.Timestamp + 80, // 9: teleport.lib.teleterm.v1.ReviewAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest + 80, // 10: teleport.lib.teleterm.v1.PromoteAccessRequestResponse.request:type_name -> teleport.lib.teleterm.v1.AccessRequest + 83, // 11: teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse.access_lists:type_name -> teleport.accesslist.v1.AccessList + 84, // 12: teleport.lib.teleterm.v1.ListKubernetesResourcesResponse.resources:type_name -> teleport.lib.teleterm.v1.KubeResource 0, // 13: teleport.lib.teleterm.v1.LoginPasswordlessResponse.prompt:type_name -> teleport.lib.teleterm.v1.PasswordlessPrompt 27, // 14: teleport.lib.teleterm.v1.LoginPasswordlessResponse.credentials:type_name -> teleport.lib.teleterm.v1.CredentialInfo - 73, // 15: teleport.lib.teleterm.v1.LoginPasswordlessRequest.init:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessRequestInit - 74, // 16: teleport.lib.teleterm.v1.LoginPasswordlessRequest.pin:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessPINResponse - 75, // 17: teleport.lib.teleterm.v1.LoginPasswordlessRequest.credential:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessCredentialResponse + 75, // 15: teleport.lib.teleterm.v1.LoginPasswordlessRequest.init:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessRequestInit + 76, // 16: teleport.lib.teleterm.v1.LoginPasswordlessRequest.pin:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessPINResponse + 77, // 17: teleport.lib.teleterm.v1.LoginPasswordlessRequest.credential:type_name -> teleport.lib.teleterm.v1.LoginPasswordlessRequest.LoginPasswordlessCredentialResponse 1, // 18: teleport.lib.teleterm.v1.FileTransferRequest.direction:type_name -> teleport.lib.teleterm.v1.FileTransferDirection - 76, // 19: teleport.lib.teleterm.v1.LoginRequest.local:type_name -> teleport.lib.teleterm.v1.LoginRequest.LocalParams - 77, // 20: teleport.lib.teleterm.v1.LoginRequest.sso:type_name -> teleport.lib.teleterm.v1.LoginRequest.SsoParams - 83, // 21: teleport.lib.teleterm.v1.ListClustersResponse.clusters:type_name -> teleport.lib.teleterm.v1.Cluster - 84, // 22: teleport.lib.teleterm.v1.ListGatewaysResponse.gateways:type_name -> teleport.lib.teleterm.v1.Gateway - 85, // 23: teleport.lib.teleterm.v1.GetServersResponse.agents:type_name -> teleport.lib.teleterm.v1.Server + 78, // 19: teleport.lib.teleterm.v1.LoginRequest.local:type_name -> teleport.lib.teleterm.v1.LoginRequest.LocalParams + 79, // 20: teleport.lib.teleterm.v1.LoginRequest.sso:type_name -> teleport.lib.teleterm.v1.LoginRequest.SsoParams + 85, // 21: teleport.lib.teleterm.v1.ListClustersResponse.clusters:type_name -> teleport.lib.teleterm.v1.Cluster + 86, // 22: teleport.lib.teleterm.v1.ListGatewaysResponse.gateways:type_name -> teleport.lib.teleterm.v1.Gateway + 87, // 23: teleport.lib.teleterm.v1.GetServersResponse.agents:type_name -> teleport.lib.teleterm.v1.Server 2, // 24: teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateRequest.state:type_name -> teleport.lib.teleterm.v1.HeadlessAuthenticationState - 85, // 25: teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse.server:type_name -> teleport.lib.teleterm.v1.Server + 87, // 25: teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse.server:type_name -> teleport.lib.teleterm.v1.Server 63, // 26: teleport.lib.teleterm.v1.ListUnifiedResourcesRequest.sort_by:type_name -> teleport.lib.teleterm.v1.SortBy 65, // 27: teleport.lib.teleterm.v1.ListUnifiedResourcesResponse.resources:type_name -> teleport.lib.teleterm.v1.PaginatedResource - 86, // 28: teleport.lib.teleterm.v1.PaginatedResource.database:type_name -> teleport.lib.teleterm.v1.Database - 85, // 29: teleport.lib.teleterm.v1.PaginatedResource.server:type_name -> teleport.lib.teleterm.v1.Server - 87, // 30: teleport.lib.teleterm.v1.PaginatedResource.kube:type_name -> teleport.lib.teleterm.v1.Kube - 88, // 31: teleport.lib.teleterm.v1.PaginatedResource.app:type_name -> teleport.lib.teleterm.v1.App + 88, // 28: teleport.lib.teleterm.v1.PaginatedResource.database:type_name -> teleport.lib.teleterm.v1.Database + 87, // 29: teleport.lib.teleterm.v1.PaginatedResource.server:type_name -> teleport.lib.teleterm.v1.Server + 89, // 30: teleport.lib.teleterm.v1.PaginatedResource.kube:type_name -> teleport.lib.teleterm.v1.Kube + 90, // 31: teleport.lib.teleterm.v1.PaginatedResource.app:type_name -> teleport.lib.teleterm.v1.App 70, // 32: teleport.lib.teleterm.v1.GetUserPreferencesResponse.user_preferences:type_name -> teleport.lib.teleterm.v1.UserPreferences 70, // 33: teleport.lib.teleterm.v1.UpdateUserPreferencesRequest.user_preferences:type_name -> teleport.lib.teleterm.v1.UserPreferences 70, // 34: teleport.lib.teleterm.v1.UpdateUserPreferencesResponse.user_preferences:type_name -> teleport.lib.teleterm.v1.UserPreferences - 89, // 35: teleport.lib.teleterm.v1.UserPreferences.cluster_preferences:type_name -> teleport.userpreferences.v1.ClusterUserPreferences - 90, // 36: teleport.lib.teleterm.v1.UserPreferences.unified_resource_preferences:type_name -> teleport.userpreferences.v1.UnifiedResourcePreferences - 91, // 37: teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest.device_web_token:type_name -> teleport.devicetrust.v1.DeviceWebToken - 92, // 38: teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse.confirmation_token:type_name -> teleport.devicetrust.v1.DeviceConfirmationToken - 48, // 39: teleport.lib.teleterm.v1.TerminalService.UpdateTshdEventsServerAddress:input_type -> teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressRequest - 34, // 40: teleport.lib.teleterm.v1.TerminalService.ListRootClusters:input_type -> teleport.lib.teleterm.v1.ListClustersRequest - 36, // 41: teleport.lib.teleterm.v1.TerminalService.ListLeafClusters:input_type -> teleport.lib.teleterm.v1.ListLeafClustersRequest - 7, // 42: teleport.lib.teleterm.v1.TerminalService.StartHeadlessWatcher:input_type -> teleport.lib.teleterm.v1.StartHeadlessWatcherRequest - 37, // 43: teleport.lib.teleterm.v1.TerminalService.ListDatabaseUsers:input_type -> teleport.lib.teleterm.v1.ListDatabaseUsersRequest - 45, // 44: teleport.lib.teleterm.v1.TerminalService.GetServers:input_type -> teleport.lib.teleterm.v1.GetServersRequest - 10, // 45: teleport.lib.teleterm.v1.TerminalService.GetAccessRequests:input_type -> teleport.lib.teleterm.v1.GetAccessRequestsRequest - 9, // 46: teleport.lib.teleterm.v1.TerminalService.GetAccessRequest:input_type -> teleport.lib.teleterm.v1.GetAccessRequestRequest - 13, // 47: teleport.lib.teleterm.v1.TerminalService.DeleteAccessRequest:input_type -> teleport.lib.teleterm.v1.DeleteAccessRequestRequest - 14, // 48: teleport.lib.teleterm.v1.TerminalService.CreateAccessRequest:input_type -> teleport.lib.teleterm.v1.CreateAccessRequestRequest - 19, // 49: teleport.lib.teleterm.v1.TerminalService.ReviewAccessRequest:input_type -> teleport.lib.teleterm.v1.ReviewAccessRequestRequest - 17, // 50: teleport.lib.teleterm.v1.TerminalService.GetRequestableRoles:input_type -> teleport.lib.teleterm.v1.GetRequestableRolesRequest - 16, // 51: teleport.lib.teleterm.v1.TerminalService.AssumeRole:input_type -> teleport.lib.teleterm.v1.AssumeRoleRequest - 21, // 52: teleport.lib.teleterm.v1.TerminalService.PromoteAccessRequest:input_type -> teleport.lib.teleterm.v1.PromoteAccessRequestRequest - 23, // 53: teleport.lib.teleterm.v1.TerminalService.GetSuggestedAccessLists:input_type -> teleport.lib.teleterm.v1.GetSuggestedAccessListsRequest - 25, // 54: teleport.lib.teleterm.v1.TerminalService.ListKubernetesResources:input_type -> teleport.lib.teleterm.v1.ListKubernetesResourcesRequest - 33, // 55: teleport.lib.teleterm.v1.TerminalService.AddCluster:input_type -> teleport.lib.teleterm.v1.AddClusterRequest - 4, // 56: teleport.lib.teleterm.v1.TerminalService.RemoveCluster:input_type -> teleport.lib.teleterm.v1.RemoveClusterRequest - 40, // 57: teleport.lib.teleterm.v1.TerminalService.ListGateways:input_type -> teleport.lib.teleterm.v1.ListGatewaysRequest - 39, // 58: teleport.lib.teleterm.v1.TerminalService.CreateGateway:input_type -> teleport.lib.teleterm.v1.CreateGatewayRequest - 42, // 59: teleport.lib.teleterm.v1.TerminalService.RemoveGateway:input_type -> teleport.lib.teleterm.v1.RemoveGatewayRequest - 43, // 60: teleport.lib.teleterm.v1.TerminalService.SetGatewayTargetSubresourceName:input_type -> teleport.lib.teleterm.v1.SetGatewayTargetSubresourceNameRequest - 44, // 61: teleport.lib.teleterm.v1.TerminalService.SetGatewayLocalPort:input_type -> teleport.lib.teleterm.v1.SetGatewayLocalPortRequest - 47, // 62: teleport.lib.teleterm.v1.TerminalService.GetAuthSettings:input_type -> teleport.lib.teleterm.v1.GetAuthSettingsRequest - 5, // 63: teleport.lib.teleterm.v1.TerminalService.GetCluster:input_type -> teleport.lib.teleterm.v1.GetClusterRequest - 32, // 64: teleport.lib.teleterm.v1.TerminalService.Login:input_type -> teleport.lib.teleterm.v1.LoginRequest - 29, // 65: teleport.lib.teleterm.v1.TerminalService.LoginPasswordless:input_type -> teleport.lib.teleterm.v1.LoginPasswordlessRequest - 6, // 66: teleport.lib.teleterm.v1.TerminalService.Logout:input_type -> teleport.lib.teleterm.v1.LogoutRequest - 30, // 67: teleport.lib.teleterm.v1.TerminalService.TransferFile:input_type -> teleport.lib.teleterm.v1.FileTransferRequest - 93, // 68: teleport.lib.teleterm.v1.TerminalService.ReportUsageEvent:input_type -> teleport.lib.teleterm.v1.ReportUsageEventRequest - 50, // 69: teleport.lib.teleterm.v1.TerminalService.UpdateHeadlessAuthenticationState:input_type -> teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateRequest - 52, // 70: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerRole:input_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerRoleRequest - 54, // 71: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerNodeToken:input_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenRequest - 56, // 72: teleport.lib.teleterm.v1.TerminalService.WaitForConnectMyComputerNodeJoin:input_type -> teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinRequest - 58, // 73: teleport.lib.teleterm.v1.TerminalService.DeleteConnectMyComputerNode:input_type -> teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeRequest - 60, // 74: teleport.lib.teleterm.v1.TerminalService.GetConnectMyComputerNodeName:input_type -> teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameRequest - 62, // 75: teleport.lib.teleterm.v1.TerminalService.ListUnifiedResources:input_type -> teleport.lib.teleterm.v1.ListUnifiedResourcesRequest - 66, // 76: teleport.lib.teleterm.v1.TerminalService.GetUserPreferences:input_type -> teleport.lib.teleterm.v1.GetUserPreferencesRequest - 68, // 77: teleport.lib.teleterm.v1.TerminalService.UpdateUserPreferences:input_type -> teleport.lib.teleterm.v1.UpdateUserPreferencesRequest - 71, // 78: teleport.lib.teleterm.v1.TerminalService.AuthenticateWebDevice:input_type -> teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest - 49, // 79: teleport.lib.teleterm.v1.TerminalService.UpdateTshdEventsServerAddress:output_type -> teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressResponse - 35, // 80: teleport.lib.teleterm.v1.TerminalService.ListRootClusters:output_type -> teleport.lib.teleterm.v1.ListClustersResponse - 35, // 81: teleport.lib.teleterm.v1.TerminalService.ListLeafClusters:output_type -> teleport.lib.teleterm.v1.ListClustersResponse - 8, // 82: teleport.lib.teleterm.v1.TerminalService.StartHeadlessWatcher:output_type -> teleport.lib.teleterm.v1.StartHeadlessWatcherResponse - 38, // 83: teleport.lib.teleterm.v1.TerminalService.ListDatabaseUsers:output_type -> teleport.lib.teleterm.v1.ListDatabaseUsersResponse - 46, // 84: teleport.lib.teleterm.v1.TerminalService.GetServers:output_type -> teleport.lib.teleterm.v1.GetServersResponse - 12, // 85: teleport.lib.teleterm.v1.TerminalService.GetAccessRequests:output_type -> teleport.lib.teleterm.v1.GetAccessRequestsResponse - 11, // 86: teleport.lib.teleterm.v1.TerminalService.GetAccessRequest:output_type -> teleport.lib.teleterm.v1.GetAccessRequestResponse - 3, // 87: teleport.lib.teleterm.v1.TerminalService.DeleteAccessRequest:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 15, // 88: teleport.lib.teleterm.v1.TerminalService.CreateAccessRequest:output_type -> teleport.lib.teleterm.v1.CreateAccessRequestResponse - 20, // 89: teleport.lib.teleterm.v1.TerminalService.ReviewAccessRequest:output_type -> teleport.lib.teleterm.v1.ReviewAccessRequestResponse - 18, // 90: teleport.lib.teleterm.v1.TerminalService.GetRequestableRoles:output_type -> teleport.lib.teleterm.v1.GetRequestableRolesResponse - 3, // 91: teleport.lib.teleterm.v1.TerminalService.AssumeRole:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 22, // 92: teleport.lib.teleterm.v1.TerminalService.PromoteAccessRequest:output_type -> teleport.lib.teleterm.v1.PromoteAccessRequestResponse - 24, // 93: teleport.lib.teleterm.v1.TerminalService.GetSuggestedAccessLists:output_type -> teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse - 26, // 94: teleport.lib.teleterm.v1.TerminalService.ListKubernetesResources:output_type -> teleport.lib.teleterm.v1.ListKubernetesResourcesResponse - 83, // 95: teleport.lib.teleterm.v1.TerminalService.AddCluster:output_type -> teleport.lib.teleterm.v1.Cluster - 3, // 96: teleport.lib.teleterm.v1.TerminalService.RemoveCluster:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 41, // 97: teleport.lib.teleterm.v1.TerminalService.ListGateways:output_type -> teleport.lib.teleterm.v1.ListGatewaysResponse - 84, // 98: teleport.lib.teleterm.v1.TerminalService.CreateGateway:output_type -> teleport.lib.teleterm.v1.Gateway - 3, // 99: teleport.lib.teleterm.v1.TerminalService.RemoveGateway:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 84, // 100: teleport.lib.teleterm.v1.TerminalService.SetGatewayTargetSubresourceName:output_type -> teleport.lib.teleterm.v1.Gateway - 84, // 101: teleport.lib.teleterm.v1.TerminalService.SetGatewayLocalPort:output_type -> teleport.lib.teleterm.v1.Gateway - 94, // 102: teleport.lib.teleterm.v1.TerminalService.GetAuthSettings:output_type -> teleport.lib.teleterm.v1.AuthSettings - 83, // 103: teleport.lib.teleterm.v1.TerminalService.GetCluster:output_type -> teleport.lib.teleterm.v1.Cluster - 3, // 104: teleport.lib.teleterm.v1.TerminalService.Login:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 28, // 105: teleport.lib.teleterm.v1.TerminalService.LoginPasswordless:output_type -> teleport.lib.teleterm.v1.LoginPasswordlessResponse - 3, // 106: teleport.lib.teleterm.v1.TerminalService.Logout:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 31, // 107: teleport.lib.teleterm.v1.TerminalService.TransferFile:output_type -> teleport.lib.teleterm.v1.FileTransferProgress - 3, // 108: teleport.lib.teleterm.v1.TerminalService.ReportUsageEvent:output_type -> teleport.lib.teleterm.v1.EmptyResponse - 51, // 109: teleport.lib.teleterm.v1.TerminalService.UpdateHeadlessAuthenticationState:output_type -> teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateResponse - 53, // 110: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerRole:output_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerRoleResponse - 55, // 111: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerNodeToken:output_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenResponse - 57, // 112: teleport.lib.teleterm.v1.TerminalService.WaitForConnectMyComputerNodeJoin:output_type -> teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse - 59, // 113: teleport.lib.teleterm.v1.TerminalService.DeleteConnectMyComputerNode:output_type -> teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeResponse - 61, // 114: teleport.lib.teleterm.v1.TerminalService.GetConnectMyComputerNodeName:output_type -> teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameResponse - 64, // 115: teleport.lib.teleterm.v1.TerminalService.ListUnifiedResources:output_type -> teleport.lib.teleterm.v1.ListUnifiedResourcesResponse - 67, // 116: teleport.lib.teleterm.v1.TerminalService.GetUserPreferences:output_type -> teleport.lib.teleterm.v1.GetUserPreferencesResponse - 69, // 117: teleport.lib.teleterm.v1.TerminalService.UpdateUserPreferences:output_type -> teleport.lib.teleterm.v1.UpdateUserPreferencesResponse - 72, // 118: teleport.lib.teleterm.v1.TerminalService.AuthenticateWebDevice:output_type -> teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse - 79, // [79:119] is the sub-list for method output_type - 39, // [39:79] is the sub-list for method input_type - 39, // [39:39] is the sub-list for extension type_name - 39, // [39:39] is the sub-list for extension extendee - 0, // [0:39] is the sub-list for field type_name + 91, // 35: teleport.lib.teleterm.v1.UserPreferences.cluster_preferences:type_name -> teleport.userpreferences.v1.ClusterUserPreferences + 92, // 36: teleport.lib.teleterm.v1.UserPreferences.unified_resource_preferences:type_name -> teleport.userpreferences.v1.UnifiedResourcePreferences + 93, // 37: teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest.device_web_token:type_name -> teleport.devicetrust.v1.DeviceWebToken + 94, // 38: teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse.confirmation_token:type_name -> teleport.devicetrust.v1.DeviceConfirmationToken + 90, // 39: teleport.lib.teleterm.v1.GetAppResponse.app:type_name -> teleport.lib.teleterm.v1.App + 48, // 40: teleport.lib.teleterm.v1.TerminalService.UpdateTshdEventsServerAddress:input_type -> teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressRequest + 34, // 41: teleport.lib.teleterm.v1.TerminalService.ListRootClusters:input_type -> teleport.lib.teleterm.v1.ListClustersRequest + 36, // 42: teleport.lib.teleterm.v1.TerminalService.ListLeafClusters:input_type -> teleport.lib.teleterm.v1.ListLeafClustersRequest + 7, // 43: teleport.lib.teleterm.v1.TerminalService.StartHeadlessWatcher:input_type -> teleport.lib.teleterm.v1.StartHeadlessWatcherRequest + 37, // 44: teleport.lib.teleterm.v1.TerminalService.ListDatabaseUsers:input_type -> teleport.lib.teleterm.v1.ListDatabaseUsersRequest + 45, // 45: teleport.lib.teleterm.v1.TerminalService.GetServers:input_type -> teleport.lib.teleterm.v1.GetServersRequest + 10, // 46: teleport.lib.teleterm.v1.TerminalService.GetAccessRequests:input_type -> teleport.lib.teleterm.v1.GetAccessRequestsRequest + 9, // 47: teleport.lib.teleterm.v1.TerminalService.GetAccessRequest:input_type -> teleport.lib.teleterm.v1.GetAccessRequestRequest + 13, // 48: teleport.lib.teleterm.v1.TerminalService.DeleteAccessRequest:input_type -> teleport.lib.teleterm.v1.DeleteAccessRequestRequest + 14, // 49: teleport.lib.teleterm.v1.TerminalService.CreateAccessRequest:input_type -> teleport.lib.teleterm.v1.CreateAccessRequestRequest + 19, // 50: teleport.lib.teleterm.v1.TerminalService.ReviewAccessRequest:input_type -> teleport.lib.teleterm.v1.ReviewAccessRequestRequest + 17, // 51: teleport.lib.teleterm.v1.TerminalService.GetRequestableRoles:input_type -> teleport.lib.teleterm.v1.GetRequestableRolesRequest + 16, // 52: teleport.lib.teleterm.v1.TerminalService.AssumeRole:input_type -> teleport.lib.teleterm.v1.AssumeRoleRequest + 21, // 53: teleport.lib.teleterm.v1.TerminalService.PromoteAccessRequest:input_type -> teleport.lib.teleterm.v1.PromoteAccessRequestRequest + 23, // 54: teleport.lib.teleterm.v1.TerminalService.GetSuggestedAccessLists:input_type -> teleport.lib.teleterm.v1.GetSuggestedAccessListsRequest + 25, // 55: teleport.lib.teleterm.v1.TerminalService.ListKubernetesResources:input_type -> teleport.lib.teleterm.v1.ListKubernetesResourcesRequest + 33, // 56: teleport.lib.teleterm.v1.TerminalService.AddCluster:input_type -> teleport.lib.teleterm.v1.AddClusterRequest + 4, // 57: teleport.lib.teleterm.v1.TerminalService.RemoveCluster:input_type -> teleport.lib.teleterm.v1.RemoveClusterRequest + 40, // 58: teleport.lib.teleterm.v1.TerminalService.ListGateways:input_type -> teleport.lib.teleterm.v1.ListGatewaysRequest + 39, // 59: teleport.lib.teleterm.v1.TerminalService.CreateGateway:input_type -> teleport.lib.teleterm.v1.CreateGatewayRequest + 42, // 60: teleport.lib.teleterm.v1.TerminalService.RemoveGateway:input_type -> teleport.lib.teleterm.v1.RemoveGatewayRequest + 43, // 61: teleport.lib.teleterm.v1.TerminalService.SetGatewayTargetSubresourceName:input_type -> teleport.lib.teleterm.v1.SetGatewayTargetSubresourceNameRequest + 44, // 62: teleport.lib.teleterm.v1.TerminalService.SetGatewayLocalPort:input_type -> teleport.lib.teleterm.v1.SetGatewayLocalPortRequest + 47, // 63: teleport.lib.teleterm.v1.TerminalService.GetAuthSettings:input_type -> teleport.lib.teleterm.v1.GetAuthSettingsRequest + 5, // 64: teleport.lib.teleterm.v1.TerminalService.GetCluster:input_type -> teleport.lib.teleterm.v1.GetClusterRequest + 32, // 65: teleport.lib.teleterm.v1.TerminalService.Login:input_type -> teleport.lib.teleterm.v1.LoginRequest + 29, // 66: teleport.lib.teleterm.v1.TerminalService.LoginPasswordless:input_type -> teleport.lib.teleterm.v1.LoginPasswordlessRequest + 6, // 67: teleport.lib.teleterm.v1.TerminalService.Logout:input_type -> teleport.lib.teleterm.v1.LogoutRequest + 30, // 68: teleport.lib.teleterm.v1.TerminalService.TransferFile:input_type -> teleport.lib.teleterm.v1.FileTransferRequest + 95, // 69: teleport.lib.teleterm.v1.TerminalService.ReportUsageEvent:input_type -> teleport.lib.teleterm.v1.ReportUsageEventRequest + 50, // 70: teleport.lib.teleterm.v1.TerminalService.UpdateHeadlessAuthenticationState:input_type -> teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateRequest + 52, // 71: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerRole:input_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerRoleRequest + 54, // 72: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerNodeToken:input_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenRequest + 56, // 73: teleport.lib.teleterm.v1.TerminalService.WaitForConnectMyComputerNodeJoin:input_type -> teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinRequest + 58, // 74: teleport.lib.teleterm.v1.TerminalService.DeleteConnectMyComputerNode:input_type -> teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeRequest + 60, // 75: teleport.lib.teleterm.v1.TerminalService.GetConnectMyComputerNodeName:input_type -> teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameRequest + 62, // 76: teleport.lib.teleterm.v1.TerminalService.ListUnifiedResources:input_type -> teleport.lib.teleterm.v1.ListUnifiedResourcesRequest + 66, // 77: teleport.lib.teleterm.v1.TerminalService.GetUserPreferences:input_type -> teleport.lib.teleterm.v1.GetUserPreferencesRequest + 68, // 78: teleport.lib.teleterm.v1.TerminalService.UpdateUserPreferences:input_type -> teleport.lib.teleterm.v1.UpdateUserPreferencesRequest + 71, // 79: teleport.lib.teleterm.v1.TerminalService.AuthenticateWebDevice:input_type -> teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest + 73, // 80: teleport.lib.teleterm.v1.TerminalService.GetApp:input_type -> teleport.lib.teleterm.v1.GetAppRequest + 49, // 81: teleport.lib.teleterm.v1.TerminalService.UpdateTshdEventsServerAddress:output_type -> teleport.lib.teleterm.v1.UpdateTshdEventsServerAddressResponse + 35, // 82: teleport.lib.teleterm.v1.TerminalService.ListRootClusters:output_type -> teleport.lib.teleterm.v1.ListClustersResponse + 35, // 83: teleport.lib.teleterm.v1.TerminalService.ListLeafClusters:output_type -> teleport.lib.teleterm.v1.ListClustersResponse + 8, // 84: teleport.lib.teleterm.v1.TerminalService.StartHeadlessWatcher:output_type -> teleport.lib.teleterm.v1.StartHeadlessWatcherResponse + 38, // 85: teleport.lib.teleterm.v1.TerminalService.ListDatabaseUsers:output_type -> teleport.lib.teleterm.v1.ListDatabaseUsersResponse + 46, // 86: teleport.lib.teleterm.v1.TerminalService.GetServers:output_type -> teleport.lib.teleterm.v1.GetServersResponse + 12, // 87: teleport.lib.teleterm.v1.TerminalService.GetAccessRequests:output_type -> teleport.lib.teleterm.v1.GetAccessRequestsResponse + 11, // 88: teleport.lib.teleterm.v1.TerminalService.GetAccessRequest:output_type -> teleport.lib.teleterm.v1.GetAccessRequestResponse + 3, // 89: teleport.lib.teleterm.v1.TerminalService.DeleteAccessRequest:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 15, // 90: teleport.lib.teleterm.v1.TerminalService.CreateAccessRequest:output_type -> teleport.lib.teleterm.v1.CreateAccessRequestResponse + 20, // 91: teleport.lib.teleterm.v1.TerminalService.ReviewAccessRequest:output_type -> teleport.lib.teleterm.v1.ReviewAccessRequestResponse + 18, // 92: teleport.lib.teleterm.v1.TerminalService.GetRequestableRoles:output_type -> teleport.lib.teleterm.v1.GetRequestableRolesResponse + 3, // 93: teleport.lib.teleterm.v1.TerminalService.AssumeRole:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 22, // 94: teleport.lib.teleterm.v1.TerminalService.PromoteAccessRequest:output_type -> teleport.lib.teleterm.v1.PromoteAccessRequestResponse + 24, // 95: teleport.lib.teleterm.v1.TerminalService.GetSuggestedAccessLists:output_type -> teleport.lib.teleterm.v1.GetSuggestedAccessListsResponse + 26, // 96: teleport.lib.teleterm.v1.TerminalService.ListKubernetesResources:output_type -> teleport.lib.teleterm.v1.ListKubernetesResourcesResponse + 85, // 97: teleport.lib.teleterm.v1.TerminalService.AddCluster:output_type -> teleport.lib.teleterm.v1.Cluster + 3, // 98: teleport.lib.teleterm.v1.TerminalService.RemoveCluster:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 41, // 99: teleport.lib.teleterm.v1.TerminalService.ListGateways:output_type -> teleport.lib.teleterm.v1.ListGatewaysResponse + 86, // 100: teleport.lib.teleterm.v1.TerminalService.CreateGateway:output_type -> teleport.lib.teleterm.v1.Gateway + 3, // 101: teleport.lib.teleterm.v1.TerminalService.RemoveGateway:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 86, // 102: teleport.lib.teleterm.v1.TerminalService.SetGatewayTargetSubresourceName:output_type -> teleport.lib.teleterm.v1.Gateway + 86, // 103: teleport.lib.teleterm.v1.TerminalService.SetGatewayLocalPort:output_type -> teleport.lib.teleterm.v1.Gateway + 96, // 104: teleport.lib.teleterm.v1.TerminalService.GetAuthSettings:output_type -> teleport.lib.teleterm.v1.AuthSettings + 85, // 105: teleport.lib.teleterm.v1.TerminalService.GetCluster:output_type -> teleport.lib.teleterm.v1.Cluster + 3, // 106: teleport.lib.teleterm.v1.TerminalService.Login:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 28, // 107: teleport.lib.teleterm.v1.TerminalService.LoginPasswordless:output_type -> teleport.lib.teleterm.v1.LoginPasswordlessResponse + 3, // 108: teleport.lib.teleterm.v1.TerminalService.Logout:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 31, // 109: teleport.lib.teleterm.v1.TerminalService.TransferFile:output_type -> teleport.lib.teleterm.v1.FileTransferProgress + 3, // 110: teleport.lib.teleterm.v1.TerminalService.ReportUsageEvent:output_type -> teleport.lib.teleterm.v1.EmptyResponse + 51, // 111: teleport.lib.teleterm.v1.TerminalService.UpdateHeadlessAuthenticationState:output_type -> teleport.lib.teleterm.v1.UpdateHeadlessAuthenticationStateResponse + 53, // 112: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerRole:output_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerRoleResponse + 55, // 113: teleport.lib.teleterm.v1.TerminalService.CreateConnectMyComputerNodeToken:output_type -> teleport.lib.teleterm.v1.CreateConnectMyComputerNodeTokenResponse + 57, // 114: teleport.lib.teleterm.v1.TerminalService.WaitForConnectMyComputerNodeJoin:output_type -> teleport.lib.teleterm.v1.WaitForConnectMyComputerNodeJoinResponse + 59, // 115: teleport.lib.teleterm.v1.TerminalService.DeleteConnectMyComputerNode:output_type -> teleport.lib.teleterm.v1.DeleteConnectMyComputerNodeResponse + 61, // 116: teleport.lib.teleterm.v1.TerminalService.GetConnectMyComputerNodeName:output_type -> teleport.lib.teleterm.v1.GetConnectMyComputerNodeNameResponse + 64, // 117: teleport.lib.teleterm.v1.TerminalService.ListUnifiedResources:output_type -> teleport.lib.teleterm.v1.ListUnifiedResourcesResponse + 67, // 118: teleport.lib.teleterm.v1.TerminalService.GetUserPreferences:output_type -> teleport.lib.teleterm.v1.GetUserPreferencesResponse + 69, // 119: teleport.lib.teleterm.v1.TerminalService.UpdateUserPreferences:output_type -> teleport.lib.teleterm.v1.UpdateUserPreferencesResponse + 72, // 120: teleport.lib.teleterm.v1.TerminalService.AuthenticateWebDevice:output_type -> teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse + 74, // 121: teleport.lib.teleterm.v1.TerminalService.GetApp:output_type -> teleport.lib.teleterm.v1.GetAppResponse + 81, // [81:122] is the sub-list for method output_type + 40, // [40:81] is the sub-list for method input_type + 40, // [40:40] is the sub-list for extension type_name + 40, // [40:40] is the sub-list for extension extendee + 0, // [0:40] is the sub-list for field type_name } func init() { file_teleport_lib_teleterm_v1_service_proto_init() } @@ -5531,7 +5636,7 @@ func file_teleport_lib_teleterm_v1_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_teleport_lib_teleterm_v1_service_proto_rawDesc, NumEnums: 3, - NumMessages: 75, + NumMessages: 77, NumExtensions: 0, NumServices: 1, }, diff --git a/gen/proto/go/teleport/lib/teleterm/v1/service_grpc.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/service_grpc.pb.go index da9eec4127ccf..317a9e59b7e3c 100644 --- a/gen/proto/go/teleport/lib/teleterm/v1/service_grpc.pb.go +++ b/gen/proto/go/teleport/lib/teleterm/v1/service_grpc.pb.go @@ -76,6 +76,7 @@ const ( TerminalService_GetUserPreferences_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/GetUserPreferences" TerminalService_UpdateUserPreferences_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/UpdateUserPreferences" TerminalService_AuthenticateWebDevice_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/AuthenticateWebDevice" + TerminalService_GetApp_FullMethodName = "/teleport.lib.teleterm.v1.TerminalService/GetApp" ) // TerminalServiceClient is the client API for TerminalService service. @@ -213,6 +214,9 @@ type TerminalServiceClient interface { // See // https://github.com/gravitational/teleport.e/blob/master/rfd/0009e-device-trust-web-support.md#device-web-authentication. AuthenticateWebDevice(ctx context.Context, in *AuthenticateWebDeviceRequest, opts ...grpc.CallOption) (*AuthenticateWebDeviceResponse, error) + // GetApp returns details of an app resource. It does not include information about AWS roles and + // FQDN. + GetApp(ctx context.Context, in *GetAppRequest, opts ...grpc.CallOption) (*GetAppResponse, error) } type terminalServiceClient struct { @@ -636,6 +640,16 @@ func (c *terminalServiceClient) AuthenticateWebDevice(ctx context.Context, in *A return out, nil } +func (c *terminalServiceClient) GetApp(ctx context.Context, in *GetAppRequest, opts ...grpc.CallOption) (*GetAppResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetAppResponse) + err := c.cc.Invoke(ctx, TerminalService_GetApp_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // TerminalServiceServer is the server API for TerminalService service. // All implementations must embed UnimplementedTerminalServiceServer // for forward compatibility. @@ -771,6 +785,9 @@ type TerminalServiceServer interface { // See // https://github.com/gravitational/teleport.e/blob/master/rfd/0009e-device-trust-web-support.md#device-web-authentication. AuthenticateWebDevice(context.Context, *AuthenticateWebDeviceRequest) (*AuthenticateWebDeviceResponse, error) + // GetApp returns details of an app resource. It does not include information about AWS roles and + // FQDN. + GetApp(context.Context, *GetAppRequest) (*GetAppResponse, error) mustEmbedUnimplementedTerminalServiceServer() } @@ -901,6 +918,9 @@ func (UnimplementedTerminalServiceServer) UpdateUserPreferences(context.Context, func (UnimplementedTerminalServiceServer) AuthenticateWebDevice(context.Context, *AuthenticateWebDeviceRequest) (*AuthenticateWebDeviceResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method AuthenticateWebDevice not implemented") } +func (UnimplementedTerminalServiceServer) GetApp(context.Context, *GetAppRequest) (*GetAppResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetApp not implemented") +} func (UnimplementedTerminalServiceServer) mustEmbedUnimplementedTerminalServiceServer() {} func (UnimplementedTerminalServiceServer) testEmbeddedByValue() {} @@ -1624,6 +1644,24 @@ func _TerminalService_AuthenticateWebDevice_Handler(srv interface{}, ctx context return interceptor(ctx, in, info, handler) } +func _TerminalService_GetApp_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAppRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TerminalServiceServer).GetApp(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TerminalService_GetApp_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TerminalServiceServer).GetApp(ctx, req.(*GetAppRequest)) + } + return interceptor(ctx, in, info, handler) +} + // TerminalService_ServiceDesc is the grpc.ServiceDesc for TerminalService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1783,6 +1821,10 @@ var TerminalService_ServiceDesc = grpc.ServiceDesc{ MethodName: "AuthenticateWebDevice", Handler: _TerminalService_AuthenticateWebDevice_Handler, }, + { + MethodName: "GetApp", + Handler: _TerminalService_GetApp_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/app_pb.ts b/gen/proto/ts/teleport/lib/teleterm/v1/app_pb.ts index 5764dffcd6ab8..faf0f10553d74 100644 --- a/gen/proto/ts/teleport/lib/teleterm/v1/app_pb.ts +++ b/gen/proto/ts/teleport/lib/teleterm/v1/app_pb.ts @@ -123,13 +123,15 @@ export interface App { * proxy hostname] if public_addr is not present. * If the app belongs to a leaf cluster, fqdn is equal to [name].[root cluster proxy hostname]. * - * fqdn is not present for SAML applications. + * fqdn is not present for SAML applications. Available only when the app was fetched through the + * ListUnifiedResources RPC. * * @generated from protobuf field: string fqdn = 10; */ fqdn: string; /** - * aws_roles is a list of AWS IAM roles for the application representing AWS console. + * aws_roles is a list of AWS IAM roles for the application representing AWS console. Available + * only when the app wast fetched through the ListUnifiedResources RPC. * * @generated from protobuf field: repeated teleport.lib.teleterm.v1.AWSRole aws_roles = 11; */ diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.client.ts b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.client.ts index a44f65be9bd09..a64b91fe8b1d8 100644 --- a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.client.ts +++ b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.client.ts @@ -24,6 +24,8 @@ import type { RpcTransport } from "@protobuf-ts/runtime-rpc"; import type { ServiceInfo } from "@protobuf-ts/runtime-rpc"; import { TerminalService } from "./service_pb"; +import type { GetAppResponse } from "./service_pb"; +import type { GetAppRequest } from "./service_pb"; import type { AuthenticateWebDeviceResponse } from "./service_pb"; import type { AuthenticateWebDeviceRequest } from "./service_pb"; import type { UpdateUserPreferencesResponse } from "./service_pb"; @@ -394,6 +396,13 @@ export interface ITerminalServiceClient { * @generated from protobuf rpc: AuthenticateWebDevice(teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest) returns (teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse); */ authenticateWebDevice(input: AuthenticateWebDeviceRequest, options?: RpcOptions): UnaryCall; + /** + * GetApp returns details of an app resource. It does not include information about AWS roles and + * FQDN. + * + * @generated from protobuf rpc: GetApp(teleport.lib.teleterm.v1.GetAppRequest) returns (teleport.lib.teleterm.v1.GetAppResponse); + */ + getApp(input: GetAppRequest, options?: RpcOptions): UnaryCall; } /** * TerminalService is used by the Electron app to communicate with the tsh daemon. @@ -815,4 +824,14 @@ export class TerminalServiceClient implements ITerminalServiceClient, ServiceInf const method = this.methods[39], opt = this._transport.mergeOptions(options); return stackIntercept("unary", this._transport, method, opt, input); } + /** + * GetApp returns details of an app resource. It does not include information about AWS roles and + * FQDN. + * + * @generated from protobuf rpc: GetApp(teleport.lib.teleterm.v1.GetAppRequest) returns (teleport.lib.teleterm.v1.GetAppResponse); + */ + getApp(input: GetAppRequest, options?: RpcOptions): UnaryCall { + const method = this.methods[40], opt = this._transport.mergeOptions(options); + return stackIntercept("unary", this._transport, method, opt, input); + } } diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.grpc-server.ts b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.grpc-server.ts index dbce83e2bd6b9..58ff275ad0295 100644 --- a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.grpc-server.ts +++ b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.grpc-server.ts @@ -21,6 +21,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // +import { GetAppResponse } from "./service_pb"; +import { GetAppRequest } from "./service_pb"; import { AuthenticateWebDeviceResponse } from "./service_pb"; import { AuthenticateWebDeviceRequest } from "./service_pb"; import { UpdateUserPreferencesResponse } from "./service_pb"; @@ -387,6 +389,13 @@ export interface ITerminalService extends grpc.UntypedServiceImplementation { * @generated from protobuf rpc: AuthenticateWebDevice(teleport.lib.teleterm.v1.AuthenticateWebDeviceRequest) returns (teleport.lib.teleterm.v1.AuthenticateWebDeviceResponse); */ authenticateWebDevice: grpc.handleUnaryCall; + /** + * GetApp returns details of an app resource. It does not include information about AWS roles and + * FQDN. + * + * @generated from protobuf rpc: GetApp(teleport.lib.teleterm.v1.GetAppRequest) returns (teleport.lib.teleterm.v1.GetAppResponse); + */ + getApp: grpc.handleUnaryCall; } /** * @grpc/grpc-js definition for the protobuf service teleport.lib.teleterm.v1.TerminalService. @@ -799,5 +808,15 @@ export const terminalServiceDefinition: grpc.ServiceDefinition requestDeserialize: bytes => AuthenticateWebDeviceRequest.fromBinary(bytes), responseSerialize: value => Buffer.from(AuthenticateWebDeviceResponse.toBinary(value)), requestSerialize: value => Buffer.from(AuthenticateWebDeviceRequest.toBinary(value)) + }, + getApp: { + path: "/teleport.lib.teleterm.v1.TerminalService/GetApp", + originalName: "GetApp", + requestStream: false, + responseStream: false, + responseDeserialize: bytes => GetAppResponse.fromBinary(bytes), + requestDeserialize: bytes => GetAppRequest.fromBinary(bytes), + responseSerialize: value => Buffer.from(GetAppResponse.toBinary(value)), + requestSerialize: value => Buffer.from(GetAppRequest.toBinary(value)) } }; diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.ts b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.ts index a2801abdca9d1..dd9139492d9c3 100644 --- a/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.ts +++ b/gen/proto/ts/teleport/lib/teleterm/v1/service_pb.ts @@ -1179,6 +1179,24 @@ export interface AuthenticateWebDeviceResponse { */ confirmationToken?: DeviceConfirmationToken; } +/** + * @generated from protobuf message teleport.lib.teleterm.v1.GetAppRequest + */ +export interface GetAppRequest { + /** + * @generated from protobuf field: string app_uri = 1; + */ + appUri: string; +} +/** + * @generated from protobuf message teleport.lib.teleterm.v1.GetAppResponse + */ +export interface GetAppResponse { + /** + * @generated from protobuf field: teleport.lib.teleterm.v1.App app = 1; + */ + app?: App; +} /** * PasswordlessPrompt describes different prompts we need from users * during the passwordless login flow. @@ -5235,6 +5253,99 @@ class AuthenticateWebDeviceResponse$Type extends MessageType { + constructor() { + super("teleport.lib.teleterm.v1.GetAppRequest", [ + { no: 1, name: "app_uri", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): GetAppRequest { + const message = globalThis.Object.create((this.messagePrototype!)); + message.appUri = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetAppRequest): GetAppRequest { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* string app_uri */ 1: + message.appUri = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GetAppRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* string app_uri = 1; */ + if (message.appUri !== "") + writer.tag(1, WireType.LengthDelimited).string(message.appUri); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message teleport.lib.teleterm.v1.GetAppRequest + */ +export const GetAppRequest = new GetAppRequest$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class GetAppResponse$Type extends MessageType { + constructor() { + super("teleport.lib.teleterm.v1.GetAppResponse", [ + { no: 1, name: "app", kind: "message", T: () => App } + ]); + } + create(value?: PartialMessage): GetAppResponse { + const message = globalThis.Object.create((this.messagePrototype!)); + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: GetAppResponse): GetAppResponse { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* teleport.lib.teleterm.v1.App app */ 1: + message.app = App.internalBinaryRead(reader, reader.uint32(), options, message.app); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: GetAppResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* teleport.lib.teleterm.v1.App app = 1; */ + if (message.app) + App.internalBinaryWrite(message.app, writer.tag(1, WireType.LengthDelimited).fork(), options).join(); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message teleport.lib.teleterm.v1.GetAppResponse + */ +export const GetAppResponse = new GetAppResponse$Type(); /** * @generated ServiceType for protobuf service teleport.lib.teleterm.v1.TerminalService */ @@ -5278,5 +5389,6 @@ export const TerminalService = new ServiceType("teleport.lib.teleterm.v1.Termina { name: "ListUnifiedResources", options: {}, I: ListUnifiedResourcesRequest, O: ListUnifiedResourcesResponse }, { name: "GetUserPreferences", options: {}, I: GetUserPreferencesRequest, O: GetUserPreferencesResponse }, { name: "UpdateUserPreferences", options: {}, I: UpdateUserPreferencesRequest, O: UpdateUserPreferencesResponse }, - { name: "AuthenticateWebDevice", options: {}, I: AuthenticateWebDeviceRequest, O: AuthenticateWebDeviceResponse } + { name: "AuthenticateWebDevice", options: {}, I: AuthenticateWebDeviceRequest, O: AuthenticateWebDeviceResponse }, + { name: "GetApp", options: {}, I: GetAppRequest, O: GetAppResponse } ]); diff --git a/lib/teleterm/apiserver/handler/handler_apps.go b/lib/teleterm/apiserver/handler/handler_apps.go index 846369c8bda8b..4ab5aa85403a0 100644 --- a/lib/teleterm/apiserver/handler/handler_apps.go +++ b/lib/teleterm/apiserver/handler/handler_apps.go @@ -17,12 +17,47 @@ package handler import ( + "context" + + "github.com/gravitational/trace" + "github.com/gravitational/teleport/api/types" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" + "github.com/gravitational/teleport/lib/teleterm/api/uri" "github.com/gravitational/teleport/lib/teleterm/clusters" "github.com/gravitational/teleport/lib/ui" ) +func (h *Handler) GetApp(ctx context.Context, req *api.GetAppRequest) (*api.GetAppResponse, error) { + appURI, err := uri.Parse(req.AppUri) + if err != nil { + return nil, trace.Wrap(err) + } + + proxyClient, err := h.DaemonService.GetCachedClient(ctx, appURI) + if err != nil { + return nil, trace.Wrap(err) + } + + var app types.Application + if err := clusters.AddMetadataToRetryableError(ctx, func() error { + var err error + app, err = clusters.GetApp(ctx, proxyClient.CurrentCluster(), appURI.GetAppName()) + return trace.Wrap(err) + }); err != nil { + return nil, trace.Wrap(err) + } + + clustersApp := clusters.App{ + URI: appURI, + App: app, + } + + return &api.GetAppResponse{ + App: newAPIApp(clustersApp), + }, nil +} + func newAPIApp(clusterApp clusters.App) *api.App { app := clusterApp.App diff --git a/proto/teleport/lib/teleterm/v1/app.proto b/proto/teleport/lib/teleterm/v1/app.proto index ebd361306752a..1b5184be80bf2 100644 --- a/proto/teleport/lib/teleterm/v1/app.proto +++ b/proto/teleport/lib/teleterm/v1/app.proto @@ -75,9 +75,11 @@ message App { // proxy hostname] if public_addr is not present. // If the app belongs to a leaf cluster, fqdn is equal to [name].[root cluster proxy hostname]. // - // fqdn is not present for SAML applications. + // fqdn is not present for SAML applications. Available only when the app was fetched through the + // ListUnifiedResources RPC. string fqdn = 10; - // aws_roles is a list of AWS IAM roles for the application representing AWS console. + // aws_roles is a list of AWS IAM roles for the application representing AWS console. Available + // only when the app wast fetched through the ListUnifiedResources RPC. repeated AWSRole aws_roles = 11; // TCPPorts is a list of ports and port ranges that an app agent can forward connections to. // Only applicable to TCP App Access. diff --git a/proto/teleport/lib/teleterm/v1/service.proto b/proto/teleport/lib/teleterm/v1/service.proto index 20f5221d7a6a5..d816f75bd9a6f 100644 --- a/proto/teleport/lib/teleterm/v1/service.proto +++ b/proto/teleport/lib/teleterm/v1/service.proto @@ -177,6 +177,10 @@ service TerminalService { // See // https://github.com/gravitational/teleport.e/blob/master/rfd/0009e-device-trust-web-support.md#device-web-authentication. rpc AuthenticateWebDevice(AuthenticateWebDeviceRequest) returns (AuthenticateWebDeviceResponse); + + // GetApp returns details of an app resource. It does not include information about AWS roles and + // FQDN. + rpc GetApp(GetAppRequest) returns (GetAppResponse); } message EmptyResponse {} @@ -660,3 +664,11 @@ message AuthenticateWebDeviceResponse { // authentication attempt. teleport.devicetrust.v1.DeviceConfirmationToken confirmation_token = 1; } + +message GetAppRequest { + string app_uri = 1; +} + +message GetAppResponse { + App app = 1; +} diff --git a/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts b/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts index 5ab36a94f74fb..bb49c87e475e1 100644 --- a/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts +++ b/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts @@ -17,6 +17,7 @@ */ import { + makeApp, makeAppGateway, makeRootCluster, } from 'teleterm/services/tshd/testHelpers'; @@ -109,6 +110,7 @@ export class MockTshClient implements TshdClient { }, }); startHeadlessWatcher = () => new MockedUnaryCall({}); + getApp = () => new MockedUnaryCall({ app: makeApp() }); } export class MockVnetClient implements VnetClient { diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx index bd31e84d80035..a18e9c40e72a9 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx @@ -22,27 +22,33 @@ import { PropsWithChildren, useEffect, useMemo, + useRef, useState, } from 'react'; import styled from 'styled-components'; import { Alert, + Box, ButtonSecondary, disappear, Flex, H1, + Indicator, Link, rotate360, Text, } from 'design'; import { Check, Spinner } from 'design/Icon'; +import { PortRange } from 'gen-proto-ts/teleport/lib/teleterm/v1/app_pb'; import { Gateway } from 'gen-proto-ts/teleport/lib/teleterm/v1/gateway_pb'; import { TextSelectCopy } from 'shared/components/TextSelectCopy'; import Validation from 'shared/components/Validation'; import { Attempt } from 'shared/hooks/useAsync'; import { debounce } from 'shared/utils/highbar'; +import { formatPortRange } from 'teleterm/services/tshd/app'; + import { PortFieldInput } from '../components/FieldInputs'; export function AppGateway(props: { @@ -53,6 +59,8 @@ export function AppGateway(props: { changeTargetPort(port: string): void; changeTargetPortAttempt: Attempt; disconnect(): void; + getTcpPorts(): void; + tcpPortsAttempt: Attempt; }) { const { gateway } = props; @@ -84,6 +92,7 @@ export function AppGateway(props: { // When the app is not multi-port, targetSubresourceName is empty and the user cannot change it. const isMultiPort = gateway.protocol === 'TCP' && gateway.targetSubresourceName; + const targetPortRef = useRef(null); return ( - -

App Connection

- - Close Connection - -
+ + +

App Connection

+ + Close Connection + +
- {disconnectAttempt.status === 'error' && ( - - Could not close the connection - - )} - - - - - } - defaultValue={gateway.localPort} - onChange={handleLocalPortChange} - mb={0} - /> - {isMultiPort && ( + {disconnectAttempt.status === 'error' && ( + + Could not close the connection + + )} + + + } - required - defaultValue={gateway.targetSubresourceName} - onChange={handleTargetPortChange} + defaultValue={gateway.localPort} + onChange={handleLocalPortChange} mb={0} /> - )} - + {isMultiPort && ( + + } + required + defaultValue={gateway.targetSubresourceName} + onChange={handleTargetPortChange} + mb={0} + ref={targetPortRef} + /> + )} + + + + {props.tcpPortsAttempt.status === 'success' && ( + + Available target ports:{' '} + {props.tcpPortsAttempt.data.map((portRange, index) => ( + { + const port = portRange.port.toString(); + targetPortRef.current.value = port; + changeTargetPort(port); + }} + > + {formatPortRange(portRange)} + + ))} + + )} + {props.tcpPortsAttempt.status === 'processing' && ( + + + + )}
-
- Access the app at: - -
- - {changeLocalPortAttempt.status === 'error' && ( - - Could not change the local port - - )} - - {changeTargetPortAttempt.status === 'error' && ( - - Could not change the target port - - )} - - - The connection is made through an authenticated proxy so no extra - credentials are necessary. See{' '} - - the documentation - {' '} - for more details. - + +
+ Access the app at: + +
+ + {changeLocalPortAttempt.status === 'error' && ( + + Could not change the local port + + )} + + {changeTargetPortAttempt.status === 'error' && ( + + Could not change the target port + + )} + + {props.tcpPortsAttempt.status === 'error' && ( + + Could not fetch available target ports + + )} + + + The connection is made through an authenticated proxy so no extra + credentials are necessary. See{' '} + + the documentation + {' '} + for more details. + +
); } diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx index 936f1c8a399b1..5e15e36eae582 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx @@ -19,10 +19,10 @@ import { Meta } from '@storybook/react'; import { Flex } from 'design'; -import { wait } from 'shared/utils/wait'; +import { usePromiseRejectedOnUnmount, wait } from 'shared/utils/wait'; import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient'; -import { makeAppGateway } from 'teleterm/services/tshd/testHelpers'; +import { makeApp, makeAppGateway } from 'teleterm/services/tshd/testHelpers'; import { DocumentGatewayApp } from 'teleterm/ui/DocumentGatewayApp/DocumentGatewayApp'; import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; @@ -35,6 +35,7 @@ type StoryProps = { changeLocalPort: 'succeed' | 'throw-error'; changeTargetPort: 'succeed' | 'throw-error'; disconnect: 'succeed' | 'throw-error'; + getTcpPorts: 'succeed' | 'throw-error' | 'processing' | 'many-ports'; }; const meta: Meta = { @@ -60,6 +61,12 @@ const meta: Meta = { control: { type: 'radio' }, options: ['succeed', 'throw-error'], }, + getTcpPorts: { + if: { arg: 'online' }, + control: { type: 'radio' }, + options: ['succeed', 'throw-error', 'processing', 'many-ports'], + description: 'Used only for multi-port TCP apps.', + }, }, args: { appType: 'web', @@ -67,6 +74,7 @@ const meta: Meta = { changeLocalPort: 'succeed', changeTargetPort: 'succeed', disconnect: 'succeed', + getTcpPorts: 'succeed', }, }; export default meta; @@ -100,6 +108,8 @@ export function Story(props: StoryProps) { documentGateway.targetSubresourceName = '4242'; } + const infinitePromise = usePromiseRejectedOnUnmount(); + const appContext = new MockAppContext(); appContext.workspacesService.setState(draftState => { draftState.rootClusterUri = rootClusterUri; @@ -154,6 +164,34 @@ export function Story(props: StoryProps) { : undefined ) ); + + if (props.getTcpPorts === 'processing') { + appContext.tshd.getApp = () => infinitePromise; + } else { + let tcpPorts = [ + { port: 1337, endPort: 4242 }, + { port: 17231, endPort: 0 }, + { port: 27381, endPort: 28400 }, + ]; + if (props.getTcpPorts === 'many-ports') { + tcpPorts = new Array(9).fill(tcpPorts).flat(); + } + + appContext.tshd.getApp = () => + wait(500).then( + () => + new MockedUnaryCall( + { + app: makeApp({ + tcpPorts, + }), + }, + props.getTcpPorts === 'throw-error' + ? new Error('something went wrong') + : undefined + ) + ); + } } else { appContext.clustersService.createGateway = () => Promise.reject(new Error('failed to create gateway')); diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx index 32f22e90e2af8..6d142417c4616 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx @@ -15,22 +15,28 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import { ComponentProps } from 'react'; +import { ComponentProps, useCallback, useEffect } from 'react'; import { z } from 'zod'; +import { useAsync } from 'shared/hooks/useAsync'; + +import { useAppContext } from 'teleterm/ui/appContextProvider'; import Document from 'teleterm/ui/Document'; import { DocumentGateway } from 'teleterm/ui/services/workspacesService'; import { PortFieldInput } from '../components/FieldInputs'; import { FormFields, OfflineGateway } from '../components/OfflineGateway'; import { useGateway } from '../DocumentGateway/useGateway'; +import { retryWithRelogin } from '../utils'; import { AppGateway } from './AppGateway'; export function DocumentGatewayApp(props: { doc: DocumentGateway; visible: boolean; }) { - const { doc } = props; + const { doc, visible } = props; + const appCtx = useAppContext(); + const { tshd } = appCtx; const { gateway, defaultPort, @@ -58,6 +64,27 @@ export function DocumentGatewayApp(props: { formSchema = multiPortSchema; } + const [tcpPortsAttempt, getTcpPorts] = useAsync( + useCallback( + () => + retryWithRelogin(appCtx, doc.targetUri, () => + tshd + .getApp({ appUri: doc.targetUri }) + .then(({ response }) => response.app.tcpPorts) + ), + [appCtx, doc.targetUri, tshd] + ) + ); + + useEffect(() => { + // Fetch TCP ports, but only when the gateway points at a multi-port TCP app and when the tab is + // visible. This is so that if the user reopens a session with a lot of app gateways, we don't + // fetch all ports at once. + if (visible && isMultiPort && tcpPortsAttempt.status === '') { + void getTcpPorts(); + } + }, [visible, isMultiPort, tcpPortsAttempt, getTcpPorts]); + return ( {!connected ? ( @@ -98,6 +125,8 @@ export function DocumentGatewayApp(props: { changeLocalPortAttempt={changeLocalPortAttempt} changeTargetPort={changeTargetPort} changeTargetPortAttempt={changeTargetPortAttempt} + getTcpPorts={getTcpPorts} + tcpPortsAttempt={tcpPortsAttempt} /> )} From 18f49d7f5465174cc604c36b59273e05e5b7e24a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Mon, 20 Jan 2025 11:48:06 +0100 Subject: [PATCH 5/6] Allow opening multiple gateways for the same app but different target port * Ensure uniqueness of gateways in tshd * Add comments to a bunch of related types and functions * Refactor ClustersService.findGatewayByConnectionParams This makes it work with app gateways too. * Remove gatewayUri from TrackedGatewayConnection * Update document title on target port change * MenuLogin: Support custom ButtonComponent and buttonText * setUpAppGateway: Accept app URI instead of whole app This will let us call this function from places which do not have access to the whole app object. * useGateway: Update doc title after creating gateway * Change list of buttons with ports to MenuLogin with ports --- lib/teleterm/daemon/daemon.go | 11 + lib/teleterm/daemon/daemon_test.go | 51 +++- lib/teleterm/daemon/gateway.go | 66 +++++ .../shared/components/MenuLogin/MenuLogin.tsx | 8 +- .../shared/components/MenuLogin/types.ts | 6 + .../teleterm/src/services/tshd/app.ts | 4 +- .../teleterm/src/services/tshd/gateway.ts | 34 ++- .../src/ui/DocumentCluster/ActionButtons.tsx | 6 +- .../src/ui/DocumentGateway/useGateway.ts | 26 +- .../src/ui/DocumentGatewayApp/AppGateway.tsx | 131 +++++---- .../DocumentGatewayApp.story.tsx | 1 + .../DocumentGatewayApp/DocumentGatewayApp.tsx | 40 +-- .../DocumentGatewayCliClient.tsx | 8 +- .../DocumentGatewayKube.tsx | 7 +- .../DocumentTerminal/useDocumentTerminal.ts | 15 +- .../teleterm/src/ui/fixtures/mocks.ts | 10 +- .../ui/services/clusters/clustersService.ts | 52 ++-- ...> connectionTrackerService.legacy.test.ts} | 3 +- .../connectionTrackerService.test.tsx | 255 ++++++++++++++++++ .../connectionTrackerService.ts | 27 +- .../trackedConnectionOperationsFactory.ts | 40 ++- .../trackedConnectionUtils.ts | 96 ++++++- .../ui/services/connectionTracker/types.ts | 9 +- .../documentsService/connectToApp.test.ts | 18 +- .../documentsService/connectToApp.ts | 27 +- .../documentsService/documentsService.ts | 8 +- .../documentsService/documentsUtils.ts | 54 +++- .../documentsService/types.ts | 34 ++- 28 files changed, 843 insertions(+), 204 deletions(-) create mode 100644 lib/teleterm/daemon/gateway.go rename web/packages/teleterm/src/ui/services/connectionTracker/{connectionTrackerService.test.ts => connectionTrackerService.legacy.test.ts} (98%) create mode 100644 web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.tsx diff --git a/lib/teleterm/daemon/daemon.go b/lib/teleterm/daemon/daemon.go index 3848c43caa2a4..b9824064ba86b 100644 --- a/lib/teleterm/daemon/daemon.go +++ b/lib/teleterm/daemon/daemon.go @@ -334,6 +334,10 @@ func (s *Service) createGateway(ctx context.Context, params CreateGatewayParams) return gateway, nil } + if err := s.checkIfGatewayAlreadyExists(targetURI, params); err != nil { + return nil, trace.Wrap(err) + } + clusterClient, err := s.GetCachedClient(ctx, targetURI.GetClusterURI()) if err != nil { return nil, trace.Wrap(err) @@ -520,6 +524,13 @@ func (s *Service) SetGatewayTargetSubresourceName(ctx context.Context, gatewayUR return nil, trace.Wrap(err) } + if err := s.checkIfGatewayAlreadyExists(gateway.TargetURI(), CreateGatewayParams{ + TargetURI: gateway.TargetURI().String(), + TargetSubresourceName: targetSubresourceName, + }); err != nil { + return nil, trace.Wrap(err) + } + targetURI := gateway.TargetURI() switch { case targetURI.IsApp(): diff --git a/lib/teleterm/daemon/daemon_test.go b/lib/teleterm/daemon/daemon_test.go index 9c46057f7ac30..56c7c56d32b23 100644 --- a/lib/teleterm/daemon/daemon_test.go +++ b/lib/teleterm/daemon/daemon_test.go @@ -19,6 +19,7 @@ package daemon import ( + "cmp" "context" "errors" "net" @@ -78,11 +79,13 @@ func (m *mockGatewayCreator) CreateGateway(ctx context.Context, params clusters. KubernetesCluster: params.TargetURI.GetKubeName(), } + targetURI := params.TargetURI + config := gateway.Config{ LocalPort: params.LocalPort, TargetURI: params.TargetURI, TargetUser: params.TargetUser, - TargetName: params.TargetURI.GetDbName() + params.TargetURI.GetKubeName(), + TargetName: cmp.Or(targetURI.GetDbName(), targetURI.GetKubeName(), targetURI.GetAppName()), TargetSubresourceName: params.TargetSubresourceName, Protocol: defaults.ProtocolPostgres, Insecure: true, @@ -242,6 +245,52 @@ func TestGatewayCRUD(t *testing.T) { require.Equal(t, wantGateway, actualGateway) }, }, + { + name: "CreateGateway returns error if db gateway already exists", + gatewayNamesToCreate: []string{"gateway"}, + appendGatewayTargetURI: uri.NewClusterURI("foo").AppendDB, + testFunc: func(t *testing.T, c *gatewayCRUDTestContext, daemon *Service) { + createdGateway := c.nameToGateway["gateway"] + _, err := daemon.CreateGateway(context.Background(), CreateGatewayParams{ + TargetURI: createdGateway.TargetURI().String(), + TargetUser: createdGateway.TargetUser(), + }) + require.Error(t, err) + require.True(t, trace.IsAlreadyExists(err)) + }, + }, + { + name: "CreateGateway returns error if app gateway already exists", + gatewayNamesToCreate: []string{"gateway"}, + appendGatewayTargetURI: uri.NewClusterURI("foo").AppendApp, + testFunc: func(t *testing.T, c *gatewayCRUDTestContext, daemon *Service) { + createdGateway := c.nameToGateway["gateway"] + _, err := daemon.CreateGateway(context.Background(), CreateGatewayParams{ + TargetURI: createdGateway.TargetURI().String(), + TargetSubresourceName: createdGateway.TargetSubresourceName(), + }) + require.Error(t, err) + require.True(t, trace.IsAlreadyExists(err)) + }, + }, + { + name: "SetTargetSubresourceName returns error if db gateway already exists", + gatewayNamesToCreate: []string{"gateway"}, + appendGatewayTargetURI: uri.NewClusterURI("foo").AppendDB, + testFunc: func(t *testing.T, c *gatewayCRUDTestContext, daemon *Service) { + createdGateway := c.nameToGateway["gateway"] + _, err := daemon.CreateGateway(context.Background(), CreateGatewayParams{ + TargetURI: createdGateway.TargetURI().String(), + TargetSubresourceName: "4242", + }) + require.NoError(t, err) + + _, err = daemon.SetGatewayTargetSubresourceName(context.Background(), + createdGateway.URI().String(), "4242") + require.Error(t, err) + require.True(t, trace.IsAlreadyExists(err)) + }, + }, } for _, tt := range tests { diff --git a/lib/teleterm/daemon/gateway.go b/lib/teleterm/daemon/gateway.go new file mode 100644 index 0000000000000..d23c3279abb90 --- /dev/null +++ b/lib/teleterm/daemon/gateway.go @@ -0,0 +1,66 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package daemon + +import ( + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/lib/teleterm/api/uri" + "github.com/gravitational/teleport/lib/teleterm/gateway" +) + +func (s *Service) checkIfGatewayAlreadyExists(targetURI uri.ResourceURI, params CreateGatewayParams) error { + var resourceParamsCheckFunc func(g gateway.Gateway) error + + switch { + case targetURI.IsDB(): + resourceParamsCheckFunc = func(g gateway.Gateway) error { + if g.TargetUser() == params.TargetUser { + return trace.AlreadyExists("gateway for database %s and user %s already exists", targetURI.GetDbName(), params.TargetUser) + } + return nil + } + case targetURI.IsKube(): + // Return early for kubes as kube gateways depend on s.shouldReuseGateway. + return nil + case targetURI.IsApp(): + resourceParamsCheckFunc = func(g gateway.Gateway) error { + if g.TargetSubresourceName() == params.TargetSubresourceName { + if params.TargetSubresourceName != "" { + return trace.AlreadyExists("gateway for app %s and target port %s already exists", targetURI.GetAppName(), params.TargetSubresourceName) + } else { + return trace.AlreadyExists("gateway for app %s already exists", targetURI.GetAppName()) + } + } + return nil + } + default: + return trace.NotImplemented("gateway not supported for %s", targetURI.String()) + } + + for _, g := range s.gateways { + if g.TargetURI() != targetURI { + continue + } + + if err := resourceParamsCheckFunc(g); err != nil { + return trace.Wrap(err) + } + } + + return nil +} diff --git a/web/packages/shared/components/MenuLogin/MenuLogin.tsx b/web/packages/shared/components/MenuLogin/MenuLogin.tsx index 10038eb1e4605..be99278e42f1e 100644 --- a/web/packages/shared/components/MenuLogin/MenuLogin.tsx +++ b/web/packages/shared/components/MenuLogin/MenuLogin.tsx @@ -121,18 +121,20 @@ export const MenuLogin = React.forwardRef( }, })); + const ButtonComponent = props.ButtonComponent || ButtonBorder; + return ( - - Connect + {props.buttonText || 'Connect'} - + . */ +import { ComponentPropsWithRef, ComponentType } from 'react'; + +import { ButtonBorder } from 'design/Button'; + export type LoginItem = { url: string; login: string; @@ -42,6 +46,8 @@ export type MenuLoginProps = { placeholder?: string; required?: boolean; width?: string; + ButtonComponent?: ComponentType>; + buttonText?: string; }; export type MenuLoginHandle = { diff --git a/web/packages/teleterm/src/services/tshd/app.ts b/web/packages/teleterm/src/services/tshd/app.ts index 1d7c4a5c90c7e..18b7690a6acc5 100644 --- a/web/packages/teleterm/src/services/tshd/app.ts +++ b/web/packages/teleterm/src/services/tshd/app.ts @@ -114,10 +114,12 @@ export function getAppAddrWithProtocol(source: App): string { return addrWithProtocol; } +export const portRangeSeparator = '-'; + export const formatPortRange = (portRange: PortRange): string => portRange.endPort === 0 ? portRange.port.toString() - : `${portRange.port}-${portRange.endPort}`; + : `${portRange.port}${portRangeSeparator}${portRange.endPort}`; export const publicAddrWithTargetPort = (routeToApp: RouteToApp): string => routeToApp.targetPort diff --git a/web/packages/teleterm/src/services/tshd/gateway.ts b/web/packages/teleterm/src/services/tshd/gateway.ts index 266732bda6474..de2b8239694c0 100644 --- a/web/packages/teleterm/src/services/tshd/gateway.ts +++ b/web/packages/teleterm/src/services/tshd/gateway.ts @@ -16,7 +16,13 @@ * along with this program. If not, see . */ -import { GatewayTargetUri, routing } from 'teleterm/ui/uri'; +import { + GatewayTargetUri, + isAppUri, + isDatabaseUri, + isKubeUri, + routing, +} from 'teleterm/ui/uri'; import { GatewayCLICommand } from './types'; @@ -70,3 +76,29 @@ export function getTargetNameFromUri(targetUri: GatewayTargetUri): string { targetUri ); } + +/** + * getGatewayTargetUriKind is used when the callsite needs to distinguish between different kinds + * of targets that gateways support when given only its target URI. + */ +export function getGatewayTargetUriKind( + targetUri: string +): 'db' | 'kube' | 'app' { + if (isDatabaseUri(targetUri)) { + return 'db'; + } + + if (isKubeUri(targetUri)) { + return 'kube'; + } + + if (isAppUri(targetUri)) { + return 'app'; + } + + // TODO(ravicious): Optimally we'd use `targetUri satisfies never` here to have a type error when + // DocumentGateway['targetUri'] is changed. + // + // However, at the moment that field is essentially of type string, so there's not much we can do + // with regards to type safety. +} diff --git a/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.tsx b/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.tsx index 39147660e843e..842d265453d2b 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.tsx +++ b/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.tsx @@ -126,7 +126,11 @@ export function ConnectAppActionButton(props: { app: App }): React.JSX.Element { } function setUpGateway(targetPort?: number): void { - setUpAppGateway(appContext, props.app, { + if (!targetPort && props.app.tcpPorts.length > 0) { + targetPort = props.app.tcpPorts[0].port; + } + + setUpAppGateway(appContext, props.app.uri, { telemetry: { origin: 'resource_table' }, targetPort, }); diff --git a/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts b/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts index 8efac1231fa1a..d80c8cfacaf9f 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts +++ b/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts @@ -24,7 +24,10 @@ import { useAsync } from 'shared/hooks/useAsync'; import { useAppContext } from 'teleterm/ui/appContextProvider'; import { useWorkspaceContext } from 'teleterm/ui/Documents'; import { useStoreSelector } from 'teleterm/ui/hooks/useStoreSelector'; -import { DocumentGateway } from 'teleterm/ui/services/workspacesService'; +import { + DocumentGateway, + getDocumentGatewayTitle, +} from 'teleterm/ui/services/workspacesService'; import { isAppUri, isDatabaseUri } from 'teleterm/ui/uri'; import { retryWithRelogin } from 'teleterm/ui/utils'; @@ -66,8 +69,9 @@ export function useGateway(doc: DocumentGateway) { documentsService.update(doc.uri, { status: 'error' }); throw error; } - documentsService.update(doc.uri, { - gatewayUri: gw.uri, + documentsService.update(doc.uri, draft => { + const draftDoc = draft as DocumentGateway; + draftDoc.gatewayUri = gw.uri; // Set the port on doc to match the one returned from the daemon. By default, // createGateway is called with an empty localPort, so the daemon creates a listener on a // random port. @@ -77,11 +81,13 @@ export function useGateway(doc: DocumentGateway) { // // Alternatively, if createGateway was called from OfflineGateway, this will persist in // the doc the local port chosen by the user. - port: gw.localPort, + draftDoc.port = gw.localPort; // targetSubresourceName needs to be updated here in case the createGateway function was // called from OfflineGateway. - targetSubresourceName: gw.targetSubresourceName, - status: 'connected', + draftDoc.targetSubresourceName = gw.targetSubresourceName; + draftDoc.status = 'connected'; + // The title might need to be changed if OfflineGateway changed gateway params. + draftDoc.title = getDocumentGatewayTitle(draftDoc); }); if (isDatabaseUri(doc.targetUri)) { usageService.captureProtocolUse({ @@ -120,8 +126,12 @@ export function useGateway(doc: DocumentGateway) { name ); - documentsService.update(doc.uri, { - targetSubresourceName: updatedGateway.targetSubresourceName, + documentsService.update(doc.uri, draft => { + const draftDoc = draft as DocumentGateway; + + draftDoc.targetSubresourceName = + updatedGateway.targetSubresourceName; + draftDoc.title = getDocumentGatewayTitle(draftDoc); }); }), [ diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx index a18e9c40e72a9..f6b18799b65c6 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx @@ -20,6 +20,7 @@ import { ChangeEvent, ChangeEventHandler, PropsWithChildren, + useCallback, useEffect, useMemo, useRef, @@ -29,27 +30,31 @@ import styled from 'styled-components'; import { Alert, - Box, ButtonSecondary, disappear, Flex, H1, - Indicator, Link, rotate360, Text, } from 'design'; import { Check, Spinner } from 'design/Icon'; -import { PortRange } from 'gen-proto-ts/teleport/lib/teleterm/v1/app_pb'; import { Gateway } from 'gen-proto-ts/teleport/lib/teleterm/v1/gateway_pb'; +import { LoginItem, MenuLogin } from 'shared/components/MenuLogin'; import { TextSelectCopy } from 'shared/components/TextSelectCopy'; import Validation from 'shared/components/Validation'; -import { Attempt } from 'shared/hooks/useAsync'; +import { Attempt, useAsync } from 'shared/hooks/useAsync'; import { debounce } from 'shared/utils/highbar'; -import { formatPortRange } from 'teleterm/services/tshd/app'; - -import { PortFieldInput } from '../components/FieldInputs'; +import { + formatPortRange, + portRangeSeparator, +} from 'teleterm/services/tshd/app'; +import { useAppContext } from 'teleterm/ui/appContextProvider'; +import { PortFieldInput } from 'teleterm/ui/components/FieldInputs'; +import { useLogger } from 'teleterm/ui/hooks/useLogger'; +import { setUpAppGateway } from 'teleterm/ui/services/workspacesService'; +import { retryWithRelogin } from 'teleterm/ui/utils'; export function AppGateway(props: { gateway: Gateway; @@ -59,10 +64,12 @@ export function AppGateway(props: { changeTargetPort(port: string): void; changeTargetPortAttempt: Attempt; disconnect(): void; - getTcpPorts(): void; - tcpPortsAttempt: Attempt; }) { const { gateway } = props; + const ctx = useAppContext(); + const { tshd } = ctx; + const { targetUri } = gateway; + const logger = useLogger('AppGateway'); const { changeLocalPort, @@ -94,6 +101,53 @@ export function AppGateway(props: { gateway.protocol === 'TCP' && gateway.targetSubresourceName; const targetPortRef = useRef(null); + const [tcpPortsAttempt, getTcpPorts] = useAsync( + useCallback( + () => + retryWithRelogin(ctx, targetUri, () => + tshd + .getApp({ appUri: targetUri }) + .then(({ response }) => response.app.tcpPorts) + ), + [ctx, targetUri, tshd] + ) + ); + const currentTargetPort = parseInt(gateway.targetSubresourceName); + const getTcpPortsForMenuLogin: () => Promise = + useCallback(async () => { + const [tcpPorts, error] = await getTcpPorts(); + + if (error) { + throw error; + } + + return tcpPorts + .filter( + portRange => + // Filter out single-port port ranges that are equal to the current port. + portRange.endPort !== 0 || portRange.port != currentTargetPort + ) + .map(portRange => ({ + login: formatPortRange(portRange), + url: '', + })); + }, [getTcpPorts, currentTargetPort]); + + const onPortRangeSelect = (_, formattedPortRange: string) => { + const firstPort = formattedPortRange.split(portRangeSeparator)[0]; + const targetPort = parseInt(firstPort); + + if (Number.isNaN(targetPort)) { + logger.error('Not a number', firstPort); + return; + } + + setUpAppGateway(ctx, targetUri, { + telemetry: { origin: 'resource_table' }, + targetPort, + }); + }; + return (

App Connection

- - Close Connection - + + {isMultiPort && ( + + )} + + Close Connection + +
{disconnectAttempt.status === 'error' && ( @@ -148,32 +214,6 @@ export function AppGateway(props: { )}
- - {props.tcpPortsAttempt.status === 'success' && ( - - Available target ports:{' '} - {props.tcpPortsAttempt.data.map((portRange, index) => ( - { - const port = portRange.port.toString(); - targetPortRef.current.value = port; - changeTargetPort(port); - }} - > - {formatPortRange(portRange)} - - ))} - - )} - {props.tcpPortsAttempt.status === 'processing' && ( - - - - )} @@ -194,13 +234,8 @@ export function AppGateway(props: { )} - {props.tcpPortsAttempt.status === 'error' && ( - + {tcpPortsAttempt.status === 'error' && ( + Could not fetch available target ports )} @@ -242,7 +277,11 @@ const LabelWithAttemptStatus = (props: { // repeated when the user switches to this tab. // https://www.w3.org/TR/css-animations-1/#example-4e34d7ba - + )} diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx index 5e15e36eae582..b140fe1aac5c8 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx @@ -170,6 +170,7 @@ export function Story(props: StoryProps) { } else { let tcpPorts = [ { port: 1337, endPort: 4242 }, + { port: 4242, endPort: 0 }, { port: 17231, endPort: 0 }, { port: 27381, endPort: 28400 }, ]; diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx index 6d142417c4616..a49270c1274c4 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx @@ -15,28 +15,22 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import { ComponentProps, useCallback, useEffect } from 'react'; +import { ComponentProps } from 'react'; import { z } from 'zod'; -import { useAsync } from 'shared/hooks/useAsync'; - -import { useAppContext } from 'teleterm/ui/appContextProvider'; import Document from 'teleterm/ui/Document'; import { DocumentGateway } from 'teleterm/ui/services/workspacesService'; import { PortFieldInput } from '../components/FieldInputs'; import { FormFields, OfflineGateway } from '../components/OfflineGateway'; import { useGateway } from '../DocumentGateway/useGateway'; -import { retryWithRelogin } from '../utils'; import { AppGateway } from './AppGateway'; export function DocumentGatewayApp(props: { doc: DocumentGateway; visible: boolean; }) { - const { doc, visible } = props; - const appCtx = useAppContext(); - const { tshd } = appCtx; + const { doc } = props; const { gateway, defaultPort, @@ -64,29 +58,13 @@ export function DocumentGatewayApp(props: { formSchema = multiPortSchema; } - const [tcpPortsAttempt, getTcpPorts] = useAsync( - useCallback( - () => - retryWithRelogin(appCtx, doc.targetUri, () => - tshd - .getApp({ appUri: doc.targetUri }) - .then(({ response }) => response.app.tcpPorts) - ), - [appCtx, doc.targetUri, tshd] - ) - ); - - useEffect(() => { - // Fetch TCP ports, but only when the gateway points at a multi-port TCP app and when the tab is - // visible. This is so that if the user reopens a session with a lot of app gateways, we don't - // fetch all ports at once. - if (visible && isMultiPort && tcpPortsAttempt.status === '') { - void getTcpPorts(); - } - }, [visible, isMultiPort, tcpPortsAttempt, getTcpPorts]); - return ( - + diff --git a/web/packages/teleterm/src/ui/DocumentGatewayCliClient/DocumentGatewayCliClient.tsx b/web/packages/teleterm/src/ui/DocumentGatewayCliClient/DocumentGatewayCliClient.tsx index 2918f84f32819..61d018b54a67c 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayCliClient/DocumentGatewayCliClient.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayCliClient/DocumentGatewayCliClient.tsx @@ -50,10 +50,10 @@ export const DocumentGatewayCliClient = (props: { const { doc, visible } = props; const [hasRenderedTerminal, setHasRenderedTerminal] = useState(false); - const gateway = clustersService.findGatewayByConnectionParams( - doc.targetUri, - doc.targetUser - ); + const gateway = clustersService.findGatewayByConnectionParams({ + targetUri: doc.targetUri, + targetUser: doc.targetUser, + }); // Once we render the terminal, we want to keep it visible. Otherwise removing the gateway would // mean that this document would immediately unmount DocumentTerminal and close the PTY. diff --git a/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx b/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx index f6c98a145049e..fc3ec8ce81388 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayKube/DocumentGatewayKube.tsx @@ -54,10 +54,9 @@ export const DocumentGatewayKube = (props: { const ctx = useAppContext(); const { documentsService } = useWorkspaceContext(); const { params } = routing.parseKubeUri(doc.targetUri); - const gateway = ctx.clustersService.findGatewayByConnectionParams( - doc.targetUri, - '' - ); + const gateway = ctx.clustersService.findGatewayByConnectionParams({ + targetUri: doc.targetUri, + }); const connected = !!gateway; const [connectAttempt, createGateway] = useAsync(async () => { diff --git a/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts b/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts index 1245045473f0d..3b9e60c124db0 100644 --- a/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts +++ b/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts @@ -416,10 +416,10 @@ function createCmd( } if (doc.kind === 'doc.gateway_cli_client') { - const gateway = clustersService.findGatewayByConnectionParams( - doc.targetUri, - doc.targetUser - ); + const gateway = clustersService.findGatewayByConnectionParams({ + targetUri: doc.targetUri, + targetUser: doc.targetUser, + }); if (!gateway) { // This shouldn't happen as DocumentGatewayCliClient doesn't render DocumentTerminal before // the gateway is found. In any case, if it does happen for some reason, the user will see @@ -449,10 +449,9 @@ function createCmd( } if (doc.kind === 'doc.gateway_kube') { - const gateway = clustersService.findGatewayByConnectionParams( - doc.targetUri, - '' - ); + const gateway = clustersService.findGatewayByConnectionParams({ + targetUri: doc.targetUri, + }); if (!gateway) { throw new Error(`No gateway found for ${doc.targetUri}`); } diff --git a/web/packages/teleterm/src/ui/fixtures/mocks.ts b/web/packages/teleterm/src/ui/fixtures/mocks.ts index fad0cd858d520..08a15915d9b1a 100644 --- a/web/packages/teleterm/src/ui/fixtures/mocks.ts +++ b/web/packages/teleterm/src/ui/fixtures/mocks.ts @@ -45,18 +45,22 @@ export class MockAppContext extends AppContext { }); } - addRootClusterWithDoc(cluster: Cluster, doc: Document) { + addRootClusterWithDoc(cluster: Cluster, doc: Document | undefined) { this.clustersService.setState(draftState => { draftState.clusters.set(cluster.uri, cluster); }); this.workspacesService.setState(draftState => { draftState.rootClusterUri = cluster.uri; draftState.workspaces[cluster.uri] = { - documents: [doc], - location: doc.uri, + documents: [doc].filter(Boolean), + location: doc?.uri, localClusterUri: cluster.uri, accessRequests: undefined, }; }); } + + addRootCluster(cluster: Cluster) { + this.addRootClusterWithDoc(cluster, undefined); + } } diff --git a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts index bdd07e57cd7b5..aa33da6a1247d 100644 --- a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts +++ b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts @@ -39,6 +39,7 @@ import { type CloneableAbortSignal, type TshdClient, } from 'teleterm/services/tshd'; +import { getGatewayTargetUriKind } from 'teleterm/services/tshd/gateway'; import { AssumedRequest } from 'teleterm/services/tshd/types'; import { NotificationsService } from 'teleterm/ui/services/notifications'; import { UsageService } from 'teleterm/ui/services/usage'; @@ -561,7 +562,7 @@ export class ClustersService extends ImmutableStore // since we will no longer have to support old kube connections. // See call in `trackedConnectionOperationsFactory.ts` for more details. async removeKubeGateway(kubeUri: uri.KubeUri) { - const gateway = this.findGatewayByConnectionParams(kubeUri, ''); + const gateway = this.findGatewayByConnectionParams({ targetUri: kubeUri }); if (gateway) { await this.removeGateway(gateway.uri); } @@ -613,23 +614,44 @@ export class ClustersService extends ImmutableStore return this.state.gateways.get(gatewayUri); } - findGatewayByConnectionParams( - targetUri: uri.GatewayTargetUri, - targetUser: string - ) { - let found: Gateway; + findGatewayByConnectionParams({ + targetUri, + targetUser, + targetSubresourceName, + }: { + targetUri: uri.GatewayTargetUri; + targetUser?: string; + targetSubresourceName?: string; + }): Gateway | undefined { + const targetKind = getGatewayTargetUriKind(targetUri); + + for (const gateway of this.state.gateways.values()) { + if (gateway.targetUri !== targetUri) { + continue; + } - for (const [, gateway] of this.state.gateways) { - if ( - gateway.targetUri === targetUri && - gateway.targetUser === targetUser - ) { - found = gateway; - break; + switch (targetKind) { + case 'db': { + if (gateway.targetUser === targetUser) { + return gateway; + } + break; + } + case 'kube': { + // Kube gateways match only on targetUri. + return gateway; + } + case 'app': { + if (gateway.targetSubresourceName === targetSubresourceName) { + return gateway; + } + break; + } + default: { + targetKind satisfies never; + } } } - - return found; } /** diff --git a/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.ts b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.legacy.test.ts similarity index 98% rename from web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.ts rename to web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.legacy.test.ts index 1b465499c8902..c7a4c9c0795e4 100644 --- a/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.ts +++ b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.legacy.test.ts @@ -43,6 +43,8 @@ afterEach(() => { jest.restoreAllMocks(); }); +// TODO(ravicious): Rewrite those tests to use MockAppContext instead of manually mocking everything. + it('removeItemsBelongingToRootCluster removes connections', () => { jest.mock('../workspacesService'); @@ -75,7 +77,6 @@ it('removeItemsBelongingToRootCluster removes connections', () => { targetUser: 'alice', targetName: 'test', targetSubresourceName: 'pg', - gatewayUri: '/gateways/4f68927b-579c-47a8-b965-efa8159203c9', }, { kind: 'connection.kube', diff --git a/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.tsx b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.tsx new file mode 100644 index 0000000000000..f7963e45059b8 --- /dev/null +++ b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.test.tsx @@ -0,0 +1,255 @@ +/** + * Teleport + * Copyright (C) 2023 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'jest-canvas-mock'; + +import { within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { act, ComponentType, createRef } from 'react'; + +import { render, screen } from 'design/utils/testing'; +import { App } from 'gen-proto-ts/teleport/lib/teleterm/v1/app_pb'; + +import Logger, { NullService } from 'teleterm/logger'; +import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient'; +import { + makeApp, + makeAppGateway, + makeRootCluster, +} from 'teleterm/services/tshd/testHelpers'; +import { ResourcesContextProvider } from 'teleterm/ui/DocumentCluster/resourcesContext'; +import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; +import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; +import { DocumentsService } from 'teleterm/ui/services/workspacesService'; +import { TabHost } from 'teleterm/ui/TabHost'; +import { IAppContext } from 'teleterm/ui/types'; +import { unique } from 'teleterm/ui/utils'; + +import { TrackedGatewayConnection } from './types'; + +beforeAll(() => { + Logger.init(new NullService()); +}); + +test('updating target port creates new connection', async () => { + const user = userEvent.setup(); + const { ctx, docsService, app, Component } = setupTests(); + + const doc1 = docsService.createGatewayDocument({ + targetName: app.name, + targetUri: app.uri, + targetUser: undefined, + targetSubresourceName: '1337', + origin: 'resource_table', + }); + // Add without opening. It's not necessary and it'll be easier to verify activating connections + // later if we don't open the doc at this point. + docsService.add(doc1); + + render(); + + // Wait for the gateway to be created. + expect(await screen.findByText('Close Connection')).toBeInTheDocument(); + expect(ctx.connectionTracker.getConnections()).toHaveLength(1); + const conn1337 = ctx.connectionTracker.getConnections()[0]; + expect(conn1337.title).toEqual(`${app.name}:1337`); + + // Update target port. + let targetPortInput = screen.getByLabelText('Target Port'); + await user.clear(targetPortInput); + await user.type(targetPortInput, '4242'); + // We have to lose focus of that field, otherwise React is going to warn about updates not wrapped + // in act when the focus on the page changes after opening a new doc. + await user.tab(); + expect( + await screen.findByTitle('Target Port successfully updated', undefined, { + // There's a 1s debounce on port fields. + timeout: 2000, + }) + ).toBeInTheDocument(); + + // Verify connections. + expect(ctx.connectionTracker.getConnections()).toHaveLength(2); + const conn4242 = ctx.connectionTracker.getConnections()[1]; + expect(conn4242.id).not.toEqual(conn1337.id); + expect(conn4242.title).toEqual(`${app.name}:4242`); + + await act(async () => { + await ctx.connectionTracker.activateItem(conn4242.id, { + origin: 'resource_table', + }); + }); + expect(docsService.getLocation()).toEqual(doc1.uri); + + await act(async () => { + await ctx.connectionTracker.activateItem(conn1337.id, { + origin: 'resource_table', + }); + }); + expect(docsService.getDocuments()).toHaveLength(2); + expect(docsService.getLocation()).not.toEqual(doc1.uri); +}); + +test('updating target port to match connection params of gateway created by other doc is possible', async () => { + const user = userEvent.setup(); + const { ctx, docsService, app, Component } = setupTests(); + + const baseDocumentGatewayFields = { + targetName: app.name, + targetUri: app.uri, + targetUser: undefined, + origin: 'resource_table' as const, + }; + const doc1 = docsService.createGatewayDocument({ + ...baseDocumentGatewayFields, + targetSubresourceName: '1337', + }); + // Add without opening. It's not necessary and it'll be easier to verify activating connections + // later if we don't open the doc at this point. + docsService.add(doc1); + + render(); + + // Wait for the gateways to be created. + expect(await screen.findByText('Close Connection')).toBeInTheDocument(); + expect(ctx.connectionTracker.getConnections()).toHaveLength(1); + const conn1337Id = ctx.connectionTracker.getConnections()[0].id; + + // Create a second gateway. + const doc2 = docsService.createGatewayDocument({ + ...baseDocumentGatewayFields, + targetSubresourceName: '4242', + }); + await act(async () => { + docsService.add(doc2); + }); + const doc2Node = await screen.findByTestId(doc2.uri); + expect( + await within(doc2Node).findByText('Close Connection') + ).toBeInTheDocument(); + expect(ctx.connectionTracker.getConnections()).toHaveLength(2); + const conn4242Id = ctx.connectionTracker.getConnections()[1].id; + expect(conn4242Id).not.toEqual(conn1337Id); + + // Close the second gateway. + await user.click(within(doc2Node).getByText('Close Connection')); + expect(ctx.connectionTracker.findConnection(conn4242Id).connected).toBe( + false + ); + expect(ctx.connectionTracker.findConnection(conn1337Id).connected).toBe(true); + + // Update target port from 1337 to 4242. + let targetPortInput = screen.getByLabelText('Target Port'); + await user.clear(targetPortInput); + await user.type(targetPortInput, '4242'); + await user.tab(); + expect( + await screen.findByTitle('Target Port successfully updated', undefined, { + // There's a 1s debounce on port fields. + timeout: 2000, + }) + ).toBeInTheDocument(); + + // Verify that connection for 4242 is now connected and the connection for 1337 went offline. + expect(ctx.connectionTracker.getConnections()).toHaveLength(2); + const conn4242 = ctx.connectionTracker.findConnection( + conn4242Id + ) as TrackedGatewayConnection; + const conn1337 = ctx.connectionTracker.findConnection( + conn1337Id + ) as TrackedGatewayConnection; + expect(conn4242.connected).toBe(true); + expect(conn1337.connected).toBe(false); + // The ports are expected to be the same. We just changed doc with port 1337 to port 4242, so the + // corresponding connection has changed from conn1337 to conn4242. conn4242 got updated with the + // port set on doc1. + expect(conn4242).toBeTruthy(); + expect(conn4242.port).toEqual(conn1337.port); + + await act(async () => { + await ctx.connectionTracker.activateItem(conn4242Id, { + origin: 'resource_table', + }); + }); + expect(docsService.getLocation()).toEqual(doc1.uri); +}); + +function setupTests(): { + ctx: IAppContext; + docsService: DocumentsService; + app: App; + Component: ComponentType; +} { + const ctx = new MockAppContext(); + const rootCluster = makeRootCluster(); + ctx.addRootCluster(rootCluster); + ctx.workspacesService.setState(draft => { + draft.rootClusterUri = rootCluster.uri; + }); + + const docsService = ctx.workspacesService.getWorkspaceDocumentService( + rootCluster.uri + ); + + const app = makeApp({ + tcpPorts: [ + { port: 1337, endPort: 0 }, + { port: 4242, endPort: 0 }, + ], + endpointUri: 'tcp://localhost', + }); + + let gatewayLocalPort = 0; + jest.spyOn(ctx.tshd, 'createGateway').mockImplementation(async req => { + gatewayLocalPort++; + + return new MockedUnaryCall( + makeAppGateway({ + ...req, + protocol: 'TCP', + uri: `/gateways/${unique()}`, + localPort: req.localPort || gatewayLocalPort.toString(), + }) + ); + }); + jest + .spyOn(ctx.tshd, 'setGatewayTargetSubresourceName') + .mockImplementation(async req => { + const gateway = ctx.clustersService.findGateway(req.gatewayUri); + const updatedGateway = { + ...gateway, + targetSubresourceName: req.targetSubresourceName, + }; + + return new MockedUnaryCall(updatedGateway); + }); + jest + .spyOn(ctx.tshd, 'getApp') + .mockResolvedValue(new MockedUnaryCall({ app })); + + const ref = createRef(); + const Component = () => ( + + + + + + ); + + return { ctx, docsService, app, Component }; +} diff --git a/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts index 8e382f8a6299a..e444834106036 100644 --- a/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts +++ b/web/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts @@ -101,6 +101,10 @@ export class ConnectionTrackerService extends ImmutableStore c.id === id); + } + findConnectionByDocument(document: Document): TrackedConnection { switch (document.kind) { case 'doc.terminal_tsh_node': @@ -199,14 +203,17 @@ export class ConnectionTrackerService extends ImmutableStore { switch (i.kind) { case 'connection.gateway': { - i.connected = !!this._clusterService.findGateway(i.gatewayUri); + i.connected = !!this._clusterService.findGatewayByConnectionParams({ + targetUri: i.targetUri, + targetUser: i.targetUser, + targetSubresourceName: i.targetSubresourceName, + }); break; } case 'connection.kube': { - i.connected = !!this._clusterService.findGatewayByConnectionParams( - i.kubeUri, - '' - ); + i.connected = !!this._clusterService.findGatewayByConnectionParams({ + targetUri: i.kubeUri, + }); break; } default: { @@ -245,9 +252,11 @@ export class ConnectionTrackerService extends ImmutableStore { let gwDoc = documentsService .getDocuments() - .find(getGatewayDocumentByConnection(connection)); + .find(getGatewayDocumentByConnection(connection)) as DocumentGateway; if (!gwDoc) { + const gw = this._clustersService.findGatewayByConnectionParams({ + targetUri: connection.targetUri, + targetUser: connection.targetUser, + targetSubresourceName: connection.targetSubresourceName, + }); + gwDoc = documentsService.createGatewayDocument({ targetUri: connection.targetUri, targetName: connection.targetName, targetUser: connection.targetUser, targetSubresourceName: connection.targetSubresourceName, title: connection.title, - gatewayUri: connection.gatewayUri, + // If the doc was closed but the gateway is still running, it's important for the + // doc to reopen with the existing gateway URI. Otherwise the doc would attempt to + // create a new gateway with the same connection params. + gatewayUri: gw?.uri, port: connection.port, origin: params.origin, }); @@ -139,16 +149,22 @@ export class TrackedConnectionOperationsFactory { documentsService.open(gwDoc.uri); }, disconnect: async () => { - return this._clustersService - .removeGateway(connection.gatewayUri) - .then(() => { - documentsService - .getDocuments() - .filter(getGatewayDocumentByConnection(connection)) - .forEach(document => { - documentsService.close(document.uri); - }); - }); + // When disconnecting, assume that the gateway exists. If a gateway doesn't exist, the UI is + // supposed to expose the remove operation, not the disconnect operation. + const gw = this._clustersService.findGatewayByConnectionParams({ + targetUri: connection.targetUri, + targetUser: connection.targetUser, + targetSubresourceName: connection.targetSubresourceName, + }); + + return this._clustersService.removeGateway(gw.uri).then(() => { + documentsService + .getDocuments() + .filter(getGatewayDocumentByConnection(connection)) + .forEach(document => { + documentsService.close(document.uri); + }); + }); }, remove: async () => {}, }; diff --git a/web/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionUtils.ts b/web/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionUtils.ts index 7f724d3f38321..2fe91129e37cc 100644 --- a/web/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionUtils.ts +++ b/web/packages/teleterm/src/ui/services/connectionTracker/trackedConnectionUtils.ts @@ -17,26 +17,65 @@ */ import { + Document, DocumentGateway, DocumentGatewayKube, DocumentTshKube, DocumentTshNode, DocumentTshNodeWithServerId, + getDocumentGatewayTargetUriKind, isDocumentTshNodeWithServerId, } from 'teleterm/ui/services/workspacesService'; import { unique } from 'teleterm/ui/utils/uid'; import { + TrackedConnection, TrackedGatewayConnection, TrackedKubeConnection, TrackedServerConnection, } from './types'; -export function getGatewayConnectionByDocument(document: DocumentGateway) { - return (i: TrackedGatewayConnection) => - i.kind === 'connection.gateway' && - i.targetUri === document.targetUri && - i.targetUser === document.targetUser; +/* + * Getting a connection by a document. + */ + +/** + * + * getGatewayConnectionByDocument looks for a connection that has the same gateway params as the + * document. + * + * --- + * + * This function is used in two scenarios. It's used when recreating the list of connections based + * on open documents. If there's no connection found that matches DocumentGateway, a new connection + * is added to the list. + * + * It's also used when opening new gateways for databases and apps to find an existing connection + * and call it's `activate` handler, which is going to open an existing document. If no existing + * connection is found, a new document is added to the workspace. + */ +export function getGatewayConnectionByDocument( + document: DocumentGateway +): (c: TrackedConnection) => boolean { + const targetKind = getDocumentGatewayTargetUriKind(document.targetUri); + + switch (targetKind) { + case 'db': { + return c => + c.kind === 'connection.gateway' && + c.targetUri === document.targetUri && + c.targetUser === document.targetUser; + } + case 'app': { + return c => + c.kind === 'connection.gateway' && + c.targetUri === document.targetUri && + c.targetSubresourceName === document.targetSubresourceName; + } + default: { + targetKind satisfies never; + } + } } export function getServerConnectionByDocument(document: DocumentTshNode) { @@ -60,13 +99,49 @@ export function getGatewayKubeConnectionByDocument( i.kind === 'connection.kube' && i.kubeUri === document.targetUri; } +/* + * Getting a document by a connection. + */ + +/** + * getGatewayDocumentByConnection looks for a DocumentGateway that has the same gateway params as + * the connection. + * + * --- + * + * This function is used in two scenarios. It's used when activating (clicking) a connection in the + * connections list to find a document to open if there's already a gateway for the given connection. + * + * The `activate` handler is also called when the user attempts to open a gateway for a database or + * an app. That UI action first prepares a doc with provided gateway parameters. If there's a + * connection which matches the gateway parameters from the doc (getGatewayConnectionByDocument), + * its `activate` handler is called. + * + * The second scenario is when disconnecting a connection from the connections list to find a + * document which should be closed. + */ export function getGatewayDocumentByConnection( connection: TrackedGatewayConnection -) { - return (i: DocumentGateway) => - i.kind === 'doc.gateway' && - i.targetUri === connection.targetUri && - i.targetUser === connection.targetUser; +): (d: Document) => boolean { + const targetKind = getDocumentGatewayTargetUriKind(connection.targetUri); + + switch (targetKind) { + case 'db': { + return d => + d.kind === 'doc.gateway' && + d.targetUri === connection.targetUri && + d.targetUser === connection.targetUser; + } + case 'app': { + return d => + d.kind === 'doc.gateway' && + d.targetUri === connection.targetUri && + d.targetSubresourceName === connection.targetSubresourceName; + } + default: { + targetKind satisfies never; + } + } } export function getGatewayKubeDocumentByConnection( @@ -105,7 +180,6 @@ export function createGatewayConnection( targetUser: document.targetUser, targetName: document.targetName, targetSubresourceName: document.targetSubresourceName, - gatewayUri: document.gatewayUri, }; } diff --git a/web/packages/teleterm/src/ui/services/connectionTracker/types.ts b/web/packages/teleterm/src/ui/services/connectionTracker/types.ts index 8534f67ca72e2..a43af4c9b36c5 100644 --- a/web/packages/teleterm/src/ui/services/connectionTracker/types.ts +++ b/web/packages/teleterm/src/ui/services/connectionTracker/types.ts @@ -16,13 +16,7 @@ * along with this program. If not, see . */ -import { - AppUri, - DatabaseUri, - GatewayUri, - KubeUri, - ServerUri, -} from 'teleterm/ui/uri'; +import { AppUri, DatabaseUri, KubeUri, ServerUri } from 'teleterm/ui/uri'; type TrackedConnectionBase = { connected: boolean; @@ -43,7 +37,6 @@ export interface TrackedGatewayConnection extends TrackedConnectionBase { targetName: string; targetUser?: string; port?: string; - gatewayUri: GatewayUri; targetSubresourceName?: string; } diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.test.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.test.ts index 5397cbd2f8b92..542a0b7bf2f35 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.test.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.test.ts @@ -131,15 +131,8 @@ describe('setUpAppGateway', () => { endpointUri: 'tcp://localhost', tcpPorts: [{ port: 1234, endPort: 0 }], }), - expectedTargetSubresourceName: '1234', - }, - { - name: 'creates tunnel for a multi-port TCP app with a preselected target port', - app: makeApp({ - endpointUri: 'tcp://localhost', - tcpPorts: [{ port: 1234, endPort: 0 }], - }), targetPort: 1234, + expectedTitle: 'foo:1234', }, { name: 'creates tunnel for a web app', @@ -147,11 +140,11 @@ describe('setUpAppGateway', () => { endpointUri: 'http://localhost:3000', }), }, - ])('$name', async ({ app, targetPort, expectedTargetSubresourceName }) => { + ])('$name', async ({ app, targetPort, expectedTitle }) => { const appContext = new MockAppContext(); setTestCluster(appContext); - await setUpAppGateway(appContext, app, { + await setUpAppGateway(appContext, app.uri, { telemetry: { origin: 'resource_table' }, targetPort, }); @@ -166,11 +159,10 @@ describe('setUpAppGateway', () => { port: undefined, status: '', targetName: 'foo', - targetSubresourceName: - expectedTargetSubresourceName || targetPort?.toString() || undefined, + targetSubresourceName: targetPort?.toString(), targetUri: '/clusters/teleport-local/apps/foo', targetUser: '', - title: 'foo', + title: expectedTitle || 'foo', uri: expect.any(String), }); }); diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.ts index 2711bae403b0d..71fe3ce14d597 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.ts @@ -25,7 +25,7 @@ import { isWebApp, } from 'teleterm/services/tshd/app'; import { IAppContext } from 'teleterm/ui/types'; -import { routing } from 'teleterm/ui/uri'; +import { AppUri, routing } from 'teleterm/ui/uri'; import { DocumentOrigin } from './types'; @@ -115,35 +115,36 @@ export async function connectToApp( return; } - await setUpAppGateway(ctx, target, { telemetry }); + let targetPort: number; + if (target.tcpPorts.length > 0) { + targetPort = target.tcpPorts[0].port; + } + + await setUpAppGateway(ctx, target.uri, { telemetry, targetPort }); } export async function setUpAppGateway( ctx: IAppContext, - target: App, + targetUri: AppUri, options: { telemetry: { origin: DocumentOrigin }; /** - * targetPort allows the caller to preselect the target port for the gateway. Works only with - * multi-port TCP apps. If it's not specified and the app is multi-port, the first port from - * it's TCP ports is used instead. + * targetPort allows the caller to preselect the target port for the gateway. Should be passed + * only for multi-port TCP apps. */ targetPort?: number; } ) { - const rootClusterUri = routing.ensureRootClusterUri(target.uri); + const rootClusterUri = routing.ensureRootClusterUri(targetUri); const documentsService = ctx.workspacesService.getWorkspaceDocumentService(rootClusterUri); const doc = documentsService.createGatewayDocument({ - targetUri: target.uri, + targetUri: targetUri, origin: options.telemetry.origin, - targetName: routing.parseAppUri(target.uri).params.appId, + targetName: routing.parseAppUri(targetUri).params.appId, targetUser: '', - targetSubresourceName: - target.tcpPorts.length > 0 - ? (options.targetPort || target.tcpPorts[0].port).toString() - : undefined, + targetSubresourceName: options.targetPort?.toString(), }); const connectionToReuse = ctx.connectionTracker.findConnectionByDocument(doc); diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts index 79420a6cab0a1..cd7350996ae9a 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts @@ -29,6 +29,7 @@ import { } from 'teleterm/ui/uri'; import { unique } from 'teleterm/ui/utils/uid'; +import { getDocumentGatewayTitle } from './documentsUtils'; import { CreateAccessRequestDocumentOpts, CreateGatewayDocumentOpts, @@ -155,9 +156,8 @@ export class DocumentsService { origin, } = opts; const uri = routing.getDocUri({ docId: unique() }); - const title = targetUser ? `${targetUser}@${targetName}` : targetName; - return { + const doc: DocumentGateway = { uri, kind: 'doc.gateway', targetUri, @@ -165,11 +165,13 @@ export class DocumentsService { targetName, targetSubresourceName, gatewayUri, - title, + title: undefined, port, origin, status: '', }; + doc.title = getDocumentGatewayTitle(doc); + return doc; } createGatewayCliDocument({ diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts index 6bd654e01d963..9eb9008aa66f3 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsUtils.ts @@ -16,9 +16,18 @@ * along with this program. If not, see . */ -import { ClusterOrResourceUri, routing } from 'teleterm/ui/uri'; +import { + ClusterOrResourceUri, + isAppUri, + isDatabaseUri, + routing, +} from 'teleterm/ui/uri'; -import { Document, isDocumentTshNodeWithServerId } from './types'; +import { + Document, + DocumentGateway, + isDocumentTshNodeWithServerId, +} from './types'; /** * getResourceUri returns the URI of the cluster resource that is the subject of the document. @@ -62,3 +71,44 @@ export function getResourceUri( return undefined; } } + +/** + * getDocumentGatewayTargetUriKind is used when the callsite needs to distinguish between different + * kinds of targets that DocumentGateway supports when given only its target URI. + */ +export function getDocumentGatewayTargetUriKind( + targetUri: DocumentGateway['targetUri'] +): 'db' | 'app' { + if (isDatabaseUri(targetUri)) { + return 'db'; + } + + if (isAppUri(targetUri)) { + return 'app'; + } + + // TODO(ravicious): Optimally we'd use `targetUri satisfies never` here to have a type error when + // DocumentGateway['targetUri'] is changed. + // + // However, at the moment that field is essentially of type string, so there's not much we can do + // with regards to type safety. +} + +export function getDocumentGatewayTitle(doc: DocumentGateway): string { + const { targetName, targetUri, targetUser, targetSubresourceName } = doc; + const targetKind = getDocumentGatewayTargetUriKind(targetUri); + + switch (targetKind) { + case 'db': { + return targetUser ? `${targetUser}@${targetName}` : targetName; + } + case 'app': { + return targetSubresourceName + ? `${targetName}:${targetSubresourceName}` + : targetName; + } + default: { + targetKind satisfies never; + } + } +} diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts index c767a8efbddfe..d39f12167515d 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts @@ -101,21 +101,47 @@ export interface DocumentTshKube extends DocumentBase { origin: DocumentOrigin; } +/** + * DocumentGateway is used for database and app gateways. The two are distinguished by the kind of + * resource that targetUri points to. + */ export interface DocumentGateway extends DocumentBase { kind: 'doc.gateway'; - // status is used merely to show a progress bar when the gateway is being set up. + /** status is used merely to show a progress bar when the gateway is being set up. */ status: '' | 'connecting' | 'connected' | 'error'; + /** + * gatewayUri is not present until the gateway described by the document is created. + */ gatewayUri?: uri.GatewayUri; targetUri: uri.DatabaseUri | uri.AppUri; + /** + * targetUser is used only for db gateways and must contain the db user. Connect allows only a + * single doc.gateway to exist per targetUri + targetUser combo. + */ targetUser: string; + /** + * targetName contains the name of the target resource as shown in the UI. This field could be + * removed in favor of targetUri, which always includes the target name anyway. + */ targetName: string; /** * targetSubresourceName contains database name for db gateways and target port for TCP app - * gateways. - * A DocumentGateway created for a multi-port TCP app is expected to always have this field - * present. + * gateways. A DocumentGateway created for a multi-port TCP app is expected to always have this + * field present. + * + * For app gateways, Connect allows only a single doc.gateway to exist per targetUri + + * targetSubresourceName combo. + * + * For db gateways, targetSubresourceName is not taken into account when considering document + * "uniqueness". */ targetSubresourceName: string | undefined; + /** + * port is the local port on which the gateway accepts connections. + * + * If empty, tshd is going to created a listener on a random port and then this field will be + * updated to match that random port. + */ port?: string; origin: DocumentOrigin; } From 29cd5a87f3024c0179724a6913e80d286646c138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Mon, 20 Jan 2025 13:29:33 +0100 Subject: [PATCH 6/6] make grpc --- .../go/teleport/lib/teleterm/v1/service.pb.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go index 33bc0bb10b906..6f51534ebec7c 100644 --- a/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go +++ b/gen/proto/go/teleport/lib/teleterm/v1/service.pb.go @@ -4036,10 +4036,11 @@ func (x *AuthenticateWebDeviceResponse) GetConfirmationToken() *v12.DeviceConfir } type GetAppRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - AppUri string `protobuf:"bytes,1,opt,name=app_uri,json=appUri,proto3" json:"app_uri,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AppUri string `protobuf:"bytes,1,opt,name=app_uri,json=appUri,proto3" json:"app_uri,omitempty"` } func (x *GetAppRequest) Reset() { @@ -4080,10 +4081,11 @@ func (x *GetAppRequest) GetAppUri() string { } type GetAppResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - App *App `protobuf:"bytes,1,opt,name=app,proto3" json:"app,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + App *App `protobuf:"bytes,1,opt,name=app,proto3" json:"app,omitempty"` } func (x *GetAppResponse) Reset() {