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