github.com/kbehouse/nsc@v0.0.6/cmd/addimport.go (about) 1 /* 2 * Copyright 2018-2021 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 "strings" 22 23 "github.com/kbehouse/nsc/cmd/store" 24 cli "github.com/nats-io/cliprompts/v2" 25 "github.com/nats-io/jwt/v2" 26 "github.com/nats-io/nkeys" 27 "github.com/spf13/cobra" 28 ) 29 30 func createAddImportCmd() *cobra.Command { 31 var params AddImportParams 32 cmd := &cobra.Command{ 33 Use: "import", 34 Short: "Add an import", 35 Args: MaxArgs(0), 36 Example: params.longHelp(), 37 SilenceUsage: true, 38 RunE: func(cmd *cobra.Command, args []string) error { 39 return RunAction(cmd, args, ¶ms) 40 }, 41 } 42 cmd.Flags().StringVarP(¶ms.tokenSrc, "token", "u", "", "path to token file can be a local path or an url (private imports only)") 43 44 cmd.Flags().StringVarP(¶ms.name, "name", "n", "", "import name") 45 cmd.Flags().StringVarP(¶ms.local, "local-subject", "s", "", "local subject") 46 params.srcAccount.BindFlags("src-account", "", nkeys.PrefixByteAccount, cmd) 47 cmd.Flags().StringVarP(¶ms.remote, "remote-subject", "", "", "remote subject (only public imports)") 48 cmd.Flags().BoolVarP(¶ms.service, "service", "", false, "service") 49 cmd.Flags().BoolVarP(¶ms.share, "share", "", false, "share data when tracking latency (service only)") 50 params.AccountContextParams.BindFlags(cmd) 51 52 return cmd 53 } 54 55 func init() { 56 addCmd.AddCommand(createAddImportCmd()) 57 } 58 59 type AddImportParams struct { 60 AccountContextParams 61 SignerParams 62 srcAccount PubKeyParams 63 claim *jwt.AccountClaims 64 local string 65 token []byte 66 tokenSrc string 67 remote string 68 service bool 69 name string 70 public bool 71 share bool 72 } 73 74 func (p *AddImportParams) longHelp() string { 75 v := `toolname add import -i 76 toolname add import --token <filepath> --local-subject <sub> 77 toolname add import --token <some-http-url> --local-subject <sub> 78 toolname add import --src-account <account_pubkey> --remote-subject <remote-sub> --local-subject <sub>` 79 80 return strings.Replace(v, "toolname", GetToolName(), -1) 81 } 82 83 func (p *AddImportParams) SetDefaults(ctx ActionCtx) error { 84 if !InteractiveFlag { 85 p.public = ctx.AllSet("token") 86 set := ctx.CountSet("token", "remote-subject", "src-account") 87 if p.public && set > 1 { 88 ctx.CurrentCmd().SilenceErrors = false 89 ctx.CurrentCmd().SilenceUsage = false 90 return errors.New("private imports require src-account, remote-subject and service to be unset") 91 } 92 if !p.public && set != 2 { 93 ctx.CurrentCmd().SilenceErrors = false 94 ctx.CurrentCmd().SilenceUsage = false 95 return errors.New("public imports require src-account, remote-subject") 96 } 97 } 98 99 if err := p.AccountContextParams.SetDefaults(ctx); err != nil { 100 return err 101 } 102 103 p.SignerParams.SetDefaults(nkeys.PrefixByteOperator, true, ctx) 104 105 if p.name == "" { 106 p.name = p.remote 107 } 108 109 return nil 110 } 111 112 func (p *AddImportParams) getAvailableExports(ctx ActionCtx) ([]AccountExport, error) { 113 // these are sorted by account name 114 found, err := GetAllExports() 115 if err != nil { 116 return nil, err 117 } 118 119 ac, err := ctx.StoreCtx().Store.ReadAccountClaim(ctx.StoreCtx().Account.Name) 120 if err != nil { 121 return nil, err 122 } 123 124 var filtered []AccountExport 125 for _, f := range found { 126 // FIXME: filtering on the target account, should eliminate exports the account already has 127 if f.Subject != ac.Subject { 128 filtered = append(filtered, f) 129 } 130 } 131 132 return filtered, nil 133 } 134 135 func (p *AddImportParams) addLocalExport(ctx ActionCtx) (bool, error) { 136 // see if we have any exports 137 available, err := p.getAvailableExports(ctx) 138 if err != nil { 139 return false, err 140 } 141 142 if len(available) > 0 { 143 // we have some exports that they may want 144 ok, err := cli.Confirm("pick from locally available exports", true) 145 if err != nil { 146 return false, err 147 } 148 if ok { 149 var choices []AccountExportChoice 150 for _, v := range available { 151 choices = append(choices, v.Choices()...) 152 } 153 var labels = AccountExportChoices(choices).String() 154 // fixme: need to have validators on this 155 156 var c *AccountExportChoice 157 for { 158 idx, err := cli.Select("select the export", "", labels) 159 if err != nil { 160 return false, err 161 } 162 if choices[idx].Selection == nil { 163 ctx.CurrentCmd().Printf("%q is an account grouping not an export\n", labels[idx]) 164 continue 165 } 166 c = &choices[idx] 167 break 168 } 169 170 targetAccountPK := ctx.StoreCtx().Account.PublicKey 171 p.srcAccount.publicKey = c.Subject 172 p.name = c.Selection.Name 173 174 ac, err := ctx.StoreCtx().Store.ReadAccountClaim(ctx.StoreCtx().Account.Name) 175 if err != nil { 176 return false, err 177 } 178 179 p.claim = ac 180 subject := string(c.Selection.Subject) 181 182 // rewrite account token subject to include importing account id 183 if c.Selection.AccountTokenPosition > 0 { 184 idx := int(c.Selection.AccountTokenPosition) - 1 185 tk := strings.Split(string(c.Selection.Subject), ".") 186 if idx > len(tk) { 187 return false, fmt.Errorf("AccountTokenPosition greater than subject is long") 188 } 189 if tk[idx] != "*" { 190 return false, fmt.Errorf("AccountTokenPosition needs to point at wildcard *") 191 } 192 tk[idx] = targetAccountPK 193 subject = strings.Join(tk, ".") 194 195 // set local subject to not include the account id 196 if p.local == "" { 197 for i := idx; i < len(tk)-1; i++ { 198 tk[idx] = tk[idx+1] 199 } 200 tk2 := tk[0 : len(tk)-1] 201 p.local = strings.Join(tk2, ".") 202 } 203 } 204 205 if c.Selection.IsService() && c.Selection.Subject.HasWildCards() { 206 subject, err = cli.Prompt("export subject", subject, cli.Val(func(s string) error { 207 sub := jwt.Subject(s) 208 var vr jwt.ValidationResults 209 sub.Validate(&vr) 210 if len(vr.Issues) > 0 { 211 return errors.New(vr.Issues[0].Description) 212 } 213 return nil 214 })) 215 if err != nil { 216 return false, err 217 } 218 } 219 p.remote = subject 220 p.service = c.Selection.IsService() 221 if p.service && p.local == "" { 222 p.local = subject 223 } 224 if c.Selection.TokenReq { 225 if err := p.generateToken(ctx, c); err != nil { 226 return false, err 227 } 228 } 229 return true, nil 230 } 231 } 232 return false, nil 233 } 234 235 func (p *AddImportParams) generateToken(ctx ActionCtx, c *AccountExportChoice) error { 236 // load the source account 237 srcAC, err := ctx.StoreCtx().Store.ReadAccountClaim(c.Name) 238 if err != nil { 239 return err 240 } 241 242 var ap GenerateActivationParams 243 ap.Name = c.Name 244 ap.claims = srcAC 245 ap.accountKey.publicKey = ctx.StoreCtx().Account.PublicKey 246 ap.export = *c.Selection 247 ap.subject = p.remote 248 249 // collect the possible signers 250 var signers []string 251 signers = append(signers, srcAC.Subject) 252 signers = append(signers, srcAC.SigningKeys.Keys()...) 253 254 ap.SignerParams.SetPrompt(fmt.Sprintf("select the signing key for account %q [%s]", srcAC.Name, srcAC.Subject)) 255 if err := ap.SelectFromSigners(ctx, signers); err != nil { 256 return err 257 } 258 259 if _, err := ap.Run(ctx); err != nil { 260 return err 261 } 262 263 p.token = []byte(ap.Token()) 264 return p.initFromActivation(ctx) 265 } 266 267 func (p *AddImportParams) addManualExport(_ ActionCtx) error { 268 var err error 269 p.public, err = cli.Confirm("is the export public?", true) 270 if err != nil { 271 return err 272 } 273 if p.public { 274 if err := p.srcAccount.Edit(); err != nil { 275 return err 276 } 277 p.remote, err = cli.Prompt("remote subject", p.remote, cli.Val(func(v string) error { 278 t := jwt.Subject(v) 279 var vr jwt.ValidationResults 280 t.Validate(&vr) 281 if len(vr.Issues) > 0 { 282 return errors.New(vr.Issues[0].Description) 283 } 284 return nil 285 })) 286 p.service, err = cli.Confirm("is import a service", true) 287 if err != nil { 288 return err 289 } 290 } else { 291 p.tokenSrc, err = cli.Prompt("token path or url", p.tokenSrc, cli.Val(func(s string) error { 292 p.tokenSrc = s 293 p.token, err = p.loadImport() 294 if err != nil { 295 return err 296 } 297 return nil 298 })) 299 if err != nil { 300 return err 301 } 302 } 303 return nil 304 } 305 306 func (p *AddImportParams) PreInteractive(ctx ActionCtx) error { 307 var err error 308 if err = p.AccountContextParams.Edit(ctx); err != nil { 309 return err 310 } 311 312 ok, err := p.addLocalExport(ctx) 313 if err != nil { 314 return err 315 } 316 if !ok { 317 return p.addManualExport(ctx) 318 } 319 if p.service { 320 if p.share, err = cli.Confirm("share information when tracking latency?", false); err != nil { 321 return err 322 } 323 } 324 return nil 325 } 326 327 func (p *AddImportParams) loadImport() ([]byte, error) { 328 data, err := LoadFromFileOrURL(p.tokenSrc) 329 if err != nil { 330 return nil, fmt.Errorf("error loading %#q: %v", p.tokenSrc, err) 331 } 332 v, err := jwt.ParseDecoratedJWT(data) 333 if err != nil { 334 return nil, fmt.Errorf("error loading %#q: %v", p.tokenSrc, err) 335 } 336 return []byte(v), nil 337 } 338 339 func (p *AddImportParams) Load(ctx ActionCtx) error { 340 var err error 341 342 if err = p.AccountContextParams.Validate(ctx); err != nil { 343 return err 344 } 345 346 p.claim, err = ctx.StoreCtx().Store.ReadAccountClaim(p.AccountContextParams.Name) 347 if err != nil { 348 return err 349 } 350 351 if p.tokenSrc != "" { 352 if err := p.initFromActivation(ctx); err != nil { 353 return err 354 } 355 } 356 357 return nil 358 } 359 360 func (p *AddImportParams) initFromActivation(_ ActionCtx) error { 361 var err error 362 if p.token == nil { 363 p.token, err = p.loadImport() 364 if err != nil { 365 return err 366 } 367 } 368 369 ac, err := jwt.DecodeActivationClaims(string(p.token)) 370 if err != nil { 371 return err 372 } 373 374 if p.name == "" { 375 p.name = ac.Name 376 } 377 p.remote = string(ac.ImportSubject) 378 p.service = ac.ImportType == jwt.Service 379 if p.service && p.local == "" { 380 p.local = p.remote 381 } 382 383 p.srcAccount.publicKey = ac.Issuer 384 if ac.IssuerAccount != "" { 385 p.srcAccount.publicKey = ac.IssuerAccount 386 } 387 if ac.Subject != "public" && p.claim.Subject != ac.Subject { 388 return fmt.Errorf("activation is not intended for this account - it is for %q", ac.Subject) 389 } 390 return nil 391 } 392 393 func (p *AddImportParams) checkServiceSubject(s string) error { 394 // if we are not dealing with a service ignore 395 if !p.service { 396 return nil 397 } 398 for _, v := range p.claim.Imports { 399 // ignore streams 400 if v.IsStream() { 401 continue 402 } 403 if s == string(v.Subject) { 404 return fmt.Errorf("%s is already in use by a different service import", s) 405 } 406 } 407 return nil 408 } 409 410 func (p *AddImportParams) PostInteractive(ctx ActionCtx) error { 411 var err error 412 413 if p.name == "" { 414 p.name = p.remote 415 } 416 417 p.name, err = cli.Prompt("name", p.name, cli.NewLengthValidator(1)) 418 if err != nil { 419 return err 420 } 421 422 p.local, err = cli.Prompt("local subject", p.local, cli.Val(func(s string) error { 423 if s == "" { 424 return nil 425 } 426 if err := p.checkServiceSubject(s); err != nil { 427 return err 428 } 429 430 vr := jwt.CreateValidationResults() 431 sub := jwt.Subject(s) 432 sub.Validate(vr) 433 if !vr.IsEmpty() { 434 return errors.New(vr.Issues[0].Error()) 435 } 436 return nil 437 })) 438 if err != nil { 439 return err 440 } 441 442 if err = p.SignerParams.Edit(ctx); err != nil { 443 return err 444 } 445 446 return nil 447 } 448 449 func (p *AddImportParams) Validate(ctx ActionCtx) error { 450 var err error 451 452 if p.claim.Subject == p.srcAccount.publicKey { 453 return fmt.Errorf("export issuer is this account") 454 } 455 456 if err = p.AccountContextParams.Validate(ctx); err != nil { 457 return err 458 } 459 460 if err = p.srcAccount.Valid(); err != nil { 461 return err 462 } 463 464 kind := jwt.Stream 465 if p.service { 466 kind = jwt.Service 467 } else if p.share { 468 return fmt.Errorf("only services can set the share property") 469 } 470 471 for _, im := range p.filter(kind, p.claim.Imports) { 472 remote := string(im.Subject) 473 if im.Account == p.srcAccount.publicKey && remote == p.remote { 474 return fmt.Errorf("account already imports %s %q from %s", kind, im.Subject, p.srcAccount.publicKey) 475 } 476 } 477 478 if err = p.SignerParams.Resolve(ctx); err != nil { 479 return err 480 } 481 482 return nil 483 } 484 485 func (p *AddImportParams) filter(kind jwt.ExportType, imports jwt.Imports) jwt.Imports { 486 var buf jwt.Imports 487 for _, v := range imports { 488 if v.Type == kind { 489 buf.Add(v) 490 } 491 } 492 return buf 493 } 494 495 func (p *AddImportParams) Run(ctx ActionCtx) (store.Status, error) { 496 var err error 497 p.claim.Imports.Add(p.createImport()) 498 499 token, err := p.claim.Encode(p.signerKP) 500 if err != nil { 501 return nil, err 502 } 503 504 ac, err := jwt.DecodeAccountClaims(token) 505 if err != nil { 506 return nil, err 507 } 508 509 var vr jwt.ValidationResults 510 ac.Validate(&vr) 511 errs := vr.Errors() 512 if len(errs) > 0 { 513 return nil, errs[0] 514 } 515 516 kind := jwt.Stream 517 if p.service { 518 kind = jwt.Service 519 } 520 521 r := store.NewDetailedReport(false) 522 StoreAccountAndUpdateStatus(ctx, token, r) 523 if r.HasNoErrors() { 524 r.AddOK("added %s import %q", kind, p.remote) 525 } 526 return r, err 527 } 528 529 func (p *AddImportParams) createImport() *jwt.Import { 530 var im jwt.Import 531 im.Name = p.name 532 im.Subject = jwt.Subject(p.remote) 533 im.LocalSubject = jwt.RenamingSubject(p.local) 534 im.Account = p.srcAccount.publicKey 535 im.Type = jwt.Stream 536 537 if p.service { 538 im.Type = jwt.Service 539 im.Share = p.share 540 } 541 if p.tokenSrc != "" { 542 if IsURL(p.tokenSrc) { 543 im.Token = p.tokenSrc 544 } 545 } 546 if p.token != nil { 547 im.Token = string(p.token) 548 } 549 550 return &im 551 }