Repository: nats-io/nsc (go)

Issue:
Add an option to create a scoped signing-key in one command
### What motivated this proposal?

Currently, to create a scoped signing-key, one hase to call `nsc --sk generate`, note the (non machine parseable) ouput, then call the command `nsc --sk KEYJUSTCREATED --role blah`.


### What is the proposed change?

Allow the use of the `--role` parameter when using `nsc --sk generate`, so the key is immediately scoped, and can be referred by its role further in the key deployment process.

### Who benefits from this change?

Using decentralized authentication for NATS is a very good feature. Scoped signing-keys is even better.
The current way the creation is implemented is error prone, because you either depend on one's copy/paste skills, or parsing an stdout output which you have no warranty that it won't change in the future.

### What alternatives have you evaluated?

I need to check the generate return code, and if it is valid, grep / awk / sed the output to get the key argument to pass to the next nsc call. Very inefficient.

Pull request description:
Enhancements to edit signing-key
[FEAT] changed `edit signing-key` to allow `--sk generate`, or infer the sk if the value for the role is given or just the role is given

FIX #633

Patch:
diff --git a/cmd/editscopedsk.go b/cmd/editscopedsk.go
index d9ba332..8d639c1 100644
--- a/cmd/editscopedsk.go
+++ b/cmd/editscopedsk.go
@@ -40,8 +40,8 @@ nsc edit signing-key --account <n> --sk <sk> --allow-sub <subject>,...
 			return RunAction(cmd, args, &params)
 		},
 	}
-	cmd.Flags().StringVarP(&params.skName, "sk", "", "", "signing key to set scope for or role name for already existing scoped signing key")
 	cmd.Flags().StringVarP(&params.role, "role", "", "", "role associated with the signing key scope")
+	params.sk.BindFlagsForOne("sk", "", nkeys.PrefixByteAccount, cmd)
 	params.AccountContextParams.BindFlags(cmd)
 	params.UserPermissionLimits.BindFlags(cmd)
 	return cmd
@@ -52,6 +52,7 @@ func init() {
 }
 
 type EditScopedSkParams struct {
+	sk     SigningKeysParams
 	skName string
 	role   string
 	claim  *jwt.AccountClaims
@@ -66,6 +67,7 @@ func (p *EditScopedSkParams) SetDefaults(ctx ActionCtx) error {
 		return err
 	}
 	p.SignerParams.SetDefaults(nkeys.PrefixByteOperator, true, ctx)
+
 	return nil
 }
 
@@ -80,16 +82,51 @@ func (p *EditScopedSkParams) Load(ctx ActionCtx) error {
 		return err
 	}
 
-	if p.skName == "" {
-		ctx.CurrentCmd().SilenceUsage = false
-		return fmt.Errorf("signing key is required")
-	}
-
 	p.claim, err = ctx.StoreCtx().Store.ReadAccountClaim(p.AccountContextParams.Name)
 	if err != nil {
 		return err
 	}
 
+	if p.sk.Empty() && p.role != "" {
+		kp := keyByRoleName(ctx.StoreCtx().KeyStore, p.claim, p.role)
+		if kp != nil {
+			s, err := kp.PublicKey()
+			if err != nil {
+				return err
+			}
+			p.sk.paths = append(p.sk.paths, s)
+		}
+	}
+
+	// try to normalize the values
+	if err := p.sk.Valid(); err != nil {
+		// if we are not valid, that means we didn't get a sk or "generate"
+		if len(p.sk.paths) > 0 {
+			// try that as a scope role
+			kp := keyByRoleName(ctx.StoreCtx().KeyStore, p.claim, p.sk.paths[0])
+			if kp != nil {
+				s, err := kp.PublicKey()
+				if err != nil {
+					return err
+				}
+				p.skName = s
+			}
+		}
+	}
+
+	if p.skName == "" {
+		pks, err := p.sk.PublicKeys()
+		if err != nil {
+			return err
+		}
+		if len(pks) > 0 {
+			p.skName = pks[0]
+		} else {
+			ctx.CurrentCmd().SilenceUsage = false
+			return fmt.Errorf("signing key is required")
+		}
+	}
+
 	s, found := p.claim.SigningKeys.GetScope(p.skName)
 	if !found {
 		kp := keyByRoleName(ctx.StoreCtx().KeyStore, p.claim, p.skName)
diff --git a/cmd/signingkeyseditor.go b/cmd/signingkeyseditor.go
index 9e65338..7ba4e01 100644
--- a/cmd/signingkeyseditor.go
+++ b/cmd/signingkeyseditor.go
@@ -43,6 +43,17 @@ func (e *SigningKeysParams) BindFlags(flagName string, shorthand string, kind nk
 	}
 }
 
