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