Skip to content

Commit

Permalink
Merge pull request #1047 from ripienaar/operator_generate
Browse files Browse the repository at this point in the history
Support interactive guided config generation for full resolver nats
  • Loading branch information
ripienaar committed Apr 25, 2024
2 parents ecbe409 + d6a496d commit 180f5ae
Show file tree
Hide file tree
Showing 11 changed files with 533 additions and 240 deletions.
19 changes: 10 additions & 9 deletions cli/auth_account_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package cli
import (
"encoding/json"
"fmt"
au "github.com/nats-io/natscli/internal/auth"
"io"
"net/url"
"os"
Expand Down Expand Up @@ -305,7 +306,7 @@ func (c *authAccountCommand) queryAction(_ *fisk.ParseContext) error {
return err
}

acct, err := selectAccount(oper, c.accountName, "")
acct, err := au.SelectAccount(oper, c.accountName, "")
if err != nil {
return err
}
Expand Down Expand Up @@ -422,7 +423,7 @@ func (c *authAccountCommand) skRmAction(_ *fisk.ParseContext) error {
return err
}

sk, err := selectSigningKey(acct, c.skRole)
sk, err := au.SelectSigningKey(acct, c.skRole)
if err != nil {
return err
}
Expand Down Expand Up @@ -462,7 +463,7 @@ func (c *authAccountCommand) skInfoAction(_ *fisk.ParseContext) error {
return err
}

sk, err := selectSigningKey(acct, c.skRole)
sk, err := au.SelectSigningKey(acct, c.skRole)
if err != nil {
return err
}
Expand Down Expand Up @@ -511,7 +512,7 @@ func (c *authAccountCommand) skAddAction(_ *fisk.ParseContext) error {
}
}

limits := scope.(userLimitsManager).UserPermissionLimits()
limits := scope.(au.UserLimitsManager).UserPermissionLimits()
limits.Subs = c.maxSubs
limits.Payload = c.maxPayload
limits.BearerToken = c.bearerAllowed
Expand All @@ -524,7 +525,7 @@ func (c *authAccountCommand) skAddAction(_ *fisk.ParseContext) error {
limits.AllowedConnectionTypes = c.connectionTypes()
}

err = scope.(userLimitsManager).SetUserPermissionLimits(limits)
err = scope.(au.UserLimitsManager).SetUserPermissionLimits(limits)
if err != nil {
return err
}
Expand Down Expand Up @@ -623,7 +624,7 @@ func (c *authAccountCommand) editAction(_ *fisk.ParseContext) error {
}