+func (e *SigningKeysParams) BindFlagsForOne(flagName string, shorthand string, kind nkeys.PrefixByte, cmd *cobra.Command) {
+	e.flagName = flagName
+	e.kind = kind
+	if kind == nkeys.PrefixByteCurve {
+		e.paths = make([]string, 1)
+		cmd.Flags().StringVarP(&e.paths[0], flagName, shorthand, "", `curve key or keypath or the value "generate" to generate a curve encryption target key on the fly`)
+	} else {
+		cmd.Flags().StringSliceVarP(&e.paths, flagName, shorthand, nil, `signing key or keypath or the value "generate" to generate a key pair on the fly`)
+	}
+}
+
 func (e *SigningKeysParams) valid(s string) error {
 	if s == "generate" {
 		return nil
@@ -96,6 +107,10 @@ func (e *SigningKeysParams) Valid() error {
 	return nil
 }
 
+func (e *SigningKeysParams) Empty() bool {
+	return len(e.paths) == 0
+}
+
 func (e *SigningKeysParams) PublicKeys() ([]string, error) {
 	var keys []string
 	for _, v := range e.paths {


Test patch:
diff --git a/cmd/editscopedsk_test.go b/cmd/editscopedsk_test.go
index 03f3404..d32f78f 100644
--- a/cmd/editscopedsk_test.go
+++ b/cmd/editscopedsk_test.go
@@ -138,3 +138,85 @@ func Test_EditScopedSk_ResolveAny(t *testing.T) {
 		"--sk", "foo", "--subs", "10")
 	require.Error(t, err)
 }
+
+func Test_EditScopedSkAddGenerates(t *testing.T) {
+	ts := NewTestStore(t, "edit scope")
+	defer ts.Done(t)
+
+	_, err := ts.Store.ReadOperatorClaim()
+	require.NoError(t, err)
+
+	ts.AddAccount(t, "A")
+
+	// add the scope with a generate
+	_, _, err = ExecuteCmd(createEditSkopedSkCmd(), "--sk", "generate")
+	require.NoError(t, err)
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.Len(t, ac.SigningKeys.Keys(), 1)
+	pk := ac.SigningKeys.Keys()[0]
+	scope, ok := ac.SigningKeys.GetScope(pk)
+	require.True(t, ok)
+	us, ok := scope.(*jwt.UserScope)
+	require.True(t, ok)
+	require.NotNil(t, us)
+
+	// get the scope with the key
+	_, _, err = ExecuteCmd(createEditSkopedSkCmd(), "--sk", pk, "--role", "foo")
+	require.NoError(t, err)
+
+	ac, err = ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.Len(t, ac.SigningKeys.Keys(), 1)
+	scope, ok = ac.SigningKeys.GetScope(pk)
+	require.True(t, ok)
+	us, ok = scope.(*jwt.UserScope)
+	require.True(t, ok)
+	require.NotNil(t, us)
+	require.Equal(t, us.Role, "foo")
+}
+
+func Test_EditScopedSkByRole(t *testing.T) {
+	ts := NewTestStore(t, "edit scope")
+	defer ts.Done(t)
+
+	_, err := ts.Store.ReadOperatorClaim()
+	require.NoError(t, err)
+
+	ts.AddAccount(t, "A")
+
+	// add the scope with a generate
+	_, _, err = ExecuteCmd(createEditSkopedSkCmd(), "--sk", "generate", "--role", "foo")
+	require.NoError(t, err)
+
+	// get the scope by saying that the key is the role
+	_, _, err = ExecuteCmd(createEditSkopedSkCmd(), "--sk", "foo", "--allow-pub", ">")
+	require.NoError(t, err)
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.Len(t, ac.SigningKeys.Keys(), 1)
+	scope, ok := ac.SigningKeys.GetScope(ac.SigningKeys.Keys()[0])
+	require.True(t, ok)
+	us, ok := scope.(*jwt.UserScope)
+	require.True(t, ok)
+	require.NotNil(t, us)
+	require.Equal(t, us.Role, "foo")
+	require.Len(t, us.Template.Pub.Allow, 1)
+
+	// get the scope by just specifying the role
+	_, _, err = ExecuteCmd(createEditSkopedSkCmd(), "--role", "foo", "--allow-sub", ">")
+	require.NoError(t, err)
+
+	ac, err = ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.Len(t, ac.SigningKeys.Keys(), 1)
+	scope, ok = ac.SigningKeys.GetScope(ac.SigningKeys.Keys()[0])
+	require.True(t, ok)
+	us, ok = scope.(*jwt.UserScope)
+	require.True(t, ok)
+	require.NotNil(t, us)
+	require.Equal(t, us.Role, "foo")
+	require.Len(t, us.Template.Sub.Allow, 1)
+}