Repository: nats-io/nsc
Pull request #318 (2020-11-05 18:52:51)

[Fixed] managed stores with op signing key create signed accounts
Basically allow for multiple kinds to be specified.
Why this check exist is a mystery to me. Specifically because the caller also provides which keys are ok to use.

This makes it work such that in managed mode you can be set up with an operator signing key


Patch:
diff --git a/cmd/adduser.go b/cmd/adduser.go
index 12246f9..4eb8f9d 100644
--- a/cmd/adduser.go
+++ b/cmd/adduser.go
@@ -174,17 +174,35 @@ func (p *AddUserParams) PreInteractive(ctx ActionCtx) error {
 		return err
 	}
 
-	if err = p.SignerParams.Edit(ctx); err != nil {
+	signers, err := validUserSigners(ctx, p.Name)
+	if err != nil {
 		return err
 	}
-
-	return nil
+	p.SignerParams.SetPrompt("select the key to sign the user")
+	return p.SignerParams.SelectFromSigners(ctx, signers)
 }
 
 func (p *AddUserParams) Load(_ ActionCtx) error {
 	return nil
 }
 
+func validUserSigners(ctx ActionCtx, accName string) ([]string, error) {
+	ac, err := ctx.StoreCtx().Store.ReadAccountClaim(accName)
+	if err != nil {
+		return nil, err
+	}
+	var signers []string
+	if ctx.StoreCtx().KeyStore.HasPrivateKey(ac.Subject) {
+		signers = append(signers, ac.Subject)
+	}
+	for _, signingKey := range ac.SigningKeys {
+		if ctx.StoreCtx().KeyStore.HasPrivateKey(signingKey) {
+			signers = append(signers, signingKey)
+		}
+	}
+	return signers, nil
+}
+
 func (p *AddUserParams) PostInteractive(_ ActionCtx) error {
 	return nil
 }
diff --git a/cmd/common.go b/cmd/common.go
index 4c5f6bd..67af15d 100644
--- a/cmd/common.go
+++ b/cmd/common.go
@@ -237,7 +237,7 @@ func NKeyValidator(kind nkeys.PrefixByte) cli.Validator {
 	}
 }
 
