Repository: nats-io/nsc (go)

Issue:
Add a way to get keys from objects for scripting
To facilitate scripting we need a way to have `nsc` write the public key of an object (operator, account, user, activation token). 

For example:
```bash
$ nsc print key ./nsc/accounts/nats/KO/accounts/SYS/SYS.jwt
ABQKAY62555JHCLGRBRQSEAVXI5VSTMZIMLYXDYI6WCYZWD4ISEECZ2Y
```

This would allow scripts to direct output to env vars, passed to helm charts, etc, avoiding manual copy-and-paste in setting up system accounts, etc.


Pull request description:
Add a way to extract fields easily from JWTs in nsc
Fix #264 - added `--json` flag to describe, which will print a json of the body in the JWT. This allows tooling such as jq to easily extract any field in the body of the JWT:
`nsc describe operator --json | jq .iss`
`nsc describe account --json | jq .sub`

Patch:
diff --git a/cmd/describe.go b/cmd/describe.go
index f5d7ff5..4840792 100644
--- a/cmd/describe.go
+++ b/cmd/describe.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018 The NATS Authors
+ * Copyright 2018-2020 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
@@ -16,12 +16,19 @@
 package cmd
 
 import (
+	"bytes"
+	"encoding/base64"
+	"encoding/json"
+	"errors"
+	"fmt"
+
 	"github.com/spf13/cobra"
 )
 
 var Raw bool
 var WideFlag bool
 var Wide = noopNameFilter
+var Json bool
 
 type WideFun = func(a string) string
 