jsEnabled := acct.Limits().JetStream().IsJetStreamEnabled()
limits := acct.Limits().(operatorLimitsManager).OperatorLimits()
limits := acct.Limits().(au.OperatorLimitsManager).OperatorLimits()
// copy existing settings into the flag settings so parsing treats those as defaults unless users set values
if c.maxPayloadString == "" {
c.maxPayloadString = strconv.Itoa(int(limits.Payload))
Expand Down Expand Up @@ -783,7 +784,7 @@ func (c *authAccountCommand) infoAction(_ *fisk.ParseContext) error {
}

func (c *authAccountCommand) updateAccount(acct ab.Account, js bool) error {
limits := acct.Limits().(operatorLimitsManager).OperatorLimits()
limits := acct.Limits().(au.OperatorLimitsManager).OperatorLimits()
limits.Conn = c.maxConns
limits.Subs = c.maxSubs
limits.Payload = c.maxPayload
Expand All @@ -806,7 +807,7 @@ func (c *authAccountCommand) updateAccount(acct ab.Account, js bool) error {
limits.JetStreamLimits.MaxAckPending = c.maxAckPending
}

err := acct.Limits().(operatorLimitsManager).SetOperatorLimits(limits)
err := acct.Limits().(au.OperatorLimitsManager).SetOperatorLimits(limits)
if err != nil {
return err
}
Expand Down Expand Up @@ -891,7 +892,7 @@ func (c *authAccountCommand) addAction(_ *fisk.ParseContext) error {
}
}

if isAuthItemKnown(operator.Accounts().List(), c.accountName) {
if au.IsAuthItemKnown(operator.Accounts().List(), c.accountName) {
return fmt.Errorf("account %s already exist", c.accountName)
}

Expand Down
3 changes: 2 additions & 1 deletion cli/auth_account_imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"fmt"
au "github.com/nats-io/natscli/internal/auth"
"io"
"os"
"sort"
Expand Down Expand Up @@ -90,7 +91,7 @@ func (c *authAccountCommand) importAddAction(_ *fisk.ParseContext) error {
return err
}

src, err := selectAccount(op, c.importAccount, "Select the Source account")
src, err := au.SelectAccount(op, c.importAccount, "Select the Source account")
if err != nil {
return fmt.Errorf("could not select source account: %v", err)
}
Expand Down
226 changes: 38 additions & 188 deletions cli/auth_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,21 @@ package cli
import (
"errors"
"fmt"
"github.com/synadia-io/jwt-auth-builder.go/providers/nsc"
"path/filepath"
"sort"
"strings"

"github.com/AlecAivazis/survey/v2"
"github.com/nats-io/jwt/v2"
"github.com/nats-io/natscli/columns"
au "github.com/nats-io/natscli/internal/auth"
ab "github.com/synadia-io/jwt-auth-builder.go"
"github.com/synadia-io/jwt-auth-builder.go/providers/nsc"
)

func configureAuthCommand(app commandHost) {
auth := app.Command("auth", "NATS Decentralized Authentication")

// todo:
// - store role name, currently its the pub key not name
// - Support generating full server configs not just memory ones
// - Improve maintaining pub/sub permissions for a user, perhaps allow interactive edits of yaml?

auth.HelpLong("WARNING: This is experimental and subject to massive change, do not use yet")
Expand All @@ -47,188 +45,13 @@ func init() {
registerCommand("auth", 0, configureAuthCommand)
}

type listWithNames interface {
Name() string
}

type operatorLimitsManager interface {
OperatorLimits() jwt.OperatorLimits
SetOperatorLimits(limits jwt.OperatorLimits) error
}

type userLimitsManager interface {
UserPermissionLimits() jwt.UserPermissionLimits
SetUserPermissionLimits(limits jwt.UserPermissionLimits) error
}

func sortedAuthNames[list listWithNames](items []list) []string {
var 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
}

func selectOperatorAccount(operatorName string, accountName string, pick bool) (*ab.AuthImpl, ab.Operator, ab.Account, error) {
auth, operator, err := selectOperator(operatorName, pick, true)
if err != nil {
return nil, nil, nil, err
}

if accountName == "" || !isAuthItemKnown(operator.Accounts().List(), accountName) {
if !pick {
return nil, nil, nil, fmt.Errorf("unknown Account: %v", 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)),
}, &accountName)
if err != nil {
return nil, nil, nil, err
}
}

acct, err := operator.Accounts().Get(accountName)
if acct == nil || errors.Is(err, ab.ErrNotFound) {
return nil, nil, nil, fmt.Errorf("unknown Account: %v", accountName)
} else if err != nil {
return nil, nil, nil, err
}

return auth, operator, acct, nil
}

func selectAccount(op ab.Operator, choice string, prompt string) (ab.Account, error) {
accts := op.Accounts().List()
if len(accts) == 0 {
return nil, fmt.Errorf("no accounts found")
}

sort.SliceStable(accts, func(i, j int) bool {
return accts[i].Name() < accts[j].Name()
})

if choice != "" {
// look on name
acct, _ := op.Accounts().Get(choice)
if acct != nil {
return acct, nil
}

// look on subject
for _, acct := range accts {
if acct.Subject() == choice {
return acct, nil
}
}
}

var subjects []string
for _, acct := range accts {
subjects = append(subjects, acct.Subject())
}

// not found now make lists
answ := 0

if prompt == "" {
prompt = "Select an Account"
}

err := survey.AskOne(&survey.Select{
Message: prompt,
Options: subjects,
Description: func(value string, index int) string {
return accts[index].Name()
},
}, &answ)
if err != nil {
return nil, err
}

return accts[answ], nil
}

func selectSigningKey(acct ab.Account, choice string) (ab.ScopeLimits, error) {
if choice != "" {
// choice is a role and we have just one key for that role
scopes, _ := acct.ScopedSigningKeys().GetScopeByRole(choice)
if len(scopes) == 1 {
return scopes[0], nil
}

// its a public key so we try that
scope, _ := acct.ScopedSigningKeys().GetScope(choice)
if scope != nil {
return scope, nil
}
}

sks := acct.ScopedSigningKeys().List()
if len(sks) == 0 {
return nil, fmt.Errorf("no signing keys found")
}

type k struct {
scope ab.ScopeLimits
description string
}
var choices []k

for _, sk := range sks {
scope, _ := acct.ScopedSigningKeys().GetScope(sk)

// if they gave us a key and we have it just use that
if scope.Key() == choice {
return scope, nil
}

var description string
if scope.Description() == "" {
description = scope.Role()
} else {
description = fmt.Sprintf("%s %s", scope.Role(), scope.Description())
}

choices = append(choices, k{
scope: scope,
description: description,
})
}

answ := 0

err := survey.AskOne(&survey.Select{
Message: "Select Signing Key",
Options: sks,
Description: func(value string, index int) string {
return choices[index].description
},
}, &answ)
func getAuthBuilder() (*ab.AuthImpl, error) {
storeDir, err := nscStore()
if err != nil {
return nil, err
}

return choices[answ].scope, nil
return ab.NewAuth(nsc.NewNscProvider(filepath.Join(storeDir, "stores"), filepath.Join(storeDir, "keys")))
}

func selectOperator(operatorName string, pick bool, useSelected bool) (*ab.AuthImpl, ab.Operator, error) {
Expand All @@ -247,7 +70,7 @@ func selectOperator(operatorName string, pick bool, useSelected bool) (*ab.AuthI
}
}

if operatorName == "" || !isAuthItemKnown(operators, operatorName) {
if operatorName == "" || !au.IsAuthItemKnown(operators, operatorName) {
if !pick {
return nil, nil, fmt.Errorf("unknown operator: %v", operatorName)
}
Expand All @@ -260,7 +83,7 @@ func selectOperator(operatorName string, pick bool, useSelected bool) (*ab.AuthI
return nil, nil, fmt.Errorf("cannot pick an Operator without a terminal and no operator name supplied")
}

names := sortedAuthNames(auth.Operators().List())
names := au.SortedAuthNames(auth.Operators().List())
if len(names) == 0 {
return nil, nil, fmt.Errorf("no operators found")
}
Expand All @@ -285,13 +108,40 @@ func selectOperator(operatorName string, pick bool, useSelected bool) (*ab.AuthI
return auth, op, nil
}

func getAuthBuilder() (*ab.AuthImpl, error) {
storeDir, err := nscStore()
func selectOperatorAccount(operatorName string, accountName string, pick bool) (*ab.AuthImpl, ab.Operator, ab.Account, error) {
auth, operator, err := selectOperator(operatorName, pick, true)
if err != nil {
return nil, err
return nil, nil, nil, err
}

return ab.NewAuth(nsc.NewNscProvider(filepath.Join(storeDir, "stores"), filepath.Join(storeDir, "keys")))
if accountName == "" || !au.IsAuthItemKnown(operator.Accounts().List(), accountName) {
if !pick {
return nil, nil, nil, fmt.Errorf("unknown Account: %v", accountName)
}

if !isTerminal() {
return nil, nil, nil, fmt.Errorf("cannot pick an Account without a terminal and no Account name supplied")
}

names := au.SortedAuthNames(operator.Accounts().List())
err = askOne(&survey.Select{
Message: "Select an Account",
Options: names,
PageSize: selectPageSize(len(names)),
}, &accountName)
if err != nil {
return nil, nil, nil, err
}
}

acct, err := operator.Accounts().Get(accountName)
if acct == nil || errors.Is(err, ab.ErrNotFound) {
return nil, nil, nil, fmt.Errorf("unknown Account: %v", accountName)
} else if err != nil {
return nil, nil, nil, err
}

return auth, operator, acct, nil
}

func renderUserLimits(limits ab.UserLimits, cols *columns.Writer) error {
Expand Down

0 comments on commit 180f5ae

Please sign in to comment.