It deleted the user and their tokens at once, while every other delete asks and takes -y/--yes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
676 lines
19 KiB
Go
676 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/term"
|
|
|
|
"housecarl/internal/api"
|
|
"housecarl/internal/model"
|
|
)
|
|
|
|
func historyCmd() *cobra.Command {
|
|
cmd := &cobra.Command{Use: "history", Short: "The history of configuration changes, and undo"}
|
|
var limit int
|
|
list := &cobra.Command{
|
|
Use: "list", Short: "List recent changes", Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var hs []model.HistoryEntry
|
|
if err := c.Get(ctx(), "/history", q("limit", strconv.Itoa(limit)), &hs); err != nil {
|
|
return err
|
|
}
|
|
return output(hs, func(w io.Writer) {
|
|
fmt.Fprintln(w, "ID\tWHEN\tBY\tVIA\tCHANGES\tDESCRIPTION")
|
|
for _, h := range hs {
|
|
desc := h.Description
|
|
if h.RevertedBy != 0 {
|
|
desc += fmt.Sprintf(" (undone by %d)", h.RevertedBy)
|
|
}
|
|
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%d\t%s\n", h.ID, h.Time.Local().Format("2006-01-02 15:04"), h.Username, h.Via, len(h.Changes), desc)
|
|
}
|
|
})
|
|
},
|
|
}
|
|
list.Flags().IntVar(&limit, "limit", 30, "how many")
|
|
cmd.AddCommand(list)
|
|
cmd.AddCommand(&cobra.Command{
|
|
Use: "show ID", Short: "Show a change: each object before and after", Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var h model.HistoryEntry
|
|
if err := c.Get(ctx(), "/history/"+args[0], nil, &h); err != nil {
|
|
return err
|
|
}
|
|
return output(h, func(w io.Writer) {
|
|
fmt.Fprintf(w, "%d\t%s by %s (%s): %s\n", h.ID, h.Time.Local().Format("2006-01-02 15:04:05"), h.Username, h.Via, h.Description)
|
|
for _, ch := range h.Changes {
|
|
fmt.Fprintf(w, "\n%s %s %s\n", ch.Action(), ch.Kind, ch.Label)
|
|
for _, line := range diffJSON(ch.Before, ch.After) {
|
|
fmt.Fprintln(w, " "+line)
|
|
}
|
|
}
|
|
})
|
|
},
|
|
})
|
|
cmd.AddCommand(&cobra.Command{
|
|
Use: "diff ID", Short: "Show what a change did to the ruleset", Args: cobra.ExactArgs(1),
|
|
Long: `Shows the ruleset's diff for a change: the configuration before and after
|
|
it, rendered by this version of Housecarl.`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var d struct {
|
|
Diff string `json:"diff"`
|
|
}
|
|
if err := c.Get(ctx(), "/history/"+args[0]+"/diff", nil, &d); err != nil {
|
|
return err
|
|
}
|
|
return output(d, func(w io.Writer) {
|
|
if d.Diff == "" {
|
|
fmt.Fprintln(w, "the ruleset didn't change")
|
|
return
|
|
}
|
|
printDiff(w, d.Diff)
|
|
})
|
|
},
|
|
})
|
|
cmd.AddCommand(&cobra.Command{
|
|
Use: "undo ID",
|
|
Short: "Undo a change",
|
|
Long: `Undoes a change by applying it backwards. It is refused when an object the
|
|
change touched has changed since; undo the later changes first.`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var resp api.MutationResponse
|
|
if err := c.Do(ctx(), "POST", "/history/"+args[0]+"/revert", nil, nil, &resp); err != nil {
|
|
return err
|
|
}
|
|
return report(&resp)
|
|
},
|
|
})
|
|
return cmd
|
|
}
|
|
|
|
// diffJSON lists the fields that differ between two versions of an
|
|
// object: "name: office -> lab". An object within (settings' builtins)
|
|
// is compared field by field: "builtins.drop_invalid: false -> true".
|
|
func diffJSON(before, after json.RawMessage) []string {
|
|
var a, b map[string]any
|
|
json.Unmarshal(before, &a)
|
|
json.Unmarshal(after, &b)
|
|
a, b = flatten(a, "", map[string]any{}), flatten(b, "", map[string]any{})
|
|
keys := map[string]bool{}
|
|
for k := range a {
|
|
keys[k] = true
|
|
}
|
|
for k := range b {
|
|
keys[k] = true
|
|
}
|
|
var names []string
|
|
for k := range keys {
|
|
names = append(names, k)
|
|
}
|
|
sort.Strings(names)
|
|
var out []string
|
|
show := func(v any, ok bool) string {
|
|
if !ok {
|
|
return "-"
|
|
}
|
|
x, _ := json.Marshal(v)
|
|
return string(x)
|
|
}
|
|
for _, k := range names {
|
|
va, oka := a[k]
|
|
vb, okb := b[k]
|
|
sa, sb := show(va, oka), show(vb, okb)
|
|
if sa == sb {
|
|
continue
|
|
}
|
|
switch {
|
|
case before == nil:
|
|
out = append(out, k+": "+sb)
|
|
case after == nil:
|
|
out = append(out, k+": "+sa)
|
|
default:
|
|
out = append(out, k+": "+sa+" -> "+sb)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// flatten lifts the fields of objects within m to dotted keys; lists
|
|
// stay whole.
|
|
func flatten(m map[string]any, prefix string, out map[string]any) map[string]any {
|
|
for k, v := range m {
|
|
if o, ok := v.(map[string]any); ok && len(o) > 0 {
|
|
flatten(o, prefix+k+".", out)
|
|
} else {
|
|
out[prefix+k] = v
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func whoamiCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "whoami", Short: "Show who you are to the server", Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var me api.Whoami
|
|
if err := c.Get(ctx(), "/auth/whoami", nil, &me); err != nil {
|
|
return err
|
|
}
|
|
return output(me, func(w io.Writer) {
|
|
fmt.Fprintf(w, "User:\t%s (%s)\n", me.User.Username, me.Method)
|
|
fmt.Fprintf(w, "Administrator:\t%v\n", me.Admin)
|
|
fmt.Fprintf(w, "May:\t%s\n", strings.Join(me.Permissions, ", "))
|
|
fmt.Fprintf(w, "Server:\tHousecarl %s\n", me.Build.String())
|
|
})
|
|
},
|
|
}
|
|
}
|
|
|
|
func readSecret(prompt string) (string, error) {
|
|
fmt.Fprint(os.Stderr, prompt)
|
|
if term.IsTerminal(int(os.Stdin.Fd())) {
|
|
b, err := term.ReadPassword(int(os.Stdin.Fd()))
|
|
fmt.Fprintln(os.Stderr)
|
|
return strings.TrimSpace(string(b)), err
|
|
}
|
|
var s string
|
|
_, err := fmt.Fscanln(os.Stdin, &s)
|
|
return strings.TrimSpace(s), err
|
|
}
|
|
|
|
func userID(name string) (int64, error) {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
var us []api.User
|
|
if err := c.Get(ctx(), "/users", nil, &us); err != nil {
|
|
return 0, err
|
|
}
|
|
for _, u := range us {
|
|
if strings.EqualFold(u.Username, name) || strconv.FormatInt(u.ID, 10) == name {
|
|
return u.ID, nil
|
|
}
|
|
}
|
|
return 0, fmt.Errorf("no user %s", name)
|
|
}
|
|
|
|
func userCmd() *cobra.Command {
|
|
cmd := &cobra.Command{Use: "user", Aliases: []string{"users"}, Short: "Users (administrators only)"}
|
|
cmd.AddCommand(&cobra.Command{
|
|
Use: "list", Short: "List users", Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var us []api.User
|
|
if err := c.Get(ctx(), "/users", nil, &us); err != nil {
|
|
return err
|
|
}
|
|
return output(us, func(w io.Writer) {
|
|
fmt.Fprintln(w, "ID\tUSER\tADMIN\tGRANTS\tPASSWORD\tDISABLED\tLAST LOGIN")
|
|
for _, u := range us {
|
|
login := "-"
|
|
if u.LastLoginAt != nil {
|
|
login = fmtWhen(*u.LastLoginAt)
|
|
}
|
|
fmt.Fprintf(w, "%d\t%s\t%v\t%s\t%v\t%v\t%s\n", u.ID, u.Username, u.Admin, orDash(strings.Join(u.GrantStrings, " ")), u.HasPassword, u.Disabled, login)
|
|
}
|
|
})
|
|
},
|
|
})
|
|
var admin, noPassword bool
|
|
var role, comment string
|
|
var grants []string
|
|
add := &cobra.Command{
|
|
Use: "add USER",
|
|
Short: "Add a user (asks for the password)",
|
|
Long: `Adds a user. Without --admin, give what they may do: a role (viewer,
|
|
operator, log) or grants such as --grant config:read --grant sets:write:guests.
|
|
--no-password makes a service account that only uses API tokens.`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
in := api.UserInput{Username: &args[0], Admin: &admin}
|
|
if !noPassword {
|
|
pw, err := readSecret("Password for " + args[0] + ": ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
in.Password = &pw
|
|
}
|
|
if role != "" {
|
|
in.Role = &role
|
|
}
|
|
if grants != nil {
|
|
in.Grants = &grants
|
|
}
|
|
if comment != "" {
|
|
in.Comment = &comment
|
|
}
|
|
var u api.User
|
|
if err := c.Do(ctx(), "POST", "/users", nil, in, &u); err != nil {
|
|
return err
|
|
}
|
|
if !flagQuiet {
|
|
fmt.Printf("added user %s (%d)\n", u.Username, u.ID)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
f := add.Flags()
|
|
f.BoolVar(&admin, "admin", false, "an administrator (may do everything)")
|
|
f.StringVar(&role, "role", "", "a role: viewer, operator or log")
|
|
f.StringArrayVar(&grants, "grant", nil, "a grant, resource:access (config, sets, log, audit; read or write; sets:write:SET,SET)")
|
|
f.BoolVar(&noPassword, "no-password", false, "no password: API tokens only")
|
|
f.StringVar(&comment, "comment", "", "a comment")
|
|
cmd.AddCommand(add)
|
|
|
|
var setPassword, disable, enable bool
|
|
update := &cobra.Command{
|
|
Use: "update USER", Short: "Change a user", Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
id, err := userID(args[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c, _ := newClient()
|
|
in := api.UserInput{}
|
|
fl := cmd.Flags()
|
|
if fl.Changed("admin") {
|
|
in.Admin = &admin
|
|
}
|
|
if fl.Changed("role") {
|
|
in.Role = &role
|
|
}
|
|
if fl.Changed("grant") {
|
|
in.Grants = &grants
|
|
}
|
|
if fl.Changed("comment") {
|
|
in.Comment = &comment
|
|
}
|
|
if disable || enable {
|
|
in.Disabled = &disable
|
|
}
|
|
if setPassword {
|
|
pw, err := readSecret("New password for " + args[0] + ": ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
in.Password = &pw
|
|
}
|
|
var u api.User
|
|
if err := c.Do(ctx(), "PATCH", "/users/"+strconv.FormatInt(id, 10), nil, in, &u); err != nil {
|
|
return err
|
|
}
|
|
if !flagQuiet {
|
|
fmt.Printf("updated user %s\n", u.Username)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
f = update.Flags()
|
|
f.BoolVar(&admin, "admin", false, "make (or with --admin=false, unmake) an administrator")
|
|
f.StringVar(&role, "role", "", "replace the grants with a role's")
|
|
f.StringArrayVar(&grants, "grant", nil, "replace the grants (repeat for several)")
|
|
f.BoolVar(&setPassword, "password", false, "set a new password (asks for it)")
|
|
f.BoolVar(&disable, "disable", false, "disable the user (logs them out)")
|
|
f.BoolVar(&enable, "enable", false, "enable the user again")
|
|
f.StringVar(&comment, "comment", "", "a comment")
|
|
cmd.AddCommand(update)
|
|
var yes bool
|
|
del := &cobra.Command{
|
|
Use: "delete USER", Short: "Delete a user and their tokens", Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if !confirm(fmt.Sprintf("Delete user %s and their tokens?", args[0]), yes) {
|
|
return fmt.Errorf("not deleted")
|
|
}
|
|
id, err := userID(args[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c, _ := newClient()
|
|
if err := c.Do(ctx(), "DELETE", "/users/"+strconv.FormatInt(id, 10), nil, nil, nil); err != nil {
|
|
return err
|
|
}
|
|
if !flagQuiet {
|
|
fmt.Println("deleted user", args[0])
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
del.Flags().BoolVarP(&yes, "yes", "y", false, "don't ask")
|
|
cmd.AddCommand(del)
|
|
return cmd
|
|
}
|
|
|
|
func tokenCmd() *cobra.Command {
|
|
cmd := &cobra.Command{Use: "token", Aliases: []string{"tokens"}, Short: "API tokens"}
|
|
var all bool
|
|
var user string
|
|
list := &cobra.Command{
|
|
Use: "list", Short: "List your tokens (--all: everyone's, for administrators)", Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path := "/me/tokens"
|
|
if all {
|
|
path = "/tokens"
|
|
}
|
|
var ts []api.Token
|
|
if err := c.Get(ctx(), path, nil, &ts); err != nil {
|
|
return err
|
|
}
|
|
return output(ts, func(w io.Writer) {
|
|
fmt.Fprintln(w, "ID\tUSER\tNAME\tGRANTS\tEXPIRES\tLAST USED")
|
|
for _, t := range ts {
|
|
exp, used := "never", "never"
|
|
if t.ExpiresAt != nil {
|
|
exp = fmtWhen(*t.ExpiresAt)
|
|
}
|
|
if t.LastUsedAt != nil {
|
|
used = fmtWhen(*t.LastUsedAt) + " from " + t.LastUsedIP
|
|
}
|
|
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\n", t.ID, t.Username, t.Name, strings.Join(t.GrantStrings, " "), exp, used)
|
|
}
|
|
})
|
|
},
|
|
}
|
|
list.Flags().BoolVar(&all, "all", false, "everyone's tokens")
|
|
cmd.AddCommand(list)
|
|
var role, expires string
|
|
var grants []string
|
|
var save bool
|
|
create := &cobra.Command{
|
|
Use: "create NAME",
|
|
Short: "Make an API token (the secret is shown once)",
|
|
Long: `Makes an API token. It has all of its user's permissions unless --role or
|
|
--grant narrows it. On the server as root, --user names whose token it is.
|
|
--save puts it in the CLI's configuration.`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path := "/me/tokens"
|
|
if user != "" {
|
|
id, err := userID(user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path = "/users/" + strconv.FormatInt(id, 10) + "/tokens"
|
|
}
|
|
in := api.TokenInput{Name: args[0], Role: role, Grants: grants, ExpiresIn: expires}
|
|
var t api.Token
|
|
if err := c.Do(ctx(), "POST", path, nil, in, &t); err != nil {
|
|
return err
|
|
}
|
|
if save {
|
|
cfg, err := loadCLIConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cfg.Token = t.Secret
|
|
if err := saveCLIConfig(cfg); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintln(os.Stderr, "saved in", flagConfig)
|
|
}
|
|
if flagOutput == "json" || flagOutput == "yaml" {
|
|
return output(t, nil)
|
|
}
|
|
fmt.Println(t.Secret)
|
|
if !flagQuiet {
|
|
fmt.Fprintln(os.Stderr, "this is the only time the token is shown; keep it safe")
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
f := create.Flags()
|
|
f.StringVar(&role, "role", "", "narrow it to a role: viewer, operator or log")
|
|
f.StringArrayVar(&grants, "grant", nil, "narrow it to grants (resource:access)")
|
|
f.StringVar(&expires, "expires", "", "expire after this long (90d, 720h)")
|
|
f.StringVar(&user, "user", "", "the user it belongs to (administrators)")
|
|
f.BoolVar(&save, "save", false, "save it as this CLI's token")
|
|
cmd.AddCommand(create)
|
|
var grace, rexp string
|
|
var current, rsave bool
|
|
rotate := &cobra.Command{
|
|
Use: "rotate [ID]",
|
|
Short: "Give a token a new secret (shown once)",
|
|
Long: `Gives a token a new secret. It keeps its ID, name and permissions, so only
|
|
the programs that use it need the new secret. The old secret stops at once,
|
|
or after --grace (up to 7d) so you can update them without an outage. A
|
|
token that expires gets its lifetime again, from now, unless --expires says
|
|
otherwise (90d, or never).
|
|
|
|
hc token rotate --current --save # this CLI's own token`,
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id := ""
|
|
switch {
|
|
case current && len(args) == 0:
|
|
var me api.Whoami
|
|
if err := c.Get(ctx(), "/auth/whoami", nil, &me); err != nil {
|
|
return err
|
|
}
|
|
if me.TokenID == 0 {
|
|
return fmt.Errorf("this CLI isn't using a token (%s); give the token's ID", me.Method)
|
|
}
|
|
id = strconv.FormatInt(me.TokenID, 10)
|
|
case len(args) == 1 && !current:
|
|
id = args[0]
|
|
default:
|
|
return fmt.Errorf("give the token's ID, or --current")
|
|
}
|
|
body := api.TokenRotation{Grace: grace}
|
|
switch rexp {
|
|
case "":
|
|
case "never":
|
|
body.NoExpiry = true
|
|
default:
|
|
body.ExpiresIn = rexp
|
|
}
|
|
var t api.Token
|
|
if err := c.Do(ctx(), "POST", "/tokens/"+id+"/rotate", nil, body, &t); err != nil {
|
|
return err
|
|
}
|
|
if rsave {
|
|
cfg, err := loadCLIConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cfg.Token = t.Secret
|
|
if err := saveCLIConfig(cfg); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintln(os.Stderr, "saved in", flagConfig)
|
|
}
|
|
if flagOutput == "json" || flagOutput == "yaml" {
|
|
return output(t, nil)
|
|
}
|
|
fmt.Println(t.Secret)
|
|
if !flagQuiet {
|
|
if t.PrevExpiresAt != nil {
|
|
fmt.Fprintf(os.Stderr, "the old secret works until %s\n", t.PrevExpiresAt.Local().Format("2006-01-02 15:04"))
|
|
} else {
|
|
fmt.Fprintln(os.Stderr, "the old secret no longer works")
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
f = rotate.Flags()
|
|
f.StringVar(&grace, "grace", "", "keep the old secret working this long (30m, 24h, up to 7d)")
|
|
f.StringVar(&rexp, "expires", "", "a new lifetime from now (90d), or never")
|
|
f.BoolVar(¤t, "current", false, "rotate the token this CLI uses")
|
|
f.BoolVar(&rsave, "save", false, "save the new secret as this CLI's token")
|
|
cmd.AddCommand(rotate)
|
|
cmd.AddCommand(&cobra.Command{
|
|
Use: "revoke ID", Short: "Revoke a token", Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := c.Do(ctx(), "DELETE", "/tokens/"+args[0], nil, nil, nil); err != nil {
|
|
return err
|
|
}
|
|
if !flagQuiet {
|
|
fmt.Println("revoked token", args[0])
|
|
}
|
|
return nil
|
|
},
|
|
})
|
|
return cmd
|
|
}
|
|
|
|
func passwordCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "password", Short: "Change your own password", Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cur, err := readSecret("Current password: ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pw, err := readSecret("New password: ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := c.Do(ctx(), "POST", "/me/password", nil, api.PasswordChange{Current: cur, Password: pw}, nil); err != nil {
|
|
return err
|
|
}
|
|
fmt.Println("password changed")
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func auditCmd() *cobra.Command {
|
|
var user, action, search string
|
|
var limit int
|
|
cmd := &cobra.Command{
|
|
Use: "audit", Short: "Show the audit log: logins, users, tokens, changes, confirmations, set entries", Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var evs []model.AuditEvent
|
|
if err := c.Get(ctx(), "/audit", q("user", user, "action", action, "q", search, "limit", strconv.Itoa(limit)), &evs); err != nil {
|
|
return err
|
|
}
|
|
return output(evs, func(w io.Writer) {
|
|
fmt.Fprintln(w, "WHEN\tUSER\tFROM\tACTION\tOBJECT\tRESULT\tDETAIL")
|
|
for _, e := range evs {
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", e.Time.Local().Format("2006-01-02 15:04:05"), orDash(e.Username), orDash(e.IP), e.Action, orDash(e.Object), e.Result, e.Detail)
|
|
}
|
|
})
|
|
},
|
|
}
|
|
f := cmd.Flags()
|
|
f.StringVar(&user, "user", "", "only this user's")
|
|
f.StringVar(&action, "action", "", "only actions starting with this (auth., config., set.)")
|
|
f.StringVar(&search, "search", "", "only events whose object or detail contains this")
|
|
f.IntVar(&limit, "limit", 100, "how many")
|
|
return cmd
|
|
}
|
|
|
|
func configCmd() *cobra.Command {
|
|
cmd := &cobra.Command{Use: "config", Short: "This CLI's settings: the server and token"}
|
|
cmd.AddCommand(&cobra.Command{
|
|
Use: "show", Short: "Show the CLI's configuration", Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cfg, err := loadCLIConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tok := "-"
|
|
if cfg.Token != "" {
|
|
tok = cfg.Token[:min(len(cfg.Token), 24)] + "…"
|
|
}
|
|
fmt.Printf("file: %s\nserver: %s\ntoken: %s\ninsecure: %v\nca_cert: %s\n", flagConfig, orDash(cfg.Server)+" (empty: the local socket)", tok, cfg.Insecure, orDash(cfg.CACert))
|
|
return nil
|
|
},
|
|
})
|
|
cmd.AddCommand(&cobra.Command{
|
|
Use: "set KEY [VALUE]",
|
|
Short: "Set server, token (asks when no value), insecure or ca_cert",
|
|
Args: cobra.RangeArgs(1, 2),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cfg, err := loadCLIConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
val := ""
|
|
if len(args) == 2 {
|
|
val = args[1]
|
|
} else if args[0] == "token" {
|
|
if val, err = readSecret("API token: "); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
switch args[0] {
|
|
case "server":
|
|
cfg.Server = val
|
|
case "token":
|
|
cfg.Token = val
|
|
case "insecure":
|
|
cfg.Insecure = val == "true" || val == "yes" || val == "on"
|
|
case "ca_cert", "ca-cert":
|
|
cfg.CACert = val
|
|
default:
|
|
return fmt.Errorf("unknown setting %q (use server, token, insecure or ca_cert)", args[0])
|
|
}
|
|
if err := saveCLIConfig(cfg); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintln(os.Stderr, "saved in", flagConfig)
|
|
return nil
|
|
},
|
|
})
|
|
return cmd
|
|
}
|