-func SeedNKeyValidatorMatching(kind nkeys.PrefixByte, pukeys []string) cli.Validator {
+func SeedNKeyValidatorMatching(pukeys []string, kinds ...nkeys.PrefixByte) cli.Validator {
 	return func(v string) error {
 		if v == "" {
 			return fmt.Errorf("value cannot be empty")
@@ -260,8 +260,17 @@ func SeedNKeyValidatorMatching(kind nkeys.PrefixByte, pukeys []string) cli.Valid
 		if err != nil {
 			return err
 		}
-		if t != kind {
-			return fmt.Errorf("specified key is not valid for an %s", kind.String())
+		foundKind := false
+		kindNames := []string{}
+		for _, kind := range kinds {
+			if t == kind {
+				foundKind = true
+				break
+			}
+			kindNames = append(kindNames, kind.String())
+		}
+		if !foundKind {
+			return fmt.Errorf("specified key is not valid for any of %v", kindNames)
 		}
 
 		pk, err := nk.PublicKey()
diff --git a/cmd/edituser.go b/cmd/edituser.go
index 04bd0b9..9056f2c 100644
--- a/cmd/edituser.go
+++ b/cmd/edituser.go
@@ -156,7 +156,12 @@ func (p *EditUserParams) PreInteractive(ctx ActionCtx) error {
 		}
 	}
 
-	return nil
+	signers, err := validUserSigners(ctx, p.Name)
+	if err != nil {
+		return err
+	}
+	p.SignerParams.SetPrompt("select the key to sign the user")
+	return p.SignerParams.SelectFromSigners(ctx, signers)
 }
 
 func (p *EditUserParams) Load(ctx ActionCtx) error {
@@ -211,9 +216,6 @@ func (p *EditUserParams) PostInteractive(ctx ActionCtx) error {
 	if err := p.GenericClaimsParams.Edit(p.claim.Tags); err != nil {
 		return err
 	}
-	if err := p.SignerParams.Edit(ctx); err != nil {
-		return err
-	}
 	return nil
 }
 
diff --git a/cmd/signerparams.go b/cmd/signerparams.go
index 9c808f0..f61d588 100644
--- a/cmd/signerparams.go
+++ b/cmd/signerparams.go
@@ -22,22 +22,21 @@ import (
 
 	cli "github.com/nats-io/cliprompts/v2"
 	"github.com/nats-io/nkeys"
+	"github.com/nats-io/nsc/cmd/store"
 )
 
 // SignerParams is shared UI for a signer (-K flag). The key
 // for a signer is never generated and must be provided
 type SignerParams struct {
-	kind     nkeys.PrefixByte
+	kind     []nkeys.PrefixByte
 	signerKP nkeys.KeyPair
 	prompt   string
 }
 
 func (p *SignerParams) SetDefaults(kind nkeys.PrefixByte, allowManaged bool, ctx ActionCtx) {
-	p.kind = kind
-	if allowManaged {
-		if ctx.StoreCtx().Store.IsManaged() && p.kind == nkeys.PrefixByteOperator {
-			p.kind = nkeys.PrefixByteAccount
-		}
+	p.kind = append(p.kind, kind)
+	if allowManaged && ctx.StoreCtx().Store.IsManaged() && kind == nkeys.PrefixByteOperator {
+		p.kind = append(p.kind, nkeys.PrefixByteAccount)
 	}
 }
 
@@ -65,7 +64,7 @@ func (p *SignerParams) SelectFromSigners(ctx ActionCtx, signers []string) error
 	// if we have more than one key, we prompt
 	if len(keys) == 1 && len(notFound) == 0 {
 		var err error
-		p.signerKP, err = ctx.StoreCtx().ResolveKey(p.kind, keys[0])
+		p.signerKP, err = ctx.StoreCtx().ResolveKey(keys[0], p.kind...)
 		if err != nil {
 			return err
 		}
@@ -86,17 +85,21 @@ func (p *SignerParams) SelectFromSigners(ctx ActionCtx, signers []string) error
 		}
 		// if it is the extra option, ask for a path/key
 		if idx != -1 && choice == idx {
-			label := fmt.Sprintf("path to signer %s nkey or nkey", p.kind.String())
+			kinds := []string{}
+			for _, kind := range p.kind {
+				kinds = append(kinds, kind.String())
+			}
+			label := fmt.Sprintf("path to signer %s nkey or nkey", kinds)
 			// key must be one from signing keys
-			KeyPathFlag, err = cli.Prompt(label, "", cli.Val(SeedNKeyValidatorMatching(p.kind, signers)))
+			KeyPathFlag, err = cli.Prompt(label, "", cli.Val(SeedNKeyValidatorMatching(signers, p.kind...)))
 			if err != nil {
 				return err
 			}
-			p.signerKP, err = ctx.StoreCtx().ResolveKey(p.kind, KeyPathFlag)
+			p.signerKP, err = ctx.StoreCtx().ResolveKey(KeyPathFlag, p.kind...)
 			return err
 		} else {
 			// they picked one
-			p.signerKP, err = ctx.StoreCtx().ResolveKey(p.kind, keys[choice])
+			p.signerKP, err = ctx.StoreCtx().ResolveKey(keys[choice], p.kind...)
 			return err
 		}
 	}
@@ -107,7 +110,7 @@ func (p *SignerParams) SelectFromSigners(ctx ActionCtx, signers []string) error
 func (p *SignerParams) Edit(ctx ActionCtx) error {
 	var err error
 	sctx := ctx.StoreCtx()
-	p.signerKP, _ = sctx.ResolveKey(p.kind, KeyPathFlag)
+	p.signerKP, _ = sctx.ResolveKey(KeyPathFlag, p.kind...)
 
 	if p.signerKP != nil && ctx.StoreCtx().Store.IsManaged() {
 		return nil
@@ -132,19 +135,22 @@ func (p *SignerParams) getSigners(ctx ActionCtx) ([]string, error) {
 	sctx := ctx.StoreCtx()
 	ks := sctx.KeyStore
 	var signers []string
-	var err error
-	switch p.kind {
-	case nkeys.PrefixByteOperator:
-		KeyPathFlag = ks.GetKeyPath(sctx.Operator.PublicKey)
-		signers, err = ctx.StoreCtx().GetOperatorKeys()
-		if err != nil {
-			return nil, err
-		}
-	case nkeys.PrefixByteAccount:
-		KeyPathFlag = ks.GetKeyPath(sctx.Account.PublicKey)
-		signers, err = ctx.StoreCtx().GetAccountKeys(sctx.Account.Name)
-		if err != nil {
-			return nil, err
+	for _, kind := range p.kind {
+		switch kind {
+		case nkeys.PrefixByteOperator:
+			KeyPathFlag = ks.GetKeyPath(sctx.Operator.PublicKey)
+			sgnrs, err := ctx.StoreCtx().GetOperatorKeys()
+			if err != nil {
+				return nil, err
+			}
+			signers = append(signers, sgnrs...)
+		case nkeys.PrefixByteAccount:
+			KeyPathFlag = ks.GetKeyPath(sctx.Account.PublicKey)
+			sgnrs, err := ctx.StoreCtx().GetAccountKeys(sctx.Account.Name)
+			if err != nil {
+				return nil, err
+			}
+			signers = append(signers, sgnrs...)
 		}
 	}
 	return signers, nil
@@ -157,7 +163,7 @@ func (p *SignerParams) Resolve(ctx ActionCtx) error {
 	var err error
 	// if they specified -K resolve or fail
 	if KeyPathFlag != "" {
-		p.signerKP, err = ctx.StoreCtx().ResolveKey(p.kind, KeyPathFlag)
+		p.signerKP, err = ctx.StoreCtx().ResolveKey(KeyPathFlag, p.kind...)
 		if err != nil {
 			return err
 		}
@@ -187,25 +193,33 @@ func (p *SignerParams) Resolve(ctx ActionCtx) error {
 	if selected == "" {
 		return fmt.Errorf("unable to resolve any of the following signing keys in the keystore: %s", strings.Join(signers, ", "))
 	}
-	p.signerKP, err = ctx.StoreCtx().ResolveKey(p.kind, selected)
+	p.signerKP, err = ctx.StoreCtx().ResolveKey(selected, p.kind...)
 	return err
 }
 
 func (p *SignerParams) ForceManagedAccountKey(ctx ActionCtx, kp nkeys.KeyPair) {
-	if ctx.StoreCtx().Store.IsManaged() && p.signerKP == nil {
-		// use the account as the signer
-		p.signerKP = kp
-		// check we have a private key available
-		pk, _ := p.signerKP.PrivateKey()
-		if pk == nil {
-			// try to load it
-			pub, _ := p.signerKP.PublicKey()
-			kp, err := ctx.StoreCtx().KeyStore.GetKeyPair(pub)
-			if err == nil {
-				pk, _ := kp.PrivateKey()
-				if pk != nil {
-					p.signerKP = kp
-				}
+	p.Resolve(ctx)
+	if !ctx.StoreCtx().Store.IsManaged() {
+		return
+	}
+	if p.signerKP != nil && store.KeyPairTypeOk(nkeys.PrefixByteAccount, p.signerKP) {
+		p.signerKP = nil
+	}
+	if p.signerKP != nil {
+		return
+	}
+	// use the account as the signer
+	p.signerKP = kp
+	// check we have a private key available
+	pk, _ := p.signerKP.PrivateKey()
+	if pk == nil {
+		// try to load it
+		pub, _ := p.signerKP.PublicKey()
+		kp, err := ctx.StoreCtx().KeyStore.GetKeyPair(pub)
+		if err == nil {
+			pk, _ := kp.PrivateKey()
+			if pk != nil {
+				p.signerKP = kp
 			}
 		}
 	}
diff --git a/cmd/store/store.go b/cmd/store/store.go
index c8a5d3e..b10f265 100644
--- a/cmd/store/store.go
+++ b/cmd/store/store.go
@@ -27,6 +27,7 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"sort"
 	"strings"
 	"sync"
 	"time"
@@ -748,37 +749,56 @@ func (s *Store) GetContext() (*Context, error) {
 	return &c, nil
 }
 
-func (ctx *Context) ResolveKey(kind nkeys.PrefixByte, flagValue string) (nkeys.KeyPair, error) {
+func (ctx *Context) ResolveKey(flagValue string, kinds ...nkeys.PrefixByte) (nkeys.KeyPair, error) {
 	kp, err := ResolveKey(flagValue)
 	if err != nil {
 		return nil, err
 	}
-	if kp == nil {
-		var pk string
-		switch kind {
-		case nkeys.PrefixByteAccount:
-			pk = ctx.Account.PublicKey
-		case nkeys.PrefixByteOperator:
-			pk = ctx.Operator.PublicKey
+	sort.Slice(kinds, func(i, j int) bool {
+		switch kind := kinds[i]; {
+		case kind == nkeys.PrefixByteAccount && kinds[j] == nkeys.PrefixByteOperator:
+			return false
+		case kind == nkeys.PrefixByteUser && kinds[j] == nkeys.PrefixByteOperator:
+			return false
+		case kind == nkeys.PrefixByteUser && kinds[j] == nkeys.PrefixByteAccount:
+			return false
 		default:
-			return nil, fmt.Errorf("unsupported key %d resolution", kind)
+			return true
 		}
-		// don't try to resolve empty
-		if pk != "" {
-			kp, err = ctx.KeyStore.GetKeyPair(pk)
-			if err != nil {
-				return nil, err
+	})
+	for _, kind := range kinds {
+		if kp == nil {
+			var pk string
+			switch kind {
+			case nkeys.PrefixByteAccount:
+				pk = ctx.Account.PublicKey
+			case nkeys.PrefixByteOperator:
+				pk = ctx.Operator.PublicKey
+			default:
+				return nil, fmt.Errorf("unsupported key %d resolution", kind)
+			}
+			// don't try to resolve empty
+			if pk != "" {
+				kp, err = ctx.KeyStore.GetKeyPair(pk)
+				if err != nil {
+					continue
+				}
+			}
+			// not found
+			if kp == nil {
+				continue
 			}
 		}
-		// not found
-		if kp == nil {
-			return nil, nil
+		if !KeyPairTypeOk(kind, kp) {
+			err = fmt.Errorf("unexpected resolved keytype type")
+			continue
+		}
+		if kp != nil {
+			err = nil
+			break
 		}
 	}
-	if !KeyPairTypeOk(kind, kp) {
-		return nil, fmt.Errorf("unexpected resolved keytype type")
-	}
-	return kp, nil
+	return kp, err
 }
 
 func (ctx *Context) PickAccount(name string) (string, error) {


Test patch:
diff --git a/cmd/addaccount_test.go b/cmd/addaccount_test.go
index 7e027bc..fb08b5d 100644
--- a/cmd/addaccount_test.go
+++ b/cmd/addaccount_test.go
@@ -17,6 +17,8 @@ package cmd
 
 import (
 	"fmt"
+	"io/ioutil"
+	"path/filepath"
 	"testing"
 
 	"github.com/nats-io/jwt"
@@ -124,6 +126,45 @@ func Test_AddAccountManagedStore(t *testing.T) {
 	require.NoError(t, err)
 }
 
+func Test_AddAccountManagedStoreWithSigningKey(t *testing.T) {
+	ts := NewEmptyStore(t)
+	defer ts.Done(t)
+	_, pub, kp := CreateOperatorKey(t)
+	oc := jwt.NewOperatorClaims(pub)
+	oc.Name = "O"
+	s1, psk, sk := CreateOperatorKey(t)
+	ts.KeyStore.Store(sk)
+	oc.SigningKeys.Add(psk)
+	token, err := oc.Encode(kp)
+	require.NoError(t, err)
+	tf := filepath.Join(ts.Dir, "O.jwt")
+	err = Write(tf, []byte(token))
+	require.NoError(t, err)
+	_, _, err = ExecuteCmd(createAddOperatorCmd(), "--url", tf)
+	require.NoError(t, err)
+	// sign with the signing key
+	inputs := []interface{}{"A", true, "0", "0", 0, string(s1)}
+	_, _, err = ExecuteInteractiveCmd(HoistRootFlags(CreateAddAccountCmd()), inputs)
+	require.NoError(t, err)
+	accJWT, err := ioutil.ReadFile(filepath.Join(ts.Dir, "store", "O", "accounts", "A", "A.jwt"))
+	require.NoError(t, err)
+	ac, err := jwt.DecodeAccountClaims(string(accJWT))
+	require.NoError(t, err)
+	require.False(t, ac.IsSelfSigned())
+	require.Equal(t, ac.Issuer, psk)
+	require.True(t, oc.DidSign(ac))
+	// sign with the account key
+	inputs = []interface{}{"B", true, "0", "0", 1, string(s1)}
+	_, _, err = ExecuteInteractiveCmd(HoistRootFlags(CreateAddAccountCmd()), inputs)
+	require.NoError(t, err)
+	accJWT, err = ioutil.ReadFile(filepath.Join(ts.Dir, "store", "O", "accounts", "B", "B.jwt"))
+	require.NoError(t, err)
+	ac, err = jwt.DecodeAccountClaims(string(accJWT))
+	require.NoError(t, err)
+	require.True(t, ac.IsSelfSigned())
+	require.False(t, oc.DidSign(ac))
+}
+
 func Test_AddAccountInteractiveSigningKey(t *testing.T) {
 	ts := NewTestStore(t, "O")
 	defer ts.Done(t)
diff --git a/cmd/addimport_test.go b/cmd/addimport_test.go
index 86ea893..216de07 100644
--- a/cmd/addimport_test.go
+++ b/cmd/addimport_test.go
@@ -399,7 +399,7 @@ func Test_AddImport_LocalImportsInteractive(t *testing.T) {
 	require.Equal(t, apub, ac.Imports[0].Account)
 
 	// B, pick, service q, name q service, local subj qq
-	input = []interface{}{1, true, 2, "q service", "qq"}
+	input = []interface{}{1, true, 2, "q service", "qq", 0}
 	_, _, err = ExecuteInteractiveCmd(cmd, input)
 	require.NoError(t, err)
 
diff --git a/cmd/addoperator_test.go b/cmd/addoperator_test.go
index 19d1eb4..f5382b1 100644
--- a/cmd/addoperator_test.go
+++ b/cmd/addoperator_test.go
@@ -33,7 +33,6 @@ import (
 
 func Test_AddOperator(t *testing.T) {
 	ts := NewEmptyStore(t)
-	t.Log(ts.Dir)
 
 	_, err := os.Lstat(filepath.Join(ts.Dir, "store"))
 	if err != nil && !os.IsNotExist(err) {
diff --git a/cmd/adduser_test.go b/cmd/adduser_test.go
index 4ed9b1f..877c933 100644
--- a/cmd/adduser_test.go
+++ b/cmd/adduser_test.go
@@ -250,7 +250,7 @@ func Test_AddUserWithInteractiveAccountCtx(t *testing.T) {
 	ts.AddAccount(t, "B")
 
 	// adding to user bb to B
-	inputs := []interface{}{1, "bb", true, "0", "0"}
+	inputs := []interface{}{1, "bb", true, "0", "0", 0}
 	cmd := CreateAddUserCmd()
 	HoistRootFlags(cmd)
 	_, _, err := ExecuteInteractiveCmd(cmd, inputs)
@@ -263,7 +263,7 @@ func Test_AddUserWithInteractiveAccountCtx(t *testing.T) {
 	require.Empty(t, uc.IssuerAccount)
 
 	// adding to user aa to A
-	inputs = []interface{}{0, "aa", true, "0", "0"}
+	inputs = []interface{}{0, "aa", true, "0", "0", 0}
 	_, _, err = ExecuteInteractiveCmd(cmd, inputs)
 	require.NoError(t, err)
 	apk := ts.GetAccountPublicKey(t, "A")
@@ -389,6 +389,44 @@ func Test_AddUserWithSigningKeyOnly(t *testing.T) {
 	_, _, err = ExecuteCmd(HoistRootFlags(CreateAddUserCmd()), "--name", "AAA")
 	require.NoError(t, err)
 
-	_, err = ts.Store.ReadUserClaim("A", "AAA")
+	claim, err := ts.Store.ReadUserClaim("A", "AAA")
 	require.NoError(t, err)
+	require.NotEmpty(t, claim.IssuerAccount)
+	require.NotEqual(t, claim.Issuer, claim.IssuerAccount)
+	require.Equal(t, claim.Issuer, pk)
+}
+
+func Test_AddUserWithSigningKeyInteractive(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	// create a signing key
+	kp, err := nkeys.CreateAccount()
+	require.NoError(t, err)
+	_, err = ts.KeyStore.Store(kp)
+	require.NoError(t, err)
+	pk, err := kp.PublicKey()
+	require.NoError(t, err)
+	require.True(t, ts.KeyStore.HasPrivateKey(pk))
+
+	ts.AddAccount(t, "A")
+	_, _, err = ExecuteCmd(createEditAccount(), "--sk", pk)
+	require.NoError(t, err)
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.NotNil(t, ac)
+	require.True(t, ts.KeyStore.HasPrivateKey(ac.Subject))
+
+	inputs := []interface{}{"AAA", true, "0", "0", 1}
+	cmd := CreateAddUserCmd()
+	HoistRootFlags(cmd)
+	_, _, err = ExecuteInteractiveCmd(cmd, inputs)
+	require.NoError(t, err)
+
+	claim, err := ts.Store.ReadUserClaim("A", "AAA")
+	require.NoError(t, err)
+	require.NotEmpty(t, claim.IssuerAccount)
+	require.NotEqual(t, claim.Issuer, claim.IssuerAccount)
+	require.Equal(t, claim.Issuer, pk)
 }
diff --git a/cmd/common_test.go b/cmd/common_test.go
index 2cc9771..7fc3171 100644
--- a/cmd/common_test.go
+++ b/cmd/common_test.go
@@ -406,7 +406,7 @@ func Test_SeedNKeyValidatorMatching(t *testing.T) {
 		{string(as1), true},
 	}
 
-	fun := SeedNKeyValidatorMatching(nkeys.PrefixByteAccount, validPubs)
+	fun := SeedNKeyValidatorMatching(validPubs, nkeys.PrefixByteAccount)
 	for i, kt := range keyTests {
 		err := fun(kt.arg)
 		var failed bool
diff --git a/cmd/deleteexport_test.go b/cmd/deleteexport_test.go
index f3d2a62..3e7fd43 100644
--- a/cmd/deleteexport_test.go
+++ b/cmd/deleteexport_test.go
@@ -66,7 +66,7 @@ func Test_DeleteExportInteractiveManagedStore(t *testing.T) {
 	cmd := createDeleteExportCmd()
 	HoistRootFlags(cmd)
 
-	input := []interface{}{0, 0, ts.GetAccountKeyPath(t, "A")}
+	input := []interface{}{0, 0, 0, ts.GetAccountKeyPath(t, "A")}
 	_, _, err := ExecuteInteractiveCmd(cmd, input)
 	require.NoError(t, err)
 
diff --git a/cmd/edituser_test.go b/cmd/edituser_test.go
index 237caef..b3f2b98 100644
--- a/cmd/edituser_test.go
+++ b/cmd/edituser_test.go
@@ -20,6 +20,8 @@ import (
 	"testing"
 	"time"
 
+	"github.com/nats-io/nkeys"
+
 	cli "github.com/nats-io/cliprompts/v2"
 	"github.com/nats-io/jwt"
 	"github.com/stretchr/testify/require"
@@ -365,3 +367,78 @@ func Test_EditUserBearerToken(t *testing.T) {
 	require.NoError(t, err)
 	require.False(t, u.BearerToken)
 }
+
+func Test_EditUserWithSigningKeyOnly(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	// create a signing key
+	kp, err := nkeys.CreateAccount()
+	require.NoError(t, err)
+	_, err = ts.KeyStore.Store(kp)
+	require.NoError(t, err)
+	pk, err := kp.PublicKey()
+	require.NoError(t, err)
+	require.True(t, ts.KeyStore.HasPrivateKey(pk))
+
+	ts.AddAccount(t, "A")
+	_, _, err = ExecuteCmd(createEditAccount(), "--sk", pk)
+	require.NoError(t, err)
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.NotNil(t, ac)
+	ts.KeyStore.Remove(ac.Subject)
+	require.False(t, ts.KeyStore.HasPrivateKey(ac.Subject))
+
+	_, _, err = ExecuteCmd(HoistRootFlags(CreateAddUserCmd()), "--name", "AAA")
+	require.NoError(t, err)
+	_, _, err = ExecuteCmd(HoistRootFlags(createEditUserCmd()), "--name", "AAA", "--payload", "5")
+	require.NoError(t, err)
+
+	claim, err := ts.Store.ReadUserClaim("A", "AAA")
+	require.NoError(t, err)
+	require.Equal(t, claim.Limits.Payload, int64(5))
+	require.NotEmpty(t, claim.IssuerAccount)
+	require.NotEqual(t, claim.Issuer, claim.IssuerAccount)
+	require.Equal(t, claim.Issuer, pk)
+}
+
+func Test_EditUserWithSigningKeyInteractive(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	// create a signing key
+	kp, err := nkeys.CreateAccount()
+	require.NoError(t, err)
+	_, err = ts.KeyStore.Store(kp)
+	require.NoError(t, err)
+	pk, err := kp.PublicKey()
+	require.NoError(t, err)
+	require.True(t, ts.KeyStore.HasPrivateKey(pk))
+
+	ts.AddAccount(t, "A")
+	_, _, err = ExecuteCmd(createEditAccount(), "--sk", pk)
+	require.NoError(t, err)
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.NotNil(t, ac)
+	require.True(t, ts.KeyStore.HasPrivateKey(ac.Subject))
+
+	_, _, err = ExecuteCmd(HoistRootFlags(CreateAddUserCmd()), "--name", "AAA")
+	require.NoError(t, err)
+
+	inputs := []interface{}{1, "5", "0", "0", false}
+	cmd := createEditUserCmd()
+	HoistRootFlags(cmd)
+	_, _, err = ExecuteInteractiveCmd(cmd, inputs, "--name", "AAA")
+	require.NoError(t, err)
+
+	claim, err := ts.Store.ReadUserClaim("A", "AAA")
+	require.NoError(t, err)
+	require.Equal(t, claim.Limits.Payload, int64(5))
+	require.NotEmpty(t, claim.IssuerAccount)
+	require.NotEqual(t, claim.Issuer, claim.IssuerAccount)
+	require.Equal(t, claim.Issuer, pk)
+}