Repository: nats-io/nsc (go)

Issue:
nsc in interactive mode adds unexpected field issuer_account wrongly pointing to nsc default account
Given the following sequence of commands: I create an operator, two accounts, then a users in each account (using interactive mode).
The generated jwt for the user in the account that's not the default account contains an extra field issuer_account pointing to the default account.

decodejwt is a shell function I wrote for myself, does what you'd expect.

```
19:54:34  ~  nsc add operator OP
[ OK ] generated and stored operator key "OA4LSFBW2RYBDL4HCSXJSS4YGFVE43OSQY4UY6KJ7QLE73PJ6SNBORBV"
[ OK ] added operator "OP"
 19:54:41  ~  nsc add account SYS
[ OK ] generated and stored account key "AA6PHN3KJT3AAYWL225BFOVIDGUEJL3VAIBERQ2DLUX4Z7YCKY5Y4OGJ"
[ OK ] added account "SYS"
 19:54:44  ~  nsc add account USERS
[ OK ] generated and stored account key "AAZJZFMKOV2HRMXS4675AFTFBEUIJ5FH7YXQUNY42766NP47F6666W5W"
[ OK ] added account "USERS"
 19:54:50  ~  nsc add user -i
? select account SYS
? user name S1
? generate an user nkey Yes
? valid 0
? valid until (0 is always) 0
[ OK ] generated and stored user key "UAVZJE7YE74LLGRXEXEUALQ2UKF3QQ75W7AK7JCOAZWDEVZMIDXAD57O"
[ OK ] generated user creds file `~/.nkeys/creds/OP/SYS/S1.creds`
[ OK ] added user "S1" to account "SYS"
 19:55:02  ~  nsc add user -i
? select account USERS
? user name U1
? generate an user nkey Yes
? valid 0
? valid until (0 is always) 0
[ OK ] generated and stored user key "UCXMY5GM44F42SV25U5NBZXSC23W3YSDBR6ZY6UFUIPGCYJ25WURN3XF"
[ OK ] generated user creds file `~/.nkeys/creds/OP/USERS/U1.creds`
[ OK ] added user "U1" to account "USERS"
 19:55:24  ~  cd .nsc/nats/OP
 19:55:26  ~/.nsc/nats/OP  ls
OP.jwt   accounts
 19:55:28  ~/.nsc/nats/OP  cd accounts
 19:55:38  ~/.nsc/nats/OP/accounts  decodejwt SYS/SYS.jwt
{"typ":"jwt","alg":"ed25519"}

{"jti":"MM2FKSEGK442KI4E6V3L2HPBK4QZJ3XL3TOMN3CQHZU54VBB7PDQ","iat":1585353281,"iss":"OA4LSFBW2RYBDL4HCSXJSS4YGFVE43OSQY4UY6KJ7QLE73PJ6SNBORBV","name":"SYS","sub":"AA6PHN3KJT3AAYWL225BFOVIDGUEJL3VAIBERQ2DLUX4Z7YCKY5Y4OGJ","type":"account","nats":{"limits":{"subs":-1,"conn":-1,"leaf":-1,"imports":-1,"exports":-1,"data":-1,"payload":-1,"wildcards":true}}}%                                                                                                                                  19:55:47  ~/.nsc/nats/OP/accounts  decodejwt USERS/USERS.jwt
{"typ":"jwt","alg":"ed25519"}

{"jti":"XHSZROPHQKEINY3XJOP6RH77ICDEFBM4EADRWK64D3AQMJQB4KPA","iat":1585353284,"iss":"OA4LSFBW2RYBDL4HCSXJSS4YGFVE43OSQY4UY6KJ7QLE73PJ6SNBORBV","name":"USERS","sub":"AAZJZFMKOV2HRMXS4675AFTFBEUIJ5FH7YXQUNY42766NP47F6666W5W","type":"account","nats":{"limits":{"subs":-1,"conn":-1,"leaf":-1,"imports":-1,"exports":-1,"data":-1,"payload":-1,"wildcards":true}}}%                                                                                                                           19:55:58  ~/.nsc/nats/OP/accounts  decodejwt SYS/users/S1.jwt
{"typ":"jwt","alg":"ed25519"}

{"jti":"N7EFRX2FTV5YFQF7HFE3QHCEAP4UE4Q4NZG2ILP723AESQZYMIXA","iat":1585353300,"iss":"AA6PHN3KJT3AAYWL225BFOVIDGUEJL3VAIBERQ2DLUX4Z7YCKY5Y4OGJ","name":"S1","sub":"UAVZJE7YE74LLGRXEXEUALQ2UKF3QQ75W7AK7JCOAZWDEVZMIDXAD57O","type":"user","nats":{"pub":{},"sub":{}},"issuer_account":"AAZJZFMKOV2HRMXS4675AFTFBEUIJ5FH7YXQUNY42766NP47F6666W5W"}%                                                                                                                 19:56:19  ~/.nsc/nats/OP/accounts  decodejwt USERS/users/U1.jwt
{"typ":"jwt","alg":"ed25519"}

{"jti":"B35Z364J7QE55E3D4RFNDKK7DT2KC4CCSYHFYJS2RYGHRYKWN6MA","iat":1585353315,"iss":"AAZJZFMKOV2HRMXS4675AFTFBEUIJ5FH7YXQUNY42766NP47F6666W5W","name":"U1","sub":"UCXMY5GM44F42SV25U5NBZXSC23W3YSDBR6ZY6UFUIPGCYJ25WURN3XF","type":"user","nats":{"pub":{},"sub":{}}}%

Why does S1 from account SYS have an extra field, compared to S1 from account USERS.
Why is this "issuer_account" pointing to USERS? (see below)
For completeness I list all keys, so I can see the structure, it appears as I would have thought it'd happen?

19:56:44  ~/.nsc/nats/OP/accounts  nsc list keys --all | grep AAZJZFMKOV2HRMXS4675AFTFBEUIJ5FH7YXQUNY42766NP47F6666W5W
│  USERS │ AAZJZFMKOV2HRMXS4675AFTFBEUIJ5FH7YXQUNY42766NP47F6666W5W │             │ *      │
19:56:52  ~/.nsc/nats/OP/accounts  nsc list keys --all
╭──────────────────────────────────────────────────────────────────────────────────────────╮
│                                           Keys                                           │
├────────┬──────────────────────────────────────────────────────────┬─────────────┬────────┤
│ Entity │ Key                                                      │ Signing Key │ Stored │
├────────┼──────────────────────────────────────────────────────────┼─────────────┼────────┤
│ OP     │ OA4LSFBW2RYBDL4HCSXJSS4YGFVE43OSQY4UY6KJ7QLE73PJ6SNBORBV │             │ *      │
│  SYS   │ AA6PHN3KJT3AAYWL225BFOVIDGUEJL3VAIBERQ2DLUX4Z7YCKY5Y4OGJ │             │ *      │
│   S1   │ UAVZJE7YE74LLGRXEXEUALQ2UKF3QQ75W7AK7JCOAZWDEVZMIDXAD57O │             │ *      │
│  USERS │ AAZJZFMKOV2HRMXS4675AFTFBEUIJ5FH7YXQUNY42766NP47F6666W5W │             │ *      │
│   U1   │ UCXMY5GM44F42SV25U5NBZXSC23W3YSDBR6ZY6UFUIPGCYJ25WURN3XF │             │ *      │
╰────────┴──────────────────────────────────────────────────────────┴─────────────┴────────╯

20:15:21  ~/.nsc/nats/OP/accounts  nsc env
╭──────────────────────────────────────────╮
│             NSC Environment              │
├──────────────────┬─────┬─────────────────┤
│ Setting          │ Set │ Effective Value │
├──────────────────┼─────┼─────────────────┤
│ $NKEYS_PATH      │ No  │ ~/.nkeys        │
│ $NSC_HOME        │ No  │ ~/.nsc          │
│ Config           │     │ ~/.nsc/nsc.json │
├──────────────────┼─────┼─────────────────┤
│ From CWD         │     │ Yes             │
│ Stores Dir       │     │ ~/.nsc/nats     │
│ Default Operator │     │ OP              │
│ Default Account  │     │ USERS           │
╰──────────────────┴─────┴─────────────────╯

 20:15:21  ~/.nsc/nats/OP/accounts 
```

