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