diff --git a/cli/account_command.go b/cli/account_command.go index ada9bcec..8ac407cf 100644 --- a/cli/account_command.go +++ b/cli/account_command.go @@ -472,31 +472,20 @@ func (c *actCmd) infoAction(_ *fisk.ParseContext) error { } cols.Indent(2) + defer cols.Indent(0) + cols.Println(title) if len(p.Allow) > 0 { sort.Strings(p.Allow) - for i, perm := range p.Allow { - if i == 0 { - cols.AddRow("Allow", perm) - } else { - cols.AddRow("", perm) - } - } + cols.AddStringsAsValue("Allow", p.Allow) } if len(p.Deny) > 0 { cols.Println() sort.Strings(p.Deny) - for i, perm := range p.Deny { - if i == 0 { - cols.AddRow("Deny", perm) - } else { - cols.AddRow("", perm) - } - } + cols.AddStringsAsValue("Deny", p.Deny) } - cols.Indent(0) } if ui != nil && ui.Permissions != nil { diff --git a/cli/auth_account_command.go b/cli/auth_account_command.go new file mode 100644 index 00000000..975857aa --- /dev/null +++ b/cli/auth_account_command.go @@ -0,0 +1,863 @@ +package cli + +import ( + "fmt" + "io" + "os" + "path/filepath" + "time" + + "github.com/AlecAivazis/survey/v2" + "github.com/choria-io/fisk" + "github.com/dustin/go-humanize" + ab "github.com/synadia-io/jwt-auth-builder.go" + "github.com/synadia-io/jwt-auth-builder.go/providers/nsc" +) + +type authAccountCommand struct { + accountName string + operatorName string + expiry time.Duration + bearerAllowed bool + maxSubs int64 + maxConns int64 + maxPayloadString string + maxPayload int64 + maxLeafnodes int64 + maxImports int64 + maxExports int64 + jetStream bool + defaults bool + maxAckPending int64 + storeMaxString string + storeMax int64 + storeMaxStreamString string + storeMaxStream int64 + memMaxString string + memMax int64 + memMaxStreamString string + memMaxStream int64 + streamSizeRequired bool + maxStreams int64 + maxConsumers int64 + listNames bool + force bool + skRole string + locale string + connTypes []string + pubAllow []string + pubDeny []string + subAllow []string + subDeny []string +} + +func configureAuthAccountCommand(auth commandHost) { + c := &authAccountCommand{} + + // TODO: + // - rm is written but builder doesnt remove from disk https://github.com/synadia-io/jwt-auth-builder.go/issues/22 + // - edit should diff and prompt + // - imports/exports + + acct := auth.Command("account", "Manage NATS Accounts").Hidden().Alias("a").Alias("acct") + + addCreateFlags := func(f *fisk.CmdClause, edit bool) { + f.Flag("expiry", "How long this account should be valid for as a duration").PlaceHolder("DURATION").DurationVar(&c.expiry) + f.Flag("bearer", "Allows bearer tokens").Default("false").BoolVar(&c.bearerAllowed) + f.Flag("subscriptions", "Maximum allowed subscriptions").Default("-1").Int64Var(&c.maxSubs) + f.Flag("connections", "Maximum allowed connections").Default("-1").Int64Var(&c.maxConns) + f.Flag("payload", "Maximum allowed payload").PlaceHolder("BYTES").StringVar(&c.maxPayloadString) + f.Flag("leafnodes", "Maximum allowed Leafnode connections").Default("-1").Int64Var(&c.maxLeafnodes) + f.Flag("imports", "Maximum allowed imports").Default("-1").Int64Var(&c.maxImports) + f.Flag("exports", "Maximum allowed exports").Default("-1").Int64Var(&c.maxExports) + f.Flag("jetstream", "Enables JetStream").Default("false").UnNegatableBoolVar(&c.jetStream) + f.Flag("js-streams", "Sets the maximum Streams the account can have").Default("-1").Int64Var(&c.maxStreams) + f.Flag("js-consumers", "Sets the maximum Consumers the account can have").Default("-1").Int64Var(&c.maxConsumers) + f.Flag("js-disk", "Sets a Disk Storage quota").PlaceHolder("BYTES").StringVar(&c.storeMaxString) + f.Flag("js-disk-stream", "Sets the maximum size a Disk Storage stream may be").PlaceHolder("BYTES").Default("-1").StringVar(&c.storeMaxStreamString) + f.Flag("js-memory", "Sets a Memory Storage quota").PlaceHolder("BYTES").StringVar(&c.memMaxString) + f.Flag("js-memory-stream", "Sets the maximum size a Memory Storage stream may be").PlaceHolder("BYTES").Default("-1").StringVar(&c.memMaxStreamString) + f.Flag("js-max-pending", "Default Max Ack Pending for Tier 0 limits").PlaceHolder("MESSAGES").Int64Var(&c.maxAckPending) + f.Flag("js-stream-size-required", "Requires Streams to have a maximum size declared").UnNegatableBoolVar(&c.streamSizeRequired) + } + + add := acct.Command("add", "Adds a new Account").Action(c.addAction) + add.Arg("name", "Unique name for this Account").StringVar(&c.accountName) + add.Flag("operator", "Operator to add the account to").StringVar(&c.operatorName) + addCreateFlags(add, false) + add.Flag("defaults", "Accept default values without prompting").UnNegatableBoolVar(&c.defaults) + + info := acct.Command("info", "Show Account information").Alias("i").Alias("show").Alias("view").Action(c.infoAction) + info.Arg("name", "Account to view").StringVar(&c.accountName) + info.Flag("operator", "Operator hosting the account").StringVar(&c.operatorName) + + edit := acct.Command("edit", "Edit account settings").Alias("update").Action(c.editAction) + edit.Arg("name", "Unique name for this Account").StringVar(&c.accountName) + edit.Flag("operator", "Operator to add the account to").StringVar(&c.operatorName) + addCreateFlags(edit, false) + + ls := acct.Command("ls", "List accounts").Action(c.lsAction) + ls.Arg("operator", "Operator to act on").StringVar(&c.operatorName) + ls.Flag("names", "Show just the Account names").UnNegatableBoolVar(&c.listNames) + + rm := acct.Command("rm", "Removes an account").Action(c.rmAction) + rm.Arg("name", "Account to view").StringVar(&c.accountName) + rm.Flag("operator", "Operator hosting the account").StringVar(&c.operatorName) + rm.Flag("force", "Removes without prompting").Short('f').UnNegatableBoolVar(&c.force) + + sk := acct.Command("keys", "Manage Scoped Signing Keys").Alias("sk").Alias("s") + + skadd := sk.Command("add", "Adds a signing key").Alias("new").Alias("a").Alias("n").Action(c.skAddAction) + skadd.Arg("name", "Account to act on").StringVar(&c.accountName) + skadd.Arg("role", "The role to add a key for").StringVar(&c.skRole) + skadd.Flag("operator", "Operator to act on").StringVar(&c.operatorName) + skadd.Flag("subscriptions", "Maximum allowed subscriptions").Default("-1").Int64Var(&c.maxSubs) + skadd.Flag("payload", "Maximum allowed payload").PlaceHolder("BYTES").StringVar(&c.maxPayloadString) + skadd.Flag("bearer", "Allows bearer tokens").Default("false").BoolVar(&c.bearerAllowed) + skadd.Flag("locale", "Locale for the client").StringVar(&c.locale) + skadd.Flag("connection", "Set the allowed connections (nats, ws, wsleaf, mqtt)").EnumsVar(&c.connTypes, "nats", "ws", "leaf", "wsleaf", "mqtt") + skadd.Flag("pub-allow", "Sets subjects where publishing is allowed").StringsVar(&c.pubAllow) + skadd.Flag("pub-deny", "Sets subjects where publishing is allowed").StringsVar(&c.pubDeny) + skadd.Flag("sub-allow", "Sets subjects where subscribing is allowed").StringsVar(&c.subAllow) + skadd.Flag("sub-deny", "Sets subjects where subscribing is allowed").StringsVar(&c.subDeny) + + skInfo := sk.Command("info", "Show information for a Scoped Signing Key").Action(c.skInfoAction) + skInfo.Arg("name", "Account to view").StringVar(&c.accountName) + skInfo.Arg("key", "The role or key to view").StringVar(&c.skRole) + skInfo.Flag("operator", "Operator to act on").StringVar(&c.operatorName) + + skls := sk.Command("list", "List Scoped Signing Keys").Alias("ls").Action(c.skListAction) + skls.Arg("name", "Account to act on").StringVar(&c.accountName) + skls.Flag("operator", "Operator to act on").StringVar(&c.operatorName) + + skrm := sk.Command("rm", "Remove a scoped signing key").Action(c.skRmAction) + skrm.Arg("name", "Account to act on").StringVar(&c.accountName) + skrm.Arg("key", "The key to remove").StringVar(&c.skRole) + skrm.Flag("operator", "Operator to act on").StringVar(&c.operatorName) + skrm.Flag("force", "Removes without prompting").Short('f').UnNegatableBoolVar(&c.force) + +} +func (c *authAccountCommand) skRmAction(_ *fisk.ParseContext) error { + _, _, acct, err := c.selectAccount(true) + if err != nil { + return err + } + + if c.skRole == "" { + err := askOne(&survey.Input{ + Message: "Scoped Signing Key", + Help: "The key to remove", + }, &c.skRole, survey.WithValidator(survey.Required)) + if err != nil { + return err + } + } + + if !c.force { + ok, err := askConfirmation(fmt.Sprintf("Really remove the Scoped Signing Key %s", c.skRole), false) + if err != nil { + return err + } + + if !ok { + return nil + } + } + + ok, err := acct.ScopedSigningKeys().Delete(c.skRole) + if err != nil { + return err + } + + if !ok { + return fmt.Errorf("key %q not found", c.skRole) + } + + fmt.Printf("key %q removed\n", c.skRole) + return nil +} + +func (c *authAccountCommand) skInfoAction(_ *fisk.ParseContext) error { + _, _, acct, err := c.selectAccount(true) + if err != nil { + return err + } + + if c.skRole == "" { + err := askOne(&survey.Input{ + Message: "Role or Key", + Help: "The key to view by role or key name", + }, &c.skRole, survey.WithValidator(survey.Required)) + if err != nil { + return err + } + } + + var ok bool + sk := acct.ScopedSigningKeys().GetScopeByRole(c.skRole) + if sk == nil { + sk, ok = acct.ScopedSigningKeys().GetScope(c.skRole) + if !ok { + return fmt.Errorf("no role or scope found matching %q", c.skRole) + } + } + + out, err := c.showSk(sk) + if err != nil { + return err + } + + fmt.Println(out) + return nil +} + +func (c *authAccountCommand) skAddAction(_ *fisk.ParseContext) error { + auth, _, acct, err := c.selectAccount(true) + if err != nil { + return err + } + + if c.skRole == "" { + err := askOne(&survey.Input{ + Message: "Role Name", + Help: "The role to associate with this key", + }, &c.skRole, survey.WithValidator(survey.Required)) + if err != nil { + return err + } + } + + if c.maxPayloadString != "" { + c.maxPayload, err = parseStringAsBytes(c.maxPayloadString) + if err != nil { + return err + } + } + + limits, err := acct.ScopedSigningKeys().AddScope(c.skRole) + if err != nil { + return err + } + + err = limits.SetMaxSubscriptions(c.maxSubs) + if err != nil { + return err + } + + err = limits.SetMaxPayload(c.maxPayload) + if err != nil { + return err + } + + err = limits.SetBearerToken(c.bearerAllowed) + if err != nil { + return err + } + + err = limits.SetLocale(c.locale) + if err != nil { + return err + } + + if len(c.connTypes) > 0 { + err = limits.ConnectionTypes().Set(c.connectionTypes()...) + if err != nil { + return err + } + } + + err = limits.PubPermissions().SetAllow(c.pubAllow...) + if err != nil { + return err + } + err = limits.PubPermissions().SetDeny(c.pubDeny...) + if err != nil { + return err + } + + err = limits.SubPermissions().SetAllow(c.subAllow...) + if err != nil { + return err + } + err = limits.SubPermissions().SetDeny(c.subDeny...) + if err != nil { + return err + } + + err = auth.Commit() + if err != nil { + return err + } + + return c.fShowSk(os.Stdout, limits) +} + +func (c *authAccountCommand) fShowSk(w io.Writer, limits ab.ScopeLimits) error { + out, err := c.showSk(limits) + if err != nil { + return err + } + + _, err = fmt.Fprintln(w, out) + + return err +} + +func (c *authAccountCommand) showSk(limits ab.ScopeLimits) (string, error) { + cols := newColumns("Scoped Signing Key %s", limits.Key()) + + cols.AddSectionTitle("Config") + + cols.AddRow("Key", limits.Key()) + cols.AddRow("Role", limits.Role()) + cols.AddRow("Locale", limits.Locale()) + cols.AddRow("Bearer Token", limits.BearerToken()) + + cols.AddSectionTitle("Limits") + cols.AddRowUnlimitedIf("Payload", limits.MaxPayload(), limits.MaxPayload() == -1) + cols.AddRowUnlimited("Subscriptions", limits.MaxSubscriptions(), -1) + if limits.MaxData() > 0 { + cols.AddRow("Data", limits.MaxData()) + } + cols.AddRow("Connection Types", limits.ConnectionTypes().Types()) + cols.AddStringsAsValue("Connection Sources", limits.ConnectionSources().Sources()) + + cols.AddSectionTitle("Permissions") + cols.Indent(2) + cols.AddSectionTitle("Publish") + if len(limits.PubPermissions().Allow()) > 0 || len(limits.PubPermissions().Deny()) > 0 { + if len(limits.PubPermissions().Allow()) > 0 { + cols.AddStringsAsValue("Allow", limits.PubPermissions().Allow()) + } + if len(limits.PubPermissions().Deny()) > 0 { + cols.AddStringsAsValue("Deny", limits.PubPermissions().Deny()) + } + } else { + cols.Println("No permissions defined") + } + + cols.AddSectionTitle("Subscribe") + if len(limits.SubPermissions().Allow()) > 0 || len(limits.SubPermissions().Deny()) > 0 { + if len(limits.SubPermissions().Allow()) > 0 { + cols.AddStringsAsValue("Allow", limits.PubPermissions().Allow()) + } + + if len(limits.SubPermissions().Deny()) > 0 { + cols.AddStringsAsValue("Deny", limits.PubPermissions().Deny()) + } + } else { + cols.Println("No permissions defined") + } + + cols.Indent(0) + + return cols.Render() +} + +func (c *authAccountCommand) connectionTypes() []string { + var types []string + + for _, t := range c.connTypes { + switch t { + case "nats": + types = append(types, "STANDARD") + case "ws": + types = append(types, "WEBSOCKET") + case "wsleaf": + types = append(types, "LEAFNODE_WS") + case "leaf": + types = append(types, "LEAFNODE") + case "mqtt": + types = append(types, "MQTT") + } + } + + return types +} + +func (c *authAccountCommand) skListAction(_ *fisk.ParseContext) error { + _, _, acct, err := c.selectAccount(true) + if err != nil { + return err + } + + var table *tbl + + if len(acct.ScopedSigningKeys().List()) > 0 { + table = newTableWriter("Scoped Signing Keys") + table.AddHeaders("Name", "Role", "Key", "Pub Perms", "Sub Perms") + for _, sk := range acct.ScopedSigningKeys().List() { + scope, _ := acct.ScopedSigningKeys().GetScope(sk) + + pubs := len(scope.PubPermissions().Allow()) + len(scope.PubPermissions().Deny()) + subs := len(scope.SubPermissions().Allow()) + len(scope.SubPermissions().Deny()) + + table.AddRow(sk, scope.Role(), scope.Key(), scope.MaxSubscriptions(), pubs, subs) + } + fmt.Println(table.Render()) + fmt.Println() + } + + if len(acct.ScopedSigningKeys().ListRoles()) > 0 { + table = newTableWriter("Roles") + table.AddHeaders("Name", "Key") + for _, r := range acct.ScopedSigningKeys().ListRoles() { + role := acct.ScopedSigningKeys().GetScopeByRole(r) + if role == nil { + continue + } + + table.AddRow(role.Role(), role.Key()) + } + fmt.Println(table.Render()) + } + + if table == nil { + fmt.Println("No Scoped Signing Keys or Roles defined") + } + + return nil +} + +func (c *authAccountCommand) editAction(_ *fisk.ParseContext) error { + auth, operator, acct, err := c.selectAccount(true) + if err != nil { + return err + } + + // TODO: need to think if we should support disabling jetstream here, possibly by turning + // --jetstream into a bool and adding an isSet variable, then we could disable it + err = c.updateAccount(acct, c.jetStream || acct.Limits().JetStream().IsJetStreamEnabled()) + if err != nil { + return err + } + + err = auth.Commit() + if err != nil { + return err + } + + return c.fShowAccount(os.Stdout, operator, acct) +} + +func (c *authAccountCommand) rmAction(_ *fisk.ParseContext) error { + fmt.Println("WARNING: At present deleting is not supported by the nsc store") + fmt.Println() + + auth, operator, account, err := c.selectAccount(true) + if err != nil { + return err + } + + if !c.force { + ok, err := askConfirmation(fmt.Sprintf("Really remove the Accouint %s", c.accountName), false) + if err != nil { + return err + } + + if !ok { + return nil + } + } + + err = operator.Accounts().Delete(account.Name()) + if err != nil { + return err + } + + err = auth.Commit() + if err != nil { + return err + } + + fmt.Printf("Removed account %s\n", account.Name()) + return nil +} + +func (c *authAccountCommand) lsAction(_ *fisk.ParseContext) error { + _, operator, err := c.selectOperator(true) + if err != nil { + return err + } + + list := operator.Accounts().List() + if len(list) == 0 { + fmt.Println("No Accounts found") + return nil + } + + if c.listNames { + for _, op := range list { + fmt.Println(op.Name()) + } + return nil + } + + table := newTableWriter("Accounts") + table.AddHeaders("Name", "Subject", "Users", "JetStream", "System") + for _, acct := range operator.Accounts().List() { + system := "" + js := "" + if acct.Subject() == operator.SystemAccount().Subject() { + system = "true" + } + if acct.Limits().JetStream().IsJetStreamEnabled() { + js = "true" + } + + table.AddRow(acct.Name(), acct.Subject(), len(acct.Users().List()), js, system) + } + fmt.Println(table.Render()) + + return nil +} + +func (c *authAccountCommand) infoAction(_ *fisk.ParseContext) error { + _, operator, account, err := c.selectAccount(true) + if err != nil { + return err + } + + return c.fShowAccount(os.Stdout, operator, account) +} + +func (c *authAccountCommand) updateAccount(acct ab.Account, js bool) error { + limits := acct.Limits().OperatorLimits() + limits.Conn = c.maxConns + limits.Subs = c.maxSubs + limits.Payload = c.maxPayload + limits.LeafNodeConn = c.maxLeafnodes + limits.Exports = c.maxExports + limits.Imports = c.maxImports + limits.DisallowBearer = !c.bearerAllowed + + if js { + if c.storeMaxStream > 0 { + limits.JetStreamLimits.DiskMaxStreamBytes = c.storeMaxStream + } + if c.memMaxStream > 0 { + limits.JetStreamLimits.MemoryMaxStreamBytes = c.memMaxStream + } + limits.JetStreamLimits.DiskStorage = c.storeMax + limits.JetStreamLimits.MemoryStorage = c.memMax + limits.JetStreamLimits.MaxBytesRequired = c.streamSizeRequired + limits.JetStreamLimits.Consumer = c.maxConsumers + limits.JetStreamLimits.Streams = c.maxStreams + limits.JetStreamLimits.MaxAckPending = c.maxAckPending + } + + err := acct.Limits().SetOperatorLimits(limits) + if err != nil { + return err + } + + if c.expiry > 0 { + err = acct.SetExpiry(time.Now().Add(c.expiry).Unix()) + if err != nil { + return err + } + } + + return nil +} + +func (c *authAccountCommand) addAction(_ *fisk.ParseContext) error { + auth, operator, err := c.selectOperator(true) + if err != nil { + return err + } + + if c.accountName == "" { + err := askOne(&survey.Input{ + Message: "Account Name", + Help: "A unique name for the Account being added", + }, &c.accountName, survey.WithValidator(survey.Required)) + if err != nil { + return err + } + } + + if isAuthItemKnown(operator.Accounts().List(), c.accountName) { + return fmt.Errorf("account %s already exist", c.accountName) + } + + acct, err := operator.Accounts().Add(c.accountName) + if err != nil { + return err + } + + if !c.defaults { + if c.maxConns == -1 { + c.maxConns, err = askOneInt("Maximum Connections", "-1", "The maximum amount of client connections allowed for this account, set using --connections") + if err != nil { + return err + } + } + + if c.maxSubs == -1 { + c.maxSubs, err = askOneInt("Maximum Subscriptions", "-1", "The maximum amount of subscriptions allowed for this account, set using --subscriptions") + if err != nil { + return err + } + } + + if c.maxPayloadString == "" { + c.maxPayload, err = askOneBytes("Maximum Message Payload", "-1", "The maximum size any message may have, set using --payload", "") + if err != nil { + return err + } + c.maxPayloadString = "" + } + + fmt.Println() + } + + if c.maxPayloadString != "" { + c.maxPayload, err = parseStringAsBytes(c.maxPayloadString) + if err != nil { + return err + } + } + + if c.jetStream { + if c.storeMaxString == "" { + c.storeMax, err = askOneBytes("Maximum JetStream Disk Storage", "1GB", "Maximum amount of disk this account may use, set using --js-disk", "JetStream requires maximum Disk usage set") + if err != nil { + return err + } + } + if c.memMaxString == "" { + c.memMax, err = askOneBytes("Maximum JetStream Memory Storage", "1GB", "Maximum amount of memory this account may use, set using --js-memory", "JetStream requires maximum Memory usage set") + if err != nil { + return err + } + } + + if c.storeMaxString != "" { + c.storeMax, err = parseStringAsBytes(c.storeMaxString) + if err != nil { + return err + } + } + if c.memMaxString != "" { + c.memMax, err = parseStringAsBytes(c.memMaxString) + if err != nil { + return err + } + } + + if c.memMaxStreamString != "-1" { + c.memMaxStream, err = parseStringAsBytes(c.memMaxStreamString) + if err != nil { + return err + } + } + if c.storeMaxStreamString != "-1" { + c.storeMaxStream, err = parseStringAsBytes(c.storeMaxStreamString) + if err != nil { + return err + } + } + } + + err = c.updateAccount(acct, c.jetStream) + if err != nil { + return err + } + + err = auth.Commit() + if err != nil { + return err + } + + return c.fShowAccount(os.Stdout, operator, acct) +} + +func (c *authAccountCommand) fShowAccount(w io.Writer, operator ab.Operator, acct ab.Account) error { + out, err := c.showAccount(operator, acct) + if err != nil { + return err + } + + _, err = fmt.Fprintln(w, out) + + return err +} + +func (c *authAccountCommand) showAccount(operator ab.Operator, acct ab.Account) (string, error) { + limits := acct.Limits() + js := limits.JetStream() + + cols := newColumns("Account %s (%s)", acct.Name(), acct.Subject()) + cols.AddSectionTitle("Configuration") + cols.AddRow("Name", acct.Name()) + cols.AddRow("Account", operator.Name()) + cols.AddRow("Issuer", acct.Issuer()) + cols.AddRow("System Account", operator.SystemAccount().Subject() == acct.Subject()) + cols.AddRow("JetStream", js.IsJetStreamEnabled()) + cols.AddRowIf("Expiry", time.Unix(acct.Expiry(), 0), acct.Expiry() > 0) + cols.AddRow("Users", len(acct.Users().List())) + cols.AddSectionTitle("Limits") + cols.AddRow("Bearer Tokens Allowed", !limits.DisallowBearerTokens()) + cols.AddRowUnlimited("Subscriptions", limits.MaxSubscriptions(), -1) + cols.AddRowUnlimited("Connections", limits.MaxConnections(), -1) + cols.AddRowUnlimitedIf("Maximum Payload", humanize.IBytes(uint64(limits.MaxPayload())), limits.MaxPayload() <= 0) + if limits.MaxData() > 0 { + cols.AddRow("Data", limits.MaxData()) // only showing when set as afaik its a ngs thing + } + cols.AddRowUnlimited("Leafnodes", limits.MaxLeafNodeConnections(), -1) + cols.AddRowUnlimited("Imports", limits.MaxImports(), -1) + cols.AddRowUnlimited("Exports", limits.MaxExports(), -1) + + if js.IsJetStreamEnabled() { + cols.Indent(2) + cols.AddSectionTitle("JetStream Limits") + + tiers := c.validTiers(acct) + + cols.Indent(4) + for _, tc := range tiers { + tier, _ := js.Get(tc) + if tier == nil { + continue + } + + if tc == 0 { + cols.AddSectionTitle("Account Default Limits") + } else { + cols.AddSectionTitle("Tier %d", tc) + } + + if unlimited, _ := tier.IsUnlimited(); unlimited { + cols.Indent(6) + cols.Println("Unlimited") + cols.Indent(4) + continue + } + + maxAck, _ := tier.MaxAckPending() + maxMem, _ := tier.MaxMemoryStorage() + maxMemStream, _ := tier.MaxMemoryStreamSize() + maxConns, _ := tier.MaxConsumers() + maxDisk, _ := tier.MaxDiskStorage() + maxDiskStream, _ := tier.MaxDiskStreamSize() + streams, _ := tier.MaxStreams() + streamSizeRequired, _ := tier.MaxStreamSizeRequired() + + cols.AddRowUnlimited("Max Ack Pending", maxAck, 0) + cols.AddRowUnlimited("Maximum Streams", streams, -1) + cols.AddRowUnlimited("Max Consumers", maxConns, -1) + cols.AddRow("Max Stream Size Required", streamSizeRequired) + cols.AddRow("Max File Storage", humanize.IBytes(uint64(maxDisk))) + cols.AddRowIf("Max File Storage Stream Size", humanize.IBytes(uint64(maxDiskStream)), maxDiskStream > 0) + cols.AddRow("Max Memory Storage", humanize.IBytes(uint64(maxMem))) + cols.AddRowIf("Max Memory Storage Stream Size", humanize.IBytes(uint64(maxMemStream)), maxMemStream > 0) + } + + cols.Indent(0) + } + + return cols.Render() +} + +func (c *authAccountCommand) validTiers(acct ab.Account) []int8 { + tiers := []int8{} + for i := int8(0); i <= 5; i++ { + tier, _ := acct.Limits().JetStream().Get(i) + if tier != nil { + tiers = append(tiers, i) + } + } + + if len(tiers) > 1 { + tiers = tiers[1:] + } + + return tiers +} + +func (c *authAccountCommand) selectAccount(pick bool) (*ab.AuthImpl, ab.Operator, ab.Account, error) { + auth, operator, err := c.selectOperator(pick) + if err != nil { + return nil, nil, nil, err + } + + if c.accountName == "" || !isAuthItemKnown(operator.Accounts().List(), c.accountName) { + if !pick { + return nil, nil, nil, fmt.Errorf("unknown Account: %v", c.accountName) + } + + if !isTerminal() { + return nil, nil, nil, fmt.Errorf("cannot pick an Account without a terminal and no Account name supplied") + } + + names := sortedAuthNames(operator.Accounts().List()) + err = askOne(&survey.Select{ + Message: "Select an Account", + Options: names, + PageSize: selectPageSize(len(names)), + }, &c.accountName) + if err != nil { + return nil, nil, nil, err + } + } + + acct := operator.Accounts().Get(c.accountName) + if operator == nil { + return nil, nil, nil, fmt.Errorf("unknown Account: %v", c.accountName) + } + + return auth, operator, acct, nil + +} +func (c *authAccountCommand) selectOperator(pick bool) (*ab.AuthImpl, ab.Operator, error) { + auth, err := c.getAuth() + if err != nil { + return nil, nil, err + } + + if c.operatorName == "" || !isAuthItemKnown(auth.Operators().List(), c.operatorName) { + if !pick { + return nil, nil, fmt.Errorf("unknown operator: %v", c.operatorName) + } + + operators := auth.Operators().List() + if len(operators) == 1 { + return auth, operators[0], nil + } + + if !isTerminal() { + return nil, nil, fmt.Errorf("cannot pick an Operator without a terminal and no operator name supplied") + } + + names := sortedAuthNames(auth.Operators().List()) + if len(names) == 0 { + return nil, nil, fmt.Errorf("no operators found") + } + + err = askOne(&survey.Select{ + Message: "Select an Operator", + Options: names, + PageSize: selectPageSize(len(names)), + }, &c.operatorName) + if err != nil { + return nil, nil, err + } + } + + op := auth.Operators().Get(c.operatorName) + if op == nil { + return nil, nil, fmt.Errorf("unknown operator: %v", c.operatorName) + } + + return auth, op, nil +} + +func (c *authAccountCommand) getAuth() (*ab.AuthImpl, error) { + storeDir, err := nscStore() + if err != nil { + return nil, err + } + + return ab.NewAuth(nsc.NewNscProvider(filepath.Join(storeDir, "stores"), filepath.Join(storeDir, "keys"))) +} diff --git a/cli/auth_command.go b/cli/auth_command.go index dcb90565..fba2296f 100644 --- a/cli/auth_command.go +++ b/cli/auth_command.go @@ -1,14 +1,43 @@ package cli +import ( + "sort" +) + func configureAuthCommand(app commandHost) { auth := app.Command("auth", "NATS Decentralized Authentication") auth.HelpLong("WARNING: This is experimental and subject to massive change, do not use yet") configureAuthOperatorCommand(auth) + configureAuthAccountCommand(auth) configureAuthNkeyCommand(auth) } func init() { registerCommand("auth", 0, configureAuthCommand) } + +type listWithNames interface { + Name() string +} + +func sortedAuthNames[list listWithNames](items []list) []string { + res := []string{} + for _, i := range items { + res = append(res, i.Name()) + } + + sort.Strings(res) + return res +} + +func isAuthItemKnown[list listWithNames](items []list, name string) bool { + for _, op := range items { + if op.Name() == name { + return true + } + } + + return false +} diff --git a/cli/auth_operator_command.go b/cli/auth_operator_command.go index bc802ccd..0a28a6c8 100644 --- a/cli/auth_operator_command.go +++ b/cli/auth_operator_command.go @@ -10,7 +10,6 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/choria-io/fisk" - "github.com/nats-io/natscli/columns" ab "github.com/synadia-io/jwt-auth-builder.go" "github.com/synadia-io/jwt-auth-builder.go/providers/nsc" ) @@ -33,9 +32,6 @@ type authOperatorCommand struct { func configureAuthOperatorCommand(auth commandHost) { c := &authOperatorCommand{} - // TODO: - // rm - but nsc doesnt delete operators - op := auth.Command("operator", "Manage NATS Operators").Hidden().Alias("o").Alias("op") // TODO: @@ -52,7 +48,7 @@ func configureAuthOperatorCommand(auth commandHost) { info.Arg("name", "Operator to view").StringVar(&c.operatorName) ls := op.Command("list", "List Operators").Alias("ls").Action(c.lsAction) - ls.Flag("names", "Show just the Operator names").BoolVar(&c.listNames) + ls.Flag("names", "Show just the Operator names").UnNegatableBoolVar(&c.listNames) edit := op.Command("edit", "Edit an Operator").Alias("update").Action(c.editAction) edit.Arg("name", "Operator to edit").StringVar(&c.operatorName) @@ -68,15 +64,15 @@ func configureAuthOperatorCommand(auth commandHost) { gen.Flag("output", "Write resolver to a file").PlaceHolder("FILE").Short('o').StringVar(&c.outputFile) gen.Flag("force", "Overwrite existing files without prompting").Short('f').UnNegatableBoolVar(&c.force) - sk := op.Command("keys", "Manage Operator signing keys").Alias("sk").Alias("s") + sk := op.Command("keys", "Manage Operator Signing Keys").Alias("sk").Alias("s") - skls := sk.Command("list", "List signing keys").Alias("ls").Action(c.skListAction) + skls := sk.Command("list", "List Signing Keys").Alias("ls").Action(c.skListAction) skls.Arg("name", "Operator to act on").StringVar(&c.operatorName) - skadd := sk.Command("add", "Adds a new signing key").Alias("new").Alias("create").Action(c.skAddAction) + skadd := sk.Command("add", "Adds a new Signing Key").Alias("new").Alias("create").Action(c.skAddAction) skadd.Arg("name", "Operator to act on").StringVar(&c.operatorName) - skrm := sk.Command("rm", "Removes a signing key").Alias("delete").Action(c.skRmAction) + skrm := sk.Command("rm", "Removes a Signing Key").Alias("delete").Action(c.skRmAction) skrm.Arg("name", "Operator to act on").StringVar(&c.operatorName) skrm.Arg("key", "The public key to remove").StringVar(&c.pubKey) skrm.Flag("force", "Remove without prompting").Short('f').UnNegatableBoolVar(&c.force) @@ -234,6 +230,7 @@ func (c *authOperatorCommand) fShowOperator(w io.Writer, op ab.Operator) error { return err } + func (c *authOperatorCommand) editAction(_ *fisk.ParseContext) error { auth, operator, err := c.selectOperator(true) if err != nil { @@ -291,7 +288,7 @@ func (c *authOperatorCommand) lsAction(_ *fisk.ParseContext) error { list := auth.Operators().List() if len(list) == 0 { - fmt.Println("No operators found") + fmt.Println("No Operators found") return nil } @@ -316,7 +313,7 @@ func (c *authOperatorCommand) addAction(_ *fisk.ParseContext) error { if c.operatorName == "" { err := askOne(&survey.Input{ Message: "Operator Name", - Help: "A unique name for the operator being added", + Help: "A unique name for the Operator being added", }, &c.operatorName, survey.WithValidator(survey.Required)) if err != nil { return err @@ -328,7 +325,7 @@ func (c *authOperatorCommand) addAction(_ *fisk.ParseContext) error { return err } - if c.isKnown(auth, c.operatorName) { + if isAuthItemKnown(auth.Operators().List(), c.operatorName) { return fmt.Errorf("operator %s already exist", c.operatorName) } @@ -382,42 +379,31 @@ func (c *authOperatorCommand) addAction(_ *fisk.ParseContext) error { return c.fShowOperator(os.Stdout, auth.Operators().Get(c.operatorName)) } -func (c *authOperatorCommand) isKnown(auth *ab.AuthImpl, name string) bool { - for _, op := range auth.Operators().List() { - if op.Name() == name { - return true - } - } - - return false -} - func (c *authOperatorCommand) selectOperator(pick bool) (*ab.AuthImpl, ab.Operator, error) { auth, err := c.getAuth() if err != nil { return nil, nil, err } - if c.operatorName == "" || !c.isKnown(auth, c.operatorName) { + if c.operatorName == "" || !isAuthItemKnown(auth.Operators().List(), c.operatorName) { if !pick { - return nil, nil, fmt.Errorf("unknown operator: %v", c.operatorName) + return nil, nil, fmt.Errorf("unknown Operator: %v", c.operatorName) } operators := auth.Operators().List() + if len(operators) == 0 { + return nil, nil, fmt.Errorf("no operators found") + } + if len(operators) == 1 { return auth, operators[0], nil } if !isTerminal() { - return nil, nil, fmt.Errorf("cannot pick an Operator without a terminal and no operator name supplied") - } - - names := []string{} - for _, op := range auth.Operators().List() { - names = append(names, op.Name()) + return nil, nil, fmt.Errorf("cannot pick an Operator without a terminal and no Operator name supplied") } - sort.Strings(names) + names := sortedAuthNames(auth.Operators().List()) err = askOne(&survey.Select{ Message: "Select an Operator", Options: names, @@ -430,14 +416,14 @@ func (c *authOperatorCommand) selectOperator(pick bool) (*ab.AuthImpl, ab.Operat op := auth.Operators().Get(c.operatorName) if op == nil { - return nil, nil, fmt.Errorf("unknown operator: %v", c.operatorName) + return nil, nil, fmt.Errorf("unknown Operator: %v", c.operatorName) } return auth, op, nil } func (c *authOperatorCommand) getAuth() (*ab.AuthImpl, error) { - storeDir, err := nscDir() + storeDir, err := nscStore() if err != nil { return nil, err } @@ -446,7 +432,7 @@ func (c *authOperatorCommand) getAuth() (*ab.AuthImpl, error) { } func (c *authOperatorCommand) showOperator(operator ab.Operator) (string, error) { - cols := columns.New("Operator %s (%s)", operator.Name(), operator.Subject()) + cols := newColumns("Operator %s (%s)", operator.Name(), operator.Subject()) cols.AddSectionTitle("Configuration") cols.AddRow("Name", operator.Name()) cols.AddRow("Subject", operator.Subject()) diff --git a/cli/util.go b/cli/util.go index 31033503..0fd9ef6d 100644 --- a/cli/util.go +++ b/cli/util.go @@ -1452,8 +1452,8 @@ func barGraph(w io.Writer, data map[string]float64, caption string, width int, b return nil } -func nscDir() (string, error) { - parent, err := xdgConfigHome() +func nscStore() (string, error) { + parent, err := xdgShareHome() if err != nil { return "", err } @@ -1467,6 +1467,24 @@ func nscDir() (string, error) { return dir, nil } +func xdgShareHome() (string, error) { + parent := os.Getenv("XDG_DATA_HOME") + if parent != "" { + return parent, nil + } + + u, err := user.Current() + if err != nil { + return "", err + } + + if u.HomeDir == "" { + return "", fmt.Errorf("cannot determine home directory") + } + + return filepath.Join(u.HomeDir, ".local", "share"), nil +} + func xdgConfigHome() (string, error) { parent := os.Getenv("XDG_CONFIG_HOME") if parent != "" { @@ -1482,5 +1500,5 @@ func xdgConfigHome() (string, error) { return "", fmt.Errorf("cannot determine home directory") } - return filepath.Join(u.HomeDir, parent, ".config"), nil + return filepath.Join(u.HomeDir, ".config"), nil } diff --git a/columns/columns.go b/columns/columns.go index 4763ded8..6c43d2bb 100644 --- a/columns/columns.go +++ b/columns/columns.go @@ -152,6 +152,24 @@ func (w *Writer) Render() (string, error) { return buf.String(), nil } +// AddRowUnlimitedIf puts "unlimited" as a value if unlimited is true +func (w *Writer) AddRowUnlimitedIf(t string, v any, unlimited bool) { + if unlimited { + w.AddRow(t, "unlimited") + } else { + w.AddRow(t, v) + } +} + +// AddRowUnlimited puts "unlimited" as a value when v == unlimited +func (w *Writer) AddRowUnlimited(t string, v int64, unlimited int64) { + if v == unlimited { + w.AddRow(t, "unlimited") + } else { + w.AddRow(t, v) + } +} + // AddRow adds a row, v will be formatted if time.Time, time.Duration, []string, floats, ints and uints func (w *Writer) AddRow(t string, v any) { w.rows = append(w.rows, &columnRow{kind: kindRow, values: []any{strings.TrimSuffix(t, w.sep), F(v)}}) diff --git a/go.mod b/go.mod index a87fd864..fc60837e 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/HdrHistogram/hdrhistogram-go v1.1.2 - github.com/antonmedv/expr v1.15.4 + github.com/antonmedv/expr v1.15.5 github.com/choria-io/fisk v0.6.1 github.com/dustin/go-humanize v1.0.1 github.com/emicklei/dot v1.6.0 @@ -17,7 +17,7 @@ require ( github.com/guptarohit/asciigraph v0.5.6 github.com/jedib0t/go-pretty/v6 v6.4.9 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/klauspost/compress v1.17.3 + github.com/klauspost/compress v1.17.4 github.com/mattn/go-isatty v0.0.20 github.com/nats-io/jsm.go v0.1.1-0.20231116110128-840023588118 github.com/nats-io/jwt/v2 v2.5.3 @@ -28,10 +28,10 @@ require ( github.com/prometheus/client_golang v1.17.0 github.com/prometheus/common v0.45.0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 - github.com/synadia-io/jwt-auth-builder.go v0.0.0-20231120144214-be471b368967 + github.com/synadia-io/jwt-auth-builder.go v0.0.0-20231201125238-9011ec8a3346 github.com/tylertreat/hdrhistogram-writer v0.0.0-20210816161836-2e440612a39f - golang.org/x/crypto v0.15.0 - golang.org/x/term v0.14.0 + golang.org/x/crypto v0.16.0 + golang.org/x/term v0.15.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -50,7 +50,7 @@ require ( github.com/prometheus/procfs v0.11.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.4.0 // indirect google.golang.org/protobuf v1.31.0 // indirect diff --git a/go.sum b/go.sum index be3fc3e6..72dbbef6 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXY github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/antonmedv/expr v1.15.4 h1:CrNads8WDnDVJNWt/FeUINBO+vDNjurEwT7SoQN132o= -github.com/antonmedv/expr v1.15.4/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE= +github.com/antonmedv/expr v1.15.5 h1:y0Iz3cEwmpRz5/r3w4qQR0MfIqJGdGM1zbhD/v0G5Vg= +github.com/antonmedv/expr v1.15.5/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= @@ -53,8 +53,8 @@ github.com/jedib0t/go-pretty/v6 v6.4.9/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVf github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA= -github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -116,16 +116,16 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/synadia-io/jwt-auth-builder.go v0.0.0-20231120144214-be471b368967 h1:N0TXlx4n/raPG4gqMwUgDdjBYzGWHhW84IUXDHrDc6U= -github.com/synadia-io/jwt-auth-builder.go v0.0.0-20231120144214-be471b368967/go.mod h1:vm/hZ9cVN5pPp1OVAzNZF7RPkTY8wMszri9MeaAjiHQ= +github.com/synadia-io/jwt-auth-builder.go v0.0.0-20231201125238-9011ec8a3346 h1:6oelWMT/lshfaMBFuMJ5ueCX67KXh7C8xpiXNnDMqFg= +github.com/synadia-io/jwt-auth-builder.go v0.0.0-20231201125238-9011ec8a3346/go.mod h1:vm/hZ9cVN5pPp1OVAzNZF7RPkTY8wMszri9MeaAjiHQ= github.com/tylertreat/hdrhistogram-writer v0.0.0-20210816161836-2e440612a39f h1:SGznmvCovewbaSgBsHgdThtWsLj5aCLX/3ZXMLd1UD0= github.com/tylertreat/hdrhistogram-writer v0.0.0-20210816161836-2e440612a39f/go.mod h1:IY84XkhrEJTdHYLNy/zObs8mXuUAp9I65VyarbPSCCY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -158,12 +158,12 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= -golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=