github.com/kbehouse/nsc@v0.0.6/cmd/adduser.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  	"sort"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/kbehouse/nsc/cmd/store"
    27  	cli "github.com/nats-io/cliprompts/v2"
    28  	"github.com/nats-io/jwt/v2"
    29  	"github.com/nats-io/nkeys"
    30  	"github.com/spf13/cobra"
    31  )
    32  
    33  func CreateAddUserCmd() *cobra.Command {
    34  	var params AddUserParams
    35  	cmd := &cobra.Command{
    36  		Use:          "user",
    37  		Short:        "Add an user to the account",
    38  		Args:         cobra.MaximumNArgs(1),
    39  		SilenceUsage: true,
    40  		Example: `# Add user with a previously generated public key:
    41  nsc add user --name <n> --public-key <nkey>
    42  # Note: that unless you specify the seed, the key won't be stored in the keyring.'
    43  
    44  # Set permissions so that the user can publish and/or subscribe to the specified subjects or wildcards:
    45  nsc add user --name <n> --allow-pubsub <subject>,...
    46  nsc add user --name <n> --allow-pub <subject>,...
    47  nsc add user --name <n> --allow-sub <subject>,...
    48  
    49  # Set permissions so that the user cannot publish nor subscribe to the specified subjects or wildcards:
    50  nsc add user --name <n> --deny-pubsub <subject>,...
    51  nsc add user --name <n> --deny-pub <subject>,...
    52  nsc add user --name <n> --deny-sub <subject>,...
    53  
    54  # Set subscribe permissions with queue names (separated from subject by space)
    55  # When added this way, the corresponding remove command needs to be presented with the exact same string
    56  nsc add user --name <n> --deny-sub "<subject> <queue>,..."
    57  nsc add user --name <n> --allow-sub "<subject> <queue>,..."
    58  
    59  # To dynamically allow publishing to reply subjects, this works well for service responders:
    60  nsc add user --name <n> --allow-pub-response
    61  
    62  # A permission to publish a response can be removed after a duration from when 
    63  # the message was received:
    64  nsc add user --name <n> --allow-pub-response --response-ttl 5s
    65  
    66  # If the service publishes multiple response messages, you can specify:
    67  nsc add user --name <n> --allow-pub-response=5
    68  # See 'nsc edit export --response-type --help' to enable multiple
    69  # responses between accounts
    70  `,
    71  		RunE: func(cmd *cobra.Command, args []string) error {
    72  			return RunAction(cmd, args, &params)
    73  		},
    74  	}
    75  
    76  	cmd.Flags().StringSliceVarP(&params.tags, "tag", "", nil, "tags for user - comma separated list or option can be specified multiple times")
    77  	cmd.Flags().StringSliceVarP(&params.src, "source-network", "", nil, "source network for connection - comma separated list or option can be specified multiple times")
    78  
    79  	cmd.Flags().StringVarP(&params.userName, "name", "n", "", "name to assign the user")
    80  	cmd.Flags().StringVarP(&params.pkOrPath, "public-key", "k", "", "public key identifying the user")
    81  
    82  	cmd.Flags().BoolVarP(&params.bearer, "bearer", "", false, "no connect challenge required for user")
    83  
    84  	params.TimeParams.BindFlags(cmd)
    85  	params.AccountContextParams.BindFlags(cmd)
    86  	params.PermissionsParams.bindSetFlags(cmd, "permissions")
    87  
    88  	return cmd
    89  }
    90  
    91  func init() {
    92  	addCmd.AddCommand(CreateAddUserCmd())
    93  }
    94  
    95  type AddUserParams struct {
    96  	AccountContextParams
    97  	SignerParams
    98  	TimeParams
    99  	PermissionsParams
   100  	src           []string
   101  	tags          []string
   102  	credsFilePath string
   103  	bearer        bool
   104  	userName      string
   105  	pkOrPath      string
   106  	kp            nkeys.KeyPair
   107  }
   108  
   109  func (p *AddUserParams) SetDefaults(ctx ActionCtx) error {
   110  	p.userName = NameFlagOrArgument(p.userName, ctx)
   111  	if p.userName == "*" {
   112  		p.userName = GetRandomName(0)
   113  	}
   114  	if err := p.AccountContextParams.SetDefaults(ctx); err != nil {
   115  		return err
   116  	}
   117  	p.SignerParams.SetDefaults(nkeys.PrefixByteAccount, true, ctx)
   118  
   119  	return nil
   120  }
   121  
   122  func (p *AddUserParams) PreInteractive(ctx ActionCtx) error {
   123  	var err error
   124  	if err = p.AccountContextParams.Edit(ctx); err != nil {
   125  		return err
   126  	}
   127  
   128  	p.userName, err = cli.Prompt("user name", p.userName, cli.NewLengthValidator(1))
   129  	if err != nil {
   130  		return err
   131  	}
   132  
   133  	ok, err := cli.Confirm("generate an user nkey", true)
   134  	if err != nil {
   135  		return err
   136  	}
   137  	if !ok {
   138  		p.pkOrPath, err = cli.Prompt("path to an user nkey or nkey", p.pkOrPath, cli.Val(func(v string) error {
   139  			nk, err := store.ResolveKey(v)
   140  			if err != nil {
   141  				return err
   142  			}
   143  			if nk == nil {
   144  				return fmt.Errorf("a key is required")
   145  			}
   146  			t, err := store.KeyType(nk)
   147  			if err != nil {
   148  				return err
   149  			}
   150  			if t != nkeys.PrefixByteUser {
   151  				return errors.New("specified key is not a valid for an user")
   152  			}
   153  			return nil
   154  		}))
   155  		if err != nil {
   156  			return err
   157  		}
   158  	}
   159  
   160  	// FIXME: we won't do interactive on the response params until pub/sub/deny permissions are interactive
   161  	//if err := p.PermissionsParams.Edit(false); err != nil {
   162  	//	return err
   163  	//}
   164  
   165  	if err = p.TimeParams.Edit(); err != nil {
   166  		return err
   167  	}
   168  
   169  	signers, err := validUserSigners(ctx, p.Name)
   170  	if err != nil {
   171  		return err
   172  	}
   173  	p.SignerParams.SetPrompt("select the key to sign the user")
   174  	return p.SignerParams.SelectFromSigners(ctx, signers)
   175  }
   176  
   177  func (p *AddUserParams) Load(_ ActionCtx) error {
   178  	return nil
   179  }
   180  
   181  func validUserSigners(ctx ActionCtx, accName string) ([]string, error) {
   182  	opc, err := ctx.StoreCtx().Store.ReadOperatorClaim()
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  	ac, err := ctx.StoreCtx().Store.ReadAccountClaim(accName)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  	var signers []string
   191  	if !opc.StrictSigningKeyUsage && ctx.StoreCtx().KeyStore.HasPrivateKey(ac.Subject) {
   192  		signers = append(signers, ac.Subject)
   193  	}
   194  	for signingKey := range ac.SigningKeys {
   195  		if ctx.StoreCtx().KeyStore.HasPrivateKey(signingKey) {
   196  			signers = append(signers, signingKey)
   197  		}
   198  	}
   199  	return signers, nil
   200  }
   201  
   202  func (p *AddUserParams) PostInteractive(_ ActionCtx) error {
   203  	return nil
   204  }
   205  
   206  func (p *AddUserParams) Validate(ctx ActionCtx) error {
   207  	var err error
   208  	if p.userName == "" {
   209  		ctx.CurrentCmd().SilenceUsage = false
   210  		return fmt.Errorf("user name is required")
   211  	}
   212  
   213  	if p.userName == "*" {
   214  		p.userName = GetRandomName(0)
   215  	}
   216  
   217  	if err = p.AccountContextParams.Validate(ctx); err != nil {
   218  		return err
   219  	}
   220  
   221  	if err = p.SignerParams.Resolve(ctx); err != nil {
   222  		return err
   223  	}
   224  
   225  	if err := p.TimeParams.Validate(); err != nil {
   226  		return err
   227  	}
   228  
   229  	if err := p.PermissionsParams.Validate(); err != nil {
   230  		return err
   231  	}
   232  
   233  	if p.pkOrPath != "" {
   234  		p.kp, err = store.ResolveKey(p.pkOrPath)
   235  		if err != nil {
   236  			return err
   237  		}
   238  		if !store.KeyPairTypeOk(nkeys.PrefixByteUser, p.kp) {
   239  			return errors.New("invalid user key")
   240  		}
   241  	} else {
   242  		p.kp, err = nkeys.CreatePair(nkeys.PrefixByteUser)
   243  		if err != nil {
   244  			return err
   245  		}
   246  	}
   247  
   248  	s := ctx.StoreCtx().Store
   249  	if s.Has(store.Accounts, ctx.StoreCtx().Account.Name, store.Users, store.JwtName(p.userName)) {
   250  		return fmt.Errorf("the user %q already exists", p.userName)
   251  	}
   252  
   253  	return nil
   254  }
   255  
   256  func signerKeyIsScoped(ctx ActionCtx, accountName string, signerKP nkeys.KeyPair) bool {
   257  	// get the account JWT - must have since we resolved the user based on it
   258  	ac, err := ctx.StoreCtx().Store.ReadAccountClaim(accountName)
   259  	if err != nil {
   260  		return false
   261  	}
   262  	// extract the signer public key
   263  	pk, err := signerKP.PublicKey()
   264  	if err != nil {
   265  		return false
   266  	}
   267  	if s, ok := ac.SigningKeys.GetScope(pk); ok && s != nil {
   268  		return true
   269  	}
   270  	return false
   271  }
   272  
   273  func checkUserForScope(ctx ActionCtx, accountName string, signerKP nkeys.KeyPair, uc *jwt.UserClaims) error {
   274  	if ctx == nil || accountName == "" || signerKP == nil || uc == nil {
   275  		return errors.New("invalid arguments")
   276  	}
   277  	// get the account JWT - must have since we resolved the user based on it
   278  	ac, err := ctx.StoreCtx().Store.ReadAccountClaim(accountName)
   279  	if err != nil {
   280  		return err
   281  	}
   282  	// extract the signer public key
   283  	pk, err := signerKP.PublicKey()
   284  	if err != nil {
   285  		return err
   286  	}
   287  	if s, ok := ac.SigningKeys.GetScope(pk); ok && s != nil {
   288  		if uc.Issuer == "" {
   289  			// set issuer as this is commonly set during encoding but is required next
   290  			uc.Issuer = pk
   291  		}
   292  		if err := s.ValidateScopedSigner(uc); err != nil {
   293  			return err
   294  		}
   295  	}
   296  	return nil
   297  }
   298  
   299  func (p *AddUserParams) Run(ctx ActionCtx) (store.Status, error) {
   300  	r := store.NewDetailedReport(false)
   301  	uc, err := p.generateUserClaim(ctx, r, signerKeyIsScoped(ctx, p.AccountContextParams.Name, p.signerKP))
   302  	if err != nil {
   303  		return nil, err
   304  	}
   305  
   306  	if err := checkUserForScope(ctx, p.AccountContextParams.Name, p.signerKP, uc); err != nil {
   307  		r.AddFromError(err)
   308  		r.AddWarning("user was NOT edited as the edits conflict with signing key scope")
   309  		return r, err
   310  	}
   311  
   312  	token, err := uc.Encode(p.signerKP)
   313  	if err != nil {
   314  		return nil, err
   315  	}
   316  
   317  	st, err := ctx.StoreCtx().Store.StoreClaim([]byte(token))
   318  	if st != nil {
   319  		r.Add(st)
   320  	}
   321  	if err != nil {
   322  		r.AddFromError(err)
   323  		return r, err
   324  	}
   325  
   326  	// store the key
   327  	if p.pkOrPath == "" {
   328  		ks := ctx.StoreCtx()
   329  		var err error
   330  		if p.pkOrPath, err = ks.KeyStore.Store(p.kp); err != nil {
   331  			r.AddFromError(err)
   332  			return r, err
   333  		}
   334  		r.AddOK("generated and stored user key %q", uc.Subject)
   335  	}
   336  
   337  	pk := uc.Subject
   338  	// if they gave us a seed, it stored - try to get it
   339  	ks := ctx.StoreCtx().KeyStore
   340  	if ks.HasPrivateKey(pk) {
   341  		// we may have it - but the key we got is possibly a pub only - resolve it from the store.
   342  		p.kp, _ = ks.GetKeyPair(pk)
   343  		d, err := GenerateConfig(ctx.StoreCtx().Store, p.AccountContextParams.Name, p.userName, p.kp)
   344  		if err != nil {
   345  			r.AddError("unable to save creds: %v", err)
   346  		} else {
   347  			p.credsFilePath, err = ks.MaybeStoreUserCreds(p.AccountContextParams.Name, p.userName, d)
   348  			if err != nil {
   349  				r.AddError("error storing creds: %v", err)
   350  			} else {
   351  				r.AddOK("generated user creds file %#q", AbbrevHomePaths(p.credsFilePath))
   352  			}
   353  		}
   354  	} else {
   355  		r.AddOK("skipped generating creds file - user private key is not available")
   356  	}
   357  	if r.HasNoErrors() {
   358  		r.AddOK("added user %q to account %q", p.userName, p.AccountContextParams.Name)
   359  	}
   360  	return r, nil
   361  }
   362  
   363  func (p *AddUserParams) generateUserClaim(ctx ActionCtx, r *store.Report, scoped bool) (*jwt.UserClaims, error) {
   364  	pub, err := p.kp.PublicKey()
   365  	if err != nil {
   366  		return nil, err
   367  	}
   368  	uc := jwt.NewUserClaims(pub)
   369  	uc.Name = p.userName
   370  	uc.SetScoped(scoped)
   371  
   372  	spk, err := p.signerKP.PublicKey()
   373  	if err != nil {
   374  		return nil, err
   375  	}
   376  	if ctx.StoreCtx().Account.PublicKey != spk {
   377  		uc.IssuerAccount = ctx.StoreCtx().Account.PublicKey
   378  	}
   379  
   380  	if p.TimeParams.IsStartChanged() {
   381  		uc.NotBefore, _ = p.TimeParams.StartDate()
   382  	}
   383  
   384  	if p.TimeParams.IsExpiryChanged() {
   385  		uc.Expires, _ = p.TimeParams.ExpiryDate()
   386  	}
   387  
   388  	if s, err := p.PermissionsParams.Run(&uc.Permissions, ctx); err != nil {
   389  		return nil, err
   390  	} else if s != nil {
   391  		r.Add(s.Details...)
   392  	}
   393  
   394  	uc.Src.Add(p.src...)
   395  
   396  	uc.Tags.Add(p.tags...)
   397  	sort.Strings(uc.Tags)
   398  
   399  	uc.BearerToken = p.bearer
   400  	return uc, nil
   401  }
   402  
   403  type PermissionsParams struct {
   404  	respTTL     string
   405  	respMax     int
   406  	rmResp      bool
   407  	allowPubs   []string
   408  	allowPubsub []string
   409  	allowSubs   []string
   410  	denyPubs    []string
   411  	denyPubsub  []string
   412  	denySubs    []string
   413  	rmPerms     []string
   414  }
   415  
   416  func (p *PermissionsParams) bindSetFlags(cmd *cobra.Command, typeName string) {
   417  	cmd.Flags().StringVarP(&p.respTTL, "response-ttl", "", "", fmt.Sprintf("the amount of time the %s is valid (global) - [#ms(millis) | #s(econds) | m(inutes) | h(ours)] - Default is no time limit.", typeName))
   418  
   419  	cmd.Flags().IntVarP(&p.respMax, "allow-pub-response", "", 0, fmt.Sprintf("%s to limit how often a client can publish to reply subjects [with an optional count, --allow-pub-response=n] (global)", typeName))
   420  	cmd.Flag("allow-pub-response").NoOptDefVal = "1"
   421  
   422  	cmd.Flags().IntVarP(&p.respMax, "max-responses", "", 0, fmt.Sprintf("%s to limit how ofthen a client can publish to reply subjects [with an optional count] (global)", typeName))
   423  	cmd.Flag("max-responses").Hidden = true
   424  	cmd.Flag("max-responses").Deprecated = "use --allow-pub-response or --allow-pub-response=n"
   425  
   426  	cmd.Flags().StringSliceVarP(&p.allowPubs, "allow-pub", "", nil, fmt.Sprintf("add publish %s - comma separated list or option can be specified multiple times", typeName))
   427  	cmd.Flags().StringSliceVarP(&p.allowPubsub, "allow-pubsub", "", nil, fmt.Sprintf("add publish and subscribe %s - comma separated list or option can be specified multiple times", typeName))
   428  	cmd.Flags().StringSliceVarP(&p.allowSubs, "allow-sub", "", nil, fmt.Sprintf("add subscribe %s - comma separated list or option can be specified multiple times", typeName))
   429  	cmd.Flags().StringSliceVarP(&p.denyPubs, "deny-pub", "", nil, fmt.Sprintf("add deny publish %s - comma separated list or option can be specified multiple times", typeName))
   430  	cmd.Flags().StringSliceVarP(&p.denyPubsub, "deny-pubsub", "", nil, fmt.Sprintf("add deny publish and subscribe %s - comma separated list or option can be specified multiple times", typeName))
   431  	cmd.Flags().StringSliceVarP(&p.denySubs, "deny-sub", "", nil, fmt.Sprintf("add deny subscribe %s - comma separated list or option can be specified multiple times", typeName))
   432  }
   433  
   434  func (p *PermissionsParams) bindRemoveFlags(cmd *cobra.Command, typeName string) {
   435  	cmd.Flags().BoolVarP(&p.rmResp, "rm-response-perms", "", false, fmt.Sprintf("remove response settings from %s", typeName))
   436  	cmd.Flags().StringSliceVarP(&p.rmPerms, "rm", "", nil, fmt.Sprintf("remove publish/subscribe and deny %s - comma separated list or option can be specified multiple times", typeName))
   437  }
   438  
   439  func (p *PermissionsParams) maxResponseValidator(s string) error {
   440  	_, err := p.parseMaxResponse(s)
   441  	return err
   442  }
   443  
   444  func (p *PermissionsParams) parseMaxResponse(s string) (int, error) {
   445  	if s == "" {
   446  		return 0, nil
   447  	}
   448  	return strconv.Atoi(s)
   449  }
   450  
   451  func (p *PermissionsParams) ttlValidator(s string) error {
   452  	_, err := p.parseTTL(s)
   453  	return err
   454  }
   455  
   456  func (p *PermissionsParams) parseTTL(s string) (time.Duration, error) {
   457  	if s == "" {
   458  		return time.Duration(0), nil
   459  	}
   460  	return time.ParseDuration(s)
   461  }
   462  
   463  func (p *PermissionsParams) Edit(hasPerm bool) error {
   464  	verb := "Set"
   465  	if hasPerm {
   466  		verb = "Edit"
   467  	}
   468  	ok, err := cli.Confirm(fmt.Sprintf("%s response permissions?", verb), false)
   469  	if err != nil {
   470  		return err
   471  	}
   472  	if ok {
   473  		if hasPerm {
   474  			p.rmResp, err = cli.Confirm("delete response permission", p.rmResp)
   475  			if err != nil {
   476  				return err
   477  			}
   478  		}
   479  		if !p.rmResp {
   480  			s, err := cli.Prompt("Max number of responses", fmt.Sprintf("%d", p.respMax), cli.Val(p.maxResponseValidator))
   481  			if err != nil {
   482  				return err
   483  			}
   484  			p.respMax, _ = p.parseMaxResponse(s)
   485  			p.respTTL, err = cli.Prompt("Response TTL", p.respTTL, cli.Val(p.ttlValidator))
   486  			if err != nil {
   487  				return err
   488  			}
   489  		}
   490  	}
   491  	return nil
   492  }
   493  
   494  func (p *PermissionsParams) Validate() error {
   495  	if err := p.ttlValidator(p.respTTL); err != nil {
   496  		return err
   497  	}
   498  	for _, v := range [][]string{p.allowPubs, p.allowPubsub, p.denyPubs, p.denyPubsub} {
   499  		for _, sub := range v {
   500  			if strings.Contains(sub, " ") {
   501  				return fmt.Errorf("publish permission subject %q contains illegal space", sub)
   502  			}
   503  		}
   504  	}
   505  	for _, v := range [][]string{p.allowSubs, p.denySubs} {
   506  		for _, sub := range v {
   507  			if strings.Count(sub, " ") > 1 {
   508  				return fmt.Errorf("subscribe permission subject %q can at most contain one space", sub)
   509  			}
   510  		}
   511  	}
   512  
   513  	return nil
   514  }
   515  
   516  func (p *PermissionsParams) Run(perms *jwt.Permissions, ctx ActionCtx) (*store.Report, error) {
   517  	r := store.NewDetailedReport(true)
   518  	if p.rmResp {
   519  		perms.Resp = nil
   520  		r.AddOK("removed response permissions")
   521  		return r, nil
   522  	}
   523  
   524  	if ctx.CurrentCmd().Flag("max-responses").Changed || p.respMax != 0 {
   525  		if perms.Resp == nil {
   526  			perms.Resp = &jwt.ResponsePermission{}
   527  		}
   528  		perms.Resp.MaxMsgs = p.respMax
   529  		r.AddOK("set max responses to %d", p.respMax)
   530  	}
   531  
   532  	if p.respTTL != "" {
   533  		v, err := p.parseTTL(p.respTTL)
   534  		if err != nil {
   535  			return nil, err
   536  		}
   537  		if perms.Resp == nil {
   538  			perms.Resp = &jwt.ResponsePermission{}
   539  		}
   540  		perms.Resp.Expires = v
   541  		r.AddOK("set response ttl to %v", v)
   542  	}
   543  
   544  	var ap []string
   545  	perms.Pub.Allow.Add(p.allowPubs...)
   546  	ap = append(ap, p.allowPubs...)
   547  	perms.Pub.Allow.Add(p.allowPubsub...)
   548  	ap = append(ap, p.allowPubsub...)
   549  	for _, v := range ap {
   550  		r.AddOK("added pub pub %q", v)
   551  	}
   552  	perms.Pub.Allow.Remove(p.rmPerms...)
   553  	for _, v := range p.rmPerms {
   554  		r.AddOK("removed pub %q", v)
   555  	}
   556  	sort.Strings(perms.Pub.Allow)
   557  
   558  	var dp []string
   559  	perms.Pub.Deny.Add(p.denyPubs...)
   560  	dp = append(dp, p.denyPubs...)
   561  	perms.Pub.Deny.Add(p.denyPubsub...)
   562  	dp = append(dp, p.denyPubsub...)
   563  	for _, v := range dp {
   564  		r.AddOK("added deny pub %q", v)
   565  	}
   566  	perms.Pub.Deny.Remove(p.rmPerms...)
   567  	for _, v := range p.rmPerms {
   568  		r.AddOK("removed deny pub %q", v)
   569  	}
   570  	sort.Strings(perms.Pub.Deny)
   571  
   572  	var sa []string
   573  	perms.Sub.Allow.Add(p.allowSubs...)
   574  	sa = append(sa, p.allowSubs...)
   575  	perms.Sub.Allow.Add(p.allowPubsub...)
   576  	sa = append(sa, p.allowPubsub...)
   577  	for _, v := range sa {
   578  		r.AddOK("added sub %q", v)
   579  	}
   580  	perms.Sub.Allow.Remove(p.rmPerms...)
   581  	for _, v := range p.rmPerms {
   582  		r.AddOK("removed sub %q", v)
   583  	}
   584  	sort.Strings(perms.Sub.Allow)
   585  
   586  	perms.Sub.Deny.Add(p.denySubs...)
   587  	perms.Sub.Deny.Add(p.denyPubsub...)
   588  	perms.Sub.Deny.Remove(p.rmPerms...)
   589  	sort.Strings(perms.Sub.Deny)
   590  	return r, nil
   591  }