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