github.com/kbehouse/nsc@v0.0.6/cmd/generateprofile.go (about)

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