From d0706962d1431e9d0755d182fb5fbde7a048e847 Mon Sep 17 00:00:00 2001 From: Ev Kontsevoy Date: Fri, 1 Sep 2017 18:22:27 -0700 Subject: [PATCH 1/2] New way to customize the behavior of tctl for the enterprise users. --- Makefile | 1 - docs/2.3/admin-guide.md | 4 ++-- e | 2 +- tool/tctl/common/collection.go | 31 +++++++++++-------------------- tool/tctl/common/tctl.go | 4 ++-- tool/tctl/common/token_command.go | 2 +- tool/tctl/common/user_command.go | 30 +++++++++++++++++++++++++++--- 7 files changed, 44 insertions(+), 30 deletions(-) diff --git a/Makefile b/Makefile index da944e0598d5e..3b0b6c49b6e12 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,6 @@ goinstall: github.com/gravitational/teleport/tool/teleport \ github.com/gravitational/teleport/tool/tctl - # # make install will installs system-wide teleport # diff --git a/docs/2.3/admin-guide.md b/docs/2.3/admin-guide.md index f9dd331e83b2a..b38a1b46dec8f 100644 --- a/docs/2.3/admin-guide.md +++ b/docs/2.3/admin-guide.md @@ -465,8 +465,8 @@ of 30 hours and a minimum of 1 minute. Once authenticated, the account will beco ```bash $ tctl users ls -User Allowed to Login as ----- ------------------- +User Allowed Logins +---- -------------- admin admin,root ross ross joe joe,root diff --git a/e b/e index 4c47bdbc9236d..c4614645b44cb 160000 --- a/e +++ b/e @@ -1 +1 @@ -Subproject commit 4c47bdbc9236d4128f237d2de4316d7287d4a887 +Subproject commit c4614645b44cb52cb755c30a03c3abea6afc7fbf diff --git a/tool/tctl/common/collection.go b/tool/tctl/common/collection.go index a5d12f2e2e99c..782b0efb4bfa3 100644 --- a/tool/tctl/common/collection.go +++ b/tool/tctl/common/collection.go @@ -43,7 +43,7 @@ type roleCollection struct { func (r *roleCollection) writeText(w io.Writer) error { t := goterm.NewTable(0, 10, 5, ' ', 0) - printHeader(t, []string{"Role", "Allowed to login as", "Node Labels", "Access to resources"}) + PrintHeader(t, []string{"Role", "Allowed to login as", "Node Labels", "Access to resources"}) if len(r.roles) == 0 { _, err := io.WriteString(w, t.String()) return trace.Wrap(err) @@ -93,7 +93,7 @@ type namespaceCollection struct { func (n *namespaceCollection) writeText(w io.Writer) error { t := goterm.NewTable(0, 10, 5, ' ', 0) - printHeader(t, []string{"Name"}) + PrintHeader(t, []string{"Name"}) if len(n.namespaces) == 0 { _, err := io.WriteString(w, t.String()) return trace.Wrap(err) @@ -155,7 +155,7 @@ type serverCollection struct { func (s *serverCollection) writeText(w io.Writer) error { t := goterm.NewTable(0, 10, 5, ' ', 0) - printHeader(t, []string{"Hostname", "UUID", "Address", "Labels"}) + PrintHeader(t, []string{"Hostname", "UUID", "Address", "Labels"}) if len(s.servers) == 0 { _, err := io.WriteString(w, t.String()) return trace.Wrap(err) @@ -197,17 +197,8 @@ type userCollection struct { } func (s *userCollection) writeText(w io.Writer) error { - t := goterm.NewTable(0, 10, 5, ' ', 0) - printHeader(t, []string{"User", "Roles", "Created By"}) - if len(s.users) == 0 { - _, err := io.WriteString(w, t.String()) - return trace.Wrap(err) - } - for _, u := range s.users { - fmt.Fprintf(t, "%v\t%v\t%v\n", u.GetName(), strings.Join(u.GetRoles(), ","), u.GetCreatedBy().String()) - } - _, err := io.WriteString(w, t.String()) - return trace.Wrap(err) + // a user collection does not need this method + return nil } func (s *userCollection) writeJSON(w io.Writer) error { @@ -241,7 +232,7 @@ type authorityCollection struct { func (a *authorityCollection) writeText(w io.Writer) error { t := goterm.NewTable(0, 10, 5, ' ', 0) - printHeader(t, []string{"Cluster Name", "CA Type", "Fingerprint", "Role Map"}) + PrintHeader(t, []string{"Cluster Name", "CA Type", "Fingerprint", "Role Map"}) for _, a := range a.cas { for _, keyBytes := range a.GetCheckingKeys() { fingerprint, err := sshutils.AuthorizedKeyFingerprint(keyBytes) @@ -292,7 +283,7 @@ type reverseTunnelCollection struct { func (r *reverseTunnelCollection) writeText(w io.Writer) error { t := goterm.NewTable(0, 10, 5, ' ', 0) - printHeader(t, []string{"Cluster Name", "Dial Addresses"}) + PrintHeader(t, []string{"Cluster Name", "Dial Addresses"}) for _, tunnel := range r.tunnels { fmt.Fprintf(t, "%v\t%v\n", tunnel.GetClusterName(), strings.Join(tunnel.GetDialAddrs(), ",")) } @@ -331,7 +322,7 @@ type oidcCollection struct { func (c *oidcCollection) writeText(w io.Writer) error { t := goterm.NewTable(0, 10, 5, ' ', 0) - printHeader(t, []string{"Name", "Issuer URL", "Additional Scope"}) + PrintHeader(t, []string{"Name", "Issuer URL", "Additional Scope"}) for _, conn := range c.connectors { fmt.Fprintf(t, "%v\t%v\t%v\n", conn.GetName(), conn.GetIssuerURL(), strings.Join(conn.GetScope(), ",")) } @@ -370,7 +361,7 @@ type samlCollection struct { func (c *samlCollection) writeText(w io.Writer) error { t := goterm.NewTable(0, 10, 5, ' ', 0) - printHeader(t, []string{"Name", "SSO URL"}) + PrintHeader(t, []string{"Name", "SSO URL"}) for _, conn := range c.connectors { fmt.Fprintf(t, "%v\t%v\n", conn.GetName(), conn.GetSSO()) } @@ -409,7 +400,7 @@ type trustedClusterCollection struct { func (c *trustedClusterCollection) writeText(w io.Writer) error { t := goterm.NewTable(0, 10, 5, ' ', 0) - printHeader(t, []string{"Name", "Enabled", "Token", "Proxy Address", "Reverse Tunnel Address", "Role Map"}) + PrintHeader(t, []string{"Name", "Enabled", "Token", "Proxy Address", "Reverse Tunnel Address", "Role Map"}) for _, tc := range c.trustedClusters { fmt.Fprintf(t, "%v\t%v\t%v\t%v\t%v\t%v\n", tc.GetName(), tc.GetEnabled(), tc.GetToken(), tc.GetProxyAddress(), tc.GetReverseTunnelAddress(), tc.CombinedMapping()) } @@ -448,7 +439,7 @@ type authPreferenceCollection struct { func (c *authPreferenceCollection) writeText(w io.Writer) error { t := goterm.NewTable(0, 10, 5, ' ', 0) - printHeader(t, []string{"Type", "Second Factor"}) + PrintHeader(t, []string{"Type", "Second Factor"}) fmt.Fprintf(t, "%v\t%v\n", c.GetType(), c.GetSecondFactor()) _, err := io.WriteString(w, t.String()) return trace.Wrap(err) diff --git a/tool/tctl/common/tctl.go b/tool/tctl/common/tctl.go index 92314066aad83..1da7ff43596fe 100644 --- a/tool/tctl/common/tctl.go +++ b/tool/tctl/common/tctl.go @@ -126,8 +126,8 @@ func Run(distribution string, commands []CLICommand) { } } -// printHeader helper prints an ASCII table header to stdout -func printHeader(t *goterm.Table, cols []string) { +// PrintHeader helper prints an ASCII table header to stdout +func PrintHeader(t *goterm.Table, cols []string) { dots := make([]string, len(cols)) for i := range dots { dots[i] = strings.Repeat("-", len(cols[i])) diff --git a/tool/tctl/common/token_command.go b/tool/tctl/common/token_command.go index ad8106bef921a..9503066f8fc4d 100644 --- a/tool/tctl/common/token_command.go +++ b/tool/tctl/common/token_command.go @@ -86,7 +86,7 @@ func (c *TokenCommand) List(client *auth.TunClient) error { } tokensView := func() string { table := goterm.NewTable(0, 10, 5, ' ', 0) - printHeader(table, []string{"Token", "Role", "Expiry Time (UTC)"}) + PrintHeader(table, []string{"Token", "Role", "Expiry Time (UTC)"}) for _, t := range tokens { expiry := "never" if t.Expires.Unix() > 0 { diff --git a/tool/tctl/common/user_command.go b/tool/tctl/common/user_command.go index d27ef80bea122..5dead77b6adce 100644 --- a/tool/tctl/common/user_command.go +++ b/tool/tctl/common/user_command.go @@ -19,11 +19,12 @@ package common import ( "fmt" "net" - "os" "strconv" "strings" + "github.com/buger/goterm" "github.com/gravitational/kingpin" + "github.com/gravitational/teleport" "github.com/gravitational/teleport/lib/auth" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/service" @@ -45,6 +46,16 @@ type UserCommand struct { userUpdate *kingpin.CmdClause userList *kingpin.CmdClause userDelete *kingpin.CmdClause + + // behavior of the user command can be changed by providing + // different implementations of subcommands commands + Impl *UserCommandImpl +} + +// UserCommandImpl is used to supply alternative implementations of +// user subcommands +type UserCommandImpl struct { + List func([]services.User, *auth.TunClient) error } // Initialize allows UserCommand to plug itself into the CLI parser @@ -149,8 +160,21 @@ func (u *UserCommand) List(client *auth.TunClient) error { if err != nil { return trace.Wrap(err) } - coll := &userCollection{users: users} - coll.writeText(os.Stdout) + if len(users) == 0 { + fmt.Println("No users found") + return nil + } + // see if "user ls" command is customized + if u.Impl != nil && u.Impl.List != nil { + return u.Impl.List(users, client) + } + t := goterm.NewTable(0, 10, 5, ' ', 0) + PrintHeader(t, []string{"User", "Allowed logins"}) + for _, u := range users { + logins, _ := u.GetTraits()[teleport.TraitLogins] + fmt.Fprintf(t, "%v\t%v\n", u.GetName(), strings.Join(logins, ",")) + } + fmt.Println(t.String()) return nil } From a7db6d7ca69368f494126d224c9df3e8de76f63a Mon Sep 17 00:00:00 2001 From: Ev Kontsevoy Date: Sun, 3 Sep 2017 19:16:24 -0700 Subject: [PATCH 2/2] tctl changes (polish for 2.3) This commit refs #1137 - tctl get user/joe now works (as reported in #1247) - tctl create/rm roles changes - added synonyms for various resources - made YAML the default output for tctl get - added better help + examples for tctl get - edited error messages - minor refactoring - added the system of "command plugins" which allows enterprise version of tctl to introduce different behavior to OSS commands --- Makefile | 1 + e | 2 +- lib/services/resource.go | 10 +- tool/tctl/common/collection.go | 2 +- tool/tctl/common/resource_command.go | 147 +++++++++++++-------------- 5 files changed, 76 insertions(+), 86 deletions(-) diff --git a/Makefile b/Makefile index 3b0b6c49b6e12..da944e0598d5e 100644 --- a/Makefile +++ b/Makefile @@ -47,6 +47,7 @@ goinstall: github.com/gravitational/teleport/tool/teleport \ github.com/gravitational/teleport/tool/tctl + # # make install will installs system-wide teleport # diff --git a/e b/e index c4614645b44cb..7b682b34657cd 160000 --- a/e +++ b/e @@ -1 +1 @@ -Subproject commit c4614645b44cb52cb755c30a03c3abea6afc7fbf +Subproject commit 7b682b34657cd5ce235c0ed447b3133354fcf6dd diff --git a/lib/services/resource.go b/lib/services/resource.go index 94cead6e1fce7..7ca29ffeb8346 100644 --- a/lib/services/resource.go +++ b/lib/services/resource.go @@ -361,7 +361,7 @@ func ParseShortcut(in string) (string, error) { return "", trace.BadParameter("missing resource name") } switch strings.ToLower(in) { - case "roles": + case "role", "roles": return KindRole, nil case "namespaces", "ns": return KindNamespace, nil @@ -369,19 +369,19 @@ func ParseShortcut(in string) (string, error) { return KindAuthServer, nil case "proxies": return KindProxy, nil - case "nodes": + case "nodes", "node": return KindNode, nil case "oidc": return KindOIDCConnector, nil case "saml": return KindSAMLConnector, nil - case "users": + case "user", "users": return KindUser, nil case "cert_authorities", "cas": return KindCertAuthority, nil case "reverse_tunnels", "rts": return KindReverseTunnel, nil - case "trusted_cluster", "tc": + case "trusted_cluster", "tc", "cluster", "clusters": return KindTrustedCluster, nil case "cluster_authentication_preferences", "cap": return KindClusterAuthPreference, nil @@ -441,5 +441,5 @@ func (r *Ref) Set(v string) error { } func (r *Ref) String() string { - return fmt.Sprintf("%v/%v", r.Kind, r.Name) + return fmt.Sprintf("%s/%s", r.Kind, r.Name) } diff --git a/tool/tctl/common/collection.go b/tool/tctl/common/collection.go index 782b0efb4bfa3..c524649d72e9d 100644 --- a/tool/tctl/common/collection.go +++ b/tool/tctl/common/collection.go @@ -31,7 +31,7 @@ import ( "github.com/ghodss/yaml" ) -type collection interface { +type ResourceCollection interface { writeText(w io.Writer) error writeJSON(w io.Writer) error writeYAML(w io.Writer) error diff --git a/tool/tctl/common/resource_command.go b/tool/tctl/common/resource_command.go index 4c878cb222b3e..d87e5be8dd24c 100644 --- a/tool/tctl/common/resource_command.go +++ b/tool/tctl/common/resource_command.go @@ -24,7 +24,6 @@ import ( "github.com/gravitational/kingpin" "github.com/gravitational/teleport/lib/auth" - "github.com/gravitational/teleport/lib/backend" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/service" "github.com/gravitational/teleport/lib/services" @@ -33,6 +32,13 @@ import ( kyaml "k8s.io/apimachinery/pkg/util/yaml" ) +// ResourceCommandImpl allows to customize the implementation of certain +// subcommands of the resource command +type ResourceCommandImpl struct { + Delete func(services.Ref, *auth.TunClient) error + Create func(*services.UnknownResource, *auth.TunClient) error +} + // ResourceCommand implements `tctl get/create/list` commands for manipulating // Teleport resources type ResourceCommand struct { @@ -49,8 +55,21 @@ type ResourceCommand struct { delete *kingpin.CmdClause get *kingpin.CmdClause create *kingpin.CmdClause + + // customized implementation + Impl *ResourceCommandImpl } +const getHelp = `Examples: + + $ tctl get clusters : prints the list of all trusted clusters + $ tctl get cluster/east : prints the trusted cluster 'east' + +Same as above, but using JSON output: + + $ tctl get clusters --format=json +` + // Initialize allows ResourceCommand to plug itself into the CLI parser func (g *ResourceCommand) Initialize(app *kingpin.Application, config *service.Config) { g.config = config @@ -61,25 +80,28 @@ func (g *ResourceCommand) Initialize(app *kingpin.Application, config *service.C g.delete = app.Command("rm", "Delete a resource").Alias("del") g.delete.Arg("resource", "Resource to delete").SetValue(&g.ref) - g.get = app.Command("get", "Print a resource") - g.get.Arg("resource", "Resource type and name").SetValue(&g.ref) - g.get.Flag("format", "Format output type, one of 'yaml', 'json' or 'text'").Default(formatText).StringVar(&g.format) + g.get = app.Command("get", "Print a YAML declaration of various Teleport resources") + g.get.Arg("resource", "Resource spec: 'type/[name]'").SetValue(&g.ref) + g.get.Flag("format", "Output format: 'yaml' or 'json'").Default(formatYAML).StringVar(&g.format) g.get.Flag("namespace", "Namespace of the resources").Hidden().Default(defaults.Namespace).StringVar(&g.namespace) g.get.Flag("with-secrets", "Include secrets in resources like certificate authorities or OIDC connectors").Default("false").BoolVar(&g.withSecrets) + + g.get.Alias(getHelp) } // TryRun takes the CLI command as an argument (like "auth gen") and executes it // or returns match=false if 'cmd' does not belong to it func (g *ResourceCommand) TryRun(cmd string, client *auth.TunClient) (match bool, err error) { switch cmd { - + // tctl get case g.get.FullCommand(): err = g.Get(client) + // tctl create case g.create.FullCommand(): err = g.Create(client) + // tctl rm case g.delete.FullCommand(): err = g.Delete(client) - default: return false, nil } @@ -92,6 +114,7 @@ func (g *ResourceCommand) Get(client *auth.TunClient) error { if err != nil { return trace.Wrap(err) } + switch g.format { case formatYAML: return collection.writeYAML(os.Stdout) @@ -154,24 +177,6 @@ func (u *ResourceCommand) Create(client *auth.TunClient) error { return trace.Wrap(err) } fmt.Printf("created OIDC connector: %v\n", conn.GetName()) - case services.KindReverseTunnel: - tun, err := services.GetReverseTunnelMarshaler().UnmarshalReverseTunnel(raw.Raw) - if err != nil { - return trace.Wrap(err) - } - if err := client.UpsertReverseTunnel(tun); err != nil { - return trace.Wrap(err) - } - fmt.Printf("created reverse tunnel: %v\n", tun.GetName()) - case services.KindCertAuthority: - ca, err := services.GetCertAuthorityMarshaler().UnmarshalCertAuthority(raw.Raw) - if err != nil { - return trace.Wrap(err) - } - if err := client.UpsertCertAuthority(ca); err != nil { - return trace.Wrap(err) - } - fmt.Printf("created cert authority: %v \n", ca.GetName()) case services.KindUser: user, err := services.GetUserMarshaler().UnmarshalUser(raw.Raw) if err != nil { @@ -181,28 +186,6 @@ func (u *ResourceCommand) Create(client *auth.TunClient) error { return trace.Wrap(err) } fmt.Printf("created user: %v\n", user.GetName()) - case services.KindRole: - role, err := services.GetRoleMarshaler().UnmarshalRole(raw.Raw) - if err != nil { - return trace.Wrap(err) - } - err = role.CheckAndSetDefaults() - if err != nil { - return trace.Wrap(err) - } - if err := client.UpsertRole(role, backend.Forever); err != nil { - return trace.Wrap(err) - } - fmt.Printf("created role: %v\n", role.GetName()) - case services.KindNamespace: - ns, err := services.UnmarshalNamespace(raw.Raw) - if err != nil { - return trace.Wrap(err) - } - if err := client.UpsertNamespace(*ns); err != nil { - return trace.Wrap(err) - } - fmt.Printf("created namespace: %v\n", ns.Metadata.Name) case services.KindTrustedCluster: tc, err := services.GetTrustedClusterMarshaler().Unmarshal(raw.Raw) if err != nil { @@ -215,33 +198,38 @@ func (u *ResourceCommand) Create(client *auth.TunClient) error { case "": return trace.BadParameter("missing resource kind") default: - return trace.BadParameter("%q is not supported", raw.Kind) + // customized creation: + if u.Impl != nil && u.Impl.Create != nil { + err := u.Impl.Create(&raw, client) + if err != nil { + return trace.Wrap(err) + } + } else { + return trace.BadParameter("creating resources of type %q is not supported", raw.Kind) + } } } } // Delete deletes resource by name -func (d *ResourceCommand) Delete(client *auth.TunClient) error { - if d.ref.Kind == "" { - return trace.BadParameter("provide full resource name to delete e.g. roles/example") - } - if d.ref.Name == "" { - return trace.BadParameter("provide full resource name to delete e.g. roles/example") +func (d *ResourceCommand) Delete(client *auth.TunClient) (err error) { + if d.ref.Kind == "" || d.ref.Name == "" { + return trace.BadParameter("provide a full resource name to delete, for example:\n$ tctl rm cluster/east\n") } switch d.ref.Kind { case services.KindUser: - if err := client.DeleteUser(d.ref.Name); err != nil { + if err = client.DeleteUser(d.ref.Name); err != nil { return trace.Wrap(err) } fmt.Printf("user %v has been deleted\n", d.ref.Name) case services.KindSAMLConnector: - if err := client.DeleteSAMLConnector(d.ref.Name); err != nil { + if err = client.DeleteSAMLConnector(d.ref.Name); err != nil { return trace.Wrap(err) } fmt.Printf("SAML Connector %v has been deleted\n", d.ref.Name) case services.KindOIDCConnector: - if err := client.DeleteOIDCConnector(d.ref.Name); err != nil { + if err = client.DeleteOIDCConnector(d.ref.Name); err != nil { return trace.Wrap(err) } fmt.Printf("OIDC Connector %v has been deleted\n", d.ref.Name) @@ -250,35 +238,43 @@ func (d *ResourceCommand) Delete(client *auth.TunClient) error { return trace.Wrap(err) } fmt.Printf("reverse tunnel %v has been deleted\n", d.ref.Name) - case services.KindRole: - if err := client.DeleteRole(d.ref.Name); err != nil { - return trace.Wrap(err) - } - fmt.Printf("role %v has been deleted\n", d.ref.Name) - case services.KindNamespace: - if err := client.DeleteNamespace(d.ref.Name); err != nil { - return trace.Wrap(err) - } - fmt.Printf("namespace %v has been deleted\n", d.ref.Name) case services.KindTrustedCluster: - if err := client.DeleteTrustedCluster(d.ref.Name); err != nil { + if err = client.DeleteTrustedCluster(d.ref.Name); err != nil { return trace.Wrap(err) } fmt.Printf("trusted cluster %q has been deleted\n", d.ref.Name) - case "": - return trace.BadParameter("missing resource kind") default: - return trace.BadParameter("%q is not supported", d.ref.Kind) + if d.Impl != nil && d.Impl.Delete != nil { + return trace.Wrap(d.Impl.Delete(d.ref, client)) + } + return trace.BadParameter("deleting resoruces of type %q is not supported", d.ref.Kind) } - return nil } -func (g *ResourceCommand) getCollection(client auth.ClientI) (collection, error) { +func (g *ResourceCommand) getCollection(client auth.ClientI) (c ResourceCollection, err error) { if g.ref.Kind == "" { return nil, trace.BadParameter("specify resource to list, e.g. 'tctl get roles'") } switch g.ref.Kind { + // load user(s) + case services.KindUser: + var users services.Users + // just one? + if !g.ref.IsEmtpy() { + user, err := client.GetUser(g.ref.Name) + if err != nil { + return nil, trace.Wrap(err) + } + users = services.Users{user} + // all of them? + } else { + users, err = client.GetUsers() + if err != nil { + return nil, trace.Wrap(err) + } + } + return &userCollection{users: users}, nil case services.KindSAMLConnector: connectors, err := client.GetSAMLConnectors(g.withSecrets) if err != nil { @@ -308,12 +304,6 @@ func (g *ResourceCommand) getCollection(client auth.ClientI) (collection, error) } userAuthorities = append(userAuthorities, hostAuthorities...) return &authorityCollection{cas: userAuthorities}, nil - case services.KindUser: - users, err := client.GetUsers() - if err != nil { - return nil, trace.Wrap(err) - } - return &userCollection{users: users}, nil case services.KindNode: nodes, err := client.GetNodes(g.namespace) if err != nil { @@ -372,7 +362,6 @@ func (g *ResourceCommand) getCollection(client auth.ClientI) (collection, error) } return &trustedClusterCollection{trustedClusters: []services.TrustedCluster{trustedCluster}}, nil } - return nil, trace.BadParameter("'%v' is not supported", g.ref.Kind) }