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