Skip to content

Commit

Permalink
[v13] Implement leaf app access: tsh app login --cluster=leaf (#27197)
Browse files Browse the repository at this point in the history
* Implement leaf app access: `tsh app login --cluster=leaf`

* Address review comments.

* Speedup test execution.

* Fix parallel test run.
  • Loading branch information
Tener committed Jun 7, 2023
1 parent a07db7f commit 6ac6b15
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 34 deletions.
7 changes: 1 addition & 6 deletions tool/tsh/access_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import (
"bytes"
"context"
"fmt"
"io"
"os"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -193,10 +191,7 @@ func TestAccessRequestSearch(t *testing.T) {
},
tc.args.extraArgs...,
),
func(cf *CLIConf) error {
cf.overrideStdout = io.MultiWriter(os.Stdout, captureStdout)
return nil
},
setCopyStdout(captureStdout),
)
require.NoError(t, err)
require.Contains(t, captureStdout.String(), tc.wantTable())
Expand Down
15 changes: 8 additions & 7 deletions tool/tsh/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func onAppLogin(cf *CLIConf) error {
}

params := client.ReissueParams{
RouteToCluster: tc.SiteName,
RouteToCluster: profile.Cluster,
RouteToApp: proto.RouteToApp{
Name: app.GetName(),
SessionID: ws.GetName(),
Expand All @@ -124,10 +124,6 @@ func onAppLogin(cf *CLIConf) error {
return trace.Wrap(err)
}

if err := tc.SaveProfile(true); err != nil {
return trace.Wrap(err)
}

output := cf.Stdout()
if cf.Quiet {
output = io.Discard
Expand Down Expand Up @@ -186,7 +182,12 @@ func onAppLogin(cf *CLIConf) error {
})

default:
curlCmd, err := formatAppConfig(tc, profile, app.GetName(), app.GetPublicAddr(), appFormatCURL, rootCluster, awsRoleARN, azureIdentity, gcpServiceAccount)
publicAddr := app.GetPublicAddr()
// for remote apps, override their public address with address pointing at the public proxy address.
if rootCluster != tc.SiteName {
publicAddr = fmt.Sprintf("%v.%v", app.GetName(), tc.WebProxyHost())
}
curlCmd, err := formatAppConfig(tc, profile, app.GetName(), publicAddr, appFormatCURL, rootCluster, awsRoleARN, azureIdentity, gcpServiceAccount)
if err != nil {
return trace.Wrap(err)
}
Expand Down Expand Up @@ -349,7 +350,7 @@ func onAppConfig(cf *CLIConf) error {
if err != nil {
return trace.Wrap(err)
}
fmt.Print(conf)
_, _ = fmt.Fprint(cf.Stdout(), conf)
return nil
}

Expand Down
210 changes: 208 additions & 2 deletions tool/tsh/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,221 @@ limitations under the License.
package main

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/http/httputil"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib"
"github.com/gravitational/teleport/lib/client"
defaults2 "github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/reversetunnel"
"github.com/gravitational/teleport/lib/service"
"github.com/gravitational/teleport/lib/service/servicecfg"
)

func startDummyHTTPServer(t *testing.T, name string) string {
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", name)
_, _ = w.Write([]byte("hello"))
}))

srv.Start()

t.Cleanup(func() {
srv.Close()
})

return srv.URL
}

