Update tsh login to select clusters.

The following changes have been introduced
to tsh login behavior:

1. tsh login now accepts cluster name
as an optional positional argument:

$ tsh login clustername

2. If tsh login is called without arguments
and the current credentials are valid,
tsh login now prints status, previous behavior
always forced login:

$ tsh login
... print status if logged in...

2. If tsh login is called with the proxy
equal to current, tsh login selects cluster,
otherwise it will re-login to another proxy:

$ tsh login one
... selected cluster one

$ tsh login two
... selected cluster two

$ tsh login --proxy=example.com three
... selected cluster three because
proxy is the same

$ tsh login --proxy=acme.example.com four
...will switch to proxy acme.example.com
and cluster four
This commit is contained in:
Sasha Klizhentas 2018-06-23 18:00:02 -07:00
parent f8b8de0a0a
commit e570b24eeb
8 changed files with 151 additions and 21 deletions

View file

@ -889,7 +889,7 @@ func (i *TeleInstance) NewUnauthenticatedClient(cfg ClientConfig) (tc *client.Te
SiteName: cfg.Cluster,
ForwardAgent: cfg.ForwardAgent,
}
cconf.SetProxy(proxyHost, proxyWebPort, proxySSHPort)
cconf.SetProxy(proxyHost, proxyWebPort, proxySSHPort, 0)
return client.NewClient(cconf)
}

View file

