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