func TestAppLoginLeaf(t *testing.T) {
// TODO(tener): changing ResyncInterval defaults speeds up the tests considerably.
// It may be worth making the change global either for tests or production.
// See also SetTestTimeouts() in integration/helpers/timeouts.go
oldResyncInterval := defaults2.ResyncInterval
defaults2.ResyncInterval = time.Millisecond * 100
t.Cleanup(func() {
defaults2.ResyncInterval = oldResyncInterval
})

isInsecure := lib.IsInsecureDevMode()
lib.SetInsecureDevMode(true)
t.Cleanup(func() {
lib.SetInsecureDevMode(isInsecure)
})

connector := mockConnector(t)

alice, err := types.NewUser("alice@example.com")
require.NoError(t, err)
alice.SetRoles([]string{"access"})

// TODO(tener): consider making this default for tests.
configStorage := func(cfg *servicecfg.Config) {
cfg.Auth.StorageConfig.Params["poll_stream_period"] = 50 * time.Millisecond
}

rootAuth, rootProxy := makeTestServers(t, withClusterName(t, "root"), withBootstrap(connector, alice), withConfig(configStorage))
event, err := rootAuth.WaitForEventTimeout(time.Second, service.ProxyReverseTunnelReady)
require.NoError(t, err)
tunnel, ok := event.Payload.(reversetunnel.Server)
require.True(t, ok)

rootAppURL := startDummyHTTPServer(t, "rootapp")
rootAppServer := makeTestApplicationServer(t, rootAuth, rootProxy, servicecfg.App{Name: "rootapp", URI: rootAppURL})
_, err = rootAppServer.WaitForEventTimeout(time.Second*10, service.TeleportReadyEvent)
require.NoError(t, err)

rootProxyAddr, err := rootProxy.ProxyWebAddr()
require.NoError(t, err)
rootTunnelAddr, err := rootProxy.ProxyTunnelAddr()
require.NoError(t, err)

trustedCluster, err := types.NewTrustedCluster("localhost", types.TrustedClusterSpecV2{
Enabled: true,
Roles: []string{},
Token: staticToken,
ProxyAddress: rootProxyAddr.String(),
ReverseTunnelAddress: rootTunnelAddr.String(),
RoleMap: []types.RoleMapping{
{
Remote: "access",
Local: []string{"access"},
},
},
})
require.NoError(t, err)

leafAuth, leafProxy := makeTestServers(t, withClusterName(t, "leaf"), withConfig(configStorage))

leafAppURL := startDummyHTTPServer(t, "leafapp")
leafAppServer := makeTestApplicationServer(t, leafAuth, leafProxy, servicecfg.App{Name: "leafapp", URI: leafAppURL})
_, err = leafAppServer.WaitForEventTimeout(time.Second*10, service.TeleportReadyEvent)
require.NoError(t, err)

tryCreateTrustedCluster(t, leafAuth.GetAuthServer(), trustedCluster)

// wait for the connection to come online and the app server information propagate.
require.Eventually(t, func() bool {
conns, err := rootAuth.GetAuthServer().GetTunnelConnections("leaf")
return err == nil && len(conns) == 1
}, 10*time.Second, 100*time.Millisecond, "leaf cluster did not come online")

require.Eventually(t, func() bool {
leafSite, err := tunnel.GetSite("leaf")
require.NoError(t, err)
ap, err := leafSite.CachingAccessPoint()
require.NoError(t, err)

servers, err := ap.GetApplicationServers(context.Background(), defaults.Namespace)
if err != nil {
return false
}
return len(servers) == 1 && servers[0].GetName() == "leafapp"

}, 10*time.Second, 100*time.Millisecond, "leaf cluster did not come online")

// helpers
getHelpers := func(t *testing.T) (func(cluster string) string, func(args ...string) string) {
tmpHomePath := t.TempDir()

run := func(args []string, opts ...cliOption) string {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

captureStdout := new(bytes.Buffer)
opts = append(opts, setHomePath(tmpHomePath))
opts = append(opts, setCopyStdout(captureStdout))
err := Run(ctx, args, opts...)
require.NoError(t, err)
return captureStdout.String()
}

login := func(cluster string) string {
args := []string{
"login",
"--insecure",
"--debug",
"--auth", connector.GetName(),
"--proxy", rootProxyAddr.String(),
cluster}

opt := func(cf *CLIConf) error {
cf.mockSSOLogin = mockSSOLogin(t, rootAuth.GetAuthServer(), alice)
return nil
}

return run(args, opt)
}
tsh := func(args ...string) string { return run(args) }

return login, tsh
}

verifyAppIsAvailable := func(t *testing.T, conf string, appName string) {
var info appConfigInfo
require.NoError(t, json.Unmarshal([]byte(conf), &info))

clientCert, err := tls.LoadX509KeyPair(info.Cert, info.Key)
require.NoError(t, err)

clt := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
Certificates: []tls.Certificate{clientCert},
},
},
}

resp, err := clt.Get(fmt.Sprintf("https://%v", rootProxyAddr.Addr))
require.NoError(t, err)

respData, _ := httputil.DumpResponse(resp, true)

t.Log(string(respData))

require.Equal(t, 200, resp.StatusCode)
require.Equal(t, appName, resp.Header.Get("Server"))
_ = resp.Body.Close()
}

tests := []struct{ name, loginCluster, appCluster, appName string }{
{"root login cluster, root app cluster", "root", "root", "rootapp"},
{"root login cluster, leaf app cluster", "root", "leaf", "leafapp"},
{"leaf login cluster, root app cluster", "leaf", "root", "rootapp"},
{"leaf login cluster, leaf app cluster", "leaf", "leaf", "leafapp"},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

login, tsh := getHelpers(t)

login(tt.loginCluster)
tsh("app", "ls", "--verbose", "--format=json", "--cluster", tt.appCluster)
tsh("app", "login", tt.appName, "--cluster", tt.appCluster)
conf := tsh("app", "config", "--format=json")
verifyAppIsAvailable(t, conf, tt.appName)
tsh("logout")
})
}
}

