github.com/nats-io/nsc/v2@v2.8.7-0.20240307184528-efd7023c6896/cmd/generateprofile.go (about) 1 /* 2 * Copyright 2018-2023 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 "os" 24 "path/filepath" 25 "strings" 26 27 "github.com/nats-io/jwt/v2" 28 "github.com/nats-io/nsc/v2/cmd/store" 29 "github.com/spf13/cobra" 30 ) 31 32 func createProfileCmd() *cobra.Command { 33 var params ProfileCmdParams 34 var cmd = &cobra.Command{ 35 Use: "profile", 36 Short: "Generate a profile from nsc 'URL' that can be used by tooling", 37 Example: `profile nsc://operator 38 nsc profile nsc://operator/account 39 nsc profile nsc://operator/account/user 40 nsc profile nsc://operator/account/user?names&seeds&keys 41 nsc profile nsc://operator/account/user?operatorSeed&accountSeed&userSeed 42 nsc profile nsc://operator/account/user?operatorKey&accountKey&userKey 43 nsc profile nsc://operator?key&seed 44 nsc profile nsc://operator/account?key&seed 45 nsc profile nsc://operator/account/user?key&seed&name 46 nsc profile nsc://operator/account/user?store=/a/.nsc/nats&keystore=/foo/.nkeys 47 48 Output of the program looks like: 49 { 50 "user_creds": "<filepath>", 51 "operator" : { 52 "service": "hostport" 53 } 54 } 55 The user_creds is printed if an user is specified 56 Other options (as query string arguments): 57 keystore=<dir> that specifies the location of the keystore 58 59 store=<dir> that specifies a directory that contains the named operator 60 61 [user|account|operator]Key - includes the public key for user, account, 62 operator, If no prefix (user/account/operator is provided, it targets 63 the last object in the configuration path) 64 65 keys - includes the public keys for all entities (same as 66 userKey&accountKey&operatorKey) 67 68 [user|account|operator]Seed=<optional public key> - include the seed for 69 user, account, operator, if an argument is provided, the seed for the 70 specified public key is provided - this allows targeting a signing key. 71 If no prefix (user/account/operator is provided, it targets the last 72 object in the configuration path) 73 74 seeds - includes the private keys for all entities (same as 75 userSeed&accountSeed&operatorSeed) 76 77 [user|account|operator]Name - includes the friendly name for the for 78 user, account, operator, If no prefix (user/account/operator is provided, 79 it targets the last object in the configuration path) 80 81 names - includes the friendly names for all the entities (same as 82 userName&accountName&operatorName) 83 `, 84 85 Args: cobra.MinimumNArgs(1), 86 SilenceUsage: true, 87 RunE: func(cmd *cobra.Command, args []string) error { 88 us := args[0] 89 u, err := ParseNscURL(us) 90 if err != nil { 91 return fmt.Errorf("error parsing %q: %w", us, err) 92 } 93 config := GetConfig() 94 oldSR := config.StoreRoot 95 oldOp := config.Operator 96 oldAc := config.Account 97 defer func() { 98 _ = config.setStoreRoot(oldSR) 99 if oldOp != "" { 100 _ = config.SetOperator(oldOp) 101 } 102 if oldAc != "" { 103 _ = config.SetAccount(oldAc) 104 } 105 }() 106 107 params.nscu = u 108 q, err := u.query() 109 if err != nil { 110 return fmt.Errorf("error parsing query %q: %w", us, err) 111 } 112 if len(q) > 0 { 113 v, ok := q["store"] 114 if ok { 115 sr, err := Expand(v) 116 if err != nil { 117 return err 118 } 119 if err := config.setStoreRoot(sr); err != nil { 120 return err 121 } 122 if err := config.SetOperator(u.operator); err != nil { 123 return err 124 } 125 } 126 127 storeDir, ok := q[keystoreDir] 128 if ok { 129 ks, err := Expand(storeDir) 130 if err != nil { 131 return err 132 } 133 store.KeyStorePath = ks 134 } 135 } 136 if u.operator != "" { 137 if err := config.SetOperator(u.operator); err != nil { 138 return err 139 } 140 } 141 if u.account != "" { 142 if err := config.SetAccount(u.account); err != nil { 143 return err 144 } 145 } 146 return RunAction(cmd, args, ¶ms) 147 }, 148 } 149 cmd.Flags().StringVarP(¶ms.outputFile, "output-file", "o", "--", "output file, '--' is stdout") 150 151 return cmd 152 } 153 154 func init() { 155 generateCmd.AddCommand(createProfileCmd()) 156 } 157 158 type Arg string 159 160 const ( 161 prefix = "nsc://" 162 operatorKey Arg = "operatorkey" 163 accountKey Arg = "accountkey" 164 userKey Arg = "userkey" 165 key Arg = "key" 166 allKeys Arg = "keys" 167 operatorSeed Arg = "operatorseed" 168 accountSeed Arg = "accountseed" 169 userSeed Arg = "userseed" 170 seed Arg = "seed" 171 allSeeds Arg = "seeds" 172 operatorName Arg = "operatorname" 173 accountName Arg = "accountname" 174 userName Arg = "username" 175 name Arg = "name" 176 allNames Arg = "names" 177 keystoreDir Arg = "keystore" 178 storeDir Arg = "store" 179 ) 180 181 type ProfileCmdParams struct { 182 nscu *NscURL 183 results *Profile 184 conf *ToolConfig 185 oc *jwt.OperatorClaims 186 ac *jwt.AccountClaims 187 uc *jwt.UserClaims 188 outputFile string 189 } 190 191 type Details struct { 192 Service []string `json:"service,omitempty"` 193 Name string `json:"name,omitempty"` 194 Seed string `json:"seed,omitempty"` 195 Key string `json:"id,omitempty"` 196 } 197 198 type Profile struct { 199 UserCreds string `json:"user_creds,omitempty"` 200 Operator *Details `json:"operator,omitempty"` 201 Account *Details `json:"account,omitempty"` 202 User *Details `json:"user,omitempty"` 203 } 204 205 type NscURL struct { 206 operator string 207 account string 208 user string 209 qs string 210 } 211 212 type stringSet struct { 213 set map[string]string 214 } 215 216 func newStringSet() *stringSet { 217 var s stringSet 218 s.set = make(map[string]string) 219 return &s 220 } 221 222 func (u *stringSet) add(s string) { 223 u.set[strings.ToLower(s)] = s 224 } 225 226 func (u *stringSet) contains(s string) bool { 227 return u.set[strings.ToLower(s)] != "" 228 } 229 230 func (u *NscURL) getOperator() (string, error) { 231 return url.QueryUnescape(u.operator) 232 } 233 234 func (u *NscURL) getAccount() (string, error) { 235 return url.QueryUnescape(u.account) 236 } 237 238 func (u *NscURL) getUser() (string, error) { 239 return url.QueryUnescape(u.user) 240 } 241 242 func (u *NscURL) query() (map[Arg]string, error) { 243 q := strings.ToLower(u.qs) 244 m := make(map[Arg]string) 245 for _, e := range strings.Split(q, "&") { 246 kv := strings.Split(e, "=") 247 k := strings.ToLower(kv[0]) 248 v := "" 249 if len(kv) == 2 { 250 s, err := url.QueryUnescape(kv[1]) 251 if err != nil { 252 return nil, err 253 } 254 v = s 255 } 256 m[Arg(k)] = v 257 } 258 return m, nil 259 } 260 261 func ParseNscURL(u string) (*NscURL, error) { 262 var v NscURL 263 s := u 264 if !strings.HasPrefix(strings.ToLower(u), prefix) { 265 return nil, errors.New("invalid nsc url: expecting 'nsc://'") 266 } 267 s = s[len(prefix):] 268 269 qs := strings.Index(s, "?") 270 if qs > 0 { 271 v.qs = s[qs+1:] 272 s = s[:qs] 273 } 274 if s == "" { 275 return nil, errors.New("invalid nsc url: expecting an operator name") 276 } 277 a := strings.Split(s, "/") 278 if len(a) >= 1 { 279 v.operator = a[0] 280 } 281 if len(a) >= 2 { 282 v.account = a[1] 283 } 284 if len(a) >= 3 { 285 v.user = a[2] 286 } 287 return &v, nil 288 } 289 290 func (p *ProfileCmdParams) SetDefaults(_ ActionCtx) error { 291 return nil 292 } 293 294 func (p *ProfileCmdParams) PreInteractive(_ ActionCtx) error { 295 return nil 296 } 297 298 func (p *ProfileCmdParams) Load(_ ActionCtx) error { 299 return nil 300 } 301 302 func (p *ProfileCmdParams) PostInteractive(_ ActionCtx) error { 303 return nil 304 } 305 306 func (p *ProfileCmdParams) loadNames(c jwt.Claims) *stringSet { 307 names := newStringSet() 308 cd := c.Claims() 309 names.add(cd.Name) 310 names.add(cd.Subject) 311 312 payload := c.Payload() 313 _, ok := payload.(jwt.Operator) 314 if ok && p.conf.Operator != "" { 315 names.add(p.conf.Operator) 316 } 317 return names 318 } 319 320 // checkLoadOperator is used to check if the operator in the nsc: URL is a 321 // known operator, and thus needs to work no matter what the current context 322 // is; it will mutate the ctx Store appropriately and store away a conf. 323 func (p *ProfileCmdParams) checkLoadOperator(ctx ActionCtx) error { 324 p.conf = GetConfig() 325 var err error 326 // There's a sync mutex inside the store, we should try to only load each once. 327 allOperators := make(map[string]*store.Store) 328 aliases := make(map[string]string) 329 // We work with lower-cased aliases 330 lcURLOperator := strings.ToLower(p.nscu.operator) 331 332 // We need to be sure that we load all operators known to the local nsc store. 333 // Using ctx.StoreCtx().Store would only load the _current_ operator. 334 // We do still need to let the O.. nkey form be used though. 335 for _, opName := range p.conf.ListOperators() { 336 s, err := store.LoadStore(filepath.Join(p.conf.StoreRoot, opName)) 337 if err != nil { 338 continue 339 } 340 oc, err := s.ReadOperatorClaim() 341 if err != nil { 342 continue 343 } 344 allOperators[strings.ToLower(opName)] = s 345 aliases[opName] = opName 346 aliases[strings.ToLower(opName)] = opName 347 for alias := range p.loadNames(oc).set { 348 allOperators[alias] = s 349 aliases[alias] = opName 350 } 351 } 352 if _, ok := allOperators[lcURLOperator]; !ok { 353 return fmt.Errorf("invalid operator %q: not known to current system", p.nscu.operator) 354 } 355 p.conf.Operator = aliases[lcURLOperator] 356 ctx.StoreCtx().Store = allOperators[lcURLOperator] 357 358 oc, err := ctx.StoreCtx().Store.ReadOperatorClaim() 359 if err != nil { 360 return fmt.Errorf("could not use alternative operator %q: %w", p.nscu.operator, err) 361 } 362 363 p.nscu.operator = oc.Name 364 p.oc = oc 365 366 return nil 367 } 368 369 func (p *ProfileCmdParams) checkLoadAccount(ctx ActionCtx) error { 370 if p.nscu.account == "" { 371 return nil 372 } 373 names, err := p.conf.ListAccounts() 374 if err != nil { 375 return err 376 } 377 378 m := make(map[string]string) 379 for _, n := range names { 380 m[strings.ToLower(n)] = n 381 } 382 an := m[strings.ToLower(p.nscu.account)] 383 if an != "" { 384 ac, err := ctx.StoreCtx().Store.ReadAccountClaim(an) 385 if err != nil { 386 return err 387 } 388 p.nscu.account = ac.Name 389 p.ac = ac 390 return nil 391 } 392 393 for _, n := range names { 394 ac, err := ctx.StoreCtx().Store.ReadAccountClaim(n) 395 if err != nil { 396 continue 397 } 398 aliases := p.loadNames(ac) 399 if aliases.contains(p.nscu.account) { 400 p.nscu.account = ac.Name 401 p.ac = ac 402 return nil 403 } 404 } 405 return fmt.Errorf("invalid account %q: account was not found", p.nscu.account) 406 } 407 408 func (p *ProfileCmdParams) checkLoadUser(ctx ActionCtx) error { 409 if p.nscu.user == "" { 410 return nil 411 } 412 names, err := ctx.StoreCtx().Store.ListEntries(store.Accounts, p.nscu.account, store.Users) 413 if err != nil { 414 return err 415 } 416 417 m := make(map[string]string) 418 for _, n := range names { 419 m[strings.ToLower(n)] = n 420 } 421 un := m[strings.ToLower(p.nscu.user)] 422 if un != "" { 423 uc, err := ctx.StoreCtx().Store.ReadUserClaim(p.nscu.account, un) 424 if err != nil { 425 return err 426 } 427 p.nscu.user = uc.Name 428 p.uc = uc 429 return nil 430 } 431 432 for _, n := range names { 433 uc, err := ctx.StoreCtx().Store.ReadUserClaim(p.nscu.account, n) 434 if err != nil { 435 continue 436 } 437 aliases := p.loadNames(uc) 438 if aliases.contains(p.nscu.user) { 439 p.nscu.user = uc.Name 440 p.uc = uc 441 return nil 442 } 443 } 444 return fmt.Errorf("invalid user %q: user was not found", p.nscu.user) 445 } 446 447 func (p *ProfileCmdParams) Validate(ctx ActionCtx) error { 448 if err := p.checkLoadOperator(ctx); err != nil { 449 return err 450 } 451 if err := p.checkLoadAccount(ctx); err != nil { 452 return err 453 } 454 return p.checkLoadUser(ctx) 455 } 456 457 func (p *ProfileCmdParams) addOperatorKeys() { 458 p.results.Operator.Key = p.oc.Subject 459 } 460 461 func (p *ProfileCmdParams) addAccountKeys() { 462 if p.results.Account == nil { 463 p.results.Account = &Details{} 464 } 465 p.results.Account.Key = p.ac.Subject 466 } 467 468 func (p *ProfileCmdParams) addUserKeys() { 469 if p.results.User == nil { 470 p.results.User = &Details{} 471 } 472 p.results.User.Key = p.uc.Subject 473 } 474 475 func (p *ProfileCmdParams) addKeys() error { 476 q, err := p.nscu.query() 477 if err != nil { 478 return err 479 } 480 if len(q) == 0 { 481 return nil 482 } 483 _, keys := q[allKeys] 484 _, ok := q[operatorKey] 485 if ok || keys { 486 p.addOperatorKeys() 487 } 488 _, ok = q[accountKey] 489 if ok || keys { 490 p.addAccountKeys() 491 } 492 _, ok = q[userKey] 493 if ok || keys { 494 p.addUserKeys() 495 } 496 _, ok = q[key] 497 if ok { 498 if p.nscu.user != "" { 499 p.addUserKeys() 500 } else if p.nscu.account != "" { 501 p.addAccountKeys() 502 } else { 503 p.addOperatorKeys() 504 } 505 } 506 return nil 507 } 508 509 func (p *ProfileCmdParams) getKeys(claim jwt.Claims) []string { 510 var keys []string 511 if claim != nil { 512 keys = append(keys, claim.Claims().Subject) 513 var payload = claim.Payload() 514 oc, ok := payload.(*jwt.Operator) 515 if ok { 516 keys = append(keys, oc.SigningKeys...) 517 } 518 ac, ok := payload.(*jwt.Account) 519 if ok { 520 keys = append(keys, ac.SigningKeys.Keys()...) 521 } 522 } 523 return keys 524 } 525 526 func (p *ProfileCmdParams) resolveSeed(ctx ActionCtx, s string, keys []string) (string, error) { 527 ks := ctx.StoreCtx().KeyStore 528 if s != "" { 529 found := false 530 s = strings.ToUpper(s) 531 for _, k := range keys { 532 if s == k { 533 found = true 534 break 535 } 536 } 537 if !found { 538 return "", fmt.Errorf("%q was not found", s) 539 } 540 if ks.HasPrivateKey(s) { 541 seed, err := ks.GetSeed(s) 542 if seed != "" && err == nil { 543 return seed, err 544 } 545 } else { 546 return "", fmt.Errorf("no seed was found for %q", keys[0]) 547 } 548 } 549 for _, v := range keys { 550 if ks.HasPrivateKey(v) { 551 seed, err := ks.GetSeed(v) 552 if seed != "" && err == nil { 553 return seed, err 554 } 555 } 556 } 557 return "", fmt.Errorf("no seed was found for %q", keys[0]) 558 } 559 560 func (p *ProfileCmdParams) addOperatorSeed(ctx ActionCtx, v string) error { 561 seed, err := p.resolveSeed(ctx, v, p.getKeys(p.oc)) 562 if err != nil { 563 return err 564 } 565 p.results.Operator.Seed = seed 566 return nil 567 } 568 569 func (p *ProfileCmdParams) addAccountSeed(ctx ActionCtx, v string) error { 570 seed, err := p.resolveSeed(ctx, v, p.getKeys(p.ac)) 571 if err != nil { 572 return err 573 } 574 if p.results.Account == nil { 575 p.results.Account = &Details{} 576 } 577 p.results.Account.Seed = seed 578 return nil 579 } 580 581 func (p *ProfileCmdParams) addUserSeed(ctx ActionCtx, v string) error { 582 seed, err := p.resolveSeed(ctx, v, p.getKeys(p.uc)) 583 if err != nil { 584 return err 585 } 586 if p.results.User == nil { 587 p.results.User = &Details{} 588 } 589 p.results.User.Seed = seed 590 return nil 591 } 592 593 func (p *ProfileCmdParams) addSeeds(ctx ActionCtx) error { 594 q, err := p.nscu.query() 595 if err != nil { 596 return err 597 } 598 if len(q) == 0 { 599 return nil 600 } 601 _, seeds := q[allSeeds] 602 v, ok := q[operatorSeed] 603 if ok || seeds { 604 err := p.addOperatorSeed(ctx, v) 605 if err != nil { 606 return err 607 } 608 } 609 v, ok = q[accountSeed] 610 if ok || seeds { 611 err := p.addAccountSeed(ctx, v) 612 if err != nil { 613 return err 614 } 615 } 616 v, ok = q[userSeed] 617 if ok || seeds { 618 err := p.addUserSeed(ctx, v) 619 if err != nil { 620 return err 621 } 622 } 623 _, ok = q[seed] 624 if ok { 625 if p.nscu.user != "" { 626 err := p.addUserSeed(ctx, "") 627 if err != nil { 628 return err 629 } 630 } 631 if p.nscu.account != "" { 632 err := p.addAccountSeed(ctx, "") 633 if err != nil { 634 return err 635 } 636 } 637 if p.nscu.operator != "" { 638 err := p.addOperatorSeed(ctx, "") 639 if err != nil { 640 return err 641 } 642 } 643 } 644 return nil 645 } 646 647 func (p *ProfileCmdParams) addOperatorName() { 648 p.results.Operator.Name = p.conf.Operator 649 } 650 651 func (p *ProfileCmdParams) addAccountName() { 652 if p.results.Account == nil { 653 p.results.Account = &Details{} 654 } 655 p.results.Account.Name = p.ac.Name 656 } 657 658 func (p *ProfileCmdParams) addUserName() { 659 if p.results.User == nil { 660 p.results.User = &Details{} 661 } 662 p.results.User.Name = p.uc.Name 663 } 664 665 func (p *ProfileCmdParams) addNames() error { 666 q, err := p.nscu.query() 667 if err != nil { 668 return err 669 } 670 if len(q) == 0 { 671 return nil 672 } 673 674 _, names := q[allNames] 675 _, ok := q[operatorName] 676 if ok || names { 677 p.addOperatorName() 678 } 679 _, ok = q[accountName] 680 if ok || names { 681 p.addAccountName() 682 } 683 _, ok = q[userName] 684 if ok || names { 685 p.addUserName() 686 } 687 _, ok = q[name] 688 if ok { 689 if p.nscu.user != "" { 690 p.addUserName() 691 } 692 if p.nscu.account != "" { 693 p.addAccountName() 694 } 695 if p.nscu.operator != "" { 696 p.addOperatorName() 697 } 698 } 699 return nil 700 } 701 702 func (p *ProfileCmdParams) Run(ctx ActionCtx) (store.Status, error) { 703 p.results = &Profile{} 704 p.results.Operator = &Details{} 705 p.results.Operator.Service = p.oc.OperatorServiceURLs 706 if p.nscu.user != "" { 707 creds := ctx.StoreCtx().KeyStore.CalcUserCredsPath(p.nscu.account, p.nscu.user) 708 if _, err := os.Stat(creds); os.IsNotExist(err) { 709 // nothing 710 } else { 711 p.results.UserCreds = creds 712 } 713 } 714 if err := p.addNames(); err != nil { 715 return nil, err 716 } 717 if err := p.addKeys(); err != nil { 718 return nil, err 719 } 720 if err := p.addSeeds(ctx); err != nil { 721 return nil, err 722 } 723 v, err := json.MarshalIndent(p.results, "", " ") 724 v = append(v, '\n') 725 if err != nil { 726 return nil, err 727 } 728 if err := Write(p.outputFile, v); err != nil { 729 return nil, err 730 } 731 var s store.Status 732 if !IsStdOut(p.outputFile) { 733 s = store.OKStatus("wrote tool configuration to %#q", AbbrevHomePaths(p.outputFile)) 734 } 735 return s, nil 736 }