github.com/nats-io/nsc@v0.0.0-20221206222106-35db9400b257/cmd/edituser.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  	"fmt"
    20  	"sort"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/nats-io/jwt/v2"
    25  	"github.com/nats-io/nkeys"
    26  	"github.com/nats-io/nsc/cmd/store"
    27  	"github.com/spf13/cobra"
    28  )
    29  
    30  func createEditUserCmd() *cobra.Command {
    31  	var params EditUserParams
    32  	cmd := &cobra.Command{
    33  		Use:   "user",
    34  		Short: "Edit an user",
    35  		Long: `# Edit permissions so that the user can publish and/or subscribe to the specified subjects or wildcards:
    36  nsc edit user --name <n> --allow-pubsub <subject>,...
    37  nsc edit user --name <n> --allow-pub <subject>,...
    38  nsc edit user --name <n> --allow-sub <subject>,...
    39  
    40  # Set permissions so that the user cannot publish nor subscribe to the specified subjects or wildcards:
    41  nsc edit user --name <n> --deny-pubsub <subject>,...
    42  nsc edit user --name <n> --deny-pub <subject>,...
    43  nsc edit user --name <n> --deny-sub <subject>,...
    44  
    45  # Set subscribe permissions with queue names (separated from subject by space)
    46  # When added this way, the corresponding remove command needs to be presented with the exact same string
    47  nsc edit user --name <n> --deny-sub "<subject> <queue>,..."
    48  nsc edit user --name <n> --allow-sub "<subject> <queue>,..."
    49  
    50  # Remove a previously set permissions
    51  nsc edit user --name <n> --rm <subject>,...
    52  
    53  # To dynamically allow publishing to reply subjects, this works well for service responders:
    54  nsc edit user --name <n> --allow-pub-response
    55  
    56  # A permission to publish a response can be removed after a duration from when
    57  # the message was received:
    58  nsc edit user --name <n> --allow-pub-response --response-ttl 5s
    59  
    60  # If the service publishes multiple response messages, you can specify:
    61  nsc edit user --name <n> --allow-pub-response=5
    62  # See 'nsc edit export --response-type --help' to enable multiple
    63  # responses between accounts.
    64  
    65  # To remove response settings:
    66  nsc edit user --name <n> --rm-response-perms
    67  `,
    68  		Args:         cobra.MaximumNArgs(1),
    69  		SilenceUsage: true,
    70  		RunE: func(cmd *cobra.Command, args []string) error {
    71  			return RunAction(cmd, args, &params)
    72  		},
    73  	}
    74  	cmd.Flags().StringSliceVarP(&params.tags, "tag", "", nil, "add tags for user - comma separated list or option can be specified multiple times")
    75  	cmd.Flags().StringSliceVarP(&params.rmTags, "rm-tag", "", nil, "remove tag - comma separated list or option can be specified multiple times")
    76  	cmd.Flags().StringVarP(&params.name, "name", "n", "", "user name")
    77  	params.AccountContextParams.BindFlags(cmd)
    78  	params.GenericClaimsParams.BindFlags(cmd)
    79  	params.UserPermissionLimits.BindFlags(cmd)
    80  	return cmd
    81  }
    82  
    83  func init() {
    84  	editCmd.AddCommand(createEditUserCmd())
    85  }
    86  
    87  type EditUserParams struct {
    88  	AccountContextParams
    89  	SignerParams
    90  	GenericClaimsParams
    91  	claim         *jwt.UserClaims
    92  	name          string
    93  	token         string
    94  	credsFilePath string
    95  	UserPermissionLimits
    96  }
    97  
    98  func (p *EditUserParams) SetDefaults(ctx ActionCtx) error {
    99  	p.name = NameFlagOrArgument(p.name, ctx)
   100  	if err := p.AccountContextParams.SetDefaults(ctx); err != nil {
   101  		return err
   102  	}
   103  	p.SignerParams.SetDefaults(nkeys.PrefixByteAccount, true, ctx)
   104  
   105  	if !InteractiveFlag && ctx.NothingToDo("start", "expiry", "rm", "allow-pub", "allow-sub", "allow-pubsub",
   106  		"deny-pub", "deny-sub", "deny-pubsub", "tag", "rm-tag", "source-network", "rm-source-network", "payload",
   107  		"rm-response-perms", "max-responses", "response-ttl", "allow-pub-response", "bearer", "rm-time", "time", "conn-type",
   108  		"rm-conn-type", "subs", "data") {
   109  		ctx.CurrentCmd().SilenceUsage = false
   110  		return fmt.Errorf("specify an edit option")
   111  	}
   112  	return nil
   113  }
   114  
   115  func (p *EditUserParams) PreInteractive(ctx ActionCtx) error {
   116  	var err error
   117  	if err = p.AccountContextParams.Edit(ctx); err != nil {
   118  		return err
   119  	}
   120  
   121  	if p.name == "" {
   122  		p.name, err = ctx.StoreCtx().PickUser(p.AccountContextParams.Name)
   123  		if err != nil {
   124  			return err
   125  		}
   126  	}
   127  
   128  	signers, err := validUserSigners(ctx, p.Name)
   129  	if err != nil {
   130  		return err
   131  	}
   132  	p.SignerParams.SetPrompt("select the key to sign the user")
   133  	return p.SignerParams.SelectFromSigners(ctx, signers)
   134  }
   135  
   136  func (p *EditUserParams) Load(ctx ActionCtx) error {
   137  	var err error
   138  
   139  	if err = p.AccountContextParams.Validate(ctx); err != nil {
   140  		return err
   141  	}
   142  
   143  	if p.name == "" {
   144  		n := ctx.StoreCtx().DefaultUser(p.AccountContextParams.Name)
   145  		if n != nil {
   146  			p.name = *n
   147  		}
   148  	}
   149  
   150  	if p.name == "" {
   151  		ctx.CurrentCmd().SilenceUsage = false
   152  		return fmt.Errorf("user name is required")
   153  	}
   154  
   155  	if !ctx.StoreCtx().Store.Has(store.Accounts, p.AccountContextParams.Name, store.Users, store.JwtName(p.name)) {
   156  		return fmt.Errorf("user %q not found", p.name)
   157  	}
   158  
   159  	p.claim, err = ctx.StoreCtx().Store.ReadUserClaim(p.AccountContextParams.Name, p.name)
   160  	if err != nil {
   161  		return err
   162  	}
   163  
   164  	p.UserPermissionLimits.Load(ctx, p.claim.UserPermissionLimits)
   165  
   166  	return err
   167  }
   168  
   169  func (p *EditUserParams) PostInteractive(ctx ActionCtx) error {
   170  	// FIXME: we won't do interactive on the response params until pub/sub/deny permissions are interactive
   171  	//if err := p.PermissionsParams.Edit(p.claim.Resp != nil); err != nil {
   172  	//	return err
   173  	//}
   174  	if err := p.UserPermissionLimits.PostInteractive(ctx); err != nil {
   175  		return err
   176  	}
   177  	if p.claim.NotBefore > 0 {
   178  		p.GenericClaimsParams.Start = UnixToDate(p.claim.NotBefore)
   179  	}
   180  	if p.claim.Expires > 0 {
   181  		p.GenericClaimsParams.Expiry = UnixToDate(p.claim.Expires)
   182  	}
   183  	if err := p.GenericClaimsParams.Edit(p.claim.Tags); err != nil {
   184  		return err
   185  	}
   186  	return nil
   187  }
   188  
   189  func (p *EditUserParams) Validate(ctx ActionCtx) error {
   190  	p.UserPermissionLimits.Validate(ctx)
   191  
   192  	if err := p.GenericClaimsParams.Valid(); err != nil {
   193  		return err
   194  	}
   195  	if err := p.SignerParams.ResolveWithPriority(ctx, p.claim.Issuer); err != nil {
   196  		return err
   197  	}
   198  
   199  	if ac, err := ctx.StoreCtx().Store.ReadAccountClaim(p.AccountContextParams.Name); err != nil {
   200  		return err
   201  	} else if ac.Limits.DisallowBearer && p.bearer {
   202  		return fmt.Errorf("account disallows bearer token")
   203  	}
   204  	return nil
   205  }
   206  
   207  func (p *EditUserParams) Run(ctx ActionCtx) (store.Status, error) {
   208  	r := store.NewDetailedReport(true)
   209  	r.ReportSum = false
   210  
   211  	var err error
   212  	if err := p.GenericClaimsParams.Run(ctx, p.claim, r); err != nil {
   213  		return nil, err
   214  	}
   215  
   216  	s, err := p.UserPermissionLimits.Run(ctx, &p.claim.UserPermissionLimits)
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  	if s != nil {
   221  		r.Add(s.Details...)
   222  	}
   223  
   224  	// get the account JWT - must have since we resolved the user based on it
   225  	ac, err := ctx.StoreCtx().Store.ReadAccountClaim(p.AccountContextParams.Name)
   226  	if err != nil {
   227  		return nil, err
   228  	}
   229  
   230  	// extract the signer public key
   231  	pk, err := p.signerKP.PublicKey()
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  	// signer doesn't match - so we set IssuerAccount to the account
   236  	if pk != ac.Subject {
   237  		p.claim.IssuerAccount = ac.Subject
   238  	}
   239  
   240  	if err := checkUserForScope(ctx, p.AccountContextParams.Name, p.signerKP, p.claim); err != nil {
   241  		r.AddFromError(err)
   242  		r.AddWarning("user was NOT edited as the edits conflict with signing key scope")
   243  		return r, err
   244  	}
   245  
   246  	// we sign
   247  	p.token, err = p.claim.Encode(p.signerKP)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  
   252  	// if the signer is not allowed, the store will reject
   253  	rs, err := ctx.StoreCtx().Store.StoreClaim([]byte(p.token))
   254  	if rs != nil {
   255  		r.Add(rs)
   256  	}
   257  	if err != nil {
   258  		r.AddFromError(err)
   259  	}
   260  	if rs != nil {
   261  		r.Add(rs)
   262  	}
   263  	ks := ctx.StoreCtx().KeyStore
   264  	if ks.HasPrivateKey(p.claim.Subject) {
   265  		ukp, err := ks.GetKeyPair(p.claim.Subject)
   266  		if err != nil {
   267  			r.AddError("unable to read keypair: %v", err)
   268  		}
   269  		d, err := GenerateConfig(ctx.StoreCtx().Store, p.AccountContextParams.Name, p.name, ukp)
   270  		if err != nil {
   271  			r.AddError("unable to save creds: %v", err)
   272  		} else {
   273  			p.credsFilePath, err = ks.MaybeStoreUserCreds(p.AccountContextParams.Name, p.name, d)
   274  			if err != nil {
   275  				r.AddError("error storing creds: %v", err)
   276  			} else {
   277  				r.AddOK("generated user creds file %#q", AbbrevHomePaths(p.credsFilePath))
   278  			}
   279  		}
   280  	} else {
   281  		r.AddOK("skipped generating creds file - user private key is not available")
   282  	}
   283  	if r.HasNoErrors() {
   284  		r.AddOK("edited user %q", p.name)
   285  	}
   286  	return r, nil
   287  }
   288  
   289  const timeFormat = "hh:mm:ss"
   290  const timeLayout = "15:04:05"
   291  
   292  type timeSlice []jwt.TimeRange
   293  
   294  func (t *timeSlice) Set(val string) error {
   295  	if tk := strings.Split(val, "-"); len(tk) != 2 {
   296  		return fmt.Errorf(`require format: "%s-%s" got "%s"`, timeLayout, timeLayout, val)
   297  	} else if _, err := time.Parse(timeLayout, tk[0]); err != nil {
   298  		return fmt.Errorf(`require format: "%s-%s" could not parse start time "%s"`, timeLayout, timeLayout, tk[0])
   299  	} else if _, err := time.Parse(timeLayout, tk[1]); err != nil {
   300  		return fmt.Errorf(`require format: "%s-%s" could not parse end time "%s"`, timeLayout, timeLayout, tk[1])
   301  	} else {
   302  		*t = append(*t, jwt.TimeRange{Start: tk[0], End: tk[1]})
   303  		return nil
   304  	}
   305  }
   306  
   307  func (t *timeSlice) String() string {
   308  	values := make([]string, len(*t))
   309  	for i, r := range *t {
   310  		values[i] = fmt.Sprintf("%s-%s", r.Start, r.End)
   311  	}
   312  	return "[" + strings.Join(values, ",") + "]"
   313  }
   314  
   315  func (t *timeSlice) Type() string {
   316  	return "time-ranges"
   317  }
   318  
   319  type timeLocale string
   320  
   321  func (l *timeLocale) Set(val string) error {
   322  	v, err := time.LoadLocation(val)
   323  	if err == nil {
   324  		*l = timeLocale(v.String())
   325  	}
   326  	return err
   327  }
   328  
   329  func (l *timeLocale) String() string {
   330  	return string(*l)
   331  }
   332  
   333  func (t *timeLocale) Type() string {
   334  	return "time-locale"
   335  }
   336  
   337  type UserPermissionLimits struct {
   338  	PermissionsParams
   339  	bearer      bool
   340  	payload     NumberParams
   341  	maxData     NumberParams
   342  	maxSubs     int64
   343  	rmConnTypes []string
   344  	connTypes   []string
   345  	rmSrc       []string
   346  	src         []string
   347  	locale      timeLocale
   348  	rmTimes     []string
   349  	times       timeSlice
   350  }
   351  
   352  func (p *UserPermissionLimits) BindFlags(cmd *cobra.Command) {
   353  	cmd.Flags().VarP(&p.times, "time", "", fmt.Sprintf(`add start-end time range of the form "%s-%s" (option can be specified multiple times)`, timeFormat, timeFormat))
   354  	cmd.Flags().StringSliceVarP(&p.rmTimes, "rm-time", "", nil, fmt.Sprintf(`remove start-end time by start time "%s" (option can be specified multiple times)`, timeFormat))
   355  	cmd.Flags().VarP(&p.locale, "locale", "", "set the locale with which time values are interpreted")
   356  	cmd.Flags().StringSliceVarP(&p.src, "source-network", "", nil, "add source network for connection - comma separated list or option can be specified multiple times")
   357  	cmd.Flags().StringSliceVarP(&p.rmSrc, "rm-source-network", "", nil, "remove source network for connection - comma separated list or option can be specified multiple times")
   358  	cmd.Flags().StringSliceVarP(&p.connTypes, "conn-type", "", nil, fmt.Sprintf("set allowed connection types: %s %s %s %s %s %s - comma separated list or option can be specified multiple times",
   359  		jwt.ConnectionTypeLeafnode, jwt.ConnectionTypeMqtt, jwt.ConnectionTypeStandard, jwt.ConnectionTypeWebsocket, jwt.ConnectionTypeLeafnodeWS, jwt.ConnectionTypeMqttWS))
   360  	cmd.Flags().StringSliceVarP(&p.rmConnTypes, "rm-conn-type", "", nil, "remove connection types - comma separated list or option can be specified multiple times")
   361  	cmd.Flags().Int64VarP(&p.maxSubs, "subs", "", -1, "set maximum number of subscriptions (-1 is unlimited)")
   362  	p.maxData = -1
   363  	cmd.Flags().VarP(&p.maxData, "data", "", "set maximum data in bytes for the user (-1 is unlimited)")
   364  	p.payload = -1
   365  	cmd.Flags().VarP(&p.payload, "payload", "", "set maximum message payload in bytes for the account (-1 is unlimited)")
   366  	cmd.Flags().BoolVarP(&p.bearer, "bearer", "", false, "no connect challenge required for user")
   367  	p.PermissionsParams.bindSetFlags(cmd, "permissions")
   368  	p.PermissionsParams.bindRemoveFlags(cmd, "permissions")
   369  }
   370  
   371  func (p *UserPermissionLimits) Load(ctx ActionCtx, u jwt.UserPermissionLimits) error {
   372  	if !ctx.CurrentCmd().Flag("payload").Changed {
   373  		p.payload = NumberParams(u.Limits.Payload)
   374  	}
   375  	if !ctx.CurrentCmd().Flag("data").Changed {
   376  		p.maxData = NumberParams(u.Limits.Data)
   377  	}
   378  	if !ctx.CurrentCmd().Flag("subs").Changed {
   379  		p.maxSubs = u.Limits.Subs
   380  	}
   381  	return nil
   382  }
   383  
   384  func (p *UserPermissionLimits) PostInteractive(_ ActionCtx) error {
   385  	// FIXME: we won't do interactive on the response params until pub/sub/deny permissions are interactive
   386  	//if err := p.PermissionsParams.Edit(p.claim.Resp != nil); err != nil {
   387  	//	return err
   388  	//}
   389  	if err := p.payload.Edit("max payload (-1 unlimited)"); err != nil {
   390  		return err
   391  	}
   392  	return nil
   393  }
   394  
   395  func (p *UserPermissionLimits) Validate(ctx ActionCtx) error {
   396  	connTypes := make([]string, len(p.connTypes))
   397  	for i, k := range p.connTypes {
   398  		u := strings.ToUpper(k)
   399  		switch u {
   400  		case jwt.ConnectionTypeLeafnode, jwt.ConnectionTypeMqtt, jwt.ConnectionTypeStandard,
   401  			jwt.ConnectionTypeWebsocket, jwt.ConnectionTypeLeafnodeWS, jwt.ConnectionTypeMqttWS:
   402  		default:
   403  			return fmt.Errorf("unknown connection type %s", k)
   404  		}
   405  		connTypes[i] = u
   406  	}
   407  	rmConnTypes := make([]string, len(p.rmConnTypes))
   408  	for i, k := range p.rmConnTypes {
   409  		rmConnTypes[i] = strings.ToUpper(k)
   410  	}
   411  	p.rmConnTypes = rmConnTypes
   412  
   413  	if err := p.PermissionsParams.Validate(); err != nil {
   414  		return err
   415  	}
   416  
   417  	return nil
   418  }
   419  
   420  func (p *UserPermissionLimits) Run(ctx ActionCtx, claim *jwt.UserPermissionLimits) (*store.Report, error) {
   421  	r := store.NewDetailedReport(true)
   422  	r.ReportSum = false
   423  
   424  	var err error
   425  
   426  	flags := ctx.CurrentCmd().Flags()
   427  	claim.Limits.Payload = p.payload.Int64()
   428  	if flags.Changed("payload") {
   429  		r.AddOK("changed max imports to %d", claim.Limits.Payload)
   430  	}
   431  	claim.Limits.Data = p.maxData.Int64()
   432  	if flags.Changed("data") {
   433  		r.AddOK("changed max data to %d", claim.Limits.Data)
   434  	}
   435  	claim.Limits.Subs = p.maxSubs
   436  	if flags.Changed("subs") {
   437  		r.AddOK("changed max number of subs to %d", claim.Limits.Subs)
   438  	}
   439  
   440  	if flags.Changed("bearer") {
   441  		claim.BearerToken = p.bearer
   442  		if flags.Lookup("bearer").DefValue != fmt.Sprint(p.bearer) {
   443  			r.AddOK("changed bearer to %t", p.bearer)
   444  		} else {
   445  			r.AddOK("ignoring change to bearer - value is already %t", p.bearer)
   446  		}
   447  	}
   448  
   449  	var connTypes jwt.StringList
   450  	connTypes.Add(claim.AllowedConnectionTypes...)
   451  	connTypes.Add(p.connTypes...)
   452  	for _, v := range p.connTypes {
   453  		r.AddOK("added connection type %s", v)
   454  	}
   455  	connTypes.Remove(p.rmConnTypes...)
   456  	for _, v := range p.rmConnTypes {
   457  		r.AddOK("removed connection type %s", v)
   458  	}
   459  	claim.AllowedConnectionTypes = connTypes
   460  
   461  	var srcList jwt.CIDRList
   462  	srcList.Add(claim.Src...)
   463  	srcList.Add(p.src...)
   464  	for _, v := range p.src {
   465  		r.AddOK("added src network %s", v)
   466  	}
   467  	srcList.Remove(p.rmSrc...)
   468  	for _, v := range p.rmSrc {
   469  		r.AddOK("removed src network %s", v)
   470  	}
   471  	sort.Strings(srcList)
   472  	claim.Src = srcList
   473  
   474  	if flags.Changed("locale") {
   475  		claim.Locale = p.locale.String()
   476  	}
   477  	for _, v := range p.times {
   478  		r.AddOK("added time range %s-%s", v.Start, v.End)
   479  		claim.Times = append(claim.Times, v)
   480  	}
   481  	for _, vDel := range p.rmTimes {
   482  		for i, v := range claim.Times {
   483  			if v.Start == vDel {
   484  				r.AddOK("removed time range %s-%s", v.Start, v.End)
   485  				a := claim.Times
   486  				// Remove the element at index i from a.
   487  				copy(a[i:], a[i+1:])          // Shift a[i+1:] left one index.
   488  				a[len(a)-1] = jwt.TimeRange{} // Erase last element (write zero value).
   489  				claim.Times = a[:len(a)-1]    // Truncate slice.
   490  				break
   491  			}
   492  		}
   493  	}
   494  
   495  	s, err := p.PermissionsParams.Run(&claim.Permissions, ctx)
   496  	if err != nil {
   497  		return nil, err
   498  	}
   499  	if s != nil {
   500  		r.Add(s.Details...)
   501  	}
   502  
   503  	return r, nil
   504  }