diff --git a/lib/client/api.go b/lib/client/api.go index 0a4244e3a8a22..d6734ee010445 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -2226,7 +2226,7 @@ func (tc *TeleportClient) LogoutAll() error { return nil } -// PingAndShowMOTD pings the Teleport Proxy and displays the MOTD if it's available. +// PingAndShowMOTD pings the Teleport Proxy and displays the Message Of The Day if it's available. func (tc *TeleportClient) PingAndShowMOTD(ctx context.Context) (*webclient.PingResponse, error) { pr, err := tc.Ping(ctx) if err != nil { @@ -2441,7 +2441,7 @@ func (tc *TeleportClient) ShowMOTD(ctx context.Context) error { // use might enter at the prompt. Whatever the user enters will // be simply discarded, and the user can still CTRL+C out if they // disagree. - _, err := passwordFromConsole() + _, err := passwordFromConsoleFn() if err != nil { return trace.Wrap(err) } @@ -2869,7 +2869,7 @@ func (tc *TeleportClient) AskOTP() (token string, err error) { // AskPassword prompts the user to enter the password func (tc *TeleportClient) AskPassword() (pwd string, err error) { fmt.Printf("Enter password for Teleport user %v:\n", tc.Config.Username) - pwd, err = passwordFromConsole() + pwd, err = passwordFromConsoleFn() if err != nil { fmt.Fprintln(tc.Stderr, err) return "", trace.Wrap(err) @@ -2918,6 +2918,8 @@ func (tc *TeleportClient) getServerVersion(nodeClient *NodeClient) (string, erro } } +var passwordFromConsoleFn = passwordFromConsole + // passwordFromConsole reads from stdin without echoing typed characters to stdout func passwordFromConsole() (string, error) { // syscall.Stdin is not an int on windows. The linter will complain only on diff --git a/lib/client/export.go b/lib/client/export.go new file mode 100644 index 0000000000000..a01eb51bdd91b --- /dev/null +++ b/lib/client/export.go @@ -0,0 +1,18 @@ +// Copyright 2021 Gravitational, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package client + +// PasswordFromConsoleFn exports passwordFromConsoleFn for tests. +var PasswordFromConsoleFn = &passwordFromConsoleFn diff --git a/tool/tsh/tsh.go b/tool/tsh/tsh.go index c6fb3e9d22f31..3ebdc237ba40c 100644 --- a/tool/tsh/tsh.go +++ b/tool/tsh/tsh.go @@ -239,6 +239,9 @@ type CLIConf struct { // unsetEnvironment unsets Teleport related environment variables. unsetEnvironment bool + // overrideStderr allows to switch standard error source for resource command. Used in tests. + overrideStderr io.Writer + // mockSSOLogin used in tests to override sso login handler in teleport client. mockSSOLogin client.SSOLoginFunc @@ -249,6 +252,14 @@ type CLIConf struct { ConfigProxyTarget string } +// Stderr returns the stderr writer. +func (c *CLIConf) Stderr() io.Writer { + if c.overrideStderr != nil { + return c.overrideStderr + } + return os.Stderr +} + func main() { cmdLineOrig := os.Args[1:] var cmdLine []string @@ -1945,6 +1956,7 @@ func makeClient(cf *CLIConf, useProfileLogin bool) (*client.TeleportClient, erro } } + tc.Config.Stderr = cf.Stderr() return tc, nil } diff --git a/tool/tsh/tsh_test.go b/tool/tsh/tsh_test.go index fe444e3bfdf7e..87b18ad69c0d1 100644 --- a/tool/tsh/tsh_test.go +++ b/tool/tsh/tsh_test.go @@ -17,12 +17,15 @@ limitations under the License. package main import ( + "bufio" + "bytes" "context" "fmt" "io/ioutil" "net" "os" "path/filepath" + "strings" "testing" "time" @@ -154,7 +157,8 @@ func TestOIDCLogin(t *testing.T) { connector := mockConnector(t) - authProcess, proxyProcess := makeTestServers(t, populist, dictator, connector, alice) + motd := "MESSAGE_OF_THE_DAY_OIDC" + authProcess, proxyProcess := makeTestServersWithMotd(t, motd, populist, dictator, connector, alice) authServer := authProcess.GetAuthServer() require.NotNil(t, authServer) @@ -192,6 +196,8 @@ func TestOIDCLogin(t *testing.T) { } }() + buf := bytes.NewBuffer([]byte{}) + sc := bufio.NewScanner(buf) err = Run([]string{ "login", "--insecure", @@ -202,6 +208,7 @@ func TestOIDCLogin(t *testing.T) { }, cliOption(func(cf *CLIConf) error { cf.mockSSOLogin = mockSSOLogin(t, authServer, alice) cf.SiteName = "localhost" + cf.overrideStderr = buf return nil })) @@ -210,11 +217,22 @@ func TestOIDCLogin(t *testing.T) { // verify that auto-request happened require.True(t, didAutoRequest.Load()) + findMOTD(t, sc, motd) // if we got this far, then tsh successfully registered name change from `alice` to // `alice@example.com`, since the correct name needed to be used for the access // request to be generated. } +func findMOTD(t *testing.T, sc *bufio.Scanner, motd string) { + t.Helper() + for sc.Scan() { + if strings.Contains(sc.Text(), motd) { + return + } + } + require.Fail(t, "Failed to find %q MOTD in the logs", motd) +} + // TestLoginIdentityOut makes sure that "tsh login --out " command // writes identity credentials to the specified path. func TestLoginIdentityOut(t *testing.T) { @@ -268,7 +286,8 @@ func TestRelogin(t *testing.T) { require.NoError(t, err) alice.SetRoles([]string{"admin"}) - authProcess, proxyProcess := makeTestServers(t, connector, alice) + motd := "RELOGIN MOTD PRESENT" + authProcess, proxyProcess := makeTestServersWithMotd(t, motd, connector, alice) authServer := authProcess.GetAuthServer() require.NotNil(t, authServer) @@ -276,6 +295,8 @@ func TestRelogin(t *testing.T) { proxyAddr, err := proxyProcess.ProxyWebAddr() require.NoError(t, err) + buf := bytes.NewBuffer([]byte{}) + sc := bufio.NewScanner(buf) err = Run([]string{ "login", "--insecure", @@ -284,9 +305,11 @@ func TestRelogin(t *testing.T) { "--proxy", proxyAddr.String(), }, cliOption(func(cf *CLIConf) error { cf.mockSSOLogin = mockSSOLogin(t, authServer, alice) + cf.overrideStderr = buf return nil })) require.NoError(t, err) + findMOTD(t, sc, motd) err = Run([]string{ "login", @@ -294,10 +317,18 @@ func TestRelogin(t *testing.T) { "--debug", "--proxy", proxyAddr.String(), "localhost", - }) + }, cliOption(func(cf *CLIConf) error { + cf.overrideStderr = buf + return nil + })) require.NoError(t, err) + findMOTD(t, sc, motd) - err = Run([]string{"logout"}) + err = Run([]string{"logout"}, + cliOption(func(cf *CLIConf) error { + cf.overrideStderr = buf + return nil + })) require.NoError(t, err) err = Run([]string{ @@ -309,8 +340,10 @@ func TestRelogin(t *testing.T) { "localhost", }, cliOption(func(cf *CLIConf) error { cf.mockSSOLogin = mockSSOLogin(t, authServer, alice) + cf.overrideStderr = buf return nil })) + findMOTD(t, sc, motd) require.NoError(t, err) } @@ -480,7 +513,7 @@ func TestAccessRequestOnLeaf(t *testing.T) { }) require.NoError(t, err) - leafAuth, _ := makeTestServersWithName(t, "leafcluster") + leafAuth, _ := makeTestServersWithName(t, "leafcluster", "") tryCreateTrustedCluster(t, leafAuth.GetAuthServer(), trustedCluster) err = Run([]string{ @@ -1071,10 +1104,14 @@ func TestKubeConfigUpdate(t *testing.T) { } func makeTestServers(t *testing.T, bootstrap ...types.Resource) (auth *service.TeleportProcess, proxy *service.TeleportProcess) { - return makeTestServersWithName(t, "", bootstrap...) + return makeTestServersWithName(t, "", "", bootstrap...) +} + +func makeTestServersWithMotd(t *testing.T, motd string, bootstrap ...types.Resource) (auth *service.TeleportProcess, proxy *service.TeleportProcess) { + return makeTestServersWithName(t, "", motd, bootstrap...) } -func makeTestServersWithName(t *testing.T, name string, bootstrap ...types.Resource) (auth *service.TeleportProcess, proxy *service.TeleportProcess) { +func makeTestServersWithName(t *testing.T, name string, motd string, bootstrap ...types.Resource) (auth *service.TeleportProcess, proxy *service.TeleportProcess) { var err error // Set up a test auth server. //