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, ¶ms); 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(¶ms.allAccounts, "all-accounts", "A", false, "validate all accounts under the current operator (exclusive of -a and -f)") 62 cmd.Flags().StringVarP(¶ms.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 }