Repository: nats-io/nsc
Pull request #274 (2020-05-21 16:42:27)

Rename account
Added an internal command for renaming an account. 


Patch:
diff --git a/cmd/rename.go b/cmd/rename.go
new file mode 100644
index 0000000..ee962a6
--- /dev/null
+++ b/cmd/rename.go
@@ -0,0 +1,244 @@
+/*
+ * Copyright 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
+ *
+ * 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"
+	"os"
+
+	"github.com/nats-io/nkeys"
+
+	"github.com/nats-io/jwt"
+	"github.com/nats-io/nsc/cmd/store"
+	"github.com/spf13/cobra"
+)
+
+var renameCmd = &cobra.Command{
+	Use:    "rename",
+	Short:  "Rename operator, account or user",
+	Hidden: true,
+}
+
+func init() {
+	GetRootCmd().AddCommand(renameCmd)
+	renameCmd.AddCommand(createRenameAccountCmd())
+}
+
+func createRenameAccountCmd() *cobra.Command {
+	var params RenameAccountParams
+	var cmd = &cobra.Command{
+		Use:          "account",
+		Args:         cobra.ExactArgs(2),
+		Example:      "nsc rename account <name> <newname>",
+		SilenceUsage: true,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			if !params.yes {
+				conf := GetConfig()
+				stores := AbbrevHomePaths(conf.StoreRoot)
+				keys := AbbrevHomePaths(store.GetKeysDir())
+				cmd.Printf(`This command makes destructive changes to files and keys. The account 
+rename operation requires that the JWT be reissued with a new name, 
+reusing it's previous ID. This command will move the account, and moves
+users and associated credential files under the new account name. 
+Please backup the following directories:
+
+  %s
+  %s
+
+Run this command with the '--OK' flag, to bypass the warning.
+`, stores, keys)
+				return errors.New("required flag \"OK\" not set")
+			}
+
+			params.from = args[0]
+			params.to = args[1]
+			return RunAction(cmd, args, &params)
+		},
+	}
+
+	cmd.Flags().BoolVarP(&params.yes, "OK", "", false, "backed up")
+	cmd.Flag("OK").Hidden = true
+
+	return cmd
+}
+
+type RenameAccountParams struct {
+	SignerParams
+	from string
+	to   string
+	ac   *jwt.AccountClaims
+	yes  bool
+}
+
+func (p *RenameAccountParams) SetDefaults(ctx ActionCtx) error {
+	return nil
+}
+
+func (p *RenameAccountParams) PreInteractive(ctx ActionCtx) error {
+	return nil
+}
+
+func (p *RenameAccountParams) Load(ctx ActionCtx) error {
+	var err error
+	p.ac, err = ctx.StoreCtx().Store.ReadAccountClaim(p.from)
+
+	return err
+}
+
+func (p *RenameAccountParams) PostInteractive(ctx ActionCtx) error {
+	return nil
+}
+
+func (p *RenameAccountParams) Validate(ctx ActionCtx) error {
+	ac, err := ctx.StoreCtx().Store.ReadAccountClaim(p.from)
+	ctx.StoreCtx().Account.Name = p.from
+	ctx.StoreCtx().Account.PublicKey = ac.Subject
+	p.SignerParams.SetDefaults(nkeys.PrefixByteOperator, true, ctx)
+
+	bc, err := ctx.StoreCtx().Store.ReadAccountClaim(p.to)
+	if err == nil || bc != nil {
+		return fmt.Errorf("account %q already exists", p.to)
+	}
+	cdir := ctx.StoreCtx().KeyStore.CalcAccountCredsDir(p.to)
+	if _, err := os.Stat(cdir); os.IsExist(err) {
+		return fmt.Errorf("creds dir %q already exists", cdir)
+	}
+	return p.SignerParams.Resolve(ctx)
+}
+
+func (p *RenameAccountParams) copyAccount(ctx ActionCtx) (store.Status, error) {
+	r := store.NewReport(store.NONE, "copy account %q to %q", p.from, p.to)
+	p.ac.Name = p.to
+	token, err := p.ac.Encode(p.signerKP)
+	if err != nil {
+		r.AddError("error encoding JWT: %v", err)
+		return r, err
+	}
+	StoreAccountAndUpdateStatus(ctx, token, r)
+	s := ctx.StoreCtx().Store
+
+	if s.IsManaged() {
+		bc, err := s.ReadAccountClaim(p.to)
+		if err != nil {
+			r.AddError("unable to read account %q: %v", p.to, err)
+			return r, err
+		} else {
+			r.Add(DiffAccountLimits(p.ac, bc))
+		}
+	}
+	return r, nil
+}
+
+func (p *RenameAccountParams) copyUsers(ctx ActionCtx) (store.Status, error) {
+	r := store.NewReport(store.NONE, "move users")
+	s := ctx.StoreCtx().Store
+	users, err := s.ListEntries(store.Accounts, p.from, store.Users)
+	if err != nil {
+		r.AddError("error listing users: %v", err)
+		return r, err
+	}
+	for _, u := range users {
+		s, _ := p.moveUser(ctx, u)
+		r.Add(s)
+	}
+	return r, nil
+}
+
+func (p *RenameAccountParams) moveUser(ctx ActionCtx, u string) (store.Status, error) {
+	r := store.NewReport(store.NONE, "move user %q", u)
+	s := ctx.StoreCtx().Store
+	d, err := s.ReadRawUserClaim(p.from, u)
+	if err != nil {
+		r.AddError("unable to read user: %v", err)
+		return r, err
+	}
+	if err := s.Write(d, store.Accounts, p.to, store.Users, store.JwtName(u)); err != nil {
+		r.AddError("unable to write user: %v", err)
+		return r, err
+	}
+	r.AddOK("copied user")
+	if err := s.Delete(store.Accounts, p.from, store.Users, store.JwtName(u)); err != nil {
+		r.AddWarning("error deleting user %v", err)
+	}
+	r.AddOK("removed user from the old account")
+	return r, err
+}
+
+func (p *RenameAccountParams) moveCreds(ctx ActionCtx) (store.Status, error) {
+	r := store.NewReport(store.OK, "move creds directory")
+	fp := ctx.StoreCtx().KeyStore.CalcAccountCredsDir(p.from)
+	if _, err := os.Stat(fp); os.IsNotExist(err) {
+		r.AddOK("skipping... no creds directory found")
+		return r, nil
+	}
+	tfp := ctx.StoreCtx().KeyStore.CalcAccountCredsDir(p.to)
+	if err := os.Rename(fp, tfp); err != nil {
+		tfp = AbbrevHomePaths(tfp)
+		r.AddError("error renaming dir %q: %v", tfp, err)
+		return r, err
+	}
+	return r, nil
+}
+
+func (p *RenameAccountParams) cleanup(ctx ActionCtx) (store.Status, error) {
+	r := store.NewReport(store.NONE, "cleanup")
+	s := ctx.StoreCtx().Store
+	if err := s.Delete(store.Accounts, p.from, store.Users); err != nil {
+		fp := s.Resolve(store.Accounts, p.from, store.Users)
+		fp = AbbrevHomePaths(fp)
+		r.AddWarning("error deleting directory %s: %v", fp, err)
+	} else {
+		r.AddOK("deleted old users directory")
+	}
+	if err := s.Delete(store.Accounts, p.from, store.JwtName(p.from)); err != nil {
+		fp := s.Resolve(store.Accounts, p.from, store.JwtName(p.from))
+		fp = AbbrevHomePaths(fp)
+		r.AddWarning("error deleting jwt %s: %v", fp, err)
+	} else {
+		r.AddOK("deleted old account jwt")
+	}
+	if err := s.Delete(store.Accounts, p.from); err != nil {
+		fp := s.Resolve(store.Accounts, p.from)
+		fp = AbbrevHomePaths(fp)
+		r.AddWarning("error deleting old account %q dir: %v", fp, err)
+	} else {
+		r.AddOK("deleted old account directory")
+	}
+	return r, nil
+}
+
+func (p *RenameAccountParams) Run(ctx ActionCtx) (store.Status, error) {
+	r := store.NewReport(store.NONE, "rename account %q to %q", p.from, p.to)
+	sr, err := p.copyAccount(ctx)
+	r.Add(sr)
+	if err != nil {
+		return r, err
+	}
+	cus, err := p.copyUsers(ctx)
+	r.Add(cus)
+	if err != nil {
+		return r, err
+	}
+	mcr, err := p.moveCreds(ctx)
+	r.Add(mcr)
+	if err != nil {
+		return r, err
+	}
+	cr, err := p.cleanup(ctx)
+	r.Add(cr)
+	return r, err
+}
diff --git a/cmd/store/keys.go b/cmd/store/keys.go
index 8971e0e..0bef693 100644
--- a/cmd/store/keys.go
+++ b/cmd/store/keys.go
@@ -188,6 +188,10 @@ func (k *KeyStore) keyName(n string) string {
 	return fmt.Sprintf("%s%s", n, NKeyExtension)
 }
 
+func (k *KeyStore) CalcAccountCredsDir(account string) string {
+	return filepath.Join(GetKeysDir(), CredsDir, k.Env, account)
+}
+
 func (k *KeyStore) CalcUserCredsPath(account string, user string) string {
 	return filepath.Join(GetKeysDir(), CredsDir, k.Env, account, k.credsName(user))
 }
diff --git a/cmd/store/store.go b/cmd/store/store.go
index 0629f9f..4cd07ba 100644
--- a/cmd/store/store.go
+++ b/cmd/store/store.go
@@ -226,6 +226,10 @@ func (s *Store) resolve(name ...string) string {
 	return filepath.Join(s.Dir, filepath.Join(name...))
 }
 
+func (s *Store) Resolve(name ...string) string {
+	return filepath.Join(s.Dir, filepath.Join(name...))
+}
+
 // Has returns true if the specified asset exists
 func (s *Store) Has(name ...string) bool {
 	fp := s.resolve(name...)


Test patch:
diff --git a/cmd/push_test.go b/cmd/push_test.go
index 2545d2e..700dec3 100644
--- a/cmd/push_test.go
+++ b/cmd/push_test.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2019 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,10 +16,10 @@
 package cmd
 
 import (
-	"github.com/nats-io/jwt"
 	"runtime"
 	"testing"
 
+	"github.com/nats-io/jwt"
 	"github.com/stretchr/testify/require"
 )
 
diff --git a/cmd/rename_test.go b/cmd/rename_test.go
new file mode 100644
index 0000000..6353287
--- /dev/null
+++ b/cmd/rename_test.go
@@ -0,0 +1,107 @@
+/*
+ * Copyright 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
+ *
+ * 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/nats-io/jwt"
+
+	"github.com/stretchr/testify/require"
+)
+
+func Test_RenameAccountRequiresOK(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	ts.AddAccount(t, "A")
+	_, _, err := ExecuteCmd(createRenameAccountCmd(), "A", "B")
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "required flag \"OK\" not set")
+}
+
+func Test_RenameAccountNoUsers(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	ts.AddAccount(t, "A")
+	pk := ts.GetAccountPublicKey(t, "A")
+	_, _, err := ExecuteCmd(createRenameAccountCmd(), "A", "B", "--OK")
+	require.NoError(t, err)
+
+	_, err = ts.Store.ReadAccountClaim("A")
+	require.Error(t, err)
+
+	bc, err := ts.Store.ReadAccountClaim("B")
+	require.NoError(t, err)
+	require.Equal(t, "B", bc.Name)
+	require.Equal(t, pk, bc.Subject)
+}
+
+func Test_RenameAccountUsers(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	ts.AddAccount(t, "A")
+	ts.AddUser(t, "A", "aa")
+	ts.AddUser(t, "A", "bb")
+	pk := ts.GetAccountPublicKey(t, "A")
+	require.FileExists(t, ts.KeyStore.CalcUserCredsPath("A", "aa"))
+	require.FileExists(t, ts.KeyStore.CalcUserCredsPath("A", "bb"))
+
+	_, _, err := ExecuteCmd(createRenameAccountCmd(), "A", "B", "--OK")
+	require.NoError(t, err)
+
+	_, err = ts.Store.ReadAccountClaim("A")
+	require.Error(t, err)
+
+	bc, err := ts.Store.ReadAccountClaim("B")
+	require.NoError(t, err)
+	require.Equal(t, "B", bc.Name)
+	require.Equal(t, pk, bc.Subject)
+	require.FileExists(t, ts.KeyStore.CalcUserCredsPath("B", "aa"))
+	require.FileExists(t, ts.KeyStore.CalcUserCredsPath("B", "bb"))
+	ts.DoesNotExist(t, ts.KeyStore.CalcUserCredsPath("A", "aa"))
+	ts.DoesNotExist(t, ts.KeyStore.CalcUserCredsPath("A", "bb"))
+}
+
+func Test_RenameAccountDuplicate(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	ts.AddAccount(t, "A")
+	ts.AddAccount(t, "B")
+
+	_, _, err := ExecuteCmd(createRenameAccountCmd(), "A", "B", "--OK")
+	require.Error(t, err)
+}
+
+func Test_RenameManagedAccount(t *testing.T) {
+	as, m := RunTestAccountServer(t)
+	defer as.Close()
+
+	ts := NewTestStoreWithOperatorJWT(t, string(m["operator"]))
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "A")
+	pk := ts.GetAccountPublicKey(t, "A")
+	ac, err := jwt.DecodeAccountClaims(string(m[pk]))
+	require.Equal(t, pk, ac.Subject)
+	require.Equal(t, "A", ac.Name)
+
+	_, _, err = ExecuteCmd(createRenameAccountCmd(), "A", "B", "--OK")
+	require.NoError(t, err)
+	bc, err := jwt.DecodeAccountClaims(string(m[pk]))
+	require.NoError(t, err)
+	require.Equal(t, pk, bc.Subject)
+	require.Equal(t, "B", bc.Name)
+}
diff --git a/cmd/store/status_test.go b/cmd/store/status_test.go
index 5e2c0b0..021e4d5 100644
--- a/cmd/store/status_test.go
+++ b/cmd/store/status_test.go
@@ -162,7 +162,7 @@ func Test_Nested(t *testing.T) {
 	require.Contains(t, lines[4], fmt.Sprintf(errTemplate, "some error"))
 }
 
-func Test_ServerMessagesAllwaysShow(t *testing.T) {
+func Test_ServerMessagesAlwaysShow(t *testing.T) {
 	s := NewReport(OK, "summary")
 	s.Opt = DetailsOnErrorOrWarning
 	s.AddOK("one")
diff --git a/cmd/util_test.go b/cmd/util_test.go
index 73fe4cc..c9b8dd7 100644
--- a/cmd/util_test.go
+++ b/cmd/util_test.go
@@ -634,6 +634,11 @@ func (ts *TestStore) VerifyUser(t *testing.T, operator string, account string, u
 	}
 }
 
+func (ts *TestStore) DoesNotExist(t *testing.T, fp string) {
+	_, err := os.Stat(fp)
+	require.False(t, os.IsExist(err))
+}
+
 func Test_Util(t *testing.T) {
 	ts := NewTestStore(t, "O")
 	defer ts.Done(t)