Pull request description:
FIX #265 - adding a user in interactive mode could add bogus IssuerAccount
The generic entity code interacted with the store directly requesting a context. However add user interactive command changes the context, these changes were not visible by entity. When the entity looked for the public key of the account, it retrieved the one for the one in the context rather than the one for the user selected account.

Patch:
diff --git a/cmd/adduser.go b/cmd/adduser.go
index 594e79a..269f7ed 100644
--- a/cmd/adduser.go
+++ b/cmd/adduser.go
@@ -79,8 +79,8 @@ nsc add user --name <n> --allow-pub-response=5
 	cmd.Flags().StringSliceVarP(&params.tags, "tag", "", nil, "tags for user - comma separated list or option can be specified multiple times")
 	cmd.Flags().StringSliceVarP(&params.src, "source-network", "", nil, "source network for connection - comma separated list or option can be specified multiple times")
 
-	cmd.Flags().StringVarP(&params.name, "name", "n", "", "name to assign the user")
-	cmd.Flags().StringVarP(&params.keyPath, "public-key", "k", "", "public key identifying the user")
+	cmd.Flags().StringVarP(&params.userName, "name", "n", "", "name to assign the user")
+	cmd.Flags().StringVarP(&params.pkOrPath, "public-key", "k", "", "public key identifying the user")
 
 	params.TimeParams.BindFlags(cmd)
 	params.AccountContextParams.BindFlags(cmd)
