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

     1  /*
     2   * Copyright 2018-2019 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  	"strings"
    24  	"time"
    25  
    26  	"github.com/nats-io/nkeys"
    27  
    28  	"github.com/kbehouse/nsc/cmd/store"
    29  	cli "github.com/nats-io/cliprompts/v2"
    30  	"github.com/nats-io/jwt/v2"
    31  	"github.com/nats-io/nats.go"
    32  	"github.com/spf13/cobra"
    33  )
    34  
    35  func CreatePushCmd() *cobra.Command {
    36  	var params PushCmdParams
    37  	var cmd = &cobra.Command{
    38  		Short:   "Push an account jwt to an Account JWT Server",
    39  		Example: "push",
    40  		Use: `push (currentAccount)
    41  push -a <accountName>
    42  push -A (all accounts)
    43  push -P
    44  push -P -A (all accounts)`,
    45  		Args: MaxArgs(0),
    46  		RunE: func(cmd *cobra.Command, args []string) error {
    47  			return RunAction(cmd, args, &params)
    48  		},
    49  	}
    50  	cmd.Flags().BoolVarP(&params.allAccounts, "all", "A", false, "push all accounts under the current operator (exclusive of -a)")
    51  	cmd.Flags().BoolVarP(&params.force, "force", "F", false, "push regardless of validation issues")
    52  	cmd.Flags().StringVarP(&params.ASU, "account-jwt-server-url", "u", "", "set account jwt server url for nsc sync (only http/https/nats urls supported if updating with nsc) If a nats url is provided ")
    53  
    54  	cmd.Flags().BoolVarP(&params.diff, "diff", "D", false, "diff accounts present in nsc env and nats-account-resolver. Mutually exclusive of account-removal/prune.")
    55  	cmd.Flags().BoolVarP(&params.prune, "prune", "P", false, "prune all accounts not under the current operator. Only works with nats-resolver enabled nats-server. Mutually exclusive of account-removal/diff.")
    56  	cmd.Flags().StringVarP(&params.removeAcc, "account-removal", "R", "", "remove specific account. Only works with nats-resolver enabled nats-server. Mutually exclusive of prune/diff.")
    57  	cmd.Flags().StringVarP(&params.sysAcc, "system-account", "", "", "System account for use with nats-resolver enabled nats-server. (Default is system account specified by operator)")
    58  	cmd.Flags().StringVarP(&params.sysAccUser, "system-user", "", "", "System account user for use with nats-resolver enabled nats-server. (Default to temporarily generated user)")
    59  	params.AccountContextParams.BindFlags(cmd)
    60  	return cmd
    61  }
    62  
    63  func init() {
    64  	GetRootCmd().AddCommand(CreatePushCmd())
    65  }
    66  
    67  type PushCmdParams struct {
    68  	AccountContextParams
    69  	ASU         string
    70  	sysAccUser  string // when present use
    71  	sysAcc      string
    72  	allAccounts bool
    73  	force       bool
    74  	prune       bool
    75  	diff        bool
    76  	removeAcc   string
    77  	targeted    []string
    78  
    79  	accountList []string
    80  }
    81  
    82  func processResponse(report *store.Report, resp *nats.Msg) (bool, string, interface{}) {
    83  	// ServerInfo copied from nats-server, refresh as needed. Error and Data are mutually exclusive
    84  	serverResp := struct {
    85  		Server *struct {
    86  			Name      string    `json:"name"`
    87  			Host      string    `json:"host"`
    88  			ID        string    `json:"id"`
    89  			Cluster   string    `json:"cluster,omitempty"`
    90  			Version   string    `json:"ver"`
    91  			Seq       uint64    `json:"seq"`
    92  			JetStream bool      `json:"jetstream"`
    93  			Time      time.Time `json:"time"`
    94  		} `json:"server"`
    95  		Error *struct {
    96  			Description string `json:"description"`
    97  			Code        int    `json:"code"`
    98  		} `json:"error"`
    99  		Data interface{} `json:"data"`
   100  	}{}
   101  	if err := json.Unmarshal(resp.Data, &serverResp); err != nil {
   102  		report.AddError("failed to parse response: %v data: %s", err, string(resp.Data))
   103  	} else if srvName := serverResp.Server.Name; srvName == "" {
   104  		report.AddError("server responded without server name in info: %s", string(resp.Data))
   105  	} else if err := serverResp.Error; err != nil {
   106  		report.AddError("server %s responded with error: %s", srvName, err.Description)
   107  	} else if data := serverResp.Data; data == nil {
   108  		report.AddError("server %s responded without data: %s", srvName, string(resp.Data))
   109  	} else {
   110  		return true, srvName, data
   111  	}
   112  	return false, "", nil
   113  }
   114  
   115  // when sysAccName or sysAccUserName are "" we will try to find a suitable user
   116  func getSystemAccountUser(ctx ActionCtx, sysAccName, sysAccUserName, allowSub string, allowPubs ...string) (string, nats.Option, error) {
   117  	op, err := ctx.StoreCtx().Store.ReadOperatorClaim()
   118  	if err != nil {
   119  		return "", nil, err
   120  	} else if accNames, err := friendlyNames(ctx.StoreCtx().Operator.Name); err != nil {
   121  		return "", nil, err
   122  	} else if sysAccName == "" {
   123  		if sysAccName = accNames[op.SystemAccount]; sysAccName == "" {
   124  			return "", nil, fmt.Errorf(`system account "%s" not found`, op.SystemAccount)
   125  		}
   126  	}
   127  	getOpt := func(theJWT string, kp nkeys.KeyPair) nats.Option {
   128  		return nats.UserJWT(
   129  			func() (string, error) {
   130  				return theJWT, nil
   131  			}, func(nonce []byte) ([]byte, error) {
   132  				return kp.Sign(nonce)
   133  			})
   134  	}
   135  	// Attempt to generate temporary user credentials and
   136  	if sysAccUserName == "" {
   137  		if keys, err := ctx.StoreCtx().GetAccountKeys(sysAccName); err == nil && len(keys) > 0 {
   138  			key := ""
   139  			if op.StrictSigningKeyUsage {
   140  				if len(keys) > 1 {
   141  					key = keys[1]
   142  				} else {
   143  					key = ""
   144  				}
   145  			} else {
   146  				key = keys[0]
   147  			}
   148  			sysAccKp, err := ctx.StoreCtx().KeyStore.GetKeyPair(key)
   149  			if sysAccKp != nil && err == nil {
   150  				defer sysAccKp.Wipe()
   151  				tmpUsrKp, err := nkeys.CreateUser()
   152  				if err == nil {
   153  					tmpUsrPub, err := tmpUsrKp.PublicKey()
   154  					if err == nil {
   155  						tmpUsrClaim := jwt.NewUserClaims(tmpUsrPub)
   156  						tmpUsrClaim.IssuerAccount = op.SystemAccount
   157  						tmpUsrClaim.Expires = time.Now().Add(2 * time.Minute).Unix()
   158  						tmpUsrClaim.Name = "nsc temporary push user"
   159  						tmpUsrClaim.Pub.Allow.Add(allowPubs...)
   160  						tmpUsrClaim.Sub.Allow.Add(allowSub)
   161  						if theJWT, err := tmpUsrClaim.Encode(sysAccKp); err == nil {
   162  							return sysAccName, getOpt(theJWT, tmpUsrKp), nil
   163  						}
   164  					}
   165  				}
   166  			}
   167  		}
   168  		// in case of not finding a key, default to searching for an existing user and key
   169  	}
   170  	users := []string{sysAccUserName}
   171  	if sysAccUserName == "" {
   172  		var err error
   173  		if users, err = ctx.StoreCtx().Store.ListEntries(store.Accounts, sysAccName, store.Users); err != nil {
   174  			return "", nil, err
   175  		} else if len(users) == 0 {
   176  			return "", nil, err
   177  		}
   178  	}
   179  	for _, sysUser := range users {
   180  		claim, err := ctx.StoreCtx().Store.ReadUserClaim(sysAccName, sysUser)
   181  		if err != nil {
   182  			continue
   183  		}
   184  		kp, _ := ctx.StoreCtx().KeyStore.GetKeyPair(claim.Subject)
   185  		if kp == nil {
   186  			kp, _ = ctx.StoreCtx().KeyStore.GetKeyPair(claim.IssuerAccount)
   187  			if kp == nil {
   188  				continue
   189  			}
   190  		}
   191  		if theJWT, err := ctx.StoreCtx().Store.ReadRawUserClaim(sysAccName, sysUser); err != nil {
   192  			continue
   193  		} else {
   194  			return sysAccName, getOpt(string(theJWT), kp), nil
   195  		}
   196  	}
   197  	return "", nil, fmt.Errorf(`no system account user with corresponding nkey found`)
   198  }
   199  
   200  func (p *PushCmdParams) SetDefaults(ctx ActionCtx) error {
   201  	if p.allAccounts && p.Name != "" {
   202  		return errors.New("specify only one of --account or --all-accounts")
   203  	}
   204  	if err := p.AccountContextParams.SetDefaults(ctx); err != nil {
   205  		return err
   206  	}
   207  	if p.ASU == "" {
   208  		if op, err := ctx.StoreCtx().Store.ReadOperatorClaim(); err != nil {
   209  			return err
   210  		} else {
   211  			p.ASU = op.AccountServerURL
   212  		}
   213  	}
   214  	c := GetConfig()
   215  	var err error
   216  	if p.accountList, err = c.ListAccounts(); err != nil {
   217  		return err
   218  	}
   219  	if len(p.accountList) == 0 {
   220  		return fmt.Errorf("operator %q has no accounts", c.Operator)
   221  	}
   222  	if !p.allAccounts && !(p.prune || p.removeAcc != "" || p.diff) {
   223  		found := false
   224  		for _, v := range p.accountList {
   225  			if v == p.Name {
   226  				found = true
   227  				break
   228  			}
   229  		}
   230  		if !found {
   231  			return fmt.Errorf("account %q is not under operator %q - nsc env to check your env", p.Name, c.Operator)
   232  		}
   233  	}
   234  	return nil
   235  }
   236  
   237  func (p *PushCmdParams) validURL(s string) error {
   238  	s = strings.TrimSpace(s)
   239  	if s == "" {
   240  		return errors.New("url cannot be empty")
   241  	}
   242  
   243  	u, err := url.Parse(s)
   244  	if err != nil {
   245  		return err
   246  	}
   247  	scheme := strings.ToLower(u.Scheme)
   248  	supported := []string{"http", "https", "nats"}
   249  
   250  	ok := false
   251  	for _, v := range supported {
   252  		if scheme == v {
   253  			ok = true
   254  			break
   255  		}
   256  	}
   257  	if !ok {
   258  		return fmt.Errorf("scheme %q is not supported (%v)", scheme, strings.Join(supported, ", "))
   259  	}
   260  	return nil
   261  }
   262  
   263  func (p *PushCmdParams) PreInteractive(ctx ActionCtx) error {
   264  	var err error
   265  	if !p.allAccounts && !p.prune {
   266  		if err = p.AccountContextParams.Edit(ctx); err != nil {
   267  			return err
   268  		}
   269  	}
   270  	if p.ASU, err = cli.Prompt("Account Server URL or nats-resolver enabled nats-server URL", p.ASU, cli.Val(p.validURL)); err != nil {
   271  		return err
   272  	}
   273  	if IsNatsUrl(p.ASU) {
   274  		if p.sysAcc == "" {
   275  			if p.sysAcc, err = ctx.StoreCtx().PickAccount(p.sysAcc); err != nil {
   276  				return err
   277  			}
   278  		}
   279  		if p.sysAccUser == "" {
   280  			p.sysAccUser, err = ctx.StoreCtx().PickUser(p.sysAcc)
   281  		}
   282  	}
   283  	return err
   284  }
   285  
   286  func (p *PushCmdParams) Load(ctx ActionCtx) error {
   287  	if !p.allAccounts && !(p.prune || p.removeAcc != "" || p.diff) {
   288  		if err := p.AccountContextParams.Validate(ctx); err != nil {
   289  			return err
   290  		}
   291  	}
   292  	return nil
   293  }
   294  
   295  func (p *PushCmdParams) PostInteractive(_ ActionCtx) error {
   296  	return nil
   297  }
   298  
   299  func (p *PushCmdParams) Validate(ctx ActionCtx) error {
   300  	if p.ASU == "" {
   301  		return errors.New("no account server url or nats-server url was provided by the operator jwt")
   302  	}
   303  	if !IsNatsUrl(p.ASU) && p.prune {
   304  		return errors.New("prune only works for nats based account resolver")
   305  	}
   306  
   307  	if err := p.validURL(p.ASU); err != nil {
   308  		return err
   309  	}
   310  
   311  	if !p.force {
   312  		oc, err := ctx.StoreCtx().Store.ReadOperatorClaim()
   313  		if err != nil {
   314  			return err
   315  		}
   316  
   317  		// validate the jwts don't have issues
   318  		accounts, err := p.getSelectedAccounts()
   319  		if err != nil {
   320  			return err
   321  		}
   322  
   323  		for _, v := range accounts {
   324  			raw, err := ctx.StoreCtx().Store.Read(store.Accounts, v, store.JwtName(v))
   325  			if err != nil {
   326  				return err
   327  			}
   328  
   329  			ac, err := jwt.DecodeAccountClaims(string(raw))
   330  			if err != nil {
   331  				return fmt.Errorf("unable to push account %q: %v", v, err)
   332  			}
   333  			var vr jwt.ValidationResults
   334  			ac.Validate(&vr)
   335  			for _, e := range vr.Issues {
   336  				if e.Blocking || e.TimeCheck {
   337  					return fmt.Errorf("unable to push account %q as it has validation issues: %v", v, e.Description)
   338  				}
   339  			}
   340  			if !ctx.StoreCtx().Store.IsManaged() && !oc.DidSign(ac) {
   341  				return fmt.Errorf("unable to push account %q as it is not signed by the operator %q", v, ctx.StoreCtx().Operator.Name)
   342  			}
   343  		}
   344  	}
   345  	if p.removeAcc != "" {
   346  		if p.prune || p.diff {
   347  			return errors.New("--prune/--diff and --account-removal <account> are mutually exclusive")
   348  		}
   349  		if !nkeys.IsValidPublicAccountKey(p.removeAcc) {
   350  			if acc, err := ctx.StoreCtx().Store.ReadAccountClaim(p.removeAcc); err != nil {
   351  				return err
   352  			} else {
   353  				p.removeAcc = acc.Subject
   354  			}
   355  		}
   356  	} else if p.prune && p.diff {
   357  		return errors.New("--prune and --diff are mutually exclusive")
   358  	}
   359  
   360  	return nil
   361  }
   362  
   363  func (p *PushCmdParams) getSelectedAccounts() ([]string, error) {
   364  	if p.allAccounts {
   365  		a, err := GetConfig().ListAccounts()
   366  		if err != nil {
   367  			return nil, err
   368  		}
   369  		return a, nil
   370  	} else if !(p.prune || p.removeAcc != "" || p.diff) {
   371  		return []string{p.AccountContextParams.Name}, nil
   372  	}
   373  	return []string{}, nil
   374  }
   375  
   376  func multiRequest(nc *nats.Conn, report *store.Report, operation string, subject string, reqData []byte, respHandler func(srv string, data interface{})) int {
   377  	ib := nats.NewInbox()
   378  	sub, err := nc.SubscribeSync(ib)
   379  	if err != nil {
   380  		report.AddError("failed to subscribe to response subject: %v", err)
   381  		return 0
   382  	}
   383  	if err := nc.PublishRequest(subject, ib, reqData); err != nil {
   384  		report.AddError("failed to %s: %v", operation, err)
   385  		return 0
   386  	}
   387  	responses := 0
   388  	now := time.Now()
   389  	start := now
   390  	end := start.Add(time.Second)
   391  	for ; end.After(now); now = time.Now() { // try with decreasing timeout until we dont get responses
   392  		if resp, err := sub.NextMsg(end.Sub(now)); err != nil {
   393  			if err != nats.ErrTimeout || responses == 0 {
   394  				report.AddError("failed to get response to %s: %v", operation, err)
   395  			}
   396  		} else if ok, srv, data := processResponse(report, resp); ok {
   397  			respHandler(srv, data)
   398  			responses++
   399  			continue
   400  		}
   401  		break
   402  	}
   403  	return responses
   404  }
   405  
   406  func obtainRequestKey(ctx ActionCtx, subPrune *store.Report) (nkeys.KeyPair, string, error) {
   407  	opc, err := ctx.StoreCtx().Store.ReadOperatorClaim()
   408  	if err != nil {
   409  		subPrune.AddError("Operator needed to prune (err:%v)", err)
   410  		return nil, "", err
   411  	}
   412  	keys, err := ctx.StoreCtx().GetOperatorKeys()
   413  	if err != nil {
   414  		subPrune.AddError("Operator keys needed to prune (err:%v)", err)
   415  		return nil, "", err
   416  	}
   417  	if opc.StrictSigningKeyUsage {
   418  		if len(keys) > 1 {
   419  			keys = keys[1:]
   420  		} else {
   421  			keys = []string{}
   422  		}
   423  	}
   424  	var okp nkeys.KeyPair
   425  	for _, k := range keys {
   426  		var err error
   427  		if okp, err = ctx.StoreCtx().KeyStore.GetKeyPair(k); err == nil {
   428  			break
   429  		}
   430  	}
   431  	if okp == nil {
   432  		subPrune.AddError("Operator private key needed to prune (err:%v)", err)
   433  		return nil, "", err
   434  	}
   435  	opPk, err := okp.PublicKey()
   436  	if err != nil {
   437  		subPrune.AddError("Public key needed to prune (err:%v)", err)
   438  		return nil, "", err
   439  	}
   440  	return okp, opPk, nil
   441  }
   442  
   443  func sendDeleteRequest(ctx ActionCtx, nc *nats.Conn, deleteList []string, respList int, subPrune *store.Report) {
   444  	if len(deleteList) == 0 {
   445  		subPrune.AddOK("nothing to prune")
   446  		return
   447  	}
   448  	okp, opPk, err := obtainRequestKey(ctx, subPrune)
   449  	if err != nil {
   450  		subPrune.AddError("Could not obtain Operator key to sign the delete request (err:%v)", err)
   451  		return
   452  	}
   453  	defer okp.Wipe()
   454  
   455  	claim := jwt.NewGenericClaims(opPk)
   456  	claim.Data["accounts"] = deleteList
   457  	pruneJwt, err := claim.Encode(okp)
   458  	if err != nil {
   459  		subPrune.AddError("Could not encode delete request (err:%v)", err)
   460  	}
   461  	respPrune := multiRequest(nc, subPrune, "prune", "$SYS.REQ.CLAIMS.DELETE", []byte(pruneJwt),
   462  		func(srv string, data interface{}) {
   463  			if data, ok := data.(map[string]interface{}); ok {
   464  				subPrune.AddOK("pruned nats-server %s: %s", srv, data["message"])
   465  			} else {
   466  				subPrune.AddOK("pruned nats-server %s: %v", srv, data)
   467  			}
   468  		})
   469  	if respList > 0 {
   470  		if respPrune < respList {
   471  			subPrune.AddError("Fewer server responded to prune (%d) than to earlier list (%d)."+
   472  				" Accounts may not be completely pruned.", respPrune, respList)
   473  		} else if respPrune > respList {
   474  			subPrune.AddError("More server responded to prune (%d) than to earlier list (%d)."+
   475  				" Not every Account may have been included for pruning.", respPrune, respList)
   476  		}
   477  	}
   478  }
   479  
   480  func createMapping(ctx ActionCtx, rep *store.Report, accountList []string) (map[string]string, error) {
   481  	mapping := make(map[string]string)
   482  	for _, name := range accountList {
   483  		if claim, err := ctx.StoreCtx().Store.ReadAccountClaim(name); err != nil {
   484  			if err.(*store.ResourceErr).Err != store.ErrNotExist {
   485  				if nkeys.IsValidPublicAccountKey(name) {
   486  					mapping[name] = name
   487  					continue
   488  				}
   489  			}
   490  			rep.AddError("prune failed to create mapping for %s: %v", name, err)
   491  			return nil, err // this is a hard error, if we cant create a mapping because of it we'd end up deleting
   492  		} else {
   493  			mapping[claim.Subject] = name
   494  		}
   495  	}
   496  	return mapping, nil
   497  }
   498  
   499  func listNonPresentAccounts(nc *nats.Conn, subPrune *store.Report, mapping map[string]string) (int, []string) {
   500  	deleteList := make([]string, 0, 1024)
   501  	responseCount := multiRequest(nc, subPrune, "list accounts", "$SYS.REQ.CLAIMS.LIST", nil,
   502  		func(srv string, d interface{}) {
   503  			data := d.([]interface{})
   504  			subAccPrune := store.NewReport(store.OK, "list %d accounts from nats-server %s", len(data), srv)
   505  			subPrune.Add(subAccPrune)
   506  			for _, acc := range data {
   507  				acc := acc.(string)
   508  				if name, ok := mapping[acc]; ok {
   509  					subAccPrune.AddOK("account %s named %s exists", acc, name)
   510  				} else {
   511  					subAccPrune.AddOK("account %s only exists in server", acc)
   512  					deleteList = append(deleteList, acc)
   513  				}
   514  			}
   515  		})
   516  	subPrune.AddOK("listed accounts from a total of %d nats-server", responseCount)
   517  	return responseCount, deleteList
   518  }
   519  
   520  func (p *PushCmdParams) Run(ctx ActionCtx) (store.Status, error) {
   521  	ctx.CurrentCmd().SilenceUsage = true
   522  	var err error
   523  	p.targeted, err = p.getSelectedAccounts()
   524  	if err != nil {
   525  		return nil, err
   526  	}
   527  	r := store.NewDetailedReport(true)
   528  	if !IsNatsUrl(p.ASU) {
   529  		for _, v := range p.targeted {
   530  			sub := store.NewReport(store.OK, "push %s to account server", v)
   531  			sub.Opt = store.DetailsOnErrorOrWarning
   532  			r.Add(sub)
   533  			ps, err := p.pushAccount(v, ctx)
   534  			if ps != nil {
   535  				sub.Add(store.HoistChildren(ps)...)
   536  			}
   537  			if err != nil {
   538  				sub.AddError("failed to push account %q: %v", v, err)
   539  			}
   540  			if sub.OK() {
   541  				sub.Label = fmt.Sprintf("pushed %q to account server", v)
   542  			}
   543  		}
   544  	} else {
   545  		nats.NewInbox()
   546  		sysAcc, opt, err := getSystemAccountUser(ctx, p.sysAcc, p.sysAccUser, nats.InboxPrefix+">",
   547  			"$SYS.REQ.CLAIMS.LIST", "$SYS.REQ.CLAIMS.UPDATE", "$SYS.REQ.CLAIMS.DELETE")
   548  		if err != nil {
   549  			r.AddError("error obtaining system account user: %v", err)
   550  			return r, nil
   551  		}
   552  		nc, err := nats.Connect(p.ASU, createDefaultToolOptions("nsc_push", ctx, opt)...)
   553  		if err != nil {
   554  			r.AddError("failed to connect: %v", err)
   555  			return r, nil
   556  		}
   557  		defer nc.Close()
   558  		if len(p.targeted) != 0 {
   559  			sub := store.NewReport(store.OK, `push to nats-server "%s" using system account "%s"`,
   560  				p.ASU, sysAcc)
   561  			r.Add(sub)
   562  			for _, v := range p.targeted {
   563  				subAcc := store.NewReport(store.OK, "push %s to nats-server with nats account resolver", v)
   564  				sub.Add(subAcc)
   565  				if raw, err := ctx.StoreCtx().Store.Read(store.Accounts, v, store.JwtName(v)); err != nil {
   566  					subAcc.AddError("failed to read account %q: %v", v, err)
   567  				} else {
   568  					resp := multiRequest(nc, subAcc, "push account", "$SYS.REQ.CLAIMS.UPDATE", raw,
   569  						func(srv string, data interface{}) {
   570  							if data, ok := data.(map[string]interface{}); ok {
   571  								subAcc.AddOK("pushed %q to nats-server %s: %s", v, srv, data["message"])
   572  							} else {
   573  								subAcc.AddOK("pushed %q to nats-server %s: %v", v, srv, data)
   574  							}
   575  						})
   576  					subAcc.AddOK("pushed to a total of %d nats-server", resp)
   577  				}
   578  			}
   579  		}
   580  		if p.prune {
   581  			subPrune := store.NewReport(store.OK, "prune nats-server with nats account resolver")
   582  			r.Add(subPrune)
   583  			mapping, err := createMapping(ctx, subPrune, p.accountList)
   584  			if err != nil {
   585  				return r, nil
   586  			}
   587  			responseCount, deleteList := listNonPresentAccounts(nc, subPrune, mapping)
   588  			sendDeleteRequest(ctx, nc, deleteList, responseCount, subPrune)
   589  		} else if p.removeAcc != "" {
   590  			subRemove := store.NewReport(store.OK, "prune nats-server with nats account resolver")
   591  			r.Add(subRemove)
   592  			sendDeleteRequest(ctx, nc, []string{p.removeAcc}, -1, subRemove)
   593  		} else if p.diff {
   594  			subDiff := store.NewReport(store.OK, "diff nats-server with nats account resolver")
   595  			r.Add(subDiff)
   596  			accList, err := GetConfig().ListAccounts()
   597  			if err != nil {
   598  				subDiff.AddError("diff could not obtain account list: %v", err)
   599  				return r, nil
   600  			}
   601  			mapping, err := createMapping(ctx, subDiff, accList)
   602  			if err != nil {
   603  				subDiff.AddError("diff could not create account mapping: %v", err)
   604  				return r, nil
   605  			}
   606  
   607  			listNonPresentAccounts(nc, subDiff, mapping)
   608  		}
   609  	}
   610  	return r, nil
   611  }
   612  
   613  func (p *PushCmdParams) pushAccount(n string, ctx ActionCtx) (store.Status, error) {
   614  	raw, err := ctx.StoreCtx().Store.Read(store.Accounts, n, store.JwtName(n))
   615  	if err != nil {
   616  		return nil, err
   617  	}
   618  	c, err := jwt.DecodeAccountClaims(string(raw))
   619  	if err != nil {
   620  		return nil, err
   621  	}
   622  	u, err := AccountJwtURLFromString(p.ASU, c.Subject)
   623  	if err != nil {
   624  		return nil, err
   625  	}
   626  	return store.PushAccount(u, raw)
   627  }