Repository: nats-io/nsc (go)

Issue:
edit user doesn't provide a way to set data or sub limits

Pull request:
user limits
- [feat] added --data and --subs flags to set the data and sub limits on the user
- [fix] fixed an issue where payload was not honoring entries like "5M" or "5K"
- [chore] added test to the dataparams
- [chore] fixed some linter warnings
FIX #362

Patch:
diff --git a/cmd/describer.go b/cmd/describer.go
index 3123f6e..35d1cc3 100644
--- a/cmd/describer.go
+++ b/cmd/describer.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2019 The NATS Authors
+ * 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
@@ -347,6 +347,20 @@ func AddLimits(table *tablewriter.Table, lim jwt.Limits) {
 		table.AddRow("Max Msg Payload", "Unlimited")
 	}
 
+	if lim.Data > 0 {
+		v := fmt.Sprintf("%d bytes (≈%s)", lim.Data, humanize.Bytes(uint64(lim.Data)))
+		table.AddRow("Max Data", v)
+	} else {
+		table.AddRow("Max Data", "Unlimited")
+	}
+
+	if lim.Subs > 0 {
+		v := fmt.Sprintf("%d", lim.Subs)
+		table.AddRow("Max Subs", v)
+	} else {
+		table.AddRow("Max Subs", "Unlimited")
+	}
+
 	if len(lim.Src) != 0 {
 		table.AddRow("Network Src", lim.Src)
 	} else {
diff --git a/cmd/edituser.go b/cmd/edituser.go
index e647284..1f2d681 100644
--- a/cmd/edituser.go
+++ b/cmd/edituser.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2020 The NATS Authors
+ * 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
@@ -82,16 +82,18 @@ nsc edit user --name <n> --rm-response-perms
 	cmd.Flags().StringSliceVarP(&params.src, "source-network", "", nil, "add source network for connection - comma separated list or option can be specified multiple times")
 	cmd.Flags().StringSliceVarP(&params.rmSrc, "rm-source-network", "", nil, "remove source network for connection - comma separated list or option can be specified multiple times")
 
-	cmd.Flags().Int64VarP(&params.payload.Number, "payload", "", -1, "set maximum message payload in bytes for the account (-1 is unlimited)")
+	cmd.Flags().StringVarP(&params.payload.Value, "payload", "", "-1", "set maximum message payload in bytes for the account (-1 is unlimited)")
 
 	cmd.Flags().StringVarP(&params.name, "name", "n", "", "user name")
-	cmd.Flags().StringSliceVarP(&params.connTypes, "conn-type", "", nil,
-		fmt.Sprintf("add connection types: %s %s %s %s - comma separated list or option can be specified multiple times",
-			jwt.ConnectionTypeLeafnode, jwt.ConnectionTypeMqtt, jwt.ConnectionTypeStandard, jwt.ConnectionTypeWebsocket))
+	cmd.Flags().StringSliceVarP(&params.connTypes, "conn-type", "", nil, fmt.Sprintf("add connection types: %s %s %s %s - comma separated list or option can be specified multiple times",
+		jwt.ConnectionTypeLeafnode, jwt.ConnectionTypeMqtt, jwt.ConnectionTypeStandard, jwt.ConnectionTypeWebsocket))
 	cmd.Flags().StringSliceVarP(&params.rmConnTypes, "rm-conn-type", "", nil, "remove connection types - comma separated list or option can be specified multiple times")
 
 	cmd.Flags().BoolVarP(&params.bearer, "bearer", "", false, "no connect challenge required for user")
 
+	cmd.Flags().Int64VarP(&params.maxSubs, "subs", "", -1, "set maximum number of subscriptions (-1 is unlimited)")
+	cmd.Flags().StringVarP(&params.maxData.Value, "data", "", "-1", "set maximum data in bytes for the user (-1 is unlimited)")
+
 	params.AccountContextParams.BindFlags(cmd)
 	params.GenericClaimsParams.BindFlags(cmd)
 	params.PermissionsParams.bindSetFlags(cmd, "permissions")
@@ -114,25 +116,30 @@ type EditUserParams struct {
 	token         string
 	credsFilePath string
 
-	rmSrc       []string
-	src         []string
-	times       timeSlice
-	rmTimes     []string
-	locale      timeLocale
-	payload     DataParams
 	bearer      bool
 	connTypes   []string
+	locale      timeLocale
+	maxData     DataParams
+	maxSubs     int64
+	payload     DataParams
 	rmConnTypes []string
+	rmSrc       []string
+	rmTimes     []string
+	src         []string
+	times       timeSlice
 }
 
 func (p *EditUserParams) SetDefaults(ctx ActionCtx) error {
 	p.name = NameFlagOrArgument(p.name, ctx)
-	p.AccountContextParams.SetDefaults(ctx)
+	if err := p.AccountContextParams.SetDefaults(ctx); err != nil {
+		return err
+	}
 	p.SignerParams.SetDefaults(nkeys.PrefixByteAccount, true, ctx)
 
 	if !InteractiveFlag && ctx.NothingToDo("start", "expiry", "rm", "allow-pub", "allow-sub", "allow-pubsub",
 		"deny-pub", "deny-sub", "deny-pubsub", "tag", "rm-tag", "source-network", "rm-source-network", "payload",
-		"rm-response-perms", "max-responses", "response-ttl", "allow-pub-response", "bearer", "rm-time", "time", "conn-type", "rm-conn-type") {
+		"rm-response-perms", "max-responses", "response-ttl", "allow-pub-response", "bearer", "rm-time", "time", "conn-type",
+		"rm-conn-type", "subs", "data") {
 		ctx.CurrentCmd().SilenceUsage = false
 		return fmt.Errorf("specify an edit option")
 	}
@@ -189,13 +196,19 @@ func (p *EditUserParams) Load(ctx ActionCtx) error {
 	}
 
 	if !ctx.CurrentCmd().Flag("payload").Changed {
-		p.payload.Number = p.claim.Limits.Payload
+		p.payload.Value = fmt.Sprintf("%d", p.claim.Limits.Payload)
+	}
+	if !ctx.CurrentCmd().Flag("data").Changed {
+		p.maxData.Value = fmt.Sprintf("%d", p.claim.Limits.Data)
+	}
+	if !ctx.CurrentCmd().Flag("subs").Changed {
+		p.maxSubs = p.claim.Limits.Subs
 	}
 
 	return err
 }
 
-func (p *EditUserParams) PostInteractive(ctx ActionCtx) error {
+func (p *EditUserParams) PostInteractive(_ ActionCtx) error {
 	// FIXME: we won't do interactive on the response params until pub/sub/deny permissions are interactive
 	//if err := p.PermissionsParams.Edit(p.claim.Resp != nil); err != nil {
 	//	return err
@@ -238,6 +251,9 @@ func (p *EditUserParams) Validate(ctx ActionCtx) error {
 	if err != nil {
 		return fmt.Errorf("error parsing %s: %s", "payload", p.payload.Value)
 	}
+	if _, err := p.maxData.NumberValue(); err != nil {
+		return fmt.Errorf("error parsing %s: %s", "data", p.payload.Value)
+	}
 	if err = p.GenericClaimsParams.Valid(); err != nil {
 		return err
 	}
@@ -247,7 +263,9 @@ func (p *EditUserParams) Validate(ctx ActionCtx) error {
 	if err = p.payload.Valid(); err != nil {
 		return err
 	}
-
+	if err = p.maxData.Valid(); err != nil {
+		return err
+	}
 	if err := p.PermissionsParams.Validate(); err != nil {
 		return err
 	}
@@ -260,13 +278,23 @@ func (p *EditUserParams) Run(ctx ActionCtx) (store.Status, error) {
 	r.ReportSum = false
 
 	var err error
-	p.GenericClaimsParams.Run(ctx, p.claim, r)
+	if err := p.GenericClaimsParams.Run(ctx, p.claim, r); err != nil {
+		return nil, err
+	}
 
 	flags := ctx.CurrentCmd().Flags()
-	p.claim.Limits.Payload = p.payload.Number
+	p.claim.Limits.Payload, _ = p.payload.NumberValue()
 	if flags.Changed("payload") {
 		r.AddOK("changed max imports to %d", p.claim.Limits.Payload)
 	}
+	p.claim.Limits.Data, _ = p.maxData.NumberValue()
+	if flags.Changed("data") {
+		r.AddOK("changed max data to %d", p.claim.Limits.Data)
+	}
+	p.claim.Limits.Subs = p.maxSubs
+	if flags.Changed("subs") {
+		r.AddOK("changed max number of subs to %d", p.claim.Limits.Subs)
+	}
 
 	if flags.Changed("bearer") {
 		p.claim.BearerToken = p.bearer


Test patch:
diff --git a/cmd/dataparams_test.go b/cmd/dataparams_test.go
new file mode 100644
index 0000000..b624a79
--- /dev/null
+++ b/cmd/dataparams_test.go
@@ -0,0 +1,50 @@
+/*
+ * 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 (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestDataParams(t *testing.T) {
+	var dp DataParams
+	dp.Value = "1"
+	v, err := dp.NumberValue()
+	require.NoError(t, err)
+	require.Equal(t, int64(1), v)
+
+	dp.Value = "1B"
+	v, err = dp.NumberValue()
+	require.NoError(t, err)
+	require.Equal(t, int64(1), v)
+
+	dp.Value = "1K"
+	v, err = dp.NumberValue()
+	require.NoError(t, err)
+	require.Equal(t, int64(1024), v)
+
+	dp.Value = "1M"
+	v, err = dp.NumberValue()
+	require.NoError(t, err)
+	require.Equal(t, int64(1024*1024), v)
+
+	dp.Value = "1G"
+	v, err = dp.NumberValue()
+	require.NoError(t, err)
+	require.Equal(t, int64(1024*1024*1024), v)
+}
diff --git a/cmd/edituser_test.go b/cmd/edituser_test.go
index 1b6fbec..a4778d2 100644
--- a/cmd/edituser_test.go
+++ b/cmd/edituser_test.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2019 The NATS Authors
+ * 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
@@ -92,7 +92,7 @@ func Test_EditUserAccountRequired(t *testing.T) {
 
 	ts.AddUser(t, "A", "a")
 	ts.AddUser(t, "B", "b")
-	GetConfig().SetAccount("")
+	require.NoError(t, GetConfig().SetAccount(""))
 	_, _, err := ExecuteCmd(createEditUserCmd(), "--tag", "A")
 	require.Error(t, err)
 	require.Contains(t, err.Error(), "account is required")
@@ -390,7 +390,7 @@ func Test_EditUserWithSigningKeyOnly(t *testing.T) {
 	ac, err := ts.Store.ReadAccountClaim("A")
 	require.NoError(t, err)
 	require.NotNil(t, ac)
-	ts.KeyStore.Remove(ac.Subject)
+	require.NoError(t, ts.KeyStore.Remove(ac.Subject))
 	require.False(t, ts.KeyStore.HasPrivateKey(ac.Subject))
 
 	_, _, err = ExecuteCmd(HoistRootFlags(CreateAddUserCmd()), "--name", "AAA")
@@ -471,3 +471,37 @@ func Test_EditUserSk(t *testing.T) {
 	require.NoError(t, err)
 	require.Equal(t, uc.Issuer, pSk)
 }
+
+func Test_EditUserSubs(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	ts.AddAccount(t, "A")
+	ts.AddUser(t, "A", "U")
+	uc, err := ts.Store.ReadUserClaim("A", "U")
+	require.NoError(t, err)
+	require.Equal(t, int64(-1), uc.Subs)
+
+	_, _, err = ExecuteCmd(createEditUserCmd(), "--subs", "100")
+	require.NoError(t, err)
+	uc, err = ts.Store.ReadUserClaim("A", "U")
+	require.NoError(t, err)
+	require.Equal(t, int64(100), uc.Subs)
+	require.NoError(t, err)
+}
+
+func Test_EditUserData(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	ts.AddAccount(t, "A")
+	ts.AddUser(t, "A", "U")
+	uc, err := ts.Store.ReadUserClaim("A", "U")
+	require.NoError(t, err)
+	require.Equal(t, int64(-1), uc.Limits.Data)
+
+	_, _, err = ExecuteCmd(HoistRootFlags(createEditUserCmd()), "--data", "1K")
+	require.NoError(t, err)
+	uc, err = ts.Store.ReadUserClaim("A", "U")
+	require.NoError(t, err)
+	require.Equal(t, int64(1024), uc.Limits.Data)
+	require.NoError(t, err)
+}