@ -52,6 +52,7 @@ import (
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/moby/moby/pkg/term"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
@ -233,6 +234,14 @@ type ProfileStatus struct {
// Extensions is a list of enabled SSH features for the certificate.
Extensions []string
// Cluster is a selected cluster
Cluster string
}
// IsExpired returns true if profile is not expired yet
func (p *ProfileStatus) IsExpired(clock clockwork.Clock) bool {
return p.ValidUntil.Sub(clock.Now()) <= 0
}
// readProfile reads in the profile as well as the associated certificate
@ -301,6 +310,7 @@ func readProfile(profileDir string, profileName string) (*ProfileStatus, error)
ValidUntil: validUntil,
Extensions: extensions,
Roles: roles,
Cluster: profile.SiteName,
}, nil
}
@ -336,6 +346,15 @@ func Status(profileDir string, proxyHost string) (*ProfileStatus, []*ProfileStat
var profile *ProfileStatus
var others []*ProfileStatus
// remove ports from proxy host, because profile name is stored
// by host name
if proxyHost != "" {
proxyHost, err = utils.Host(proxyHost)
if err != nil {
return nil, nil, trace.Wrap(err)
}
}
// Construct the full path to the profile requested and make sure it exists.
profileDir = FullProfilePath(profileDir)
stat, err := os.Stat(profileDir)
@ -381,6 +400,11 @@ func Status(profileDir string, proxyHost string) (*ProfileStatus, []*ProfileStat
}
ps, err := readProfile(profileDir, file.Name())
if err != nil {
// parts of profile are missing?
// status skips these files
if trace.IsNotFound(err) {
continue
}
return nil, nil, trace.Wrap(err)
}
others = append(others, ps)
@ -403,7 +427,7 @@ func (c *Config) LoadProfile(profileDir string, proxyName string) error {
return trace.Wrap(err)
}
// apply the profile to the current configuration:
c.SetProxy(cp.ProxyHost, cp.ProxyWebPort, cp.ProxySSHPort)
c.SetProxy(cp.ProxyHost, cp.ProxyWebPort, cp.ProxySSHPort, cp.ProxyKubePort)
c.Username = cp.Username
c.SiteName = cp.SiteName
c.LocalForwardPorts, err = ParsePortForwardSpec(cp.ForwardedPorts)
@ -427,6 +451,7 @@ func (c *Config) SaveProfile(profileDir string, profileOptions ...ProfileOptions
cp.Username = c.Username
cp.ProxySSHPort = c.ProxySSHPort()
cp.ProxyWebPort = c.ProxyWebPort()
cp.ProxyKubePort = c.ProxyKubePort()
cp.ForwardedPorts = c.LocalForwardPorts.ToStringSpec()
cp.SiteName = c.SiteName
@ -446,8 +471,12 @@ func (c *Config) SaveProfile(profileDir string, profileOptions ...ProfileOptions
return nil
}
func (c *Config) SetProxy(host string, webPort, sshPort int) {
c.ProxyHostPort = fmt.Sprintf("%s:%d,%d", host, webPort, sshPort)
func (c *Config) SetProxy(host string, webPort, sshPort, kubePort int) {
if kubePort != 0 {
c.ProxyHostPort = fmt.Sprintf("%s:%d,%d,%d", host, webPort, sshPort, kubePort)
} else {
c.ProxyHostPort = fmt.Sprintf("%s:%d,%d", host, webPort, sshPort)
}
}
// ProxyHost returns the hostname of the proxy server (without any port numbers)
@ -472,6 +501,10 @@ func (c *Config) ProxyWebHostPort() string {
return net.JoinHostPort(c.ProxyHost(), strconv.Itoa(c.ProxyWebPort()))
}
func (c *Config) ProxyKubeHostPort() string {
return net.JoinHostPort(c.ProxyHost(), strconv.Itoa(c.ProxyKubePort()))
}
// ProxyWebPort returns the port number of teleport HTTP proxy stored in the config
// usually 3080 by default.
func (c *Config) ProxyWebPort() (retval int) {
@ -506,6 +539,23 @@ func (c *Config) ProxySSHPort() (retval int) {
return retval
}
// ProxyKubePort returns the port number of teleport Kubernetes proxy stored in the config
// usually 3026 by default.
func (c *Config) ProxyKubePort() (retval int) {
retval = defaults.KubeProxyListenPort
_, port, err := net.SplitHostPort(c.ProxyHostPort)
if err == nil && len(port) > 0 {
ports := strings.Split(port, ",")
if len(ports) > 2 {
retval, err = strconv.Atoi(ports[2])
if err != nil {
log.Warnf("invalid proxy Kubernetes port: '%v': %v", ports, err)
}
}
}
return retval
}
// ProxySpecified returns true if proxy has been specified
func (c *Config) ProxySpecified() bool {
return len(c.ProxyHostPort) > 0

View file

@ -43,7 +43,7 @@ func (s *APITestSuite) TestConfig(c *check.C) {
c.Assert(conf.ProxySSHHostPort(), check.Equals, "example.org:3023")
c.Assert(conf.ProxyWebHostPort(), check.Equals, "example.org:3080")
conf.SetProxy("example.org", 100, 200)
conf.SetProxy("example.org", 100, 200, 0)
c.Assert(conf.ProxyWebHostPort(), check.Equals, "example.org:100")
c.Assert(conf.ProxySSHHostPort(), check.Equals, "example.org:200")
@ -54,6 +54,12 @@ func (s *APITestSuite) TestConfig(c *check.C) {
conf.ProxyHostPort = "example.org:,200"
c.Assert(conf.ProxySSHHostPort(), check.Equals, "example.org:200")
c.Assert(conf.ProxyWebHostPort(), check.Equals, "example.org:3080")
conf.SetProxy("example.org", 100, 200, 300)
c.Assert(conf.ProxyWebHostPort(), check.Equals, "example.org:100")
c.Assert(conf.ProxySSHHostPort(), check.Equals, "example.org:200")
c.Assert(conf.ProxyKubeHostPort(), check.Equals, "example.org:300")
}
func (s *APITestSuite) TestNew(c *check.C) {

View file

@ -37,9 +37,10 @@ type ClientProfile struct {
//
// proxy configuration
//
ProxyHost string `yaml:"proxy_host,omitempty"`
ProxySSHPort int `yaml:"proxy_port,omitempty"`
ProxyWebPort int `yaml:"proxy_web_port,omitempty"`
ProxyHost string `yaml:"proxy_host,omitempty"`
ProxySSHPort int `yaml:"proxy_port,omitempty"`
ProxyWebPort int `yaml:"proxy_web_port,omitempty"`
ProxyKubePort int `yaml:"proxy_kube_port,omitempty"`
//
// auth/identity

View file

@ -6,7 +6,6 @@ import (
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
@ -22,8 +21,10 @@ func UpdateKubeconfig(tc *client.TeleportClient) error {
return trace.Wrap(err)
}
clusterName := tc.ProxyHost()
// TODO: unhardcode the port
clusterAddr := fmt.Sprintf("https://%v:%v", tc.ProxyHost(), defaults.KubeProxyListenPort)
if tc.SiteName != "" && tc.SiteName != clusterName {
clusterName = fmt.Sprintf("%v.%v", tc.SiteName, tc.ProxyHost())
}
clusterAddr := fmt.Sprintf("https://%v:%v", clusterName, tc.ProxyKubePort())
creds, err := tc.LocalAgent().GetKey()
if err != nil {

View file

@ -280,7 +280,7 @@ func (f *Forwarder) setupContext(ctx auth.AuthContext, req *http.Request, isRemo
return nil, trace.Wrap(err)
}
for _, remoteCluster := range f.Tunnel.GetSites() {
if strings.HasSuffix(req.Host, remoteCluster.GetName()+".") {
if strings.HasPrefix(req.Host, remoteCluster.GetName()+".") {
f.Debugf("Going to proxy to cluster: %v based on matching host suffix %v.", remoteCluster.GetName(), req.Host)
targetCluster = remoteCluster
isRemoteCluster = remoteCluster.GetName() != f.ClusterName
@ -561,9 +561,9 @@ type clusterSession struct {
func (f *Forwarder) getOrCreateClusterSession(ctx authContext) (*clusterSession, error) {
client := f.getClusterSession(ctx)
if client != nil {
f.Debugf("Returning existing creds for %v.", ctx)
return client, nil
}
f.Debugf("Requesting new creds for %v.", ctx)
return f.newClusterSession(ctx)
}

View file

@ -10,5 +10,11 @@ EXAMPLES:
$ tsh --proxy=host.example.com:8080,8023 login
Use port 8080 and 3023 (default) for SSH proxy:
$ tsh --proxy=host.example.com:8080 login`
$ tsh --proxy=host.example.com:8080 login
Login and select cluster "two":
$ tsh --proxy=host.example.com login two
Select cluster "two" using existing credentials and proxy:
$ tsh login two`
)

View file

@ -44,6 +44,7 @@ import (
"github.com/gravitational/trace"
gops "github.com/google/gops/agent"
"github.com/jonboulle/clockwork"
"github.com/sirupsen/logrus"
)
@ -146,6 +147,11 @@ func main() {
Run(cmd_line, false)
}
const (
clusterEnvVar = "TELEPORT_SITE"
clusterHelp = "Specify the cluster to connect"
)
// Run executes TSH client. same as main() but easier to test
func Run(args []string, underTest bool) {
var cf CLIConf
@ -159,7 +165,7 @@ func Run(args []string, underTest bool) {
app.Flag("proxy", "SSH proxy address").Envar("TELEPORT_PROXY").StringVar(&cf.Proxy)
app.Flag("nocache", "do not cache cluster discovery locally").Hidden().BoolVar(&cf.NoCache)
app.Flag("user", fmt.Sprintf("SSH proxy user [%s]", localUser)).Envar("TELEPORT_USER").StringVar(&cf.Username)
app.Flag("cluster", "Specify the cluster to connect").Envar("TELEPORT_SITE").StringVar(&cf.SiteName)
app.Flag("ttl", "Minutes to live for a SSH session").Int32Var(&cf.MinsToLive)
app.Flag("identity", "Identity file").Short('i').StringVar(&cf.IdentityFileIn)
app.Flag("compat", "OpenSSH compatibility flag").Hidden().StringVar(&cf.Compatibility)
@ -182,20 +188,26 @@ func Run(args []string, underTest bool) {
ssh.Flag("forward", "Forward localhost connections to remote server").Short('L').StringsVar(&cf.LocalForwardPorts)
ssh.Flag("local", "Execute command on localhost after connecting to SSH node").Default("false").BoolVar(&cf.LocalExec)
ssh.Flag("tty", "Allocate TTY").Short('t').BoolVar(&cf.Interactive)
ssh.Flag("cluster", clusterHelp).Envar(clusterEnvVar).StringVar(&cf.SiteName)
// join
join := app.Command("join", "Join the active SSH session")
join.Flag("cluster", clusterHelp).Envar(clusterEnvVar).StringVar(&cf.SiteName)
join.Arg("session-id", "ID of the session to join").Required().StringVar(&cf.SessionID)
// play
play := app.Command("play", "Replay the recorded SSH session")
play.Flag("cluster", clusterHelp).Envar(clusterEnvVar).StringVar(&cf.SiteName)
play.Arg("session-id", "ID of the session to play").Required().StringVar(&cf.SessionID)
// scp
scp := app.Command("scp", "Secure file copy")
scp.Flag("cluster", clusterHelp).Envar(clusterEnvVar).StringVar(&cf.SiteName)
scp.Arg("from, to", "Source and destination to copy").Required().StringsVar(&cf.CopySpec)
scp.Flag("recursive", "Recursive copy of subdirectories").Short('r').BoolVar(&cf.RecursiveCopy)
scp.Flag("port", "Port to connect to on the remote host").Short('P').Int32Var(&cf.NodePort)
scp.Flag("quiet", "Quiet mode").Short('q').BoolVar(&cf.Quiet)
// ls
ls := app.Command("ls", "List remote SSH nodes")
ls.Flag("cluster", clusterHelp).Envar(clusterEnvVar).StringVar(&cf.SiteName)
ls.Arg("labels", "List of labels to filter node list").StringVar(&cf.UserHost)
// clusters
clusters := app.Command("clusters", "List available Teleport clusters")
@ -208,6 +220,7 @@ func Run(args []string, underTest bool) {
login.Flag("format", fmt.Sprintf("Identity format [%s] or %s (for OpenSSH compatibility)",
client.DefaultIdentityFormat,
client.IdentityFormatOpenSSH)).Default(string(client.DefaultIdentityFormat)).StringVar((*string)(&cf.IdentityFormat))
login.Arg("cluster", clusterHelp).StringVar(&cf.SiteName)
login.Alias(loginUsageFooter)
// logout deletes obtained session certificates in ~/.tsh
@ -215,6 +228,7 @@ func Run(args []string, underTest bool) {
// bench
bench := app.Command("bench", "Run shell or execute a command on a remote SSH node").Hidden()
bench.Flag("cluster", clusterHelp).Envar(clusterEnvVar).StringVar(&cf.SiteName)
bench.Arg("[user@]host", "Remote hostname and the login to use").Required().StringVar(&cf.UserHost)
bench.Arg("command", "Command to execute on a remote host").Required().StringsVar(&cf.RemoteCommand)
bench.Flag("port", "SSH port on a remote host").Short('p').Int32Var(&cf.NodePort)
@ -281,7 +295,6 @@ func Run(args []string, underTest bool) {
case clusters.FullCommand():
onListSites(&cf)
case login.FullCommand():
refuseArgs(login.FullCommand(), args)
onLogin(&cf)
case logout.FullCommand():
refuseArgs(logout.FullCommand(), args)
@ -320,12 +333,47 @@ func onLogin(cf *CLIConf) {
utils.FatalError(trace.BadParameter("invalid identity format: %s", cf.IdentityFormat))
}
// Get the status of the active profile ~/.tsh/profile as well as the status
// of any other proxies the user is logged into.
profile, profiles, err := client.Status("", cf.Proxy)
if err != nil {
if !trace.IsNotFound(err) {
utils.FatalError(err)
}
}
// make the teleport client and retrieve the certificate from the proxy:
tc, err = makeClient(cf, true)
if err != nil {
utils.FatalError(err)
}
// client is already logged in and profile is not expired
if profile != nil && !profile.IsExpired(clockwork.NewRealClock()) {
switch {
// in case if nothing is specified, print current status
case cf.Proxy == "" && cf.SiteName == "":
printProfiles(profile, profiles)
return
// in case if parameters match, print current status
case host(cf.Proxy) == host(profile.ProxyURL.Host) && cf.SiteName == profile.Cluster:
printProfiles(profile, profiles)
return
// 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 != "":
tc.SaveProfile("")
if err := kubeclient.UpdateKubeconfig(tc); err != nil {
utils.FatalError(err)
}
onStatus(cf)
return
// otherwise just passthrough to standard login
default:
}
}
if cf.Username == "" {
cf.Username = tc.Username
}
@ -350,11 +398,8 @@ func onLogin(cf *CLIConf) {
// regular login (without -i flag)
tc.SaveProfile("")
if tc.SiteName != "" {
fmt.Printf("\nYou are now logged into %s as %s\n", tc.SiteName, tc.Username)
} else {
fmt.Printf("\nYou are now logged in\n")
}
onStatus(cf)
}
// onLogout deletes a "session certificate" from ~/.tsh for a given proxy
@ -849,6 +894,9 @@ func printStatus(p *client.ProfileStatus, isActive bool) {
fmt.Printf("%vProfile URL: %v\n", prefix, p.ProxyURL.String())
fmt.Printf(" Logged in as: %v\n", p.Username)
if p.Cluster != "" {
fmt.Printf(" Cluster: %v\n", p.Cluster)
}
fmt.Printf(" Roles: %v*\n", strings.Join(p.Roles, ", "))
fmt.Printf(" Logins: %v\n", strings.Join(p.Logins, ", "))
fmt.Printf(" Valid until: %v [%v]\n", p.ValidUntil, humanDuration)
@ -862,9 +910,16 @@ func onStatus(cf *CLIConf) {
// of any other proxies the user is logged into.
profile, profiles, err := client.Status("", cf.Proxy)
if err != nil {
if trace.IsNotFound(err) {
fmt.Printf("Not logged in.\n")
return
}
utils.FatalError(err)
}
printProfiles(profile, profiles)
}
func printProfiles(profile *client.ProfileStatus, profiles []*client.ProfileStatus) {
// Print the active profile.
if profile != nil {
printStatus(profile, true)
@ -882,3 +937,14 @@ func onStatus(cf *CLIConf) {
fmt.Printf(" https://gravitaitonal.com/teleport/docs/enteprise\n")
}
}
// host is a utility function that extracts
// host from the host:port pair, in case of any error
// returns the original value
func host(in string) string {
out, err := utils.Host(in)
if err != nil {
return in
}
return out
}