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, &params)
    40  		},
    41  	}
    42  	cmd.Flags().StringVarP(&params.tokenSrc, "token", "u", "", "path to token file can be a local path or an url (private imports only)")
    43  
    44  	cmd.Flags().StringVarP(&params.name, "name", "n", "", "import name")
    45  	cmd.Flags().StringVarP(&params.local, "local-subject", "s", "", "local subject")
    46  	params.srcAccount.BindFlags("src-account", "", nkeys.PrefixByteAccount, cmd)
    47  	cmd.Flags().StringVarP(&params.remote, "remote-subject", "", "", "remote subject (only public imports)")
    48  	cmd.Flags().BoolVarP(&params.service, "service", "", false, "service")
    49  	cmd.Flags().BoolVarP(&params.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  }