@@ -96,7 +96,6 @@ func init() {
 type AddUserParams struct {
 	AccountContextParams
 	SignerParams
-	Entity
 	TimeParams
 	ResponsePermsParams
 	allowPubs     []string
@@ -108,6 +107,9 @@ type AddUserParams struct {
 	src           []string
 	tags          []string
 	credsFilePath string
+	userName      string
+	pkOrPath      string
+	kp            nkeys.KeyPair
 }
 
 func (p *AddUserParams) longHelp() string {
@@ -119,17 +121,14 @@ toolName add user --name u --tag test,service_a`
 }
 
 func (p *AddUserParams) SetDefaults(ctx ActionCtx) error {
-	p.name = NameFlagOrArgument(p.name, ctx)
-	if p.name == "*" {
-		p.name = GetRandomName(0)
+	p.userName = NameFlagOrArgument(p.userName, ctx)
+	if p.userName == "*" {
+		p.userName = GetRandomName(0)
 	}
 	if err := p.AccountContextParams.SetDefaults(ctx); err != nil {
 		return err
 	}
 	p.SignerParams.SetDefaults(nkeys.PrefixByteAccount, true, ctx)
-	p.create = true
-	p.Entity.kind = nkeys.PrefixByteUser
-	p.editFn = p.editUserClaim
 
 	return nil
 }
@@ -140,9 +139,37 @@ func (p *AddUserParams) PreInteractive(ctx ActionCtx) error {
 		return err
 	}
 
-	if err = p.Entity.Edit(); err != nil {
+	p.userName, err = cli.Prompt("user name", p.userName, cli.NewLengthValidator(1))
+	if err != nil {
+		return err
+	}
+
+	ok, err := cli.Confirm("generate an user nkey", true)
+	if err != nil {
 		return err
 	}
+	if !ok {
+		p.pkOrPath, err = cli.Prompt("path to an user nkey or nkey", p.pkOrPath, cli.Val(func(v string) error {
+			nk, err := store.ResolveKey(v)
+			if err != nil {
+				return err
+			}
+			if nk == nil {
+				return fmt.Errorf("a key is required")
+			}
+			t, err := store.KeyType(nk)
+			if err != nil {
+				return err
+			}
+			if t != nkeys.PrefixByteUser {
+				return errors.New("specified key is not a valid for an user")
+			}
+			return nil
+		}))
+		if err != nil {
+			return err
+		}
+	}
 
 	// FIXME: we won't do interactive on the response params until pub/sub/deny permissions are interactive
 	//if err := p.ResponsePermsParams.Edit(false); err != nil {
@@ -170,13 +197,13 @@ func (p *AddUserParams) PostInteractive(_ ActionCtx) error {
 
 func (p *AddUserParams) Validate(ctx ActionCtx) error {
 	var err error
-	if p.name == "" {
+	if p.userName == "" {
 		ctx.CurrentCmd().SilenceUsage = false
 		return fmt.Errorf("user name is required")
 	}
 
-	if p.name == "*" {
-		p.name = GetRandomName(0)
+	if p.userName == "*" {
+		p.userName = GetRandomName(0)
 	}
 
 	if err = p.AccountContextParams.Validate(ctx); err != nil {
@@ -195,41 +222,70 @@ func (p *AddUserParams) Validate(ctx ActionCtx) error {
 		return err
 	}
 
-	return p.Entity.Valid()
+	if p.pkOrPath != "" {
+		p.kp, err = store.ResolveKey(p.pkOrPath)
+		if err != nil {
+			return err
+		}
+		if !store.KeyPairTypeOk(nkeys.PrefixByteUser, p.kp) {
+			return errors.New("invalid user key")
+		}
+	} else {
+		p.kp, err = nkeys.CreatePair(nkeys.PrefixByteUser)
+		if err != nil {
+			return err
+		}
+	}
+
+	s := ctx.StoreCtx().Store
+	if s.Has(store.Accounts, ctx.StoreCtx().Account.Name, store.Users, store.JwtName(p.userName)) {
+		return fmt.Errorf("the user %q already exists", p.userName)
+	}
+
+	return nil
 }
 
 func (p *AddUserParams) Run(ctx ActionCtx) (store.Status, error) {
-	var rs store.Status
-	var err error
+	uc, err := p.generateUserClaim(ctx)
+	if err != nil {
+		return nil, err
+	}
 
-	if err := p.Entity.StoreKeys(p.AccountContextParams.Name); err != nil {
+	token, err := uc.Encode(p.signerKP)
+	if err != nil {
 		return nil, err
 	}
 
 	r := store.NewDetailedReport(false)
-	rs, err = p.Entity.GenerateClaim(p.signerKP, ctx)
-	if rs != nil {
-		r.Add(rs)
-	}
+	st, err := ctx.StoreCtx().Store.StoreClaim([]byte(token))
 	if err != nil {
 		r.AddFromError(err)
+		return st, err
 	}
-	if rs != nil {
-		r.Add(rs)
+	if st != nil {
+		r.Add(st)
 	}
 
-	pk, _ := p.kp.PublicKey()
-	if p.generated {
-		r.AddOK("generated and stored user key %q", pk)
+	// store the key
+	if p.pkOrPath == "" {
+		ks := ctx.StoreCtx()
+		var err error
+		if p.pkOrPath, err = ks.KeyStore.Store(p.kp); err != nil {
+			r.AddFromError(err)
+			return r, err
+		}
+		r.AddOK("generated and stored user key %q", uc.Subject)
 	}
+
+	pk := uc.Subject
 	// if they gave us a seed, it stored - try to get it
 	ks := ctx.StoreCtx().KeyStore
 	if ks.HasPrivateKey(pk) {
-		d, err := GenerateConfig(ctx.StoreCtx().Store, p.AccountContextParams.Name, p.name, p.kp)
+		d, err := GenerateConfig(ctx.StoreCtx().Store, p.AccountContextParams.Name, p.userName, p.kp)
 		if err != nil {
 			r.AddError("unable to save creds: %v", err)
 		} else {
-			p.credsFilePath, err = ks.MaybeStoreUserCreds(p.AccountContextParams.Name, p.name, d)
+			p.credsFilePath, err = ks.MaybeStoreUserCreds(p.AccountContextParams.Name, p.userName, d)
 			if err != nil {
 				r.AddError("error storing creds: %v", err)
 			} else {
@@ -240,15 +296,25 @@ func (p *AddUserParams) Run(ctx ActionCtx) (store.Status, error) {
 		r.AddOK("skipped generating creds file - user private key is not available")
 	}
 	if r.HasNoErrors() {
-		r.AddOK("added user %q to account %q", p.name, p.AccountContextParams.Name)
+		r.AddOK("added user %q to account %q", p.userName, p.AccountContextParams.Name)
 	}
 	return r, nil
 }
 
-func (p *AddUserParams) editUserClaim(c interface{}, ctx ActionCtx) error {
-	uc, ok := c.(*jwt.UserClaims)
-	if !ok {
-		return errors.New("unable to cast to user claim")
+func (p *AddUserParams) generateUserClaim(ctx ActionCtx) (*jwt.UserClaims, error) {
+	pub, err := p.kp.PublicKey()
+	if err != nil {
+		return nil, err
+	}
+	uc := jwt.NewUserClaims(pub)
+	uc.Name = p.userName
+
+	spk, err := p.signerKP.PublicKey()
+	if err != nil {
+		return nil, err
+	}
+	if ctx.StoreCtx().Account.PublicKey != spk {
+		uc.IssuerAccount = ctx.StoreCtx().Account.PublicKey
 	}
 
 	if p.TimeParams.IsStartChanged() {
@@ -260,7 +326,7 @@ func (p *AddUserParams) editUserClaim(c interface{}, ctx ActionCtx) error {
 	}
 
 	if _, err := p.ResponsePermsParams.Run(uc, ctx); err != nil {
-		return err
+		return nil, err
 	}
 
 	uc.Permissions.Pub.Allow.Add(p.allowPubs...)
@@ -282,7 +348,7 @@ func (p *AddUserParams) editUserClaim(c interface{}, ctx ActionCtx) error {
 	uc.Tags.Add(p.tags...)
 	sort.Strings(uc.Tags)
 
-	return nil
+	return uc, nil
 }
 
 type ResponsePermsParams struct {
diff --git a/cmd/entity.go b/cmd/entity.go
deleted file mode 100644
index 05ee70e..0000000
--- a/cmd/entity.go
+++ /dev/null
@@ -1,220 +0,0 @@
-/*
- *
- *  * Copyright 2018-2019 The NATS Authors
- *  * Licensed under the Apache License, Version 2.0 (the "License");
- *  * you may not use this file except in compliance with the License.
- *  * You may obtain a copy of the License at
- *  *
- *  * http://www.apache.org/licenses/LICENSE-2.0
- *  *
- *  * Unless required by applicable law or agreed to in writing, software
- *  * distributed under the License is distributed on an "AS IS" BASIS,
- *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  * See the License for the specific language governing permissions and
- *  * limitations under the License.
- *
- */
-
-package cmd
-
-import (
-	"fmt"
-
-	cli "github.com/nats-io/cliprompts/v2"
-	"github.com/nats-io/jwt"
-	"github.com/nats-io/nkeys"
-	"github.com/nats-io/nsc/cmd/store"
-)
-
-type EntityClaimsEditor func(interface{}, ActionCtx) error
-
-type Entity struct {
-	create    bool
-	generated bool
-	keyPath   string
-	kind      nkeys.PrefixByte
-	kp        nkeys.KeyPair
-	name      string
-	editFn    EntityClaimsEditor
-}
-
-func (c *Entity) Valid() error {
-	var err error
-	if c.keyPath != "" {
-		c.kp, err = store.ResolveKey(c.keyPath)
-		if err != nil {
-			return err
-		}
-
-		if !store.KeyPairTypeOk(c.kind, c.kp) {
-			return fmt.Errorf("invalid %s key", c.kind.String())
-		}
-
-	} else if c.create {
-		c.kp, err = nkeys.CreatePair(c.kind)
-		if err != nil {
-			return err
-		}
-		c.generated = true
-	}
-
-	s, _ := GetStore()
-	if s == nil {
-		// this happens on init
-		return nil
-	}
-
-	ctx, err := s.GetContext()
-	if err != nil {
-		return err
-	}
-
-	exists := false
-	switch c.kind {
-	case nkeys.PrefixByteOperator:
-		exists = s.Has(store.JwtName(c.name))
-	case nkeys.PrefixByteAccount:
-		exists = s.Has(store.Accounts, c.name, store.JwtName(c.name))
-	case nkeys.PrefixByteUser:
-		exists = s.Has(store.Accounts, ctx.Account.Name, store.Users, store.JwtName(c.name))
-	default:
-		return fmt.Errorf("unexpected type of entity")
-	}
-
-	if exists {
-		return fmt.Errorf("the %s %q already exists", c.kind.String(), c.name)
-	}
-
-	return nil
-}
-
-func (c *Entity) StoreKeys(parent string) error {
-	if c.create && c.keyPath == "" {
-		s, err := GetStore()
-		if err != nil {
-			return err
-		}
-		ctx, err := s.GetContext()
-		if err != nil {
-			return err
-		}
-		if c.keyPath, err = ctx.KeyStore.Store(c.kp); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (c *Entity) KeySource() string {
-	if c.keyPath == "" {
-		return "Generated"
-	}
-	return c.keyPath
-}
-
-func (c *Entity) Edit() error {
-	var err error
-	label := c.kind.String()
-
-	c.name, err = cli.Prompt(fmt.Sprintf("%s name", label), c.name, cli.NewLengthValidator(1))
-	if err != nil {
-		return err
-	}
-
-	ok, err := cli.Confirm(fmt.Sprintf("generate an %s nkey", label), true)
-	if err != nil {
-		return err
-	}
-	if !ok {
-		c.keyPath, err = cli.Prompt(fmt.Sprintf("path to an %s nkey or nkey", label), c.keyPath, cli.Val(c.ValidateNKey()))
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (c *Entity) GenerateClaim(signer nkeys.KeyPair, ctx ActionCtx) (store.Status, error) {
-	if !c.create {
-		return nil, nil
-	}
-	// self-sign if we don't have a parent keypair
-	if signer == nil {
-		signer = c.kp
-	}
-
-	pub, err := c.kp.PublicKey()
-	if err != nil {
-		return nil, err
-	}
-
-	s, err := GetStore()
-	if err != nil {
-		return nil, err
-	}
-
-	var claim jwt.Claims
-	switch c.kind {
-	case nkeys.PrefixByteOperator:
-		claim = jwt.NewOperatorClaims(pub)
-	case nkeys.PrefixByteAccount:
-		ac := jwt.NewAccountClaims(pub)
-		if s.IsManaged() {
-			ac.Limits = jwt.OperatorLimits{}
-		}
-		claim = ac
-	case nkeys.PrefixByteUser:
-		uc := jwt.NewUserClaims(pub)
-		claim = uc
-		ctx, err := s.GetContext()
-		if err != nil {
-			return nil, err
-		}
-		spk, err := signer.PublicKey()
-		if err != nil {
-			return nil, err
-		}
-		if ctx.Account.PublicKey != spk {
-			uc.IssuerAccount = ctx.Account.PublicKey
-		}
-	case nkeys.PrefixByteCluster:
-		claim = jwt.NewClusterClaims(pub)
-	case nkeys.PrefixByteServer:
-		claim = jwt.NewServerClaims(pub)
-	}
-	d := claim.Claims()
-	d.Name = c.name
-
-	if c.editFn != nil {
-		if err = c.editFn(claim, ctx); err != nil {
-			return nil, err
-		}
-	}
-
-	token, err := claim.Encode(signer)
-	if err != nil {
-		return nil, err
-	}
-
-	return s.StoreClaim([]byte(token))
-}
-
-func (c *Entity) ValidateNKey() cli.Validator {
-	return func(v string) error {
-		nk, err := store.ResolveKey(v)
-		if err != nil {
-			return err
-		}
-		if nk == nil {
-			return fmt.Errorf("a key is required")
-		}
-		t, err := store.KeyType(nk)
-		if err != nil {
-			return err
-		}
-		if t != c.kind {
-			return fmt.Errorf("specified key is not valid for an %s", c.kind.String())
-		}
-		return nil
-	}
-}


Test patch:
diff --git a/cmd/adduser_test.go b/cmd/adduser_test.go
index a015693..2b582d6 100644
--- a/cmd/adduser_test.go
+++ b/cmd/adduser_test.go
@@ -16,9 +16,13 @@
 package cmd
 
 import (
+	"io/ioutil"
+	"path/filepath"
 	"testing"
 	"time"
 
+	"github.com/nats-io/nkeys"
+
 	"github.com/stretchr/testify/require"
 )
 
@@ -50,8 +54,9 @@ func Test_AddUser(t *testing.T) {
 func Test_AddUserNoStore(t *testing.T) {
 	// reset the store
 	ngsStore = nil
-	ForceStoreRoot(t, "")
+	require.NoError(t, ForceStoreRoot(t, ""))
 	_, _, err := ExecuteCmd(CreateAddUserCmd())
+	require.Error(t, err)
 	require.Equal(t, "no stores available", err.Error())
 }
 
@@ -238,3 +243,87 @@ func Test_AddUserWithResponsePerms2(t *testing.T) {
 	d, _ := time.ParseDuration("2ms")
 	require.Equal(t, d, uc.Resp.Expires)
 }
+
+func Test_AddUserWithInteractiveAccountCtx(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	ts.AddAccount(t, "A")
+	ts.AddAccount(t, "B")
+
+	// adding to user bb to B
+	inputs := []interface{}{1, "bb", true, "0", "0"}
+	cmd := CreateAddUserCmd()
+	HoistRootFlags(cmd)
+	_, _, err := ExecuteInteractiveCmd(cmd, inputs)
+	require.NoError(t, err)
+
+	bpk := ts.GetAccountPublicKey(t, "B")
+	uc, err := ts.Store.ReadUserClaim("B", "bb")
+	require.NoError(t, err)
+	require.Equal(t, bpk, uc.Issuer)
+	require.Empty(t, uc.IssuerAccount)
+
+	// adding to user aa to A
+	inputs = []interface{}{0, "aa", true, "0", "0"}
+	_, _, err = ExecuteInteractiveCmd(cmd, inputs)
+	require.NoError(t, err)
+	apk := ts.GetAccountPublicKey(t, "A")
+
+	uc, err = ts.Store.ReadUserClaim("A", "aa")
+	require.NoError(t, err)
+	require.Equal(t, apk, uc.Issuer)
+	require.Empty(t, uc.IssuerAccount)
+}
+
+func Test_AddUserWithInteractiveCustomKey(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	ts.AddAccount(t, "A")
+
+	kp, err := nkeys.CreateUser()
+	require.NoError(t, err)
+	sk, err := kp.Seed()
+	require.NoError(t, err)
+	pk, err := kp.PublicKey()
+	require.NoError(t, err)
+
+	inputs := []interface{}{"aa", false, string(sk), "0", "0"}
+	cmd := CreateAddUserCmd()
+	HoistRootFlags(cmd)
+	_, _, err = ExecuteInteractiveCmd(cmd, inputs)
+	require.NoError(t, err)
+
+	uc, err := ts.Store.ReadUserClaim("A", "aa")
+	require.NoError(t, err)
+	require.Equal(t, pk, uc.Subject)
+	require.Empty(t, uc.IssuerAccount)
+	require.False(t, ts.KeyStore.HasPrivateKey(pk))
+
+	inputs = []interface{}{"bb", false, pk, "0", "0"}
+	cmd = CreateAddUserCmd()
+	HoistRootFlags(cmd)
+	_, _, err = ExecuteInteractiveCmd(cmd, inputs)
+	require.NoError(t, err)
+
+	uc, err = ts.Store.ReadUserClaim("A", "bb")
+	require.NoError(t, err)
+	require.Equal(t, pk, uc.Subject)
+	require.Empty(t, uc.IssuerAccount)
+	require.False(t, ts.KeyStore.HasPrivateKey(pk))
+
+	fp := filepath.Join(ts.Dir, "key")
+	err = ioutil.WriteFile(fp, sk, 0600)
+	require.NoError(t, err)
+
+	inputs = []interface{}{"cc", false, fp, "0", "0"}
+	cmd = CreateAddUserCmd()
+	HoistRootFlags(cmd)
+	_, _, err = ExecuteInteractiveCmd(cmd, inputs)
+	require.NoError(t, err)
+
+	uc, err = ts.Store.ReadUserClaim("A", "cc")
+	require.NoError(t, err)
+	require.Equal(t, pk, uc.Subject)
+	require.Empty(t, uc.IssuerAccount)
+	require.False(t, ts.KeyStore.HasPrivateKey(pk))
+}