diff --git a/tool/teleport/testenv/test_server.go b/tool/teleport/testenv/test_server.go index 151e3bba113fa..7c2cf2ae52415 100644 --- a/tool/teleport/testenv/test_server.go +++ b/tool/teleport/testenv/test_server.go @@ -257,6 +257,10 @@ func waitForServices(t *testing.T, auth *service.TeleportProcess, cfg *servicecf if cfg.Auth.Enabled && cfg.Databases.Enabled { waitForDatabases(t, auth, cfg.Databases.Databases) } + + if cfg.Auth.Enabled && cfg.Apps.Enabled { + waitForApps(t, auth, cfg.Apps.Apps) + } } func waitForEvents(t *testing.T, svc service.Supervisor, events ...string) { @@ -295,6 +299,34 @@ func waitForDatabases(t *testing.T, auth *service.TeleportProcess, dbs []service } } +func waitForApps(t *testing.T, auth *service.TeleportProcess, apps []servicecfg.App) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + for { + select { + case <-time.After(500 * time.Millisecond): + all, err := auth.GetAuthServer().GetApplicationServers(ctx, apidefaults.Namespace) + require.NoError(t, err) + + var registered int + for _, app := range apps { + for _, a := range all { + if a.GetName() == app.Name { + registered++ + break + } + } + } + + if registered == len(apps) { + return + } + case <-ctx.Done(): + t.Fatal("Apps not registered after 10s") + } + } +} + type TestServersOpts struct { Bootstrap []types.Resource ConfigFuncs []func(cfg *servicecfg.Config) diff --git a/tool/tsh/common/app.go b/tool/tsh/common/app.go index eb7964e43a33f..18e4d78ea92f4 100644 --- a/tool/tsh/common/app.go +++ b/tool/tsh/common/app.go @@ -32,6 +32,7 @@ import ( "github.com/gravitational/trace" "github.com/gravitational/teleport" + apiclient "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/types" @@ -49,12 +50,36 @@ func onAppLogin(cf *CLIConf) error { return trace.Wrap(err) } - appInfo, err := getAppInfo(cf, tc, nil /*matchRouteToApp*/) - if err != nil { + var ( + clusterClient *client.ClusterClient + appInfo *appInfo + app types.Application + ) + if err := client.RetryWithRelogin(cf.Context, tc, func() error { + var err error + profile, err := tc.ProfileStatus() + if err != nil { + return trace.Wrap(err) + } + + clusterClient, err = tc.ConnectToCluster(cf.Context) + if err != nil { + return trace.Wrap(err) + } + + appInfo, err = getAppInfo(cf, clusterClient.AuthClient, profile, tc.SiteName, nil /*matchRouteToApp*/) + if err != nil { + return trace.Wrap(err) + } + + app, err = appInfo.GetApp(cf.Context, clusterClient.AuthClient) + return trace.Wrap(err) + }); err != nil { return trace.Wrap(err) } + defer clusterClient.Close() - app, err := appInfo.GetApp(cf.Context, tc) + rootClient, err := clusterClient.ConnectToRootCluster(cf.Context) if err != nil { return trace.Wrap(err) } @@ -65,15 +90,6 @@ func onAppLogin(cf *CLIConf) error { AccessRequests: appInfo.profile.ActiveRequests.AccessRequests, } - clusterClient, err := tc.ConnectToCluster(cf.Context) - if err != nil { - return trace.Wrap(err) - } - rootClient, err := clusterClient.ConnectToRootCluster(cf.Context) - if err != nil { - return trace.Wrap(err) - } - key, err := appLogin(cf.Context, tc, clusterClient, rootClient, appCertParams) if err != nil { return trace.Wrap(err) @@ -186,7 +202,6 @@ func printAppCommand(cf *CLIConf, tc *client.TeleportClient, app types.Applicati if err != nil { return trace.Wrap(err) } - curlCmd, err := formatAppConfig(tc, profile, routeToApp, appFormatCURL) if err != nil { return trace.Wrap(err) @@ -483,19 +498,10 @@ func serializeAppConfig(configInfo *appConfigInfo, format string) (string, error } // getAppInfo fetches app information using the user's tsh profile, -// command line args, and the ListApps endpoint if necessary. If +// command line args, and the list resources endpoint if necessary. If // provided, the matcher will be used to filter active apps in the -// tsh profile. getAppInfo will also perform re-login if necessary. -func getAppInfo(cf *CLIConf, tc *client.TeleportClient, matchRouteToApp func(tlsca.RouteToApp) bool) (*appInfo, error) { - var profile *client.ProfileStatus - if err := client.RetryWithRelogin(cf.Context, tc, func() error { - var err error - profile, err = tc.ProfileStatus() - return trace.Wrap(err) - }); err != nil { - return nil, trace.Wrap(err) - } - +// tsh profile. +func getAppInfo(cf *CLIConf, clt authclient.ClientI, profile *client.ProfileStatus, siteName string, matchRouteToApp func(tlsca.RouteToApp) bool) (*appInfo, error) { activeRoutes := profile.Apps if matchRouteToApp != nil { var filteredRoutes []tlsca.RouteToApp @@ -518,17 +524,21 @@ func getAppInfo(cf *CLIConf, tc *client.TeleportClient, matchRouteToApp func(tls } // If we didn't find an active profile for the app, get info from server. - app, err := getApp(cf.Context, tc, cf.AppName) + app, logins, err := getApp(cf.Context, clt, cf.AppName) if err != nil { return nil, trace.Wrap(err) } + if len(logins) == 0 && app.IsAWSConsole() { + logins = getARNFromRoles(cf, clt, profile, siteName, app) + } + appInfo := &appInfo{ profile: profile, RouteToApp: proto.RouteToApp{ Name: app.GetName(), PublicAddr: app.GetPublicAddr(), - ClusterName: tc.SiteName, + ClusterName: siteName, URI: app.GetURI(), }, app: app, @@ -537,7 +547,7 @@ func getAppInfo(cf *CLIConf, tc *client.TeleportClient, matchRouteToApp func(tls // If this is a cloud app, set additional applicable fields from CLI flags or roles. switch { case app.IsAWSConsole(): - awsRoleARN, err := getARNFromFlags(cf, profile, app) + awsRoleARN, err := getARNFromFlags(cf, app, logins) if err != nil { return nil, trace.Wrap(err) } @@ -585,14 +595,14 @@ func (a *appInfo) appLocalCAPath(cluster string) string { // GetApp returns the cached app or fetches it using the app route and // caches the result. -func (a *appInfo) GetApp(ctx context.Context, tc *client.TeleportClient) (types.Application, error) { +func (a *appInfo) GetApp(ctx context.Context, clt apiclient.GetResourcesClient) (types.Application, error) { a.appMu.Lock() defer a.appMu.Unlock() if a.app != nil { return a.app.Copy(), nil } // holding mutex across the api call to avoid multiple redundant api calls. - app, err := getApp(ctx, tc, a.Name) + app, _, err := getApp(ctx, clt, a.Name) if err != nil { return nil, trace.Wrap(err) } @@ -601,23 +611,30 @@ func (a *appInfo) GetApp(ctx context.Context, tc *client.TeleportClient) (types. } // getApp returns the registered application with the specified name. -func getApp(ctx context.Context, tc *client.TeleportClient, name string) (app types.Application, err error) { - var apps []types.Application - err = client.RetryWithRelogin(ctx, tc, func() error { - apps, err = tc.ListApps(ctx, &proto.ListResourcesRequest{ - Namespace: tc.Namespace, - ResourceType: types.KindAppServer, - PredicateExpression: fmt.Sprintf(`name == "%s"`, name), - }) - return trace.Wrap(err) +func getApp(ctx context.Context, clt apiclient.GetResourcesClient, name string) (app types.Application, logins []string, err error) { + // When listing a single app we only need to grab one page. + res, err := apiclient.GetEnrichedResourcePage(ctx, clt, &proto.ListResourcesRequest{ + ResourceType: types.KindAppServer, + SortBy: types.SortBy{Field: types.ResourceMetadataName}, + PredicateExpression: fmt.Sprintf(`name == "%s"`, name), + Limit: 1, + IncludeLogins: true, }) if err != nil { - return nil, trace.Wrap(err) + return nil, nil, trace.Wrap(err) } - if len(apps) == 0 { - return nil, trace.NotFound("app %q not found, use `tsh apps ls` to see registered apps", name) + + if len(res.Resources) == 0 { + return nil, nil, trace.NotFound("app %q not found, use `tsh apps ls` to see registered apps", name) } - return apps[0], nil + + appServer, ok := res.Resources[0].ResourceWithLabels.(types.AppServer) + if !ok { + log.Warnf("expected types.AppServer but received unexpected type %T", res.Resources[0].ResourceWithLabels) + return nil, nil, trace.NotFound("app %q not found, use `tsh apps ls` to see registered apps", name) + } + + return appServer.GetApp(), res.Resources[0].Logins, nil } // pickActiveApp returns the app the current profile is logged into. diff --git a/tool/tsh/common/app_aws.go b/tool/tsh/common/app_aws.go index 7a05bdb271916..0869d5c09cc01 100644 --- a/tool/tsh/common/app_aws.go +++ b/tool/tsh/common/app_aws.go @@ -21,6 +21,7 @@ package common import ( "context" "fmt" + "io" "os" "os/exec" "strings" @@ -34,6 +35,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/asciitable" "github.com/gravitational/teleport/lib/client" + "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/srv/alpnproxy" "github.com/gravitational/teleport/lib/tlsca" awsutils "github.com/gravitational/teleport/lib/utils/aws" @@ -284,7 +286,7 @@ func (a *awsApp) RunCommand(cmd *exec.Cmd) error { return nil } -func printAWSRoles(roles awsutils.Roles) { +func printAWSRoles(w io.Writer, roles awsutils.Roles) { if len(roles) == 0 { return } @@ -296,14 +298,14 @@ func printAWSRoles(roles awsutils.Roles) { t.AddRow([]string{role.Display, role.ARN}) } - fmt.Println("Available AWS roles:") - fmt.Println(t.AsBuffer().String()) + fmt.Fprintln(w, "Available AWS roles:") + fmt.Fprintln(w, t.AsBuffer().String()) } -func getARNFromFlags(cf *CLIConf, profile *client.ProfileStatus, app types.Application) (string, error) { +func getARNFromFlags(cf *CLIConf, app types.Application, logins []string) (string, error) { // Filter AWS roles by AWS account ID. If AWS account ID is empty, all // roles are returned. - roles := awsutils.FilterAWSRoles(profile.AWSRolesARNs, app.GetAWSAccountID()) + roles := awsutils.FilterAWSRoles(logins, app.GetAWSAccountID()) if cf.AWSRole == "" { if len(roles) == 1 { @@ -311,7 +313,7 @@ func getARNFromFlags(cf *CLIConf, profile *client.ProfileStatus, app types.Appli return roles[0].ARN, nil } - printAWSRoles(roles) + printAWSRoles(cf.Stdout(), roles) return "", trace.BadParameter("--aws-role flag is required") } @@ -321,7 +323,7 @@ func getARNFromFlags(cf *CLIConf, profile *client.ProfileStatus, app types.Appli return role.ARN, nil } - printAWSRoles(roles) + printAWSRoles(cf.Stdout(), roles) return "", trace.NotFound("failed to find the %q role ARN", cf.AWSRole) } @@ -331,15 +333,38 @@ func getARNFromFlags(cf *CLIConf, profile *client.ProfileStatus, app types.Appli case 1: return rolesMatched[0].ARN, nil case 0: - printAWSRoles(roles) + printAWSRoles(cf.Stdout(), roles) return "", trace.NotFound("failed to find the %q role name", cf.AWSRole) default: // Print roles matched the provided role name. - printAWSRoles(rolesMatched) + printAWSRoles(cf.Stdout(), rolesMatched) return "", trace.BadParameter("provided role name %q is ambiguous, please specify full role ARN", cf.AWSRole) } } +// getARNFromRoles fetches the available AWS ARNs logins for given app. +// If any step of fetching the roles ARNs fail, fallback into returning the +// profile ARNs. +// +// TODO(gabrielcorado): DELETE IN V18.0.0 +// This is here for backward compatibility in case the auth server +// does not support enriched resources yet. +func getARNFromRoles(cf *CLIConf, roleGetter services.CurrentUserRoleGetter, profile *client.ProfileStatus, siteName string, app types.Application) []string { + accessChecker, err := services.NewAccessCheckerForRemoteCluster(cf.Context, profile.AccessInfo(), siteName, roleGetter) + if err != nil { + log.WithError(err).Debugf("Failed to fetch user roles.") + return profile.AWSRolesARNs + } + + logins, err := accessChecker.GetAllowedLoginsForResource(app) + if err != nil { + log.WithError(err).Debugf("Failed to fetch app logins.") + return profile.AWSRolesARNs + } + + return logins +} + func matchAWSApp(app tlsca.RouteToApp) bool { return app.AWSRoleARN != "" } @@ -350,8 +375,23 @@ func pickAWSApp(cf *CLIConf) (*awsApp, error) { return nil, trace.Wrap(err) } - appInfo, err := getAppInfo(cf, tc, matchAWSApp) - if err != nil { + var appInfo *appInfo + if err := client.RetryWithRelogin(cf.Context, tc, func() error { + var err error + profile, err := tc.ProfileStatus() + if err != nil { + return trace.Wrap(err) + } + + clusterClient, err := tc.ConnectToCluster(cf.Context) + if err != nil { + return trace.Wrap(err) + } + defer clusterClient.Close() + + appInfo, err = getAppInfo(cf, clusterClient.AuthClient, profile, tc.SiteName, matchAWSApp) + return trace.Wrap(err) + }); err != nil { return nil, trace.Wrap(err) } diff --git a/tool/tsh/common/app_aws_test.go b/tool/tsh/common/app_aws_test.go index bbf6d4e087a9a..e00fd33ce393a 100644 --- a/tool/tsh/common/app_aws_test.go +++ b/tool/tsh/common/app_aws_test.go @@ -19,6 +19,7 @@ package common import ( + "bytes" "context" "crypto/tls" "os/exec" @@ -27,14 +28,13 @@ import ( "github.com/stretchr/testify/require" - "github.com/gravitational/teleport/api/breaker" "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" apiutils "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/lib" - "github.com/gravitational/teleport/lib/service" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" + testserver "github.com/gravitational/teleport/tool/teleport/testenv" ) func TestAWS(t *testing.T) { @@ -44,17 +44,25 @@ func TestAWS(t *testing.T) { connector := mockConnector(t) user, awsRole := makeUserWithAWSRole(t) - - authProcess, proxyProcess := makeTestServers(t, withBootstrap(connector, user, awsRole)) - makeTestApplicationServer(t, proxyProcess, servicecfg.App{ - Name: "aws-app", - URI: constants.AWSConsoleURL, - }) + authProcess := testserver.MakeTestServer( + t, + testserver.WithBootstrap(connector, user, awsRole), + testserver.WithConfig(func(cfg *servicecfg.Config) { + cfg.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex) + cfg.Apps.Enabled = true + cfg.Apps.Apps = []servicecfg.App{ + { + Name: "aws-app", + URI: constants.AWSConsoleURL, + }, + } + }), + ) authServer := authProcess.GetAuthServer() require.NotNil(t, authServer) - proxyAddr, err := proxyProcess.ProxyWebAddr() + proxyAddr, err := authProcess.ProxyWebAddr() require.NoError(t, err) // Log into Teleport cluster. @@ -176,6 +184,110 @@ func TestAWS(t *testing.T) { }) } +// TestAWSConsoleLogins given a AWS console application, execute a app login +// without proving a role ARN and verify the provided list of available logins +// is correct. +func TestAWSConsoleLogins(t *testing.T) { + t.Parallel() + ctx := context.Background() + isInsecure := lib.IsInsecureDevMode() + lib.SetInsecureDevMode(true) + t.Cleanup(func() { + lib.SetInsecureDevMode(isInsecure) + }) + tmpHomePath := t.TempDir() + connector := mockConnector(t) + + userARNs := []string{"arn:aws:iam::111111111111:role/user-1", "arn:aws:iam::111111111111:role/user-2"} + rootARNs := []string{"arn:aws:iam::111111111111:role/root-1", "arn:aws:iam::111111111111:role/root-2"} + rootAWSRole, err := types.NewRole("aws", types.RoleSpecV6{ + Allow: types.RoleConditions{ + AppLabels: types.Labels{types.Wildcard: apiutils.Strings{types.Wildcard}}, + AWSRoleARNs: rootARNs, + }, + }) + require.NoError(t, err) + user, err := types.NewUser("alice@example.com") + require.NoError(t, err) + user.SetRoles([]string{"access", rootAWSRole.GetName()}) + user.SetAWSRoleARNs(userARNs) + rootServer := testserver.MakeTestServer( + t, + testserver.WithClusterName(t, "root"), + testserver.WithBootstrap(connector, user, rootAWSRole), + testserver.WithConfig(func(cfg *servicecfg.Config) { + cfg.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex) + cfg.Apps.Enabled = true + cfg.Apps.Apps = []servicecfg.App{ + { + Name: "awsconsole", + URI: constants.AWSConsoleURL, + }, + } + }), + ) + + leafARNs := []string{"arn:aws:iam::999999999999:role/leaf-1", "arn:aws:iam::999999999999:role/leaf-2"} + leafAWSRole, err := types.NewRole("aws", types.RoleSpecV6{ + Allow: types.RoleConditions{ + AppLabels: types.Labels{types.Wildcard: apiutils.Strings{types.Wildcard}}, + AWSRoleARNs: leafARNs, + }, + }) + require.NoError(t, err) + leafServer := testserver.MakeTestServer( + t, + testserver.WithClusterName(t, "leaf"), + testserver.WithBootstrap(leafAWSRole), + testserver.WithConfig(func(cfg *servicecfg.Config) { + cfg.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex) + cfg.Apps.Enabled = true + cfg.Apps.Apps = []servicecfg.App{ + { + Name: "awsconsole", + URI: constants.AWSConsoleURL, + }, + } + }), + ) + testserver.SetupTrustedCluster(ctx, t, rootServer, leafServer, types.RoleMapping{Remote: "aws", Local: []string{"aws"}}) + + authServer := rootServer.GetAuthServer() + require.NotNil(t, authServer) + + proxyAddr, err := rootServer.ProxyWebAddr() + require.NoError(t, err) + + // Log into Teleport cluster. + err = Run(context.Background(), []string{ + "login", "--insecure", "--debug", "--proxy", proxyAddr.String(), + }, setHomePath(tmpHomePath), setMockSSOLogin(authServer, user, connector.GetName())) + require.NoError(t, err) + + for cluster, expectedARNs := range map[string][]string{ + "root": append(userARNs, rootARNs...), + "leaf": append(leafARNs, append(userARNs, rootARNs...)...), + } { + t.Run(cluster, func(t *testing.T) { + commandOutput := new(bytes.Buffer) + // Don't provide the `--aws-role`. We expect a failure since there + // are multiple ARN roles. + err := Run( + context.Background(), + []string{"app", "login", "--insecure", "--cluster", cluster, "awsconsole"}, + setCopyStdout(commandOutput), setHomePath(tmpHomePath), + // TODO(gabrielcorado): Given the `RetryWithRerlLogin` is going + // to perform a relogin for BadParameter error, we need to + // provide login mock here. Once the function is fixed and + // only retry `Retry` errors, this can be removed. + setMockSSOLogin(authServer, user, connector.GetName()), + ) + require.ErrorContains(t, err, "--aws-role flag is required") + require.Regexp(t, strings.Join(expectedARNs, "|"), commandOutput.String(), "mismatch on expected roles") + }) + } +} + func makeUserWithAWSRole(t *testing.T) (types.User, types.Role) { alice, err := types.NewUser("alice@example.com") require.NoError(t, err) @@ -196,35 +308,3 @@ func makeUserWithAWSRole(t *testing.T) (types.User, types.Role) { alice.SetRoles([]string{"access", awsRole.GetName()}) return alice, awsRole } - -// deprecated: Use `tools/teleport/testenv.MakeTestServer` instead. -func makeTestApplicationServer(t *testing.T, proxy *service.TeleportProcess, apps ...servicecfg.App) *service.TeleportProcess { - // Proxy uses self-signed certificates in tests. - lib.SetInsecureDevMode(true) - - cfg := servicecfg.MakeDefaultConfig() - cfg.Hostname = "localhost" - cfg.DataDir = t.TempDir() - cfg.CircuitBreakerConfig = breaker.NoopBreakerConfig() - - proxyAddr, err := proxy.ProxyWebAddr() - require.NoError(t, err) - - cfg.SetAuthServerAddress(*proxyAddr) - - token, err := proxy.Config.Token() - require.NoError(t, err) - - cfg.SetToken(token) - cfg.SSH.Enabled = false - cfg.Auth.Enabled = false - cfg.Proxy.Enabled = false - cfg.Apps.Enabled = true - cfg.Apps.Apps = apps - cfg.Log = utils.NewLoggerForTests() - // Disabling debug service for tests so that it doesn't break if the data - // directory path is too long. - cfg.DebugService.Enabled = false - - return runTeleport(t, cfg) -} diff --git a/tool/tsh/common/app_azure.go b/tool/tsh/common/app_azure.go index c547650e667d7..5032f38b746bd 100644 --- a/tool/tsh/common/app_azure.go +++ b/tool/tsh/common/app_azure.go @@ -274,8 +274,23 @@ func pickAzureApp(cf *CLIConf) (*azureApp, error) { return nil, trace.Wrap(err) } - appInfo, err := getAppInfo(cf, tc, matchAzureApp) - if err != nil { + var appInfo *appInfo + if err := client.RetryWithRelogin(cf.Context, tc, func() error { + var err error + profile, err := tc.ProfileStatus() + if err != nil { + return trace.Wrap(err) + } + + clusterClient, err := tc.ConnectToCluster(cf.Context) + if err != nil { + return trace.Wrap(err) + } + defer clusterClient.Close() + + appInfo, err = getAppInfo(cf, clusterClient.AuthClient, profile, tc.SiteName, matchAzureApp) + return trace.Wrap(err) + }); err != nil { return nil, trace.Wrap(err) } diff --git a/tool/tsh/common/app_azure_test.go b/tool/tsh/common/app_azure_test.go index 9497b8264d089..653ac84a20784 100644 --- a/tool/tsh/common/app_azure_test.go +++ b/tool/tsh/common/app_azure_test.go @@ -34,28 +34,41 @@ import ( "github.com/stretchr/testify/require" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/utils" + testserver "github.com/gravitational/teleport/tool/teleport/testenv" ) func TestAzure(t *testing.T) { + lib.SetInsecureDevMode(true) tmpHomePath := t.TempDir() connector := mockConnector(t) user, azureRole := makeUserWithAzureRole(t) - authProcess, proxyProcess := makeTestServers(t, withBootstrap(connector, user, azureRole)) - makeTestApplicationServer(t, proxyProcess, servicecfg.App{ - Name: "azure-api", - Cloud: types.CloudAzure, - }) + authProcess := testserver.MakeTestServer( + t, + testserver.WithClusterName(t, "localhost"), + testserver.WithBootstrap(connector, user, azureRole), + testserver.WithConfig(func(cfg *servicecfg.Config) { + cfg.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex) + cfg.Apps.Enabled = true + cfg.Apps.Apps = []servicecfg.App{ + { + Name: "azure-api", + Cloud: types.CloudAzure, + }, + } + }), + ) authServer := authProcess.GetAuthServer() require.NotNil(t, authServer) - proxyAddr, err := proxyProcess.ProxyWebAddr() + proxyAddr, err := authProcess.ProxyWebAddr() require.NoError(t, err) // helper function diff --git a/tool/tsh/common/app_gcp.go b/tool/tsh/common/app_gcp.go index 641a13d4a336d..faedb4608f725 100644 --- a/tool/tsh/common/app_gcp.go +++ b/tool/tsh/common/app_gcp.go @@ -369,8 +369,23 @@ func pickGCPApp(cf *CLIConf) (*gcpApp, error) { return nil, trace.Wrap(err) } - appInfo, err := getAppInfo(cf, tc, matchGCPApp) - if err != nil { + var appInfo *appInfo + if err := client.RetryWithRelogin(cf.Context, tc, func() error { + var err error + profile, err := tc.ProfileStatus() + if err != nil { + return trace.Wrap(err) + } + + clusterClient, err := tc.ConnectToCluster(cf.Context) + if err != nil { + return trace.Wrap(err) + } + defer clusterClient.Close() + + appInfo, err = getAppInfo(cf, clusterClient.AuthClient, profile, tc.SiteName, matchGCPApp) + return trace.Wrap(err) + }); err != nil { return nil, trace.Wrap(err) } diff --git a/tool/tsh/common/proxy.go b/tool/tsh/common/proxy.go index 640e3d34ccaa9..3f5e3108c4cbc 100644 --- a/tool/tsh/common/proxy.go +++ b/tool/tsh/common/proxy.go @@ -387,13 +387,31 @@ func onProxyCommandApp(cf *CLIConf) error { return trace.Wrap(err) } - appInfo, err := getAppInfo(cf, tc, nil /*matchRouteToApp*/) - if err != nil { - return trace.Wrap(err) - } + var ( + appInfo *appInfo + app types.Application + ) + if err := libclient.RetryWithRelogin(cf.Context, tc, func() error { + var err error + profile, err := tc.ProfileStatus() + if err != nil { + return trace.Wrap(err) + } - app, err := appInfo.GetApp(cf.Context, tc) - if err != nil { + clusterClient, err := tc.ConnectToCluster(cf.Context) + if err != nil { + return trace.Wrap(err) + } + defer clusterClient.Close() + + appInfo, err = getAppInfo(cf, clusterClient.AuthClient, profile, tc.SiteName, matchGCPApp) + if err != nil { + return trace.Wrap(err) + } + + app, err = appInfo.GetApp(cf.Context, clusterClient.AuthClient) + return trace.Wrap(err) + }); err != nil { return trace.Wrap(err) }