Files
teleport/tool/tsh/common/app.go
T
rosstimothy 5870768c88 Stop using text/template and html/template to enable DCE (#65401)
All existing template usage was converted to make use of
github.com/DataDog/datadog-agent/pkg/template. The DataDog package
is a fork of the stdlib template packages with method calling
removed so that reflect.MethodByName does not prevent DCE.

There were a handful of method calls in our templates that were trivially
changed to use custom functions defined via FuncMaps or specifying the
calculated value to the template instead of calling a function in the template.

A future change will add linter rules to prevent new text/template or
html/imports from landing. They have not been included here so as
to not to break enterprise code while it is migrated.
2026-04-22 15:54:43 +00:00

876 lines
27 KiB
Go

/*
* 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 <http://www.gnu.org/licenses/>.
*/
package common
import (
"context"
"fmt"
"io"
"net"
"os"
"os/exec"
"strconv"
"strings"
"sync"
template "github.com/DataDog/datadog-agent/pkg/template/text"
"github.com/ghodss/yaml"
"github.com/google/safetext/shsprintf"
"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/types"
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
logutils "github.com/gravitational/teleport/lib/utils/log"
)
// onAppLogin implements "tsh apps login" command.
func onAppLogin(cf *CLIConf) error {
tc, err := makeClient(cf)
if err != nil {
return trace.Wrap(err)
}
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()
if app.IsMCP() {
return trace.BadParameter("MCP applications are not supported. Please see 'tsh mcp config --help' for more details.")
}
if err := validateTargetPort(app, int(cf.TargetPort)); err != nil {
return trace.Wrap(err)
}
rootClient, err := clusterClient.ConnectToRootCluster(cf.Context)
if err != nil {
return trace.Wrap(err)
}
routeToApp := appInfo.RouteToApp
if cf.TargetPort != 0 {
routeToApp.TargetPort = uint32(cf.TargetPort)
}
appCertParams := client.ReissueParams{
RouteToCluster: tc.SiteName,
RouteToApp: routeToApp,
AccessRequests: appInfo.profile.ActiveRequests,
}
// When using `tsh app login`, certs should generally be saved to disk, whether standard certs or
// single-use MFA-verified 1m TTL certs. However, in cases where we are exceeding the standard
// 1m TTL for single-use certs, we must ensure the certs are not saved to disk.
saveCertificateToDisk := true
if app.GetAWSRolesAnywhereProfileARN() != "" {
singleUseCerts, err := isMFARequiredForAppAccess(cf.Context, tc, appInfo.RouteToApp)
if err != nil {
return trace.Wrap(err)
}
if singleUseCerts {
// Prevent single use certificates from being written to disk.
saveCertificateToDisk = false
appCertParams.RequesterName = proto.UserCertsRequest_TSH_APP_AWS_CREDENTIALPROCESS
// When using single use certs (aka per-session MFA), tsh cannot write the certificate or AWS credentials to disk.
// Instead, ask user to use the `--env` flag which only outputs the credentials, in an eval friendly format.
if !cf.AppLoginAWSEnvOutput {
return trace.BadParameter(`AWS access is configured to use per-session MFA and credentials are only available to a single session. Pass the --env flag to the previous command and export the credentials using eval.
Example:
eval "$(tsh apps login %s --aws-role %s --env)"
You can now run the AWS CLI or other AWS SDK based tools as usual.
Example:
aws sts get-caller-identity`,
shsprintf.EscapeDefaultContext(app.GetName()),
shsprintf.EscapeDefaultContext(appInfo.RouteToApp.AWSRoleARN),
)
}
}
}
key, err := appLogin(cf.Context, clusterClient, rootClient, appCertParams)
if err != nil {
return trace.Wrap(err)
}
if saveCertificateToDisk {
if err := tc.LocalAgent().AddAppKeyRing(key); err != nil {
return trace.Wrap(err)
}
}
appInfo, err = reloadAppInfoFromKeyring(app, appInfo, key)
if err != nil {
return trace.Wrap(err)
}
if err := writeFilesForExternalApps(cf.Stdout(), appInfo, cf.AppLoginAWSEnvOutput); err != nil {
return trace.Wrap(err)
}
if err := printAppCommand(cf, tc, app, appInfo); err != nil {
return trace.Wrap(err)
}
return nil
}
// isMFARequiredForAppAccess calls the IsMFARequired endpoint in order to get from user roles if access to the application
// requires MFA.
func isMFARequiredForAppAccess(ctx context.Context, tc *client.TeleportClient, routeToApp proto.RouteToApp) (bool, error) {
clusterClient, err := tc.ConnectToCluster(ctx)
if err != nil {
return false, trace.Wrap(err)
}
defer clusterClient.Close()
mfaResp, err := clusterClient.AuthClient.IsMFARequired(ctx, &proto.IsMFARequiredRequest{
Target: &proto.IsMFARequiredRequest_App{
App: &routeToApp,
},
})
if err != nil {
return false, trace.Wrap(err)
}
return mfaResp.GetRequired(), nil
}
func reloadAppInfoFromKeyring(app types.Application, appInfo *appInfo, key *client.KeyRing) (*appInfo, error) {
// AWS Access ugin Roles Anywhere integration receive the credentials in the certificate.
// For all other apps, the routeToApp is already correct.
if app.GetAWSRolesAnywhereProfileARN() == "" {
return appInfo, nil
}
// AWS Apps using AWS Roles Anywhere for granting access receive the AWS Credentials in the certificate.
appCerts, err := key.AppTLSCertificates()
if err != nil {
return appInfo, trace.Wrap(err)
}
for _, cert := range appCerts {
tlsID, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
if err != nil {
return appInfo, trace.Wrap(err)
}
if tlsID.RouteToApp.Name == appInfo.RouteToApp.Name {
appInfo.RouteToApp = tlscaRouteToAppToProto(tlsID.RouteToApp)
return appInfo, nil
}
}
return appInfo, nil
}
func appLogin(
ctx context.Context,
clusterClient *client.ClusterClient,
rootClient authclient.ClientI,
appCertParams client.ReissueParams,
) (*client.KeyRing, error) {
result, err := clusterClient.IssueUserCertsWithMFA(ctx, appCertParams)
if err != nil {
return nil, trace.Wrap(err)
}
return result.KeyRing, nil
}
func localProxyRequiredForApp(tc *client.TeleportClient) bool {
return tc.TLSRoutingConnUpgradeRequired
}
func printAppCommand(cf *CLIConf, tc *client.TeleportClient, app types.Application, appInfo *appInfo) error {
routeToApp := appInfo.RouteToApp
output := cf.Stdout()
if cf.Quiet {
output = io.Discard
}
switch {
case app.IsAWSConsole():
// When using env output, skip printing the login instructions because they were already emitted alongside the env var exports.
if cf.AppLoginAWSEnvOutput {
return nil
}
if routeToApp.AWSCredentialProcessCredentials != "" {
return awsNamedProfileLoginTemplate.Execute(output, map[string]string{
"awsAppName": app.GetName(),
"escapedAWSAppName": shsprintf.EscapeDefaultContext(app.GetName()),
"awsCmd": "s3 ls",
"awsRoleARN": routeToApp.AWSRoleARN,
})
}
return awsLoginTemplate.Execute(output, map[string]string{
"awsAppName": app.GetName(),
"escapedAWSAppName": shsprintf.EscapeDefaultContext(app.GetName()),
"awsCmd": "s3 ls",
"awsRoleARN": routeToApp.AWSRoleARN,
})
case app.IsAzureCloud():
if routeToApp.AzureIdentity == "" {
return trace.BadParameter("app is Azure Cloud but Azure identity is missing")
}
azureApp, err := newAzureApp(tc, cf, appInfo)
if err != nil {
return trace.Wrap(err)
}
resourceArgumentName := "--username"
// After the CLI started relying in MSAL by default, the param for the
// managed identity changed.
//
// https://learn.microsoft.com/en-us/cli/azure/release-notes-azure-cli?view=azure-cli-latest#profile
if azureApp.usingMSAL() {
resourceArgumentName = "--resource-id"
}
args := []string{"az", "login", "--identity", resourceArgumentName, routeToApp.AzureIdentity}
if cf.Debug {
args = append(args, "--debug")
}
// automatically login with right identity.
cmd := exec.Command(cf.executablePath, args...)
cmd.Stdin = os.Stdin
cmd.Stderr = cf.Stderr()
cmd.Stdout = output
logger.DebugContext(cf.Context, "Running automatic az login", "command", logutils.StringerAttr(cmd))
if err := cf.RunCommand(cmd); err != nil {
return trace.Wrap(err, "failed to automatically login with `az login` using identity %q; run with --debug for details", routeToApp.AzureIdentity)
}
return azureLoginTemplate.Execute(output, map[string]string{
"appName": app.GetName(),
"identity": routeToApp.AzureIdentity,
})
case app.IsGCP():
return gcpLoginTemplate.Execute(output, map[string]string{
"appName": app.GetName(),
"serviceAccount": routeToApp.GCPServiceAccount,
})
case app.IsTCP():
appNameWithOptionalTargetPort := app.GetName()
if routeToApp.TargetPort != 0 {
appNameWithOptionalTargetPort = net.JoinHostPort(app.GetName(), strconv.Itoa(int(routeToApp.GetTargetPort())))
}
return tcpAppLoginTemplate.Execute(output, map[string]string{
"appName": shsprintf.EscapeDefaultContext(app.GetName()),
"appNameWithOptionalTargetPort": appNameWithOptionalTargetPort,
})
case localProxyRequiredForApp(tc):
return webAppLoginProxyTemplate.Execute(output, map[string]any{
"appName": app.GetName(),
"escapedAppName": shsprintf.EscapeDefaultContext(app.GetName()),
})
default:
rootCluster, err := tc.RootClusterName(cf.Context)
if err != nil {
return trace.Wrap(err)
}
// for remote apps, override their public address with address pointing at the public proxy address.
if rootCluster != tc.SiteName {
routeToApp.PublicAddr = fmt.Sprintf("%v.%v", app.GetName(), tc.WebProxyHost())
}
profile, err := tc.ProfileStatus()
if err != nil {
return trace.Wrap(err)
}
curlCmd, err := formatAppConfig(tc, profile, routeToApp, appFormatCURL)
if err != nil {
return trace.Wrap(err)
}
return webAppLoginTemplate.Execute(output, map[string]any{
"appName": app.GetName(),
"curlCmd": curlCmd,
"insecure": cf.InsecureSkipVerify,
})
}
}
// webAppLoginTemplate is the message that gets printed to a user upon successful login
// into an HTTP application.
var webAppLoginTemplate = template.Must(template.New("").Parse(
`Logged into app {{.appName}}. Example curl command:
{{.curlCmd}}{{ if .insecure }}
WARNING: tsh was called with --insecure, so this curl command will be unable to validate the certificate presented by Teleport.
{{- end }}
`))
// webAppLoginProxyTemplate is the message that gets printed to a user upon successful login
// into an HTTP application and local proxy is required.
var webAppLoginProxyTemplate = template.Must(template.New("").Parse(
`Logged into app {{.appName}}. Start the local proxy for it:
tsh proxy app {{.escapedAppName}} -p 8080
Then connect to the application through this proxy:
curl http://127.0.0.1:8080
`))
// tcpAppLoginTemplate is the message that gets printed to a user upon successful
// login into a TCP application.
var tcpAppLoginTemplate = template.Must(template.New("").Parse(
`Logged into TCP app {{.appNameWithOptionalTargetPort}}. Start the local TCP proxy for it:
tsh proxy app {{.appName}}
Then connect to the application through this proxy.
`))
// awsLoginTemplate is the message that gets printed to a user upon successful login
// into an AWS Console application.
var awsLoginTemplate = template.Must(template.New("").Parse(
`Logged into AWS app "{{.awsAppName}}".
Your IAM role:
{{.awsRoleARN}}
Example AWS CLI command:
tsh aws {{.awsCmd}}
Or start a local proxy:
tsh proxy aws --app {{.escapedAWSAppName}}
`))
// awsNamedProfileLoginTemplate is the message that gets printed to a user upon successful login
// into an AWS Console application which provides AWS credentials in the `credential_process` schema.
// Used for named profiles, where the profile name is the same as the app name.
var awsNamedProfileLoginTemplate = template.Must(template.New("").Parse(
`Logged into AWS app "{{.awsAppName}}".
Your IAM role:
{{.awsRoleARN}}
Example AWS CLI commands:
aws --profile {{.awsAppName}} {{.awsCmd}}
AWS_PROFILE={{.awsAppName}} aws {{.awsCmd}}
`))
// azureLoginTemplate is the message that gets printed to a user upon successful login
// into an Azure application.
var azureLoginTemplate = template.Must(template.New("").Parse(
`Logged into Azure app "{{.appName}}".
Your identity: {{.identity}}
Example Azure CLI command: tsh az vm list
`))
// gcpLoginTemplate is the message that gets printed to a user upon successful login
// into a GCP application.
var gcpLoginTemplate = template.Must(template.New("").Parse(
`Logged into GCP app "{{.appName}}".
Your service account: {{.serviceAccount}}
Example command: tsh gcloud compute instances list
`))
// onAppLogout implements "tsh apps logout" command.
func onAppLogout(cf *CLIConf) error {
tc, err := makeClient(cf)
if err != nil {
return trace.Wrap(err)
}
profile, err := tc.ProfileStatus()
if err != nil {
return trace.Wrap(err)
}
activeRoutes, err := profile.AppsForCluster(tc.SiteName, tc.ClientStore)
if err != nil {
return trace.Wrap(err)
}
// If a specific app name was specified, just log out of that app.
// Otherwise, log out of all apps.
var logout []tlsca.RouteToApp
if cf.AppName != "" {
for _, app := range activeRoutes {
if app.Name == cf.AppName {
logout = append(logout, app)
}
}
if len(logout) == 0 {
// Not logged in but still try to delete any dangling files.
if err := tc.LogoutApp(cf.AppName); err != nil {
return trace.Wrap(err)
}
return trace.BadParameter("not logged into app %q", cf.AppName)
}
} else {
logout = activeRoutes
}
for _, app := range logout {
err = tc.DeleteAppSession(cf.Context, app.SessionID)
if err != nil && !trace.IsNotFound(err) {
return trace.Wrap(err)
}
err = tc.LogoutApp(app.Name)
if err != nil {
return trace.Wrap(err)
}
// remove generated local files for the provided app.
err := utils.RemoveFileIfExist(profile.AppLocalCAPath(tc.SiteName, app.Name))
if err != nil {
logger.WarnContext(cf.Context, "Failed to clean up app session",
"error", err,
"profile", profile.AppLocalCAPath(tc.SiteName, app.Name))
}
if err := removeExternalFilesForApp(app); err != nil {
logger.WarnContext(cf.Context, "Failed to clean up app external files",
"error", err,
"app", cf.AppName)
}
}
if cf.AppName == "" {
// Try to delete any dangling files even if the app sessions are expired.
if err := tc.LogoutAllApps(); err != nil {
return trace.Wrap(err)
}
if err := removeExternalFilesForAllApps(); err != nil {
logger.WarnContext(cf.Context, "Failed to clean up apps external files",
"error", err)
}
}
if len(logout) == 1 {
fmt.Printf("Logged out of app %q\n", logout[0].Name)
} else {
fmt.Println("Logged out of all apps")
}
return nil
}
// onAppConfig implements "tsh apps config" command.
func onAppConfig(cf *CLIConf) error {
tc, err := makeClient(cf)
if err != nil {
return trace.Wrap(err)
}
profile, err := tc.ProfileStatus()
if err != nil {
return trace.Wrap(err)
}
routes, err := profile.AppsForCluster(tc.SiteName, tc.ClientStore)
if err != nil {
return trace.Wrap(err)
}
app, err := pickActiveApp(cf, routes)
if err != nil {
return trace.Wrap(err)
}
routeToApp := proto.RouteToApp{
Name: app.Name,
PublicAddr: app.PublicAddr,
ClusterName: app.ClusterName,
AWSCredentialProcessCredentials: app.AWSCredentialProcessCredentials,
AWSRoleARN: app.AWSRoleARN,
AzureIdentity: app.AzureIdentity,
GCPServiceAccount: app.GCPServiceAccount,
URI: app.GetURI(),
}
conf, err := formatAppConfig(tc, profile, routeToApp, cf.Format)
if err != nil {
return trace.Wrap(err)
}
_, _ = fmt.Fprint(cf.Stdout(), conf)
return nil
}
const (
// appFormatURI prints app URI.
appFormatURI = "uri"
// appFormatCA prints app CA cert path.
appFormatCA = "ca"
// appFormatCert prints app cert path.
appFormatCert = "cert"
// appFormatKey prints app key path.
appFormatKey = "key"
// appFormatCURL prints app curl command.
appFormatCURL = "curl"
// appFormatJSON prints app URI, CA cert path, cert path, key path, and curl command in JSON format.
appFormatJSON = "json"
// appFormatYAML prints app URI, CA cert path, cert path, key path, and curl command in YAML format.
appFormatYAML = "yaml"
// appFormatAWSCredentialProcessOutput prints the credentials for accessing AWS Console using the `credential_process` schema.
// See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sourcing-external.html
appFormatAWSCredentialProcessOutput = "aws-credential-process"
)
func formatAppConfig(tc *client.TeleportClient, profile *client.ProfileStatus, routeToApp proto.RouteToApp, format string) (string, error) {
var uri string
if port := tc.WebProxyPort(); port == teleport.StandardHTTPSPort {
uri = fmt.Sprintf("https://%v", routeToApp.PublicAddr)
} else {
uri = fmt.Sprintf("https://%v:%v", routeToApp.PublicAddr, port)
}
var curlInsecureFlag string
if tc.InsecureSkipVerify {
curlInsecureFlag = "--insecure "
}
certPath := profile.AppCertPath(tc.SiteName, routeToApp.Name)
keyPath := profile.AppKeyPath(tc.SiteName, routeToApp.Name)
curlCmd := fmt.Sprintf(`curl %s\
--cert %q \
--key %q \
%v`,
curlInsecureFlag,
certPath,
keyPath,
shsprintf.EscapeDefaultContext(uri))
format = strings.ToLower(format)
switch format {
case appFormatURI:
return uri, nil
case appFormatCA:
return profile.CACertPathForCluster(tc.SiteName), nil
case appFormatCert:
return certPath, nil
case appFormatKey:
return keyPath, nil
case appFormatCURL:
return curlCmd, nil
case appFormatAWSCredentialProcessOutput:
return routeToApp.AWSCredentialProcessCredentials, nil
case appFormatJSON, appFormatYAML:
appConfig := &appConfigInfo{
Name: routeToApp.Name,
URI: uri,
CA: profile.CACertPathForCluster(tc.SiteName),
Cert: certPath,
Key: keyPath,
Curl: curlCmd,
AWSRoleARN: routeToApp.AWSRoleARN,
AzureIdentity: routeToApp.AzureIdentity,
GCPServiceAccount: routeToApp.GCPServiceAccount,
}
out, err := serializeAppConfig(appConfig, format)
if err != nil {
return "", trace.Wrap(err)
}
return out, nil
case "", "default":
t := asciitable.MakeHeadlessTable(2)
// additional spaces after `Name:` are there to enforce minimum column width,
// which helps to visually separate the two columns.
t.AddRow([]string{"Name: ", routeToApp.Name})
t.AddRow([]string{"URI:", uri})
t.AddRow([]string{"CA:", profile.CACertPathForCluster(tc.SiteName)})
t.AddRow([]string{"Cert:", certPath})
t.AddRow([]string{"Key:", keyPath})
if routeToApp.AWSRoleARN != "" {
t.AddRow([]string{"AWS ARN:", routeToApp.AWSRoleARN})
}
if routeToApp.AzureIdentity != "" {
t.AddRow([]string{"Azure Id:", routeToApp.AzureIdentity})
}
if routeToApp.GCPServiceAccount != "" {
t.AddRow([]string{"GCP Service Account:", routeToApp.GCPServiceAccount})
}
return t.AsBuffer().String(), nil
default:
acceptedFormats := []string{
"", "default",
appFormatCURL,
appFormatJSON, appFormatYAML,
appFormatURI, appFormatCA, appFormatCert, appFormatKey,
}
return "", trace.BadParameter("invalid format, expected one of %q, got %q", acceptedFormats, format)
}
}
type appConfigInfo struct {
Name string `json:"name"`
URI string `json:"uri"`
CA string `json:"ca"`
Cert string `json:"cert"`
Key string `json:"key"`
Curl string `json:"curl"`
AWSRoleARN string `json:"aws_role_arn,omitempty"`
AzureIdentity string `json:"azure_identity,omitempty"`
GCPServiceAccount string `json:"gcp_service_account,omitempty"`
}
func serializeAppConfig(configInfo *appConfigInfo, format string) (string, error) {
var out []byte
var err error
if format == appFormatJSON {
out, err = utils.FastMarshalIndent(configInfo, "", " ")
// This JSON marshaling returns a string without a newline at the end, which
// makes display of the string look wonky. Let's append it here.
out = append(out, '\n')
} else {
// The YAML marshaling does return a string with a newline, so no need to append
// another.
out, err = yaml.Marshal(configInfo)
}
return string(out), trace.Wrap(err)
}
// getAppInfo fetches app information using the user's tsh profile,
// 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.
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
for _, route := range profile.Apps {
if matchRouteToApp(route) {
filteredRoutes = append(filteredRoutes, route)
}
}
activeRoutes = filteredRoutes
}
if route, err := pickActiveApp(cf, activeRoutes); err == nil {
return &appInfo{
profile: profile,
RouteToApp: route,
isActive: true,
}, nil
} else if !trace.IsNotFound(err) {
// pickActiveApp errors are non-retryable.
return nil, trace.Wrap(&client.NonRetryableError{Err: err})
}
// If we didn't find an active profile for the app, get info from server.
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: siteName,
URI: app.GetURI(),
},
app: app,
}
// When getAppInfo gets called inside RetryWithRelogin, it will relogin on
// trace.BadParameter errors. Wrap errors from pickCloudAppLogin as they
// are not retryable.
if err := appInfo.pickCloudAppLogin(cf, logins); err != nil {
return nil, trace.Wrap(&client.NonRetryableError{Err: err})
}
return appInfo, nil
}
// pickCloudAppLogin picks the cloud identity for the app based on provided CLI
// flags and/or available logins of the Teleport user.
func (a *appInfo) pickCloudAppLogin(cf *CLIConf, logins []string) error {
// If this is a cloud app, set additional applicable fields from CLI flags or roles.
switch {
case a.app.IsAWSConsole():
awsRoleARN, err := getARNFromFlags(cf, a.app, logins)
if err != nil {
return trace.Wrap(err)
}
a.AWSRoleARN = awsRoleARN
case a.app.IsAzureCloud():
azureIdentity, err := getAzureIdentityFromFlags(cf, a.profile)
if err != nil {
return trace.Wrap(err)
}
logger.DebugContext(cf.Context, "Retrieved azure identity", "azure_identity", azureIdentity)
a.AzureIdentity = azureIdentity
case a.app.IsGCP():
gcpServiceAccount, err := getGCPServiceAccountFromFlags(cf, a.profile)
if err != nil {
return trace.Wrap(err)
}
logger.DebugContext(cf.Context, "Retrieved GCP service account", "service_account", gcpServiceAccount)
a.GCPServiceAccount = gcpServiceAccount
}
return nil
}
// appInfo wraps a RouteToApp and the corresponding app.
// Its purpose is to prevent repeated fetches of the same app,
// by lazily fetching and caching the app for use as needed.
type appInfo struct {
proto.RouteToApp
// app corresponds to the app route and may be nil, so use GetApp
// instead of accessing it directly.
app types.Application
appMu sync.Mutex
// isActive indicates an active app matched this app info.
isActive bool
// profile is a cached profile status for the current login session.
profile *client.ProfileStatus
}
// GetApp returns the cached app or fetches it using the app route and
// caches the result.
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, clt, a.Name)
if err != nil {
return nil, trace.Wrap(err)
}
a.app = app
return a.app.Copy(), nil
}
// getApp returns the registered application with the specified name.
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, nil, trace.Wrap(err)
}
if len(res.Resources) == 0 {
return nil, nil, trace.NotFound("app %q not found, use `tsh apps ls` to see registered apps", name)
}
appServer, ok := res.Resources[0].ResourceWithLabels.(types.AppServer)
if !ok {
logger.WarnContext(ctx, "expected types.AppServer but received unexpected type", "resource_type", logutils.TypeAttr(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.
//
// If logged into multiple apps, returns an error unless one was specified
// explicitly on CLI.
func pickActiveApp(cf *CLIConf, activeRoutes []tlsca.RouteToApp) (proto.RouteToApp, error) {
if cf.AppName == "" {
switch len(activeRoutes) {
case 0:
return proto.RouteToApp{}, trace.NotFound("please login using 'tsh apps login' first")
case 1:
return tlscaRouteToAppToProto(activeRoutes[0]), nil
default:
var appNames []string
for _, r := range activeRoutes {
appNames = append(appNames, r.Name)
}
return proto.RouteToApp{}, trace.BadParameter("multiple apps are available (%v), please specify one via CLI argument",
strings.Join(appNames, ", "))
}
}
for _, r := range activeRoutes {
if r.Name == cf.AppName {
return tlscaRouteToAppToProto(r), nil
}
}
return proto.RouteToApp{}, trace.NotFound("not logged into app %q", cf.AppName)
}
func tlscaRouteToAppToProto(route tlsca.RouteToApp) proto.RouteToApp {
return proto.RouteToApp{
Name: route.Name,
PublicAddr: route.PublicAddr,
ClusterName: route.ClusterName,
AWSRoleARN: route.AWSRoleARN,
AWSCredentialProcessCredentials: route.AWSCredentialProcessCredentials,
AzureIdentity: route.AzureIdentity,
GCPServiceAccount: route.GCPServiceAccount,
URI: route.URI,
}
}