github.com/kbehouse/nsc@v0.0.6/cmd/push.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 "encoding/json" 20 "errors" 21 "fmt" 22 "net/url" 23 "strings" 24 "time" 25 26 "github.com/nats-io/nkeys" 27 28 "github.com/kbehouse/nsc/cmd/store" 29 cli "github.com/nats-io/cliprompts/v2" 30 "github.com/nats-io/jwt/v2" 31 "github.com/nats-io/nats.go" 32 "github.com/spf13/cobra" 33 ) 34 35 func CreatePushCmd() *cobra.Command { 36 var params PushCmdParams 37 var cmd = &cobra.Command{ 38 Short: "Push an account jwt to an Account JWT Server", 39 Example: "push", 40 Use: `push (currentAccount) 41 push -a <accountName> 42 push -A (all accounts) 43 push -P 44 push -P -A (all accounts)`, 45 Args: MaxArgs(0), 46 RunE: func(cmd *cobra.Command, args []string) error { 47 return RunAction(cmd, args, ¶ms) 48 }, 49 } 50 cmd.Flags().BoolVarP(¶ms.allAccounts, "all", "A", false, "push all accounts under the current operator (exclusive of -a)") 51 cmd.Flags().BoolVarP(¶ms.force, "force", "F", false, "push regardless of validation issues") 52 cmd.Flags().StringVarP(¶ms.ASU, "account-jwt-server-url", "u", "", "set account jwt server url for nsc sync (only http/https/nats urls supported if updating with nsc) If a nats url is provided ") 53 54 cmd.Flags().BoolVarP(¶ms.diff, "diff", "D", false, "diff accounts present in nsc env and nats-account-resolver. Mutually exclusive of account-removal/prune.") 55 cmd.Flags().BoolVarP(¶ms.prune, "prune", "P", false, "prune all accounts not under the current operator. Only works with nats-resolver enabled nats-server. Mutually exclusive of account-removal/diff.") 56 cmd.Flags().StringVarP(¶ms.removeAcc, "account-removal", "R", "", "remove specific account. Only works with nats-resolver enabled nats-server. Mutually exclusive of prune/diff.") 57 cmd.Flags().StringVarP(¶ms.sysAcc, "system-account", "", "", "System account for use with nats-resolver enabled nats-server. (Default is system account specified by operator)") 58 cmd.Flags().StringVarP(¶ms.sysAccUser, "system-user", "", "", "System account user for use with nats-resolver enabled nats-server. (Default to temporarily generated user)") 59 params.AccountContextParams.BindFlags(cmd) 60 return cmd 61 } 62 63 func init() { 64 GetRootCmd().AddCommand(CreatePushCmd()) 65 } 66 67 type PushCmdParams struct { 68 AccountContextParams 69 ASU string 70 sysAccUser string // when present use 71 sysAcc string 72 allAccounts bool 73 force bool 74 prune bool 75 diff bool 76 removeAcc string 77 targeted []string 78 79 accountList []string 80 } 81 82 func processResponse(report *store.Report, resp *nats.Msg) (bool, string, interface{}) { 83 // ServerInfo copied from nats-server, refresh as needed. Error and Data are mutually exclusive 84 serverResp := struct { 85 Server *struct { 86 Name string `json:"name"` 87 Host string `json:"host"` 88 ID string `json:"id"` 89 Cluster string `json:"cluster,omitempty"` 90 Version string `json:"ver"` 91 Seq uint64 `json:"seq"` 92 JetStream bool `json:"jetstream"` 93 Time time.Time `json:"time"` 94 } `json:"server"` 95 Error *struct { 96 Description string `json:"description"` 97 Code int `json:"code"` 98 } `json:"error"` 99 Data interface{} `json:"data"` 100 }{} 101 if err := json.Unmarshal(resp.Data, &serverResp); err != nil { 102 report.AddError("failed to parse response: %v data: %s", err, string(resp.Data)) 103 } else if srvName := serverResp.Server.Name; srvName == "" { 104 report.AddError("server responded without server name in info: %s", string(resp.Data)) 105 } else if err := serverResp.Error; err != nil { 106 report.AddError("server %s responded with error: %s", srvName, err.Description) 107 } else if data := serverResp.Data; data == nil { 108 report.AddError("server %s responded without data: %s", srvName, string(resp.Data)) 109 } else { 110 return true, srvName, data 111 } 112 return false, "", nil 113 } 114 115 // when sysAccName or sysAccUserName are "" we will try to find a suitable user 116 func getSystemAccountUser(ctx ActionCtx, sysAccName, sysAccUserName, allowSub string, allowPubs ...string) (string, nats.Option, error) { 117 op, err := ctx.StoreCtx().Store.ReadOperatorClaim() 118 if err != nil { 119 return "", nil, err 120 } else if accNames, err := friendlyNames(ctx.StoreCtx().Operator.Name); err != nil { 121 return "", nil, err 122 } else if sysAccName == "" { 123 if sysAccName = accNames[op.SystemAccount]; sysAccName == "" { 124 return "", nil, fmt.Errorf(`system account "%s" not found`, op.SystemAccount) 125 } 126 } 127 getOpt := func(theJWT string, kp nkeys.KeyPair) nats.Option { 128 return nats.UserJWT( 129 func() (string, error) { 130 return theJWT, nil 131 }, func(nonce []byte) ([]byte, error) { 132 return kp.Sign(nonce) 133 }) 134 } 135 // Attempt to generate temporary user credentials and 136 if sysAccUserName == "" { 137 if keys, err := ctx.StoreCtx().GetAccountKeys(sysAccName); err == nil && len(keys) > 0 { 138 key := "" 139 if op.StrictSigningKeyUsage { 140 if len(keys) > 1 { 141 key = keys[1] 142 } else { 143 key = "" 144 } 145 } else { 146 key = keys[0] 147 } 148 sysAccKp, err := ctx.StoreCtx().KeyStore.GetKeyPair(key) 149 if sysAccKp != nil && err == nil { 150 defer sysAccKp.Wipe() 151 tmpUsrKp, err := nkeys.CreateUser() 152 if err == nil { 153 tmpUsrPub, err := tmpUsrKp.PublicKey() 154 if err == nil { 155 tmpUsrClaim := jwt.NewUserClaims(tmpUsrPub) 156 tmpUsrClaim.IssuerAccount = op.SystemAccount 157 tmpUsrClaim.Expires = time.Now().Add(2 * time.Minute).Unix() 158 tmpUsrClaim.Name = "nsc temporary push user" 159 tmpUsrClaim.Pub.Allow.Add(allowPubs...) 160 tmpUsrClaim.Sub.Allow.Add(allowSub) 161 if theJWT, err := tmpUsrClaim.Encode(sysAccKp); err == nil { 162 return sysAccName, getOpt(theJWT, tmpUsrKp), nil 163 } 164 } 165 } 166 } 167 } 168 // in case of not finding a key, default to searching for an existing user and key 169 } 170 users := []string{sysAccUserName} 171 if sysAccUserName == "" { 172 var err error 173 if users, err = ctx.StoreCtx().Store.ListEntries(store.Accounts, sysAccName, store.Users); err != nil { 174 return "", nil, err 175 } else if len(users) == 0 { 176 return "", nil, err 177 } 178 } 179 for _, sysUser := range users { 180 claim, err := ctx.StoreCtx().Store.ReadUserClaim(sysAccName, sysUser) 181 if err != nil { 182 continue 183 } 184 kp, _ := ctx.StoreCtx().KeyStore.GetKeyPair(claim.Subject) 185 if kp == nil { 186 kp, _ = ctx.StoreCtx().KeyStore.GetKeyPair(claim.IssuerAccount) 187 if kp == nil { 188 continue 189 } 190 } 191 if theJWT, err := ctx.StoreCtx().Store.ReadRawUserClaim(sysAccName, sysUser); err != nil { 192 continue 193 } else { 194 return sysAccName, getOpt(string(theJWT), kp), nil 195 } 196 } 197 return "", nil, fmt.Errorf(`no system account user with corresponding nkey found`) 198 } 199 200 func (p *PushCmdParams) SetDefaults(ctx ActionCtx) error { 201 if p.allAccounts && p.Name != "" { 202 return errors.New("specify only one of --account or --all-accounts") 203 } 204 if err := p.AccountContextParams.SetDefaults(ctx); err != nil { 205 return err 206 } 207 if p.ASU == "" { 208 if op, err := ctx.StoreCtx().Store.ReadOperatorClaim(); err != nil { 209 return err 210 } else { 211 p.ASU = op.AccountServerURL 212 } 213 } 214 c := GetConfig() 215 var err error 216 if p.accountList, err = c.ListAccounts(); err != nil { 217 return err 218 } 219 if len(p.accountList) == 0 { 220 return fmt.Errorf("operator %q has no accounts", c.Operator) 221 } 222 if !p.allAccounts && !(p.prune || p.removeAcc != "" || p.diff) { 223 found := false 224 for _, v := range p.accountList { 225 if v == p.Name { 226 found = true 227 break 228 } 229 } 230 if !found { 231 return fmt.Errorf("account %q is not under operator %q - nsc env to check your env", p.Name, c.Operator) 232 } 233 } 234 return nil 235 } 236 237 func (p *PushCmdParams) validURL(s string) error { 238 s = strings.TrimSpace(s) 239 if s == "" { 240 return errors.New("url cannot be empty") 241 } 242 243 u, err := url.Parse(s) 244 if err != nil { 245 return err 246 } 247 scheme := strings.ToLower(u.Scheme) 248 supported := []string{"http", "https", "nats"} 249 250 ok := false 251 for _, v := range supported { 252 if scheme == v { 253 ok = true 254 break 255 } 256 } 257 if !ok { 258 return fmt.Errorf("scheme %q is not supported (%v)", scheme, strings.Join(supported, ", ")) 259 } 260 return nil 261 } 262 263 func (p *PushCmdParams) PreInteractive(ctx ActionCtx) error { 264 var err error 265 if !p.allAccounts && !p.prune { 266 if err = p.AccountContextParams.Edit(ctx); err != nil { 267 return err 268 } 269 } 270 if p.ASU, err = cli.Prompt("Account Server URL or nats-resolver enabled nats-server URL", p.ASU, cli.Val(p.validURL)); err != nil { 271 return err 272 } 273 if IsNatsUrl(p.ASU) { 274 if p.sysAcc == "" { 275 if p.sysAcc, err = ctx.StoreCtx().PickAccount(p.sysAcc); err != nil { 276 return err 277 } 278 } 279 if p.sysAccUser == "" { 280 p.sysAccUser, err = ctx.StoreCtx().PickUser(p.sysAcc) 281 } 282 } 283 return err 284 } 285 286 func (p *PushCmdParams) Load(ctx ActionCtx) error { 287 if !p.allAccounts && !(p.prune || p.removeAcc != "" || p.diff) { 288 if err := p.AccountContextParams.Validate(ctx); err != nil { 289 return err 290 } 291 } 292 return nil 293 } 294 295 func (p *PushCmdParams) PostInteractive(_ ActionCtx) error { 296 return nil 297 } 298 299 func (p *PushCmdParams) Validate(ctx ActionCtx) error { 300 if p.ASU == "" { 301 return errors.New("no account server url or nats-server url was provided by the operator jwt") 302 } 303 if !IsNatsUrl(p.ASU) && p.prune { 304 return errors.New("prune only works for nats based account resolver") 305 } 306 307 if err := p.validURL(p.ASU); err != nil { 308 return err 309 } 310 311 if !p.force { 312 oc, err := ctx.StoreCtx().Store.ReadOperatorClaim() 313 if err != nil { 314 return err 315 } 316 317 // validate the jwts don't have issues 318 accounts, err := p.getSelectedAccounts() 319 if err != nil { 320 return err 321 } 322 323 for _, v := range accounts { 324 raw, err := ctx.StoreCtx().Store.Read(store.Accounts, v, store.JwtName(v)) 325 if err != nil { 326 return err 327 } 328 329 ac, err := jwt.DecodeAccountClaims(string(raw)) 330 if err != nil { 331 return fmt.Errorf("unable to push account %q: %v", v, err) 332 } 333 var vr jwt.ValidationResults 334 ac.Validate(&vr) 335 for _, e := range vr.Issues { 336 if e.Blocking || e.TimeCheck { 337 return fmt.Errorf("unable to push account %q as it has validation issues: %v", v, e.Description) 338 } 339 } 340 if !ctx.StoreCtx().Store.IsManaged() && !oc.DidSign(ac) { 341 return fmt.Errorf("unable to push account %q as it is not signed by the operator %q", v, ctx.StoreCtx().Operator.Name) 342 } 343 } 344 } 345 if p.removeAcc != "" { 346 if p.prune || p.diff { 347 return errors.New("--prune/--diff and --account-removal <account> are mutually exclusive") 348 } 349 if !nkeys.IsValidPublicAccountKey(p.removeAcc) { 350 if acc, err := ctx.StoreCtx().Store.ReadAccountClaim(p.removeAcc); err != nil { 351 return err 352 } else { 353 p.removeAcc = acc.Subject 354 } 355 } 356 } else if p.prune && p.diff { 357 return errors.New("--prune and --diff are mutually exclusive") 358 } 359 360 return nil 361 } 362 363 func (p *PushCmdParams) getSelectedAccounts() ([]string, error) { 364 if p.allAccounts { 365 a, err := GetConfig().ListAccounts() 366 if err != nil { 367 return nil, err 368 } 369 return a, nil 370 } else if !(p.prune || p.removeAcc != "" || p.diff) { 371 return []string{p.AccountContextParams.Name}, nil 372 } 373 return []string{}, nil 374 } 375 376 func multiRequest(nc *nats.Conn, report *store.Report, operation string, subject string, reqData []byte, respHandler func(srv string, data interface{})) int { 377 ib := nats.NewInbox() 378 sub, err := nc.SubscribeSync(ib) 379 if err != nil { 380 report.AddError("failed to subscribe to response subject: %v", err) 381 return 0 382 } 383 if err := nc.PublishRequest(subject, ib, reqData); err != nil { 384 report.AddError("failed to %s: %v", operation, err) 385 return 0 386 } 387 responses := 0 388 now := time.Now() 389 start := now 390 end := start.Add(time.Second) 391 for ; end.After(now); now = time.Now() { // try with decreasing timeout until we dont get responses 392 if resp, err := sub.NextMsg(end.Sub(now)); err != nil { 393 if err != nats.ErrTimeout || responses == 0 { 394 report.AddError("failed to get response to %s: %v", operation, err) 395 } 396 } else if ok, srv, data := processResponse(report, resp); ok { 397 respHandler(srv, data) 398 responses++ 399 continue 400 } 401 break 402 } 403 return responses 404 } 405 406 func obtainRequestKey(ctx ActionCtx, subPrune *store.Report) (nkeys.KeyPair, string, error) { 407 opc, err := ctx.StoreCtx().Store.ReadOperatorClaim() 408 if err != nil { 409 subPrune.AddError("Operator needed to prune (err:%v)", err) 410 return nil, "", err 411 } 412 keys, err := ctx.StoreCtx().GetOperatorKeys() 413 if err != nil { 414 subPrune.AddError("Operator keys needed to prune (err:%v)", err) 415 return nil, "", err 416 } 417 if opc.StrictSigningKeyUsage { 418 if len(keys) > 1 { 419 keys = keys[1:] 420 } else { 421 keys = []string{} 422 } 423 } 424 var okp nkeys.KeyPair 425 for _, k := range keys { 426 var err error 427 if okp, err = ctx.StoreCtx().KeyStore.GetKeyPair(k); err == nil { 428 break 429 } 430 } 431 if okp == nil { 432 subPrune.AddError("Operator private key needed to prune (err:%v)", err) 433 return nil, "", err 434 } 435 opPk, err := okp.PublicKey() 436 if err != nil { 437 subPrune.AddError("Public key needed to prune (err:%v)", err) 438 return nil, "", err 439 } 440 return okp, opPk, nil 441 } 442 443 func sendDeleteRequest(ctx ActionCtx, nc *nats.Conn, deleteList []string, respList int, subPrune *store.Report) { 444 if len(deleteList) == 0 { 445 subPrune.AddOK("nothing to prune") 446 return 447 } 448 okp, opPk, err := obtainRequestKey(ctx, subPrune) 449 if err != nil { 450 subPrune.AddError("Could not obtain Operator key to sign the delete request (err:%v)", err) 451 return 452 } 453 defer okp.Wipe() 454 455 claim := jwt.NewGenericClaims(opPk) 456 claim.Data["accounts"] = deleteList 457 pruneJwt, err := claim.Encode(okp) 458 if err != nil { 459 subPrune.AddError("Could not encode delete request (err:%v)", err) 460 } 461 respPrune := multiRequest(nc, subPrune, "prune", "$SYS.REQ.CLAIMS.DELETE", []byte(pruneJwt), 462 func(srv string, data interface{}) { 463 if data, ok := data.(map[string]interface{}); ok { 464 subPrune.AddOK("pruned nats-server %s: %s", srv, data["message"]) 465 } else { 466 subPrune.AddOK("pruned nats-server %s: %v", srv, data) 467 } 468 }) 469 if respList > 0 { 470 if respPrune < respList { 471 subPrune.AddError("Fewer server responded to prune (%d) than to earlier list (%d)."+ 472 " Accounts may not be completely pruned.", respPrune, respList) 473 } else if respPrune > respList { 474 subPrune.AddError("More server responded to prune (%d) than to earlier list (%d)."+ 475 " Not every Account may have been included for pruning.", respPrune, respList) 476 } 477 } 478 } 479 480 func createMapping(ctx ActionCtx, rep *store.Report, accountList []string) (map[string]string, error) { 481 mapping := make(map[string]string) 482 for _, name := range accountList { 483 if claim, err := ctx.StoreCtx().Store.ReadAccountClaim(name); err != nil { 484 if err.(*store.ResourceErr).Err != store.ErrNotExist { 485 if nkeys.IsValidPublicAccountKey(name) { 486 mapping[name] = name 487 continue 488 } 489 } 490 rep.AddError("prune failed to create mapping for %s: %v", name, err) 491 return nil, err // this is a hard error, if we cant create a mapping because of it we'd end up deleting 492 } else { 493 mapping[claim.Subject] = name 494 } 495 } 496 return mapping, nil 497 } 498 499 func listNonPresentAccounts(nc *nats.Conn, subPrune *store.Report, mapping map[string]string) (int, []string) { 500 deleteList := make([]string, 0, 1024) 501 responseCount := multiRequest(nc, subPrune, "list accounts", "$SYS.REQ.CLAIMS.LIST", nil, 502 func(srv string, d interface{}) { 503 data := d.([]interface{}) 504 subAccPrune := store.NewReport(store.OK, "list %d accounts from nats-server %s", len(data), srv) 505 subPrune.Add(subAccPrune) 506 for _, acc := range data { 507 acc := acc.(string) 508 if name, ok := mapping[acc]; ok { 509 subAccPrune.AddOK("account %s named %s exists", acc, name) 510 } else { 511 subAccPrune.AddOK("account %s only exists in server", acc) 512 deleteList = append(deleteList, acc) 513 } 514 } 515 }) 516 subPrune.AddOK("listed accounts from a total of %d nats-server", responseCount) 517 return responseCount, deleteList 518 } 519 520 func (p *PushCmdParams) Run(ctx ActionCtx) (store.Status, error) { 521 ctx.CurrentCmd().SilenceUsage = true 522 var err error 523 p.targeted, err = p.getSelectedAccounts() 524 if err != nil { 525 return nil, err 526 } 527 r := store.NewDetailedReport(true) 528 if !IsNatsUrl(p.ASU) { 529 for _, v := range p.targeted { 530 sub := store.NewReport(store.OK, "push %s to account server", v) 531 sub.Opt = store.DetailsOnErrorOrWarning 532 r.Add(sub) 533 ps, err := p.pushAccount(v, ctx) 534 if ps != nil { 535 sub.Add(store.HoistChildren(ps)...) 536 } 537 if err != nil { 538 sub.AddError("failed to push account %q: %v", v, err) 539 } 540 if sub.OK() { 541 sub.Label = fmt.Sprintf("pushed %q to account server", v) 542 } 543 } 544 } else { 545 nats.NewInbox() 546 sysAcc, opt, err := getSystemAccountUser(ctx, p.sysAcc, p.sysAccUser, nats.InboxPrefix+">", 547 "$SYS.REQ.CLAIMS.LIST", "$SYS.REQ.CLAIMS.UPDATE", "$SYS.REQ.CLAIMS.DELETE") 548 if err != nil { 549 r.AddError("error obtaining system account user: %v", err) 550 return r, nil 551 } 552 nc, err := nats.Connect(p.ASU, createDefaultToolOptions("nsc_push", ctx, opt)...) 553 if err != nil { 554 r.AddError("failed to connect: %v", err) 555 return r, nil 556 } 557 defer nc.Close() 558 if len(p.targeted) != 0 { 559 sub := store.NewReport(store.OK, `push to nats-server "%s" using system account "%s"`, 560 p.ASU, sysAcc) 561 r.Add(sub) 562 for _, v := range p.targeted { 563 subAcc := store.NewReport(store.OK, "push %s to nats-server with nats account resolver", v) 564 sub.Add(subAcc) 565 if raw, err := ctx.StoreCtx().Store.Read(store.Accounts, v, store.JwtName(v)); err != nil { 566 subAcc.AddError("failed to read account %q: %v", v, err) 567 } else { 568 resp := multiRequest(nc, subAcc, "push account", "$SYS.REQ.CLAIMS.UPDATE", raw, 569 func(srv string, data interface{}) { 570 if data, ok := data.(map[string]interface{}); ok { 571 subAcc.AddOK("pushed %q to nats-server %s: %s", v, srv, data["message"]) 572 } else { 573 subAcc.AddOK("pushed %q to nats-server %s: %v", v, srv, data) 574 } 575 }) 576 subAcc.AddOK("pushed to a total of %d nats-server", resp) 577 } 578 } 579 } 580 if p.prune { 581 subPrune := store.NewReport(store.OK, "prune nats-server with nats account resolver") 582 r.Add(subPrune) 583 mapping, err := createMapping(ctx, subPrune, p.accountList) 584 if err != nil { 585 return r, nil 586 } 587 responseCount, deleteList := listNonPresentAccounts(nc, subPrune, mapping) 588 sendDeleteRequest(ctx, nc, deleteList, responseCount, subPrune) 589 } else if p.removeAcc != "" { 590 subRemove := store.NewReport(store.OK, "prune nats-server with nats account resolver") 591 r.Add(subRemove) 592 sendDeleteRequest(ctx, nc, []string{p.removeAcc}, -1, subRemove) 593 } else if p.diff { 594 subDiff := store.NewReport(store.OK, "diff nats-server with nats account resolver") 595 r.Add(subDiff) 596 accList, err := GetConfig().ListAccounts() 597 if err != nil { 598 subDiff.AddError("diff could not obtain account list: %v", err) 599 return r, nil 600 } 601 mapping, err := createMapping(ctx, subDiff, accList) 602 if err != nil { 603 subDiff.AddError("diff could not create account mapping: %v", err) 604 return r, nil 605 } 606 607 listNonPresentAccounts(nc, subDiff, mapping) 608 } 609 } 610 return r, nil 611 } 612 613 func (p *PushCmdParams) pushAccount(n string, ctx ActionCtx) (store.Status, error) { 614 raw, err := ctx.StoreCtx().Store.Read(store.Accounts, n, store.JwtName(n)) 615 if err != nil { 616 return nil, err 617 } 618 c, err := jwt.DecodeAccountClaims(string(raw)) 619 if err != nil { 620 return nil, err 621 } 622 u, err := AccountJwtURLFromString(p.ASU, c.Subject) 623 if err != nil { 624 return nil, err 625 } 626 return store.PushAccount(u, raw) 627 }