Repository: nats-io/nsc
Pull request #415 (2021-07-13 17:01:49)

[adding] support for scoped signing key
Signed-off-by: Matthias Hanel <mh@synadia.com>

I believe user still have to improve the user


Patch:
diff --git a/cmd/describer.go b/cmd/describer.go
index 13444a0..e5db3f6 100644
--- a/cmd/describer.go
+++ b/cmd/describer.go
@@ -162,6 +162,17 @@ func (a *AccountDescriber) Describe() string {
 		buf.WriteString(NewMappingsDescriber(a.Mappings).Describe())
 	}
 
+	if len(a.SigningKeys) > 0 {
+		for _, v := range a.SigningKeys {
+			if v == nil {
+				continue
+			}
+			buf.WriteString("\n")
+			buf.WriteString(NewScopedSkDescriber(v.(*jwt.UserScope)).Describe())
+		}
+		table.AddSeparator()
+	}
+
 	return buf.String()
 }
 
@@ -255,6 +266,29 @@ func (i *MappingsDescriber) Describe() string {
 	return table.Render()
 }
 
+type ScopedSkDescriber jwt.UserScope
+
+func NewScopedSkDescriber(m *jwt.UserScope) *ScopedSkDescriber {
+	return (*ScopedSkDescriber)(m)
+}
+
+func (s *ScopedSkDescriber) Describe() string {
+	var buf bytes.Buffer
+	buf.WriteString("\n")
+	table := tablewriter.CreateTable()
+	table.AddTitle("Scoped Signing Key - Details")
+	table.AddRow("Key", s.Key)
+	table.AddRow("role", s.Role)
+	AddPermissions(table, s.Template.Permissions)
+	AddLimits(table, s.Template.Limits)
+	table.AddRow("Bearer Token", toYesNo(s.Template.BearerToken))
+	if len(s.Template.AllowedConnectionTypes) > 0 {
+		table.AddSeparator()
+		AddListValues(table, "Allowed Connection Types", s.Template.AllowedConnectionTypes)
+	}
+	return table.Render()
+}
+
 type ImportsDescriber struct {
 	jwt.Imports
 }
@@ -481,7 +515,7 @@ func (u *UserDescriber) Describe() string {
 		AddListValues(table, "Tags", u.Tags)
 	}
 
