github.com/nats-io/nsc@v0.0.0-20221206222106-35db9400b257/cmd/validate.go (about)

     1  /*
     2   * Copyright 2018-2019 The NATS Authors
     3   * Licensed under the Apache License, Version 2.0 (the "License");
     4   * you may not use this file except in compliance with the License.
     5   * You may obtain a copy of the License at
     6   *
     7   * http://www.apache.org/licenses/LICENSE-2.0
     8   *
     9   * Unless required by applicable law or agreed to in writing, software
    10   * distributed under the License is distributed on an "AS IS" BASIS,
    11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12   * See the License for the specific language governing permissions and
    13   * limitations under the License.
    14   */
    15  
    16  package cmd
    17  
    18  import (
    19  	"errors"
    20  	"fmt"
    21  	"os"
    22  	"sort"
    23  	"strings"
    24  
    25  	"github.com/nats-io/jwt/v2"
    26  	"github.com/xlab/tablewriter"
    27  
    28  	"github.com/nats-io/nsc/cmd/store"
    29  
    30  	"github.com/spf13/cobra"
    31  )
    32  
    33  func createValidateCommand() *cobra.Command {
    34  	var params ValidateCmdParams
    35  	var cmd = &cobra.Command{
    36  		Short:   "Validate an operator, account(s), and users",
    37  		Example: "validate",
    38  		Use: `validate (current operator/current account/account users)
    39  validate -a <accountName> (current operator/<accountName>/account users)
    40  validate -A (current operator/all accounts/all users)
    41  validate -f <file>`,
    42  		Args: MaxArgs(0),
    43  		RunE: func(cmd *cobra.Command, args []string) error {
    44  			cmd.SilenceUsage = false
    45  			if err := RunAction(cmd, args, &params); err != nil {
    46  				// this error was not during the sync operation return as it is
    47  				return err
    48  			}
    49  			cmd.Println(params.render(fmt.Sprintf("Operator %q", GetConfig().Operator), params.operator))
    50  			sort.Strings(params.accounts)
    51  			for _, v := range params.accounts {
    52  				cmd.Println(params.render(fmt.Sprintf("Account %q", v), params.accountValidations[v]))
    53  			}
    54  
    55  			if params.foundErrors() {
    56  				cmd.SilenceUsage = true
    57  				return errors.New("validation found errors")
    58  			}
    59  			return nil
    60  		},
    61  	}
    62  	cmd.Flags().BoolVarP(&params.allAccounts, "all-accounts", "A", false, "validate all accounts under the current operator (exclusive of -a and -f)")
    63  	cmd.Flags().StringVarP(&params.file, "file", "f", "", "validate all jwt (separated by newline) in the provided file (exclusive of -a and -A)")
    64  	params.AccountContextParams.BindFlags(cmd)
    65  	return cmd
    66  }
    67  
    68  func init() {
    69  	GetRootCmd().AddCommand(createValidateCommand())
    70  }
    71  
    72  type ValidateCmdParams struct {
    73  	AccountContextParams
    74  	allAccounts        bool
    75  	file               string
    76  	operator           *jwt.ValidationResults
    77  	accounts           []string
    78  	accountValidations map[string]*jwt.ValidationResults
    79  }
    80  
    81  func (p *ValidateCmdParams) SetDefaults(ctx ActionCtx) error {
    82  	p.accountValidations = make(map[string]*jwt.ValidationResults)
    83  	if p.allAccounts && p.Name != "" {
    84  		return errors.New("specify only one of --account or --all-accounts")
    85  	}
    86  	if p.file != "" {
    87  		if p.allAccounts || p.Name != "" {
    88  			return errors.New("specify only one of --account or --all-accounts or --file")
    89  		}
    90  	} else {
    91  		// if they specified an account name, this will validate it
    92  		if err := p.AccountContextParams.SetDefaults(ctx); err != nil {
    93  			return err
    94  		}
    95  	}
    96  
    97  	return nil
    98  }
    99  
   100  func (p *ValidateCmdParams) PreInteractive(ctx ActionCtx) error {
   101  	var err error
   102  	if !p.allAccounts && p.file == "" {
   103  		if err = p.AccountContextParams.Edit(ctx); err != nil {
   104  			return err
   105  		}
   106  	}
   107  	return err
   108  }
   109  
   110  func (p *ValidateCmdParams) Load(ctx ActionCtx) error {
   111  	if !p.allAccounts && p.Name != "" {
   112  		if err := p.AccountContextParams.Validate(ctx); err != nil {
   113  			return err
   114  		}
   115  	}
   116  	return nil
   117  }
   118  
   119  func (p *ValidateCmdParams) PostInteractive(ctx ActionCtx) error {
   120  	return nil
   121  }
   122  
   123  func (p *ValidateCmdParams) validateJWT(claim jwt.Claims) *jwt.ValidationResults {
   124  	var vr jwt.ValidationResults
   125  	claim.Validate(&vr)
   126  	if vr.IsEmpty() {
   127  		return nil
   128  	}
   129  	return &vr
   130  }
   131  
   132  func (p *ValidateCmdParams) Validate(ctx ActionCtx) error {
   133  	if p.file != "" {
   134  		return p.validateFile(ctx)
   135  	}
   136  	return p.validate(ctx)
   137  }
   138  
   139  func (p *ValidateCmdParams) validateFile(ctx ActionCtx) error {
   140  	f, err := os.ReadFile(p.file)
   141  	if err != nil {
   142  		return err
   143  	}
   144  	type entry struct {
   145  		issue jwt.ValidationIssue
   146  		cnt   int
   147  	}
   148  	summary := map[string]*entry{}
   149  	lines := strings.Split(string(f), "\n")
   150  	for _, l := range lines {
   151  		l = strings.TrimSpace(l)
   152  		if l == "" {
   153  			continue
   154  		}
   155  		// cut off what is a result of pack
   156  		if i := strings.Index(l, "|"); i != -1 {
   157  			l = l[i+1:]
   158  		}
   159  		c, err := jwt.Decode(l)
   160  		if err != nil {
   161  			return fmt.Errorf("claim decoding error: '%v' claim: '%v'", err, l)
   162  		}
   163  		subj := c.Claims().Subject
   164  		vr := jwt.ValidationResults{}
   165  		c.Validate(&vr)
   166  		if _, ok := p.accountValidations[subj]; !ok {
   167  			p.accountValidations[subj] = &jwt.ValidationResults{}
   168  			p.accounts = append(p.accounts, subj)
   169  		}
   170  		for _, vi := range vr.Issues {
   171  			p.accountValidations[subj].Add(vi)
   172  			if val, ok := summary[vi.Description]; !ok {
   173  				summary[vi.Description] = &entry{*vi, 1}
   174  			} else {
   175  				val.cnt++
   176  			}
   177  		}
   178  	}
   179  	if len(p.accounts) > 1 {
   180  		summaryAcc := "summary of all accounts"
   181  		p.accounts = append(p.accounts, summaryAcc)
   182  		vr := &jwt.ValidationResults{}
   183  		p.accountValidations[summaryAcc] = vr
   184  		for _, v := range summary {
   185  			iss := v.issue
   186  			iss.Description = fmt.Sprintf("%s (%d occurrences)", iss.Description, v.cnt)
   187  			vr.Add(&iss)
   188  		}
   189  	}
   190  	return nil
   191  }
   192  
   193  func (p *ValidateCmdParams) validate(ctx ActionCtx) error {
   194  	var err error
   195  	oc, err := ctx.StoreCtx().Store.ReadOperatorClaim()
   196  	if err != nil {
   197  		return err
   198  	}
   199  	p.operator = p.validateJWT(oc)
   200  	if !oc.DidSign(oc) {
   201  		if p.operator == nil {
   202  			p.operator = &jwt.ValidationResults{}
   203  		}
   204  		p.operator.AddError("operator is not issued by operator or operator signing key")
   205  	}
   206  
   207  	p.accounts, err = p.getSelectedAccounts()
   208  	if err != nil {
   209  		return err
   210  	}
   211  
   212  	for _, v := range p.accounts {
   213  		ac, err := ctx.StoreCtx().Store.ReadAccountClaim(v)
   214  		if err != nil {
   215  			if store.IsNotExist(err) {
   216  				continue
   217  			}
   218  			return err
   219  		}
   220  		aci := p.validateJWT(ac)
   221  		if aci != nil {
   222  			p.accountValidations[v] = aci
   223  		}
   224  		if !oc.DidSign(ac) {
   225  			if p.accountValidations[v] == nil {
   226  				p.accountValidations[v] = &jwt.ValidationResults{}
   227  			}
   228  			p.accountValidations[v].AddError("Account is not issued by operator or operator signing keys")
   229  		}
   230  		users, err := ctx.StoreCtx().Store.ListEntries(store.Accounts, v, store.Users)
   231  		if err != nil {
   232  			return err
   233  		}
   234  		for _, u := range users {
   235  			uc, err := ctx.StoreCtx().Store.ReadUserClaim(v, u)
   236  			if err != nil {
   237  				return err
   238  			}
   239  			if uvr := p.validateJWT(uc); uvr != nil {
   240  				for _, vi := range uvr.Issues {
   241  					if p.accountValidations[v] == nil {
   242  						p.accountValidations[v] = &jwt.ValidationResults{}
   243  					}
   244  					vi.Description = fmt.Sprintf("user %q: %s", u, vi.Description)
   245  					p.accountValidations[v].Add(vi)
   246  				}
   247  			}
   248  			if !ac.DidSign(uc) {
   249  				if p.accountValidations[v] == nil {
   250  					p.accountValidations[v] = &jwt.ValidationResults{}
   251  				}
   252  				p.accountValidations[v].AddError("user %q is not issued by account or account signing keys", u)
   253  			} else if oc.StrictSigningKeyUsage && uc.Issuer == ac.Subject {
   254  				p.accountValidations[v].AddError("user %q is issued by account key but operator is in strict mode", u)
   255  			}
   256  		}
   257  	}
   258  
   259  	return nil
   260  }
   261  
   262  func (p *ValidateCmdParams) getSelectedAccounts() ([]string, error) {
   263  	if p.allAccounts {
   264  		a, err := GetConfig().ListAccounts()
   265  		if err != nil {
   266  			return nil, err
   267  		}
   268  		return a, nil
   269  	} else if p.Name != "" {
   270  		return []string{p.AccountContextParams.Name}, nil
   271  	}
   272  	return nil, nil
   273  }
   274  
   275  func (p *ValidateCmdParams) Run(ctx ActionCtx) (store.Status, error) {
   276  	return nil, nil
   277  }
   278  
   279  func (p *ValidateCmdParams) foundErrors() bool {
   280  	var reports []*jwt.ValidationResults
   281  	if p.operator != nil {
   282  		reports = append(reports, p.operator)
   283  	}
   284  	for _, v := range p.accounts {
   285  		vr := p.accountValidations[v]
   286  		if vr != nil {
   287  			reports = append(reports, vr)
   288  		}
   289  	}
   290  	for _, r := range reports {
   291  		for _, ri := range r.Issues {
   292  			if ri.Blocking || ri.TimeCheck {
   293  				return true
   294  			}
   295  		}
   296  	}
   297  	return false
   298  }
   299  
   300  func (p *ValidateCmdParams) render(name string, issues *jwt.ValidationResults) string {
   301  	table := tablewriter.CreateTable()
   302  	table.AddTitle(name)
   303  	if issues != nil {
   304  		table.AddHeaders("#", " ", "Description")
   305  		for i, v := range issues.Issues {
   306  			fatal := ""
   307  			if v.Blocking || v.TimeCheck {
   308  				fatal = "!"
   309  			}
   310  			table.AddRow(i+1, fatal, v.Description)
   311  		}
   312  	} else {
   313  		table.AddRow("No issues found")
   314  	}
   315  	return table.Render()
   316  }