Repository: nats-io/nsc (go)

Issue:
Have nsc URL operator, account, user take precedence over environment
I observed the behavior of trying to generate a profile, e.g. `nsc generate profile nsc://hub/APP/user` while my nsc environment was set to a different operator. The command failed in the [validation check](https://github.com/nats-io/nsc/blob/main/cmd/generateprofile.go#L295-L314).

Knowing there are several forms of the URL that can be specified, my expectation would be that if all components (operator, account, user) are specified in the URL, it would ignore whatever is set in `nsc env`. If any components are missing, then it would fallback to the env.

Pull request description:
Set initial context of generate profile to the provided nsc url
[FIX] generate profile now sets the current context for the specified nsc:// url

[FEAT] added 'seeds', 'keys', 'names' to the options - this adds the specified values to all the entities affected by the specified nsc url

FIX #508 

Patch:
diff --git a/cmd/exportkeys.go b/cmd/exportkeys.go
index 480c3ef..b078ba8 100644
--- a/cmd/exportkeys.go
+++ b/cmd/exportkeys.go
@@ -138,6 +138,9 @@ func (p *ExportKeysParams) Run(ctx ActionCtx) (store.Status, error) {
 
 	sr := store.NewDetailedReport(true)
 	keys.KeyList, err = p.KeyCollectorParams.Run(ctx)
+	if err != nil {
+		return nil, err
+	}
 	for _, k := range keys.KeyList {
 		var j ExportJob
 		j.description = k.Pub
diff --git a/cmd/generateprofile.go b/cmd/generateprofile.go
index 662e9e8..4ee9814 100644
--- a/cmd/generateprofile.go
+++ b/cmd/generateprofile.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2022 The NATS Authors
+ * Copyright 2018-2023 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
@@ -25,9 +25,8 @@ import (
 	"strings"
 
 	"github.com/nats-io/jwt/v2"
-	"github.com/spf13/cobra"
-
 	"github.com/nats-io/nsc/v2/cmd/store"
+	"github.com/spf13/cobra"
 )
 
 func createProfileCmd() *cobra.Command {
@@ -36,14 +35,15 @@ func createProfileCmd() *cobra.Command {
 		Use:   "profile",
 		Short: "Generate a profile from nsc 'URL' that can be used by tooling",
 		Example: `profile nsc://operator
-profile nsc://operator/account
-profile nsc://operator/account/user
-profile nsc://operator/account/user?operatorSeed&accountSeed&userSeed
-profile nsc://operator/account/user?operatorKey&accountKey&userKey
-profile nsc://operator?key&seed
-profile nsc://operator/account?key&seed
-profile nsc://operator/account/user?key&seed
-profile nsc://operator/account/user?store=/a/.nsc/nats&keystore=/foo/.nkeys
+nsc profile nsc://operator/account
+nsc profile nsc://operator/account/user
+nsc profile nsc://operator/account/user?names&seeds&keys
+nsc profile nsc://operator/account/user?operatorSeed&accountSeed&userSeed
+nsc profile nsc://operator/account/user?operatorKey&accountKey&userKey
+nsc profile nsc://operator?key&seed
+nsc profile nsc://operator/account?key&seed
+nsc profile nsc://operator/account/user?key&seed&name
+nsc profile nsc://operator/account/user?store=/a/.nsc/nats&keystore=/foo/.nkeys
 
 Output of the program looks like:
 {
@@ -62,45 +62,56 @@ store=<dir> that specifies a directory that contains the named operator
 operator, If no prefix (user/account/operator is provided, it targets 
 the last object in the configuration path)
 
+keys - includes the public keys for all entities (same as 
+userKey&accountKey&operatorKey)
+
 [user|account|operator]Seed=<optional public key> - include the seed for 
 user, account, operator, if an argument is provided, the seed for the 
 specified public key is provided - this allows targeting a signing key.
 If no prefix (user/account/operator is provided, it targets the last 
 object in the configuration path)
 
+seeds - includes the private keys for all entities (same as
+userSeed&accountSeed&operatorSeed)
+
 [user|account|operator]Name - includes the friendly name for the for 
 user, account, operator, If no prefix (user/account/operator is provided, 
 it targets the last object in the configuration path)
+
+names - includes the friendly names fro all the entities (same as 
+userName&accountName&operatorName)
 		`,
 
-		Args: cobra.MinimumNArgs(1),
+		Args:         cobra.MinimumNArgs(1),
+		SilenceUsage: true,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			us := args[0]
 			u, err := ParseNscURL(us)
 			if err != nil {
 				return fmt.Errorf("error parsing %q: %w", us, err)
 			}
+			config := GetConfig()
+			oldSR := config.StoreRoot
+			oldOp := config.Operator
+			oldAc := config.Account
+			defer func() {
+				_ = config.setStoreRoot(oldSR)
+				if oldOp != "" {
+					_ = config.SetOperator(oldOp)
+				}
+				if oldAc != "" {
+					_ = config.SetAccount(oldAc)
+				}
+			}()
+
 			params.nscu = u
 			q, err := u.query()
 			if err != nil {
 				return fmt.Errorf("error parsing query %q: %w", us, err)
 			}
 			if len(q) > 0 {
-				config := GetConfig()
-				oldSR := config.StoreRoot
-				oldOp := config.Operator
-				oldAc := config.Account
 				v, ok := q["store"]
 				if ok {
-					defer func() {
-						_ = config.setStoreRoot(oldSR)
-						if oldOp != "" {
-							_ = config.SetOperator(oldOp)
-						}
-						if oldAc != "" {
-							_ = config.SetAccount(oldAc)
-						}
-					}()
 					sr, err := Expand(v)
 					if err != nil {
 						return err
@@ -122,6 +133,16 @@ it targets the last object in the configuration path)
 					store.KeyStorePath = ks
 				}
 			}
+			if u.operator != "" {
+				if err := config.SetOperator(u.operator); err != nil {
+					return err
+				}
+			}
+			if u.account != "" {
+				if err := config.SetAccount(u.account); err != nil {
+					return err
+				}
+			}
 			return RunAction(cmd, args, &params)
 		},
 		Hidden: true,
@@ -143,14 +164,17 @@ const (
 	accountKey   Arg = "accountkey"
 	userKey      Arg = "userkey"
 	key          Arg = "key"
+	allKeys      Arg = "keys"
 	operatorSeed Arg = "operatorseed"
 	accountSeed  Arg = "accountseed"
 	userSeed     Arg = "userseed"
 	seed         Arg = "seed"
+	allSeeds     Arg = "seeds"
 	operatorName Arg = "operatorname"
 	accountName  Arg = "accountname"
 	userName     Arg = "username"
 	name         Arg = "name"
+	allNames     Arg = "names"
 	keystoreDir  Arg = "keystore"
 	storeDir     Arg = "store"
 )
@@ -457,16 +481,17 @@ func (p *ProfileCmdParams) addKeys() error {
 	if len(q) == 0 {
 		return nil
 	}
+	_, keys := q[allKeys]
 	_, ok := q[operatorKey]
-	if ok {
+	if ok || keys {
 		p.addOperatorKeys()
 	}
 	_, ok = q[accountKey]
-	if ok {
+	if ok || keys {
 		p.addAccountKeys()
 	}
 	_, ok = q[userKey]
-	if ok {
+	if ok || keys {
 		p.addUserKeys()
 	}
 	_, ok = q[key]
@@ -574,22 +599,23 @@ func (p *ProfileCmdParams) addSeeds(ctx ActionCtx) error {
 	if len(q) == 0 {
 		return nil
 	}
+	_, seeds := q[allSeeds]
 	v, ok := q[operatorSeed]
-	if ok {
+	if ok || seeds {
 		err := p.addOperatorSeed(ctx, v)
 		if err != nil {
 			return err
 		}
 	}
 	v, ok = q[accountSeed]
-	if ok {
+	if ok || seeds {
 		err := p.addAccountSeed(ctx, v)
 		if err != nil {
 			return err
 		}
 	}
 	v, ok = q[userSeed]
-	if ok {
+	if ok || seeds {
 		err := p.addUserSeed(ctx, v)
 		if err != nil {
 			return err
@@ -602,12 +628,14 @@ func (p *ProfileCmdParams) addSeeds(ctx ActionCtx) error {
 			if err != nil {
 				return err
 			}
-		} else if p.nscu.account != "" {
+		}
+		if p.nscu.account != "" {
 			err := p.addAccountSeed(ctx, "")
 			if err != nil {
 				return err
 			}
-		} else {
+		}
+		if p.nscu.operator != "" {
 			err := p.addOperatorSeed(ctx, "")
 			if err != nil {
 				return err
@@ -644,25 +672,28 @@ func (p *ProfileCmdParams) addNames() error {
 		return nil
 	}
 
+	_, names := q[allNames]
 	_, ok := q[operatorName]
-	if ok {
+	if ok || names {
 		p.addOperatorName()
 	}
 	_, ok = q[accountName]
-	if ok {
+	if ok || names {
 		p.addAccountName()
 	}
 	_, ok = q[userName]
-	if ok {
+	if ok || names {
 		p.addUserName()
 	}
 	_, ok = q[name]
 	if ok {
 		if p.nscu.user != "" {
 			p.addUserName()
-		} else if p.nscu.account != "" {
+		}
+		if p.nscu.account != "" {
 			p.addAccountName()
-		} else {
+		}
+		if p.nscu.operator != "" {
 			p.addOperatorName()
 		}
 	}


Test patch:
diff --git a/cmd/generateprofile_test.go b/cmd/generateprofile_test.go
index 9ddea33..2d64de4 100644
--- a/cmd/generateprofile_test.go
+++ b/cmd/generateprofile_test.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2022 The NATS Authors
+ * Copyright 2018-2023 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
@@ -19,11 +19,12 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/url"
+	"os"
 	"path"
+	"path/filepath"
 	"testing"
 
 	"github.com/nats-io/jwt/v2"
-
 	"github.com/stretchr/testify/require"
 )
 
@@ -117,14 +118,16 @@ func Test_ProfileIDs(t *testing.T) {
 	u := ts.GetUserPublicKey(t, "A", "U")
 
 	out := path.Join(ts.Dir, "out.json")
-	nu := fmt.Sprintf("nsc://%s/%s/%s?operatorName&accountName&userName", o, a, u)
-	_, _, err := ExecuteCmd(createProfileCmd(), "-o", out, nu)
+	_, _, err := ExecuteCmd(createProfileCmd(), "-o", out, "nsc://O/A/U?keys&names")
 	require.NoError(t, err)
 
 	r := loadResults(t, out)
 	require.Equal(t, "O", r.Operator.Name)
+	require.Equal(t, o, r.Operator.Key)
 	require.Equal(t, "A", r.Account.Name)
+	require.Equal(t, a, r.Account.Key)
 	require.Equal(t, "U", r.User.Name)
+	require.Equal(t, u, r.User.Key)
 }
 
 func Test_ProfileSeedIDs(t *testing.T) {
@@ -133,7 +136,8 @@ func Test_ProfileSeedIDs(t *testing.T) {
 
 	// add a signing key
 	osk, opk, okp := CreateOperatorKey(t)
-	ts.KeyStore.Store(okp)
+	_, err := ts.KeyStore.Store(okp)
+	require.NoError(t, err)
 	oc, err := ts.Store.ReadOperatorClaim()
 	require.NoError(t, err)
 	kp, err := ts.KeyStore.GetKeyPair(oc.Issuer)
@@ -148,12 +152,8 @@ func Test_ProfileSeedIDs(t *testing.T) {
 	ts.AddAccount(t, "A")
 	ts.AddUser(t, "A", "U")
 
-	o := ts.GetOperatorPublicKey(t)
-	a := ts.GetAccountPublicKey(t, "A")
-	u := ts.GetUserPublicKey(t, "A", "U")
-
 	out := path.Join(ts.Dir, "out.json")
-	nu := fmt.Sprintf("nsc://%s/%s/%s?operatorSeed=%s", o, a, u, opk)
+	nu := fmt.Sprintf("nsc://O/A/U?operatorSeed=%s", opk)
 	_, _, err = ExecuteCmd(createProfileCmd(), "-o", out, nu)
 	require.NoError(t, err)
 
@@ -185,7 +185,7 @@ func Test_ProfileStoreAndKeysDir(t *testing.T) {
 
 	out := path.Join(ts.Dir, "out.json")
 
-	u := fmt.Sprintf("nsc://O/A/U?operatorName&accountName&userName&operatorKey&accountKey&userKey&store=%s&keyStore=%s", ts.StoreDir, ts.KeysDir)
+	u := fmt.Sprintf("nsc://O/A/U?names&keys&store=%s&keyStore=%s", ts.StoreDir, ts.KeysDir)
 
 	_, _, err = ExecuteCmd(rootCmd, "generate", "profile", "-o", out, u)
 	require.NoError(t, err)
@@ -294,3 +294,50 @@ func TestKey_ProfileBasics(t *testing.T) {
 		ts.Done(t)
 	}
 }
+
+func loadNscEnvProfile(t *testing.T, path string) *Profile {
+	data, err := os.ReadFile(path)
+	require.NoError(t, err)
+	p := Profile{}
+	require.NoError(t, json.Unmarshal(data, &p))
+	return &p
+}
+
+func TestGenerateProfile_MultipleOperators(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	ts.AddAccount(t, "A")
+	ts.AddUser(t, "A", "U")
+
+	ts.AddOperator(t, "OO")
+	ts.AddAccount(t, "A")
+	ts.AddUser(t, "A", "U")
+
+	out := filepath.Join(ts.Dir, "profile.json")
+	_, _, err := ExecuteCmd(createProfileCmd(), "--output-file", out, "nsc://O/A/U")
+	require.NoError(t, err)
+	profile := loadNscEnvProfile(t, out)
+	require.Contains(t, profile.UserCreds, "/O/A/U.creds")
+}
+
+func TestGenerateProfile_NamesSeedsKeys(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	ts.AddAccount(t, "A")
+	ts.AddUser(t, "A", "U")
+
+	out := filepath.Join(ts.Dir, "profile.json")
+	_, _, err := ExecuteCmd(createProfileCmd(), "--output-file", out, "nsc://O/A/U?&names&seeds&keys")
+	require.NoError(t, err)
+
+	profile := loadNscEnvProfile(t, out)
+	require.NotEmpty(t, profile.Operator.Name)
+	require.NotEmpty(t, profile.Operator.Key)
+	require.NotEmpty(t, profile.Operator.Seed)
+	require.NotEmpty(t, profile.Account.Name)
+	require.NotEmpty(t, profile.Account.Key)
+	require.NotEmpty(t, profile.Account.Seed)
+	require.NotEmpty(t, profile.User.Name)
+	require.NotEmpty(t, profile.User.Key)
+	require.NotEmpty(t, profile.User.Seed)
+}