@@ -62,6 +69,28 @@ var describeCmd = &cobra.Command{
 
 func init() {
 	GetRootCmd().AddCommand(describeCmd)
+	describeCmd.PersistentFlags().BoolVarP(&Json, "json", "J", false, "display JWT body as JSON")
 	describeCmd.PersistentFlags().BoolVarP(&WideFlag, "long-ids", "W", false, "display account ids on imports")
 	describeCmd.PersistentFlags().BoolVarP(&Raw, "raw", "R", false, "output the raw JWT (exclusive of long-ids)")
 }
+
+func bodyAsJson(data []byte) ([]byte, error) {
+	chunks := bytes.Split(data, []byte{'.'})
+	if len(chunks) != 3 {
+		return nil, errors.New("data is not a jwt")
+	}
+	body := chunks[1]
+	d, err := base64.RawURLEncoding.DecodeString(string(body))
+	if err != nil {
+		return nil, fmt.Errorf("error decoding base64: %v", err)
+	}
+	m := make(map[string]interface{})
+	if err := json.Unmarshal(d, &m); err != nil {
+		return nil, fmt.Errorf("error parsing json: %v", err)
+	}
+	f, err := json.MarshalIndent(m, "", " ")
+	if err != nil {
+		return nil, fmt.Errorf("error formatting json: %v", err)
+	}
+	return f, nil
+}
diff --git a/cmd/describeaccount.go b/cmd/describeaccount.go
index aeff9c9..a3a35ba 100644
--- a/cmd/describeaccount.go
+++ b/cmd/describeaccount.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018 The NATS Authors
+ * Copyright 2018-2020 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
@@ -51,8 +51,7 @@ type DescribeAccountParams struct {
 
 func (p *DescribeAccountParams) SetDefaults(ctx ActionCtx) error {
 	p.AccountContextParams.Name = NameFlagOrArgument(p.AccountContextParams.Name, ctx)
-	p.AccountContextParams.SetDefaults(ctx)
-	return nil
+	return p.AccountContextParams.SetDefaults(ctx)
 }
 
 func (p *DescribeAccountParams) PreInteractive(ctx ActionCtx) error {
@@ -65,12 +64,17 @@ func (p *DescribeAccountParams) Load(ctx ActionCtx) error {
 	if err = p.AccountContextParams.Validate(ctx); err != nil {
 		return err
 	}
-
-	if Raw {
+	if Json || Raw {
 		p.raw, err = ctx.StoreCtx().Store.ReadRawAccountClaim(p.AccountContextParams.Name)
 		if err != nil {
 			return err
 		}
+		if Json {
+			p.raw, err = bodyAsJson(p.raw)
+			if err != nil {
+				return err
+			}
+		}
 	} else {
 		ac, err := ctx.StoreCtx().Store.ReadAccountClaim(p.AccountContextParams.Name)
 		if err != nil {
@@ -82,16 +86,16 @@ func (p *DescribeAccountParams) Load(ctx ActionCtx) error {
 	return nil
 }
 
-func (p *DescribeAccountParams) Validate(ctx ActionCtx) error {
+func (p *DescribeAccountParams) Validate(_ ActionCtx) error {
 	return nil
 }
 
-func (p *DescribeAccountParams) PostInteractive(ctx ActionCtx) error {
+func (p *DescribeAccountParams) PostInteractive(_ ActionCtx) error {
 	return nil
 }
 
-func (p *DescribeAccountParams) Run(ctx ActionCtx) (store.Status, error) {
-	if Raw {
+func (p *DescribeAccountParams) Run(_ ActionCtx) (store.Status, error) {
+	if Raw || Json {
 		if !IsStdOut(p.outputFile) {
 			var err error
 			p.raw, err = jwt.DecorateJWT(string(p.raw))
diff --git a/cmd/describeoperator.go b/cmd/describeoperator.go
index 204cb0c..1f42263 100644
--- a/cmd/describeoperator.go
+++ b/cmd/describeoperator.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018 The NATS Authors
+ * Copyright 2018-2020 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
@@ -76,18 +76,23 @@ func (p *DescribeOperatorParams) SetDefaults(ctx ActionCtx) error {
 	return nil
 }
 
-func (p *DescribeOperatorParams) PreInteractive(ctx ActionCtx) error {
+func (p *DescribeOperatorParams) PreInteractive(_ ActionCtx) error {
 	return nil
 }
 
 func (p *DescribeOperatorParams) Load(ctx ActionCtx) error {
 	var err error
-
-	if Raw {
+	if Json || Raw {
 		p.raw, err = ctx.StoreCtx().Store.ReadRawOperatorClaim()
 		if err != nil {
 			return err
 		}
+		if Json {
+			p.raw, err = bodyAsJson(p.raw)
+			if err != nil {
+				return err
+			}
+		}
 	} else {
 		oc, err := ctx.StoreCtx().Store.ReadOperatorClaim()
 		if err != nil {
@@ -98,16 +103,16 @@ func (p *DescribeOperatorParams) Load(ctx ActionCtx) error {
 	return nil
 }
 
-func (p *DescribeOperatorParams) Validate(ctx ActionCtx) error {
+func (p *DescribeOperatorParams) Validate(_ ActionCtx) error {
 	return nil
 }
 
-func (p *DescribeOperatorParams) PostInteractive(ctx ActionCtx) error {
+func (p *DescribeOperatorParams) PostInteractive(_ ActionCtx) error {
 	return nil
 }
 
-func (p *DescribeOperatorParams) Run(ctx ActionCtx) (store.Status, error) {
-	if Raw {
+func (p *DescribeOperatorParams) Run(_ ActionCtx) (store.Status, error) {
+	if Raw || Json {
 		if !IsStdOut(p.outputFile) {
 			var err error
 			p.raw, err = jwt.DecorateJWT(string(p.raw))
diff --git a/cmd/describeuser.go b/cmd/describeuser.go
index fdc781c..559dbbc 100644
--- a/cmd/describeuser.go
+++ b/cmd/describeuser.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018 The NATS Authors
+ * Copyright 2018-2020 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
@@ -56,8 +56,7 @@ type DescribeUserParams struct {
 
 func (p *DescribeUserParams) SetDefaults(ctx ActionCtx) error {
 	p.user = NameFlagOrArgument(p.user, ctx)
-	p.AccountContextParams.SetDefaults(ctx)
-	return nil
+	return p.AccountContextParams.SetDefaults(ctx)
 }
 
 func (p *DescribeUserParams) PreInteractive(ctx ActionCtx) error {
@@ -93,11 +92,17 @@ func (p *DescribeUserParams) Load(ctx ActionCtx) error {
 		return fmt.Errorf("user is required")
 	}
 
-	if Raw {
+	if Json || Raw {
 		p.raw, err = ctx.StoreCtx().Store.ReadRawUserClaim(p.AccountContextParams.Name, p.user)
 		if err != nil {
 			return err
 		}
+		if Json {
+			p.raw, err = bodyAsJson(p.raw)
+			if err != nil {
+				return err
+			}
+		}
 	} else {
 		uc, err := ctx.StoreCtx().Store.ReadUserClaim(p.AccountContextParams.Name, p.user)
 		if err != nil {
@@ -108,16 +113,16 @@ func (p *DescribeUserParams) Load(ctx ActionCtx) error {
 	return nil
 }
 
-func (p *DescribeUserParams) Validate(ctx ActionCtx) error {
+func (p *DescribeUserParams) Validate(_ ActionCtx) error {
 	return nil
 }
 
-func (p *DescribeUserParams) PostInteractive(ctx ActionCtx) error {
+func (p *DescribeUserParams) PostInteractive(_ ActionCtx) error {
 	return nil
 }
 
-func (p *DescribeUserParams) Run(ctx ActionCtx) (store.Status, error) {
-	if Raw {
+func (p *DescribeUserParams) Run(_ ActionCtx) (store.Status, error) {
+	if Raw || Json {
 		if !IsStdOut(p.outputFile) {
 			var err error
 			p.raw, err = jwt.DecorateJWT(string(p.raw))


Test patch:
diff --git a/cmd/describeaccount_test.go b/cmd/describeaccount_test.go
index 31de4f5..33163c8 100644
--- a/cmd/describeaccount_test.go
+++ b/cmd/describeaccount_test.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018 The NATS Authors
+ * Copyright 2018-2020 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
@@ -16,6 +16,7 @@
 package cmd
 
 import (
+	"encoding/json"
 	"testing"
 
 	"github.com/nats-io/jwt"
@@ -82,7 +83,7 @@ func TestDescribeAccount_MultipleAccountRequired(t *testing.T) {
 
 	ts.AddAccount(t, "A")
 	ts.AddAccount(t, "B")
-	GetConfig().SetAccount("")
+	require.NoError(t, GetConfig().SetAccount(""))
 
 	_, _, err := ExecuteCmd(createDescribeAccountCmd())
 	require.Error(t, err)
@@ -166,3 +167,20 @@ func TestDescribeAccount_Latency(t *testing.T) {
 	require.NoError(t, err)
 	require.Contains(t, out, "lat (10%)")
 }
+
+func TestDescribeAccount_Json(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "A")
+	out, _, err := ExecuteCmd(rootCmd, "describe", "account", "--json")
+	// reset the global
+	Json = false
+	require.NoError(t, err)
+	m := make(map[string]interface{})
+	err = json.Unmarshal([]byte(out), &m)
+	require.NoError(t, err)
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.Equal(t, ac.Subject, m["sub"])
+}
diff --git a/cmd/describeoperator_test.go b/cmd/describeoperator_test.go
index 98c32dd..38b244b 100644
--- a/cmd/describeoperator_test.go
+++ b/cmd/describeoperator_test.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018 The NATS Authors
+ * Copyright 2018-2020 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
@@ -16,6 +16,7 @@
 package cmd
 
 import (
+	"encoding/json"
 	"testing"
 
 	"github.com/nats-io/jwt"
@@ -114,6 +115,8 @@ func TestDescribeOperator_AccountServerURL(t *testing.T) {
 
 	u := "https://asu.com:1234"
 	oc, err := ts.Store.ReadOperatorClaim()
+	require.NoError(t, err)
+	require.NotNil(t, oc)
 	oc.AccountServerURL = u
 	token, err := oc.Encode(ts.OperatorKey)
 	require.NoError(t, err)
@@ -149,3 +152,20 @@ func TestDescribeOperator_OperatorServiceURLs(t *testing.T) {
 	require.Contains(t, stdout, "nats://localhost:4222")
 	require.Contains(t, stdout, "tls://localhost:4333")
 }
+
+func TestDescribeOperator_Json(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	out, _, err := ExecuteCmd(rootCmd, "describe", "operator", "--json")
+	// reset the global
+	Json = false
+	require.NoError(t, err)
+	m := make(map[string]interface{})
+	err = json.Unmarshal([]byte(out), &m)
+	require.NoError(t, err)
+	oc, err := ts.Store.ReadOperatorClaim()
+	require.NoError(t, err)
+	require.NotNil(t, oc)
+	require.Equal(t, oc.Subject, m["sub"])
+}
diff --git a/cmd/describeuser_test.go b/cmd/describeuser_test.go
index c9785ed..c910aec 100644
--- a/cmd/describeuser_test.go
+++ b/cmd/describeuser_test.go
@@ -1,23 +1,22 @@
 /*
+ * Copyright 2018-2020 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
  *
- *  * 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.
+ * 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 (
+	"encoding/json"
 	"testing"
 
 	"github.com/nats-io/jwt"
@@ -202,3 +201,23 @@ func TestDescribeRawUser(t *testing.T) {
 	require.NoError(t, err)
 	require.Contains(t, stdout, "Issuer Account")
 }
+
+func TestDescribeUser_Json(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "A")
+	ts.AddUser(t, "A", "aa")
+
+	out, _, err := ExecuteCmd(rootCmd, "describe", "user", "--json")
+	// reset the global
+	Json = false
+	require.NoError(t, err)
+	m := make(map[string]interface{})
+	err = json.Unmarshal([]byte(out), &m)
+	require.NoError(t, err)
+	uc, err := ts.Store.ReadUserClaim("A", "aa")
+	require.NoError(t, err)
+	require.NotNil(t, uc)
+	require.Equal(t, uc.Subject, m["sub"])
+}