github.com/nats-io/nsc@v0.0.0-20221206222106-35db9400b257/cmd/adduser.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  	"sort"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	cli "github.com/nats-io/cliprompts/v2"
    27  	"github.com/nats-io/jwt/v2"
    28  	"github.com/nats-io/nkeys"
    29  	"github.com/nats-io/nsc/cmd/store"
    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 claim, err := s.ReadAccountClaim(p.AccountContextParams.Name); err != nil {
   250  		return fmt.Errorf("reading account %q failed: %v", p.AccountContextParams.Name, err)
   251  	} else if claim.Limits.DisallowBearer && p.bearer {
   252  		return fmt.Errorf("account %q forbids the use of bearer token", p.AccountContextParams.Name)
   253  	} else if s.Has(store.Accounts, p.AccountContextParams.Name, store.Users, store.JwtName(p.userName)) {
   254  		return fmt.Errorf("the user %q already exists", p.userName)
   255  	}
   256  
   257  	return nil
   258  }
   259  
   260  func signerKeyIsScoped(ctx ActionCtx, accountName string, signerKP nkeys.KeyPair) bool {
   261  	// get the account JWT - must have since we resolved the user based on it
   262  	ac, err := ctx.StoreCtx().Store.ReadAccountClaim(accountName)
   263  	if err != nil {
   264  		return false
   265  	}
   266  	// extract the signer public key
   267  	pk, err := signerKP.PublicKey()
   268  	if err != nil {
   269  		return false
   270  	}
   271  	if s, ok := ac.SigningKeys.GetScope(pk); ok && s != nil {
   272  		return true
   273  	}
   274  	return false
   275  }
   276  
   277  func checkUserForScope(ctx ActionCtx, accountName string, signerKP nkeys.KeyPair, uc *jwt.UserClaims) error {
   278  	if ctx == nil || accountName == "" || signerKP == nil || uc == nil {
   279  		return errors.New("invalid arguments")
   280  	}
   281  	// get the account JWT - must have since we resolved the user based on it
   282  	ac, err := ctx.StoreCtx().Store.ReadAccountClaim(accountName)
   283  	if err != nil {
   284  		return err
   285  	}
   286  	// extract the signer public key
   287  	pk, err := signerKP.PublicKey()
   288  	if err != nil {
   289  		return err
   290  	}
   291  	if s, ok := ac.SigningKeys.GetScope(pk); ok && s != nil {
   292  		// set issuer as this is commonly set during encoding but is required next
   293  		uc.Issuer = pk
   294  		if err := s.ValidateScopedSigner(uc); err != nil {
   295  			return err
   296  		}
   297  	}
   298  	return nil
   299  }
   300  
   301  func (p *AddUserParams) Run(ctx ActionCtx) (store.Status, error) {
   302  	r := store.NewDetailedReport(false)
   303  	uc, err := p.generateUserClaim(ctx, r, signerKeyIsScoped(ctx, p.AccountContextParams.Name, p.signerKP))
   304  	if err != nil {
   305  		return nil, err
   306  	}
   307  
   308  	if err := checkUserForScope(ctx, p.AccountContextParams.Name, p.signerKP, uc); err != nil {
   309  		r.AddFromError(err)
   310  		r.AddWarning("user was NOT edited as the edits conflict with signing key scope")
   311  		return r, err
   312  	}
   313  
   314  	token, err := uc.Encode(p.signerKP)
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  
   319  	st, err := ctx.StoreCtx().Store.StoreClaim([]byte(token))
   320  	if st != nil {
   321  		r.Add(st)
   322  	}
   323  	if err != nil {
   324  		r.AddFromError(err)
   325  		return r, err
   326  	}
   327  
   328  	// store the key
   329  	if p.pkOrPath == "" {
   330  		ks := ctx.StoreCtx()
   331  		var err error
   332  		if p.pkOrPath, err = ks.KeyStore.Store(p.kp); err != nil {
   333  			r.AddFromError(err)
   334  			return r, err
   335  		}
   336  		r.AddOK("generated and stored user key %q", uc.Subject)
   337  	}
   338  
   339  	pk := uc.Subject
   340  	// if they gave us a seed, it stored - try to get it
   341  	ks := ctx.StoreCtx().KeyStore
   342  	if ks.HasPrivateKey(pk) {
   343  		// we may have it - but the key we got is possibly a pub only - resolve it from the store.
   344  		p.kp, _ = ks.GetKeyPair(pk)
   345  		d, err := GenerateConfig(ctx.StoreCtx().Store, p.AccountContextParams.Name, p.userName, p.kp)
   346  		if err != nil {
   347  			r.AddError("unable to save creds: %v", err)
   348  		} else {
   349  			p.credsFilePath, err = ks.MaybeStoreUserCreds(p.AccountContextParams.Name, p.userName, d)
   350  			if err != nil {
   351  				r.AddError("error storing creds: %v", err)
   352  			} else {
   353  				r.AddOK("generated user creds file %#q", AbbrevHomePaths(p.credsFilePath))
   354  			}
   355  		}
   356  	} else {
   357  		r.AddOK("skipped generating creds file - user private key is not available")
   358  	}
   359  	if r.HasNoErrors() {
   360  		r.AddOK("added user %q to account %q", p.userName, p.AccountContextParams.Name)
   361  	}
   362  	return r, nil
   363  }
   364  
   365  func (p *AddUserParams) generateUserClaim(ctx ActionCtx, r *store.Report, scoped bool) (*jwt.UserClaims, error) {
   366  	pub, err := p.kp.PublicKey()
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  	uc := jwt.NewUserClaims(pub)
   371  	uc.Name = p.userName
   372  	uc.SetScoped(scoped)
   373  
   374  	spk, err := p.signerKP.PublicKey()
   375  	if err != nil {
   376  		return nil, err
   377  	}
   378  	if ctx.StoreCtx().Account.PublicKey != spk {
   379  		uc.IssuerAccount = ctx.StoreCtx().Account.PublicKey
   380  	}
   381  
   382  	if p.TimeParams.IsStartChanged() {
   383  		uc.NotBefore, _ = p.TimeParams.StartDate()
   384  	}
   385  
   386  	if p.TimeParams.IsExpiryChanged() {
   387  		uc.Expires, _ = p.TimeParams.ExpiryDate()
   388  	}
   389  
   390  	if s, err := p.PermissionsParams.Run(&uc.Permissions, ctx); err != nil {
   391  		return nil, err
   392  	} else if s != nil {
   393  		r.Add(s.Details...)
   394  	}
   395  
   396  	uc.Src.Add(p.src...)
   397  
   398  	uc.Tags.Add(p.tags...)
   399  	sort.Strings(uc.Tags)
   400  
   401  	uc.BearerToken = p.bearer
   402  	return uc, nil
   403  }
   404  
   405  type PermissionsParams struct {
   406  	respTTL     string
   407  	respMax     int
   408  	rmResp      bool
   409  	allowPubs   []string
   410  	allowPubsub []string
   411  	allowSubs   []string
   412  	denyPubs    []string
   413  	denyPubsub  []string
   414  	denySubs    []string
   415  	rmPerms     []string
   416  }
   417  
   418  func (p *PermissionsParams) bindSetFlags(cmd *cobra.Command, typeName string) {
   419  	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))
   420  
   421  	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))
   422  	cmd.Flag("allow-pub-response").NoOptDefVal = "1"
   423  
   424  	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))
   425  	cmd.Flag("max-responses").Hidden = true
   426  	cmd.Flag("max-responses").Deprecated = "use --allow-pub-response or --allow-pub-response=n"
   427  
   428  	cmd.Flags().StringSliceVarP(&p.allowPubs, "allow-pub", "", nil, fmt.Sprintf("add publish %s - comma separated list or option can be specified multiple times", typeName))
   429  	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))
   430  	cmd.Flags().StringSliceVarP(&p.allowSubs, "allow-sub", "", nil, fmt.Sprintf("add subscribe %s - comma separated list or option can be specified multiple times", typeName))
   431  	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))
   432  	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))
   433  	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))
   434  }
   435  
   436  func (p *PermissionsParams) bindRemoveFlags(cmd *cobra.Command, typeName string) {
   437  	cmd.Flags().BoolVarP(&p.rmResp, "rm-response-perms", "", false, fmt.Sprintf("remove response settings from %s", typeName))
   438  	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))
   439  }
   440  
   441  func (p *PermissionsParams) maxResponseValidator(s string) error {
   442  	_, err := p.parseMaxResponse(s)
   443  	return err
   444  }
   445  
   446  func (p *PermissionsParams) parseMaxResponse(s string) (int, error) {
   447  	if s == "" {
   448  		return 0, nil
   449  	}
   450  	return strconv.Atoi(s)
   451  }
   452  
   453  func (p *PermissionsParams) ttlValidator(s string) error {
   454  	_, err := p.parseTTL(s)
   455  	return err
   456  }
   457  
   458  func (p *PermissionsParams) parseTTL(s string) (time.Duration, error) {
   459  	if s == "" {
   460  		return time.Duration(0), nil
   461  	}
   462  	return time.ParseDuration(s)
   463  }
   464  
   465  func (p *PermissionsParams) Edit(hasPerm bool) error {
   466  	verb := "Set"
   467  	if hasPerm {
   468  		verb = "Edit"
   469  	}
   470  	ok, err := cli.Confirm(fmt.Sprintf("%s response permissions?", verb), false)
   471  	if err != nil {
   472  		return err
   473  	}
   474  	if ok {
   475  		if hasPerm {
   476  			p.rmResp, err = cli.Confirm("delete response permission", p.rmResp)
   477  			if err != nil {
   478  				return err
   479  			}
   480  		}
   481  		if !p.rmResp {
   482  			s, err := cli.Prompt("Max number of responses", fmt.Sprintf("%d", p.respMax), cli.Val(p.maxResponseValidator))
   483  			if err != nil {
   484  				return err
   485  			}
   486  			p.respMax, _ = p.parseMaxResponse(s)
   487  			p.respTTL, err = cli.Prompt("Response TTL", p.respTTL, cli.Val(p.ttlValidator))
   488  			if err != nil {
   489  				return err
   490  			}
   491  		}
   492  	}
   493  	return nil
   494  }
   495  
   496  func (p *PermissionsParams) Validate() error {
   497  	if err := p.ttlValidator(p.respTTL); err != nil {
   498  		return err
   499  	}
   500  	for _, v := range [][]string{p.allowPubs, p.allowPubsub, p.denyPubs, p.denyPubsub} {
   501  		for _, sub := range v {
   502  			if strings.Contains(sub, " ") {
   503  				return fmt.Errorf("publish permission subject %q contains illegal space", sub)
   504  			}
   505  		}
   506  	}
   507  	for _, v := range [][]string{p.allowSubs, p.denySubs} {
   508  		for _, sub := range v {
   509  			if strings.Count(sub, " ") > 1 {
   510  				return fmt.Errorf("subscribe permission subject %q can at most contain one space", sub)
   511  			}
   512  		}
   513  	}
   514  
   515  	return nil
   516  }
   517  
   518  func (p *PermissionsParams) Run(perms *jwt.Permissions, ctx ActionCtx) (*store.Report, error) {
   519  	r := store.NewDetailedReport(true)
   520  	if p.rmResp {
   521  		perms.Resp = nil
   522  		r.AddOK("removed response permissions")
   523  		return r, nil
   524  	}
   525  
   526  	if ctx.CurrentCmd().Flag("max-responses").Changed || p.respMax != 0 {
   527  		if perms.Resp == nil {
   528  			perms.Resp = &jwt.ResponsePermission{}
   529  		}
   530  		perms.Resp.MaxMsgs = p.respMax
   531  		r.AddOK("set max responses to %d", p.respMax)
   532  	}
   533  
   534  	if p.respTTL != "" {
   535  		v, err := p.parseTTL(p.respTTL)
   536  		if err != nil {
   537  			return nil, err
   538  		}
   539  		if perms.Resp == nil {
   540  			perms.Resp = &jwt.ResponsePermission{}
   541  		}
   542  		perms.Resp.Expires = v
   543  		r.AddOK("set response ttl to %v", v)
   544  	}
   545  
   546  	var ap []string
   547  	perms.Pub.Allow.Add(p.allowPubs...)
   548  	ap = append(ap, p.allowPubs...)
   549  	perms.Pub.Allow.Add(p.allowPubsub...)
   550  	ap = append(ap, p.allowPubsub...)
   551  	for _, v := range ap {
   552  		r.AddOK("added pub %q", v)
   553  	}
   554  	perms.Pub.Allow.Remove(p.rmPerms...)
   555  	for _, v := range p.rmPerms {
   556  		r.AddOK("removed pub %q", v)
   557  	}
   558  	sort.Strings(perms.Pub.Allow)
   559  
   560  	var dp []string
   561  	perms.Pub.Deny.Add(p.denyPubs...)
   562  	dp = append(dp, p.denyPubs...)
   563  	perms.Pub.Deny.Add(p.denyPubsub...)
   564  	dp = append(dp, p.denyPubsub...)
   565  	for _, v := range dp {
   566  		r.AddOK("added deny pub %q", v)
   567  	}
   568  	perms.Pub.Deny.Remove(p.rmPerms...)
   569  	for _, v := range p.rmPerms {
   570  		r.AddOK("removed deny pub %q", v)
   571  	}
   572  	sort.Strings(perms.Pub.Deny)
   573  
   574  	var sa []string
   575  	perms.Sub.Allow.Add(p.allowSubs...)
   576  	sa = append(sa, p.allowSubs...)
   577  	perms.Sub.Allow.Add(p.allowPubsub...)
   578  	sa = append(sa, p.allowPubsub...)
   579  	for _, v := range sa {
   580  		r.AddOK("added sub %q", v)
   581  	}
   582  	perms.Sub.Allow.Remove(p.rmPerms...)
   583  	for _, v := range p.rmPerms {
   584  		r.AddOK("removed sub %q", v)
   585  	}
   586  	sort.Strings(perms.Sub.Allow)
   587  
   588  	var ds []string
   589  	perms.Sub.Deny.Add(p.denySubs...)
   590  	ds = append(ds, p.denySubs...)
   591  	perms.Sub.Deny.Add(p.denyPubsub...)
   592  	ds = append(ds, p.denyPubsub...)
   593  	for _, v := range ds {
   594  		r.AddOK("added deny sub %q", v)
   595  	}
   596  
   597  	perms.Sub.Deny.Remove(p.rmPerms...)
   598  	for _, v := range p.rmPerms {
   599  		r.AddOK("removed sub %q", v)
   600  	}
   601  	sort.Strings(perms.Sub.Deny)
   602  	return r, nil
   603  }