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