func TestFormatAppConfig(t *testing.T) {
t.Parallel()

Expand All @@ -40,8 +248,6 @@ func TestFormatAppConfig(t *testing.T) {
testAppPublicAddr := "test-tp.teleport"
testCluster := "test-tp"

// func formatAppConfig(tc *client.TeleportClient, profile *client.ProfileStatus, appName,
// appPublicAddr, format, cluster string) (string, error) {
tests := []struct {
name string
tc *client.TeleportClient
Expand Down
11 changes: 2 additions & 9 deletions tool/tsh/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"crypto/rsa"
"encoding/pem"
"fmt"
"io"
"net"
"os"
"path/filepath"
Expand Down Expand Up @@ -368,10 +367,7 @@ func TestListDatabase(t *testing.T) {
"ls",
"--insecure",
"--debug",
}, func(cf *CLIConf) error {
cf.overrideStdout = io.MultiWriter(os.Stdout, captureStdout)
return nil
})
}, setCopyStdout(captureStdout))
require.NoError(t, err)
require.Contains(t, captureStdout.String(), "root-postgres")

Expand All @@ -383,10 +379,7 @@ func TestListDatabase(t *testing.T) {
"leaf1",
"--insecure",
"--debug",
}, func(cf *CLIConf) error {
cf.overrideStdout = io.MultiWriter(os.Stdout, captureStdout)
return nil
})
}, setCopyStdout(captureStdout))
require.NoError(t, err)
require.Contains(t, captureStdout.String(), "leaf-postgres")
}
Expand Down
7 changes: 1 addition & 6 deletions tool/tsh/kube_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
Expand Down Expand Up @@ -184,10 +182,7 @@ func (p *kubeTestPack) testListKube(t *testing.T) {
},
tc.args...,
),
func(cf *CLIConf) error {
cf.overrideStdout = io.MultiWriter(os.Stdout, captureStdout)
return nil
},
setCopyStdout(captureStdout),
)
require.NoError(t, err)
require.Contains(t, captureStdout.String(), tc.wantTable())
Expand Down
5 changes: 1 addition & 4 deletions tool/tsh/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -786,10 +786,7 @@ func mustGetOpenSSHConfigFile(t *testing.T) string {
var buff bytes.Buffer
err := Run(context.Background(), []string{
"config",
}, func(cf *CLIConf) error {
cf.overrideStdout = &buff
return nil
})
}, setCopyStdout(&buff))
require.NoError(t, err)

tmpDir := t.TempDir()
Expand Down
1 change: 1 addition & 0 deletions tool/tsh/tsh.go
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,7 @@ func Run(ctx context.Context, args []string, opts ...cliOption) error {
lsApps.Flag("all", "List apps from all clusters and proxies.").Short('R').BoolVar(&cf.ListAll)
appLogin := apps.Command("login", "Retrieve short-lived certificate for an app.")
appLogin.Arg("app", "App name to retrieve credentials for. Can be obtained from `tsh apps ls` output.").Required().StringVar(&cf.AppName)
appLogin.Flag("cluster", clusterHelp).Short('c').StringVar(&cf.SiteName)
appLogin.Flag("aws-role", "(For AWS CLI access only) Amazon IAM role ARN or role name.").StringVar(&cf.AWSRole)
appLogin.Flag("azure-identity", "(For Azure CLI access only) Azure managed identity name.").StringVar(&cf.AzureIdentity)
appLogin.Flag("gcp-service-account", "(For GCP CLI access only) GCP service account name.").StringVar(&cf.GCPServiceAccount)
Expand Down
6 changes: 6 additions & 0 deletions tool/tsh/tsh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import (
"github.com/gravitational/teleport/lib"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/mocku2f"
"github.com/gravitational/teleport/lib/auth/native"
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
wancli "github.com/gravitational/teleport/lib/auth/webauthncli"
"github.com/gravitational/teleport/lib/backend"
Expand Down Expand Up @@ -118,6 +119,7 @@ func init() {

func TestMain(m *testing.M) {
utils.InitLoggerForTests()
native.PrecomputeTestKeys(m)
os.Exit(m.Run())
}

Expand Down Expand Up @@ -3194,6 +3196,10 @@ func setOverrideStdout(stdout io.Writer) cliOption {
}
}

func setCopyStdout(stdout io.Writer) cliOption {
return setOverrideStdout(io.MultiWriter(os.Stdout, stdout))
}

func setHomePath(path string) cliOption {
return func(cf *CLIConf) error {
cf.HomePath = path
Expand Down

0 comments on commit 6ac6b15

Please sign in to comment.