diff --git a/lib/client/api.go b/lib/client/api.go index b632f96b9d475..4ed6441f147f3 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -2437,14 +2437,8 @@ func (tc *TeleportClient) LogoutAll() error { return nil } -// Login logs the user into a Teleport cluster by talking to a Teleport proxy. -// -// The returned Key should typically be passed to ActivateKey in order to -// update local agent state. -// -func (tc *TeleportClient) Login(ctx context.Context) (*Key, error) { - // Ping the endpoint to see if it's up and find the type of authentication - // supported. +// 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 { return nil, trace.Wrap(err) @@ -2456,6 +2450,21 @@ func (tc *TeleportClient) Login(ctx context.Context) (*Key, error) { return nil, trace.Wrap(err) } } + return pr, nil +} + +// Login logs the user into a Teleport cluster by talking to a Teleport proxy. +// +// The returned Key should typically be passed to ActivateKey in order to +// update local agent state. +// +func (tc *TeleportClient) Login(ctx context.Context) (*Key, error) { + // Ping the endpoint to see if it's up and find the type of authentication + // supported, also show the message of the day if available. + pr, err := tc.PingAndShowMOTD(ctx) + if err != nil { + return nil, trace.Wrap(err) + } // generate a new keypair. the public key will be signed via proxy if client's // password+OTP are valid @@ -2643,7 +2652,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) } diff --git a/lib/client/export_test.go b/lib/client/export.go similarity index 100% rename from lib/client/export_test.go rename to lib/client/export.go diff --git a/lib/config/configuration.go b/lib/config/configuration.go index 28ca24f86ce90..de1ed39f0d043 100644 --- a/lib/config/configuration.go +++ b/lib/config/configuration.go @@ -547,6 +547,9 @@ func applyAuthConfig(fc *FileConfig, cfg *service.Config) error { if err != nil { return trace.Wrap(err) } + } + + if fc.Auth.MessageOfTheDay != "" { cfg.Auth.Preference.SetMessageOfTheDay(fc.Auth.MessageOfTheDay) } diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 297549405e655..4ec7451f7a4e4 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -812,12 +812,13 @@ func (h *Handler) pingWithConnector(w http.ResponseWriter, r *http.Request, p ht ServerVersion: teleport.Version, } + hasMessageOfTheDay := cap.GetMessageOfTheDay() != "" if connectorName == constants.Local { - as, err := localSettings(cap) + response.Auth, err = localSettings(cap) if err != nil { return nil, trace.Wrap(err) } - response.Auth = as + response.Auth.HasMessageOfTheDay = hasMessageOfTheDay return response, nil } @@ -825,6 +826,7 @@ func (h *Handler) pingWithConnector(w http.ResponseWriter, r *http.Request, p ht oidcConnector, err := authClient.GetOIDCConnector(r.Context(), connectorName, false) if err == nil { response.Auth = oidcSettings(oidcConnector, cap) + response.Auth.HasMessageOfTheDay = hasMessageOfTheDay return response, nil } @@ -832,6 +834,7 @@ func (h *Handler) pingWithConnector(w http.ResponseWriter, r *http.Request, p ht samlConnector, err := authClient.GetSAMLConnector(r.Context(), connectorName, false) if err == nil { response.Auth = samlSettings(samlConnector, cap) + response.Auth.HasMessageOfTheDay = hasMessageOfTheDay return response, nil } @@ -839,6 +842,7 @@ func (h *Handler) pingWithConnector(w http.ResponseWriter, r *http.Request, p ht githubConnector, err := authClient.GetGithubConnector(r.Context(), connectorName, false) if err == nil { response.Auth = githubSettings(githubConnector, cap) + response.Auth.HasMessageOfTheDay = hasMessageOfTheDay return response, nil } diff --git a/tool/tsh/tsh.go b/tool/tsh/tsh.go index 759c413d18cb7..2d0b650f99e0f 100644 --- a/tool/tsh/tsh.go +++ b/tool/tsh/tsh.go @@ -259,6 +259,8 @@ type CLIConf struct { // overrideStdout allows to switch standard output source for resource command. Used in tests. overrideStdout io.Writer + // 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 @@ -302,6 +304,14 @@ func (c *CLIConf) Stdout() io.Writer { return os.Stdout } +// 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 @@ -880,23 +890,37 @@ func onLogin(cf *CLIConf) error { // in case if nothing is specified, re-fetch kube clusters and print // current status case cf.Proxy == "" && cf.SiteName == "" && cf.DesiredRoles == "" && cf.RequestID == "" && cf.IdentityFileOut == "": + _, err := tc.PingAndShowMOTD(cf.Context) + if err != nil { + return trace.Wrap(err) + } if err := updateKubeConfig(cf, tc, ""); err != nil { return trace.Wrap(err) } printProfiles(cf.Debug, profile, profiles) + return nil // in case if parameters match, re-fetch kube clusters and print // current status case host(cf.Proxy) == host(profile.ProxyURL.Host) && cf.SiteName == profile.Cluster && cf.DesiredRoles == "" && cf.RequestID == "": + _, err := tc.PingAndShowMOTD(cf.Context) + if err != nil { + return trace.Wrap(err) + } if err := updateKubeConfig(cf, tc, ""); err != nil { return trace.Wrap(err) } printProfiles(cf.Debug, profile, profiles) + return nil // proxy is unspecified or the same as the currently provided proxy, // but cluster is specified, treat this as selecting a new cluster // for the same proxy case (cf.Proxy == "" || host(cf.Proxy) == host(profile.ProxyURL.Host)) && cf.SiteName != "": + _, err := tc.PingAndShowMOTD(cf.Context) + if err != nil { + return trace.Wrap(err) + } // trigger reissue, preserving any active requests. err = tc.ReissueUserCerts(cf.Context, client.CertCacheKeep, client.ReissueParams{ AccessRequests: profile.ActiveRequests.AccessRequests, @@ -911,11 +935,16 @@ func onLogin(cf *CLIConf) error { if err := updateKubeConfig(cf, tc, ""); err != nil { return trace.Wrap(err) } + return trace.Wrap(onStatus(cf)) // proxy is unspecified or the same as the currently provided proxy, // but desired roles or request ID is specified, treat this as a // privilege escalation request for the same login session. case (cf.Proxy == "" || host(cf.Proxy) == host(profile.ProxyURL.Host)) && (cf.DesiredRoles != "" || cf.RequestID != "") && cf.IdentityFileOut == "": + _, err := tc.PingAndShowMOTD(cf.Context) + if err != nil { + return trace.Wrap(err) + } if err := executeAccessRequest(cf, tc); err != nil { return trace.Wrap(err) } @@ -2112,6 +2141,9 @@ func makeClient(cf *CLIConf, useProfileLogin bool) (*client.TeleportClient, erro } } + tc.Config.Stderr = cf.Stderr() + tc.Config.Stdout = cf.Stdout() + tc.Config.Reason = cf.Reason tc.Config.Invited = cf.Invited tc.Config.DisplayParticipantRequirements = cf.displayParticipantRequirements diff --git a/tool/tsh/tsh_test.go b/tool/tsh/tsh_test.go index 4b11a3717f425..cdc1e8fa0acc7 100644 --- a/tool/tsh/tsh_test.go +++ b/tool/tsh/tsh_test.go @@ -17,6 +17,8 @@ limitations under the License. package main import ( + "bufio" + "bytes" "context" "fmt" "io/ioutil" @@ -175,7 +177,11 @@ func TestOIDCLogin(t *testing.T) { connector := mockConnector(t) - authProcess, proxyProcess := makeTestServers(t, withBootstrap(populist, dictator, connector, alice)) + motd := "MESSAGE_OF_THE_DAY_OIDC" + authProcess, proxyProcess := makeTestServers(t, + withBootstrap(populist, dictator, connector, alice), + withMOTD(t, motd), + ) authServer := authProcess.GetAuthServer() require.NotNil(t, authServer) @@ -213,6 +219,8 @@ func TestOIDCLogin(t *testing.T) { } }() + buf := bytes.NewBuffer([]byte{}) + sc := bufio.NewScanner(buf) err = Run([]string{ "login", "--insecure", @@ -223,6 +231,7 @@ func TestOIDCLogin(t *testing.T) { }, setHomePath(tmpHomePath), cliOption(func(cf *CLIConf) error { cf.mockSSOLogin = mockSSOLogin(t, authServer, alice) cf.SiteName = "localhost" + cf.overrideStderr = buf return nil })) @@ -231,11 +240,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) { @@ -283,7 +303,11 @@ func TestRelogin(t *testing.T) { require.NoError(t, err) alice.SetRoles([]string{"access"}) - authProcess, proxyProcess := makeTestServers(t, withBootstrap(connector, alice)) + motd := "RELOGIN MOTD PRESENT" + authProcess, proxyProcess := makeTestServers(t, + withBootstrap(connector, alice), + withMOTD(t, motd), + ) authServer := authProcess.GetAuthServer() require.NotNil(t, authServer) @@ -291,6 +315,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", @@ -299,9 +325,11 @@ func TestRelogin(t *testing.T) { "--proxy", proxyAddr.String(), }, setHomePath(tmpHomePath), 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", @@ -309,10 +337,20 @@ func TestRelogin(t *testing.T) { "--debug", "--proxy", proxyAddr.String(), "localhost", - }, setHomePath(tmpHomePath)) + }, setHomePath(tmpHomePath), + 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{"logout"}, setHomePath(tmpHomePath)) + err = Run([]string{"logout"}, setHomePath(tmpHomePath), + cliOption(func(cf *CLIConf) error { + cf.overrideStderr = buf + return nil + })) require.NoError(t, err) err = Run([]string{ @@ -324,8 +362,10 @@ func TestRelogin(t *testing.T) { "localhost", }, setHomePath(tmpHomePath), cliOption(func(cf *CLIConf) error { cf.mockSSOLogin = mockSSOLogin(t, authServer, alice) + cf.overrideStderr = buf return nil })) + findMOTD(t, sc, motd) require.NoError(t, err) } @@ -1238,8 +1278,8 @@ func TestSetX11Config(t *testing.T) { } type testServersOpts struct { - bootstrap []types.Resource - authConfigFunc func(cfg *service.AuthConfig) + bootstrap []types.Resource + authConfigFuncs []func(cfg *service.AuthConfig) } type testServerOptFunc func(o *testServersOpts) @@ -1252,7 +1292,11 @@ func withBootstrap(bootstrap ...types.Resource) testServerOptFunc { func withAuthConfig(fn func(cfg *service.AuthConfig)) testServerOptFunc { return func(o *testServersOpts) { - o.authConfigFunc = fn + if o.authConfigFuncs == nil { + o.authConfigFuncs = []func(cfg *service.AuthConfig){} + } + + o.authConfigFuncs = append(o.authConfigFuncs, fn) } } @@ -1267,6 +1311,17 @@ func withClusterName(t *testing.T, n string) testServerOptFunc { }) } +func withMOTD(t *testing.T, motd string) testServerOptFunc { + oldpass := client.PasswordFromConsoleFn + *client.PasswordFromConsoleFn = func() (string, error) { + return "", nil + } + t.Cleanup(func() { *client.PasswordFromConsoleFn = *oldpass }) + return withAuthConfig(func(cfg *service.AuthConfig) { + cfg.Preference.SetMessageOfTheDay(motd) + }) +} + func makeTestServers(t *testing.T, opts ...testServerOptFunc) (auth *service.TeleportProcess, proxy *service.TeleportProcess) { var options testServersOpts for _, opt := range opts { @@ -1299,8 +1354,8 @@ func makeTestServers(t *testing.T, opts ...testServerOptFunc) (auth *service.Tel cfg.Proxy.Enabled = false cfg.Log = utils.NewLoggerForTests() - if options.authConfigFunc != nil { - options.authConfigFunc(&cfg.Auth) + for _, fn := range options.authConfigFuncs { + fn(&cfg.Auth) } auth, err = service.NewTeleport(cfg)