-	if len(u.Tags) > 0 {
+	if len(u.AllowedConnectionTypes) > 0 {
 		table.AddSeparator()
 		AddListValues(table, "Allowed Connection Types", u.AllowedConnectionTypes)
 	}
diff --git a/cmd/editscopedsk.go b/cmd/editscopedsk.go
new file mode 100644
index 0000000..3bda4a4
--- /dev/null
+++ b/cmd/editscopedsk.go
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2018-2021 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"
+	"github.com/nats-io/jwt/v2"
+	"github.com/nats-io/nkeys"
+	"github.com/nats-io/nsc/cmd/store"
+	"github.com/spf13/cobra"
+)
+
+func createEditSkopedSkCmd() *cobra.Command {
+	var params EditScopedSkParams
+	cmd := &cobra.Command{
+		Use:   "signing-key",
+		Short: "Edit a scoped signing key",
+		Long: `# Edit permissions associated with the account (n) signing key (sk):
+nsc edit signing-key --account <n> --sk <sk> --allow-pubsub <subject>,...
+nsc edit signing-key --account <n> --sk <sk> --allow-pub <subject>,...
+nsc edit signing-key --account <n> --sk <sk> --allow-sub <subject>,...
+`,
+		Args:         cobra.MaximumNArgs(1),
+		SilenceUsage: true,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return RunAction(cmd, args, &params)
+		},
+	}
+	cmd.Flags().StringVarP(&params.skName, "sk", "", "", "signing key to set scope for")
+	cmd.Flags().StringVarP(&params.role, "role", "", "", "role associated with the signing key scope")
+	params.AccountContextParams.BindFlags(cmd)
+	params.UserPermissionLimits.BindFlags(cmd)
+	return cmd
+}
+
+func init() {
+	editCmd.AddCommand(createEditSkopedSkCmd())
+}
+
+type EditScopedSkParams struct {
+	skName string
+	role   string
+	claim  *jwt.AccountClaims
+	UserPermissionLimits
+	AccountContextParams
+	SignerParams
+}
+
+func (p *EditScopedSkParams) SetDefaults(ctx ActionCtx) error {
+	p.skName = NameFlagOrArgument(p.skName, ctx)
+	if err := p.AccountContextParams.SetDefaults(ctx); err != nil {
+		return err
+	}
+	p.SignerParams.SetDefaults(nkeys.PrefixByteAccount, true, ctx)
+	return nil
+}
+
+func (p *EditScopedSkParams) PreInteractive(ctx ActionCtx) error {
+	return nil
+}
+
+func (p *EditScopedSkParams) Load(ctx ActionCtx) error {
+	var err error
+
+	if err = p.AccountContextParams.Validate(ctx); err != nil {
+		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
+	}
+
+	s, found := p.claim.SigningKeys.GetScope(p.skName)
+	if !found {
+		return fmt.Errorf("signing-key not found")
+	}
+	if s == nil {
+		s = &jwt.UserScope{}
+	}
+	return p.UserPermissionLimits.Load(ctx, s.(*jwt.UserScope).Template)
+}
+
+func (p *EditScopedSkParams) PostInteractive(ctx ActionCtx) error {
+	return nil
+}
+
+func (p *EditScopedSkParams) Validate(ctx ActionCtx) error {
+	p.UserPermissionLimits.Validate(ctx)
+
+	if err := p.SignerParams.ResolveWithPriority(ctx, p.claim.Issuer); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (p *EditScopedSkParams) Run(ctx ActionCtx) (store.Status, error) {
+	r := store.NewDetailedReport(true)
+	r.ReportSum = false
+	scope, _ := p.claim.SigningKeys.GetScope(p.skName)
+	if scope == nil {
+		scope = jwt.NewUserScope()
+		scope.(*jwt.UserScope).Key = p.skName
+	}
+	if ctx.AnySet("role") {
+		scope.(*jwt.UserScope).Role = p.role
+	}
+	s, err := p.UserPermissionLimits.Run(ctx, &(scope.(*jwt.UserScope).Template))
+	if err != nil {
+		return nil, err
+	}
+	if s != nil {
+		r.Add(s.Details...)
+	}
+	p.claim.SigningKeys.AddScopedSigner(scope)
+
+	// we sign
+	token, err := p.claim.Encode(p.signerKP)
+	if err != nil {
+		return nil, err
+	}
+
+	// if the signer is not allowed, the store will reject
+	rs, err := ctx.StoreCtx().Store.StoreClaim([]byte(token))
+	if rs != nil {
+		r.Add(rs)
+	}
+	if err != nil {
+		r.AddFromError(err)
+	}
+	if r.HasNoErrors() {
+		r.AddOK("edited signing key %q", p.skName)
+	}
+	return r, nil
+}


Test patch:
diff --git a/cmd/editscopedsk_test.go b/cmd/editscopedsk_test.go
new file mode 100644
index 0000000..dd12f5d
--- /dev/null
+++ b/cmd/editscopedsk_test.go
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2018-2021 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 (
+	"github.com/nats-io/jwt/v2"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+func Test_EditScopedSk_NotFound(t *testing.T) {
+	ts := NewTestStore(t, "edit scope")
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "A")
+
+	_, _, err := ExecuteCmd(createEditSkopedSkCmd(), "--account", "not there")
+	require.Error(t, err)
+
+	_, _, err = ExecuteCmd(createEditSkopedSkCmd(), "--account", "A", "--sk", "not there")
+	require.Error(t, err)
+}
+
+func Test_EditScopedSk_Subs(t *testing.T) {
+	ts := NewTestStore(t, "edit scope")
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "A")
+	_, pk, _ := CreateAccountKey(t)
+	_, pk2, _ := CreateAccountKey(t)
+
+	_, _, err := ExecuteCmd(createEditAccount(), "--sk", pk, "--sk", pk2)
+	require.NoError(t, err)
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.Contains(t, ac.SigningKeys, pk)
+	require.Contains(t, ac.SigningKeys, pk2)
+
+	_, _, err = ExecuteCmd(createEditSkopedSkCmd(), "--account", "A", "--sk", pk2, "--subs", "5", "--role", "foo",
+		"--allow-pub", "foo", "--allow-sub", "foo", "--deny-sub", "bar", "--conn-type", "LEAFNODE", "--data", "5kib", "--bearer")
+	require.NoError(t, err)
+
+	ac, err = ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.Contains(t, ac.SigningKeys, pk)
+	s, ok := ac.SigningKeys.GetScope(pk)
+	require.True(t, ok)
+	require.Nil(t, s)
+	require.Contains(t, ac.SigningKeys, pk2)
+	s, ok = ac.SigningKeys.GetScope(pk2)
+	require.True(t, ok)
+	require.NotNil(t, s)
+	us := s.(*jwt.UserScope)
+	require.Equal(t, us.Template.Subs, int64(5))
+	require.Equal(t, us.Template.Data, int64(5*1024))
+	require.True(t, us.Template.AllowedConnectionTypes.Contains("LEAFNODE"))
+	require.True(t, us.Template.Sub.Allow.Contains("foo"))
+	require.True(t, us.Template.Sub.Deny.Contains("bar"))
+	require.True(t, us.Template.Pub.Allow.Contains("foo"))
+	require.True(t, us.Template.BearerToken)
+	require.Equal(t, us.Role, "foo")
+}