Repository: nats-io/nsc
Pull request #413 (2021-07-12 18:48:37)

[added] support for mappings
Signed-off-by: Matthias Hanel <mh@synadia.com>


Patch:
diff --git a/cmd/addmapping.go b/cmd/addmapping.go
new file mode 100644
index 0000000..e7c5d38
--- /dev/null
+++ b/cmd/addmapping.go
@@ -0,0 +1,160 @@
+/*
+ * Copyright 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 (
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/nats-io/jwt/v2"
+	"github.com/nats-io/nkeys"
+	"github.com/nats-io/nsc/cmd/store"
+	"github.com/spf13/cobra"
+)
+
+func createAddMappingCmd() *cobra.Command {
+	var params AddMappingParams
+	cmd := &cobra.Command{
+		Use:          "mapping",
+		Short:        "Add (or modify) a mapping entry",
+		Args:         MaxArgs(0),
+		Example:      params.longHelp(),
+		SilenceUsage: true,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return RunAction(cmd, args, &params)
+		},
+	}
+	cmd.Flags().StringVarP((*string)(&params.from), "from", "f", "", "map from subject (required)")
+	cmd.Flags().StringVarP((*string)(&params.to.Subject), "to", "t", "", "to subject (required)")
+	cmd.Flags().Uint8VarP(&params.to.Weight, "weight", "", 0, "weight [1-100] of this mapping entry (default: 100)")
+	cmd.Flags().StringVarP(&params.to.Cluster, "cluster", "", "", "in which cluster this mapping should apply")
+	params.AccountContextParams.BindFlags(cmd)
+
+	return cmd
+}
+
+func init() {
+	addCmd.AddCommand(createAddMappingCmd())
+}
+
+type AddMappingParams struct {
+	AccountContextParams
+	SignerParams
+	from  jwt.Subject
+	to    jwt.WeightedMapping
+	claim *jwt.AccountClaims
+}
+
+func (p *AddMappingParams) longHelp() string {
+	s := `toolName add mapping --from "a" --to "b"
+# to modify an entry, say to set a weight after the fact
+toolName add mapping --from "a" --to "b" --weight 50
+# to add two entries from one subject, set weights and execute multiple times
+toolName add mapping --from "a" --to "c" --weight 50
+`
+	return strings.Replace(s, "toolName", GetToolName(), -1)
+}
+
+func (p *AddMappingParams) SetDefaults(ctx ActionCtx) error {
+	if err := p.AccountContextParams.SetDefaults(ctx); err != nil {
+		return err
+	}
+	p.SignerParams.SetDefaults(nkeys.PrefixByteOperator, true, ctx)
+
+	return nil
+}
+
+func (p *AddMappingParams) PreInteractive(ctx ActionCtx) error {
+	return nil
+}
+
+func (p *AddMappingParams) Load(ctx ActionCtx) error {
+	var err error
+
+	if err = p.AccountContextParams.Validate(ctx); err != nil {
+		return err
+	}
+
+	p.claim, err = ctx.StoreCtx().Store.ReadAccountClaim(p.AccountContextParams.Name)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (p *AddMappingParams) PostInteractive(_ ActionCtx) error {
+	return nil
+}
+
+func (p *AddMappingParams) Validate(ctx ActionCtx) error {
+
+	var err error
+	if p.from == "" {
+		ctx.CurrentCmd().SilenceUsage = false
+		return errors.New("from subject is required")
+	}
+	if p.to.Subject == "" {
+		return errors.New("to subject is required")
+	}
+
+	if p.claim.Mappings == nil {
+		p.claim.Mappings = jwt.Mapping{}
+	}
+
+	m := p.to
+	if v, ok := (p.claim.Mappings)[p.from]; ok {
+		set := false
+		for i, w := range v {
+			if w.Subject == m.Subject {
+				v[i] = m
+				set = true
+				break
+			}
+		}
+		if !set {
+			p.claim.Mappings[p.from] = append(v, m)
+		}
+	} else {
+		p.claim.Mappings[p.from] = []jwt.WeightedMapping{m}
+	}
+	var vr jwt.ValidationResults
+	p.claim.Mappings.Validate(&vr)
+	if vr.IsBlocking(true) {
+		return fmt.Errorf("mapping validation failed: %v", vr.Errors())
+	}
+
+	if err = p.SignerParams.Resolve(ctx); err != nil {
+		return fmt.Errorf("mapping %s", err)
+	}
+
+	return nil
+}
+
+func (p *AddMappingParams) Run(ctx ActionCtx) (store.Status, error) {
+	token, err := p.claim.Encode(p.signerKP)
+	if err != nil {
+		return nil, err
+	}
+
+	r := store.NewDetailedReport(false)
+	StoreAccountAndUpdateStatus(ctx, token, r)
+	if r.HasNoErrors() {
+		r.AddOK("added mapping %s -> %s, weight %d%%", p.from, p.to.Subject, p.to.GetWeight())
+	}
+	return r, err
+}
diff --git a/cmd/deletemapping.go b/cmd/deletemapping.go
new file mode 100644
index 0000000..c5aa6b3
--- /dev/null
+++ b/cmd/deletemapping.go
@@ -0,0 +1,141 @@
+/*
+ * Copyright 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 createDeleteMappingCmd() *cobra.Command {
+	var params DeleteMappingParams
+	cmd := &cobra.Command{
+		Use:          "mapping",
+		Short:        "Delete a mapping",
+		Args:         MaxArgs(0),
+		SilenceUsage: true,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return RunAction(cmd, args, &params)
+		},
+	}
+	cmd.Flags().StringVarP((*string)(&params.from), "from", "f", "", "map from subject (required)")
+	cmd.Flags().StringVarP((*string)(&params.to), "to", "t", "", "to subject. When present, only that particular mapping is removed. Otherwise all mappings for from subject are.")
+	params.AccountContextParams.BindFlags(cmd)
+	return cmd
+}
+
+func init() {
+	deleteCmd.AddCommand(createDeleteMappingCmd())
+}
+
+type DeleteMappingParams struct {
+	AccountContextParams
+	SignerParams
+	claim *jwt.AccountClaims
+	from  jwt.Subject
+	to    jwt.Subject
+}
+
+func (p *DeleteMappingParams) SetDefaults(ctx ActionCtx) error {
+	p.AccountContextParams.SetDefaults(ctx)
+	p.SignerParams.SetDefaults(nkeys.PrefixByteOperator, true, ctx)
+	return nil
+}
+
+func (p *DeleteMappingParams) PreInteractive(ctx ActionCtx) error {
+	var err error
+	if err = p.AccountContextParams.Edit(ctx); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (p *DeleteMappingParams) Load(ctx ActionCtx) error {
+	var err error
+
+	if err = p.AccountContextParams.Validate(ctx); err != nil {
+		return err
+	}
+
+	p.claim, err = ctx.StoreCtx().Store.ReadAccountClaim(p.AccountContextParams.Name)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (p *DeleteMappingParams) PostInteractive(ctx ActionCtx) error {
+	return nil
+}
+
+func (p *DeleteMappingParams) Validate(ctx ActionCtx) error {
+	if p.from == "" {
+		return fmt.Errorf("from subject is required")
+	}
+	if err := p.SignerParams.Resolve(ctx); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (p *DeleteMappingParams) Run(ctx ActionCtx) (store.Status, error) {
+	r := store.NewDetailedReport(true)
+	if p.to != "" {
+		list := p.claim.Mappings[p.from]
+		for i, l := range list {
+			if l.Subject == p.to {
+				if i == 0 {
+					if len(list) == 1 {
+						delete(p.claim.Mappings, p.from)
+					} else {
+						p.claim.Mappings[p.from] = list[1:]
+					}
+				} else {
+					p.claim.Mappings[p.from] = append(list[0:i], list[i+i:]...)
+				}
+				break
+			}
+		}
+	}
+	if p.to == "" || len(p.claim.Mappings[p.from]) == 0 {
+		delete(p.claim.Mappings, p.from)
+	}
+	if len(p.claim.Mappings) == 0 {
+		p.claim.Mappings = nil
+	}
+	token, err := p.claim.Encode(p.signerKP)
+	if err != nil {
+		return nil, err
+	}
+	if p.to == "" {
+		r.AddOK("deleted all mapping for %s", p.from)
+	} else {
+		r.AddOK("deleted mapping %s -> %s", p.from, p.to)
+	}
+	rs, err := ctx.StoreCtx().Store.StoreClaim([]byte(token))
+	if rs != nil {
+		r.Add(rs)
+	}
+	if err != nil {
+		r.AddFromError(err)
+	}
+	return r, nil
+}
diff --git a/cmd/describer.go b/cmd/describer.go
index b4ec3ab..13444a0 100644
--- a/cmd/describer.go
+++ b/cmd/describer.go
@@ -157,6 +157,11 @@ func (a *AccountDescriber) Describe() string {
 		buf.WriteString(NewImportsDescriber(a.Imports).Describe())
 	}
 
+	if len(a.Mappings) > 0 {
+		buf.WriteString("\n")
+		buf.WriteString(NewMappingsDescriber(a.Mappings).Describe())
+	}
+
 	return buf.String()
 }
 
@@ -224,6 +229,32 @@ func (e *ExportsDescriber) Describe() string {
 	return ret
 }
 
+type MappingsDescriber jwt.Mapping
+
+func NewMappingsDescriber(m jwt.Mapping) *MappingsDescriber {
+	d := MappingsDescriber(m)
+	return &d
+}
+
+func (i *MappingsDescriber) Describe() string {
+	table := tablewriter.CreateTable()
+	table.AddTitle("Mappings")
+	table.AddHeaders("From", "To", "Weight (%)")
+	for k, v := range *i {
+		wSum := uint8(0)
+		for i, m := range v {
+			wSum += m.GetWeight()
+			if i == 0 {
+				table.AddRow(k, m.Subject, m.GetWeight())
+			} else {
+				table.AddRow("", m.Subject, m.Weight)
+			}
+		}
+		table.AddRow("", "", fmt.Sprintf("sum=%d", wSum))
+	}
+	return table.Render()
+}
+
 type ImportsDescriber struct {
 	jwt.Imports
 }


Test patch:
diff --git a/cmd/deletemapping_test.go b/cmd/deletemapping_test.go
new file mode 100644
index 0000000..09a4b38
--- /dev/null
+++ b/cmd/deletemapping_test.go
@@ -0,0 +1,79 @@
+/*
+ * Copyright 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 (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func Test_DeleteMapping(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "A")
+	_, _, err := ExecuteCmd(createAddMappingCmd(), "--account", "A", "--from", "from1", "--to", "to1", "--weight", "50")
+	require.NoError(t, err)
+	_, _, err = ExecuteCmd(createAddMappingCmd(), "--account", "A", "--from", "from1", "--to", "to2", "--weight", "50")
+	require.NoError(t, err)
+	_, _, err = ExecuteCmd(createAddMappingCmd(), "--account", "A", "--from", "from2", "--to", "to1", "--weight", "50")
+	require.NoError(t, err)
+	_, _, err = ExecuteCmd(createAddMappingCmd(), "--account", "A", "--from", "from2", "--to", "to2", "--weight", "50")
+	require.NoError(t, err)
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.NotNil(t, ac)
+	require.Len(t, ac.Mappings, 2)
+	require.Len(t, ac.Mappings["from1"], 2)
+	require.Len(t, ac.Mappings["from2"], 2)
+
+	// remove all mappings for from1
+	_, _, err = ExecuteCmd(createDeleteMappingCmd(), "--account", "A", "--from", "from1")
+	require.NoError(t, err)
+	ac, err = ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.NotNil(t, ac)
+	require.Len(t, ac.Mappings, 1)
+	require.Len(t, ac.Mappings["from2"], 2)
+
+	// remove particular mapping to1 from from2
+	_, _, err = ExecuteCmd(createDeleteMappingCmd(), "--account", "A", "--from", "from2", "--to", "to1")
+	require.NoError(t, err)
+	ac, err = ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.NotNil(t, ac)
+	require.Len(t, ac.Mappings, 1)
+	require.Len(t, ac.Mappings["from2"], 1)
+
+	// remove non existing mapping to3 from from2
+	_, _, err = ExecuteCmd(createDeleteMappingCmd(), "--account", "A", "--from", "from2", "--to", "to3")
+	require.NoError(t, err)
+	ac, err = ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.NotNil(t, ac)
+	require.Len(t, ac.Mappings, 1)
+	require.Len(t, ac.Mappings["from2"], 1)
+
+	// remove particular mapping to2 from from2
+	_, _, err = ExecuteCmd(createDeleteMappingCmd(), "--account", "A", "--from", "from2", "--to", "to2")
+	require.NoError(t, err)
+	ac, err = ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.NotNil(t, ac)
+	require.Len(t, ac.Mappings, 0)
+}