From 795d33b3ee5772c8cfc8e85621d131dc9cc3169b Mon Sep 17 00:00:00 2001 From: Srigovind Nayak Date: Sun, 4 Feb 2024 20:10:23 +0530 Subject: [PATCH 1/2] key: move add, list, remove, passwd to sub-commands docs: improve the sub-command docs changelog: add the unreleased changelog for the key command updates key: update integration tests --- changelog/unreleased/issue-4676 | 8 + cmd/restic/cmd_key.go | 254 +------------------------ cmd/restic/cmd_key_add.go | 128 +++++++++++++ cmd/restic/cmd_key_integration_test.go | 55 +++++- cmd/restic/cmd_key_list.go | 112 +++++++++++ cmd/restic/cmd_key_passwd.go | 89 +++++++++ cmd/restic/cmd_key_remove.go | 73 +++++++ 7 files changed, 459 insertions(+), 260 deletions(-) create mode 100644 changelog/unreleased/issue-4676 create mode 100644 cmd/restic/cmd_key_add.go create mode 100644 cmd/restic/cmd_key_list.go create mode 100644 cmd/restic/cmd_key_passwd.go create mode 100644 cmd/restic/cmd_key_remove.go diff --git a/changelog/unreleased/issue-4676 b/changelog/unreleased/issue-4676 new file mode 100644 index 00000000000..e95118e726a --- /dev/null +++ b/changelog/unreleased/issue-4676 @@ -0,0 +1,8 @@ +Enhancement: Move key add, list, remove and passwd as separate sub-commands + +Restic now provides usage documentation for the `key` command. Each sub-command; +`add`, `list`, `remove` and `passwd` now have their own sub-command documentation +which can be invoked using `restic key --help`. + +https://github.com/restic/restic/issues/4676 +https://github.com/restic/restic/pull/4685 diff --git a/cmd/restic/cmd_key.go b/cmd/restic/cmd_key.go index 14609e6e93b..c687eca53d6 100644 --- a/cmd/restic/cmd_key.go +++ b/cmd/restic/cmd_key.go @@ -1,264 +1,18 @@ package main import ( - "context" - "encoding/json" - "os" - "strings" - "sync" - - "github.com/restic/restic/internal/errors" - "github.com/restic/restic/internal/repository" - "github.com/restic/restic/internal/restic" - "github.com/restic/restic/internal/ui/table" - "github.com/spf13/cobra" ) var cmdKey = &cobra.Command{ - Use: "key [flags] [list|add|remove|passwd] [ID]", + Use: "key", Short: "Manage keys (passwords)", Long: ` -The "key" command manages keys (passwords) for accessing the repository. - -EXIT STATUS -=========== - -Exit status is 0 if the command was successful, and non-zero if there was any error. -`, - DisableAutoGenTag: true, - RunE: func(cmd *cobra.Command, args []string) error { - return runKey(cmd.Context(), globalOptions, keyOpts, args) - }, +The "key" command allows you to set multiple access keys or passwords +per repository. + `, } -type KeyOptions struct { - NewPasswordFile string - Username string - Hostname string -} - -var keyOpts KeyOptions - func init() { cmdRoot.AddCommand(cmdKey) - - flags := cmdKey.Flags() - flags.StringVarP(&keyOpts.NewPasswordFile, "new-password-file", "", "", "`file` from which to read the new password") - flags.StringVarP(&keyOpts.Username, "user", "", "", "the username for new keys") - flags.StringVarP(&keyOpts.Hostname, "host", "", "", "the hostname for new keys") -} - -func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions) error { - type keyInfo struct { - Current bool `json:"current"` - ID string `json:"id"` - UserName string `json:"userName"` - HostName string `json:"hostName"` - Created string `json:"created"` - } - - var m sync.Mutex - var keys []keyInfo - - err := restic.ParallelList(ctx, s, restic.KeyFile, s.Connections(), func(ctx context.Context, id restic.ID, size int64) error { - k, err := repository.LoadKey(ctx, s, id) - if err != nil { - Warnf("LoadKey() failed: %v\n", err) - return nil - } - - key := keyInfo{ - Current: id == s.KeyID(), - ID: id.Str(), - UserName: k.Username, - HostName: k.Hostname, - Created: k.Created.Local().Format(TimeFormat), - } - - m.Lock() - defer m.Unlock() - keys = append(keys, key) - return nil - }) - - if err != nil { - return err - } - - if gopts.JSON { - return json.NewEncoder(globalOptions.stdout).Encode(keys) - } - - tab := table.New() - tab.AddColumn(" ID", "{{if .Current}}*{{else}} {{end}}{{ .ID }}") - tab.AddColumn("User", "{{ .UserName }}") - tab.AddColumn("Host", "{{ .HostName }}") - tab.AddColumn("Created", "{{ .Created }}") - - for _, key := range keys { - tab.AddRow(key) - } - - return tab.Write(globalOptions.stdout) -} - -// testKeyNewPassword is used to set a new password during integration testing. -var testKeyNewPassword string - -func getNewPassword(gopts GlobalOptions, newPasswordFile string) (string, error) { - if testKeyNewPassword != "" { - return testKeyNewPassword, nil - } - - if newPasswordFile != "" { - return loadPasswordFromFile(newPasswordFile) - } - - // Since we already have an open repository, temporary remove the password - // to prompt the user for the passwd. - newopts := gopts - newopts.password = "" - - return ReadPasswordTwice(newopts, - "enter new password: ", - "enter password again: ") -} - -func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, opts KeyOptions) error { - pw, err := getNewPassword(gopts, opts.NewPasswordFile) - if err != nil { - return err - } - - id, err := repository.AddKey(ctx, repo, pw, opts.Username, opts.Hostname, repo.Key()) - if err != nil { - return errors.Fatalf("creating new key failed: %v\n", err) - } - - err = switchToNewKeyAndRemoveIfBroken(ctx, repo, id, pw) - if err != nil { - return err - } - - Verbosef("saved new key with ID %s\n", id.ID()) - - return nil -} - -func deleteKey(ctx context.Context, repo *repository.Repository, id restic.ID) error { - if id == repo.KeyID() { - return errors.Fatal("refusing to remove key currently used to access repository") - } - - err := repository.RemoveKey(ctx, repo, id) - if err != nil { - return err - } - - Verbosef("removed key %v\n", id) - return nil -} - -func changePassword(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, newPasswordFile string) error { - pw, err := getNewPassword(gopts, newPasswordFile) - if err != nil { - return err - } - - id, err := repository.AddKey(ctx, repo, pw, "", "", repo.Key()) - if err != nil { - return errors.Fatalf("creating new key failed: %v\n", err) - } - oldID := repo.KeyID() - - err = switchToNewKeyAndRemoveIfBroken(ctx, repo, id, pw) - if err != nil { - return err - } - - err = repository.RemoveKey(ctx, repo, oldID) - if err != nil { - return err - } - - Verbosef("saved new key as %s\n", id) - - return nil -} - -func switchToNewKeyAndRemoveIfBroken(ctx context.Context, repo *repository.Repository, key *repository.Key, pw string) error { - // Verify new key to make sure it really works. A broken key can render the - // whole repository inaccessible - err := repo.SearchKey(ctx, pw, 0, key.ID().String()) - if err != nil { - // the key is invalid, try to remove it - _ = repository.RemoveKey(ctx, repo, key.ID()) - return errors.Fatalf("failed to access repository with new key: %v", err) - } - return nil -} - -func runKey(ctx context.Context, gopts GlobalOptions, opts KeyOptions, args []string) error { - if len(args) < 1 || (args[0] == "remove" && len(args) != 2) || (args[0] != "remove" && len(args) != 1) { - return errors.Fatal("wrong number of arguments") - } - - repo, err := OpenRepository(ctx, gopts) - if err != nil { - return err - } - - switch args[0] { - case "list": - if !gopts.NoLock { - var lock *restic.Lock - lock, ctx, err = lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON) - defer unlockRepo(lock) - if err != nil { - return err - } - } - - return listKeys(ctx, repo, gopts) - case "add": - lock, ctx, err := lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON) - defer unlockRepo(lock) - if err != nil { - return err - } - - return addKey(ctx, repo, gopts, opts) - case "remove": - lock, ctx, err := lockRepoExclusive(ctx, repo, gopts.RetryLock, gopts.JSON) - defer unlockRepo(lock) - if err != nil { - return err - } - - id, err := restic.Find(ctx, repo, restic.KeyFile, args[1]) - if err != nil { - return err - } - - return deleteKey(ctx, repo, id) - case "passwd": - lock, ctx, err := lockRepoExclusive(ctx, repo, gopts.RetryLock, gopts.JSON) - defer unlockRepo(lock) - if err != nil { - return err - } - - return changePassword(ctx, repo, gopts, opts.NewPasswordFile) - default: - return errors.Fatal("invalid operation") - } -} - -func loadPasswordFromFile(pwdFile string) (string, error) { - s, err := os.ReadFile(pwdFile) - if os.IsNotExist(err) { - return "", errors.Fatalf("%s does not exist", pwdFile) - } - return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile") } diff --git a/cmd/restic/cmd_key_add.go b/cmd/restic/cmd_key_add.go new file mode 100644 index 00000000000..43a38f4ebbd --- /dev/null +++ b/cmd/restic/cmd_key_add.go @@ -0,0 +1,128 @@ +package main + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/repository" + "github.com/spf13/cobra" +) + +var cmdKeyAdd = &cobra.Command{ + Use: "add", + Short: "Add a new key (password) to the repository; returns the new key ID", + Long: ` +The "add" sub-command creates a new key and validates the key. Returns the new key ID. + +EXIT STATUS +=========== + +Exit status is 0 if the command is successful, and non-zero if there was any error. + `, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + return runKeyAdd(cmd.Context(), globalOptions, keyAddOpts, args) + }, +} + +type KeyAddOptions struct { + NewPasswordFile string + Username string + Hostname string +} + +var keyAddOpts KeyAddOptions + +func init() { + cmdKey.AddCommand(cmdKeyAdd) + + flags := cmdKeyAdd.Flags() + flags.StringVarP(&keyAddOpts.NewPasswordFile, "new-password-file", "", "", "`file` from which to read the new password") + flags.StringVarP(&keyAddOpts.Username, "user", "", "", "the username for new key") + flags.StringVarP(&keyAddOpts.Hostname, "host", "", "", "the hostname for new key") +} + +func runKeyAdd(ctx context.Context, gopts GlobalOptions, opts KeyAddOptions, args []string) error { + if len(args) > 0 { + return fmt.Errorf("the key add command expects no arguments, only options - please see `restic help key add` for usage and flags") + } + + repo, err := OpenRepository(ctx, gopts) + if err != nil { + return err + } + + lock, ctx, err := lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON) + defer unlockRepo(lock) + if err != nil { + return err + } + + return addKey(ctx, repo, gopts, opts) +} + +func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, opts KeyAddOptions) error { + pw, err := getNewPassword(gopts, opts.NewPasswordFile) + if err != nil { + return err + } + + id, err := repository.AddKey(ctx, repo, pw, opts.Username, opts.Hostname, repo.Key()) + if err != nil { + return errors.Fatalf("creating new key failed: %v\n", err) + } + + err = switchToNewKeyAndRemoveIfBroken(ctx, repo, id, pw) + if err != nil { + return err + } + + Verbosef("saved new key with ID %s\n", id.ID()) + + return nil +} + +// testKeyNewPassword is used to set a new password during integration testing. +var testKeyNewPassword string + +func getNewPassword(gopts GlobalOptions, newPasswordFile string) (string, error) { + if testKeyNewPassword != "" { + return testKeyNewPassword, nil + } + + if newPasswordFile != "" { + return loadPasswordFromFile(newPasswordFile) + } + + // Since we already have an open repository, temporary remove the password + // to prompt the user for the passwd. + newopts := gopts + newopts.password = "" + + return ReadPasswordTwice(newopts, + "enter new password: ", + "enter password again: ") +} + +func loadPasswordFromFile(pwdFile string) (string, error) { + s, err := os.ReadFile(pwdFile) + if os.IsNotExist(err) { + return "", errors.Fatalf("%s does not exist", pwdFile) + } + return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile") +} + +func switchToNewKeyAndRemoveIfBroken(ctx context.Context, repo *repository.Repository, key *repository.Key, pw string) error { + // Verify new key to make sure it really works. A broken key can render the + // whole repository inaccessible + err := repo.SearchKey(ctx, pw, 0, key.ID().String()) + if err != nil { + // the key is invalid, try to remove it + _ = repository.RemoveKey(ctx, repo, key.ID()) + return errors.Fatalf("failed to access repository with new key: %v", err) + } + return nil +} diff --git a/cmd/restic/cmd_key_integration_test.go b/cmd/restic/cmd_key_integration_test.go index 34474c3af8f..72fa914daed 100644 --- a/cmd/restic/cmd_key_integration_test.go +++ b/cmd/restic/cmd_key_integration_test.go @@ -13,7 +13,7 @@ import ( func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string { buf, err := withCaptureStdout(func() error { - return runKey(context.TODO(), gopts, KeyOptions{}, []string{"list"}) + return runKeyList(context.TODO(), gopts, []string{}) }) rtest.OK(t, err) @@ -36,7 +36,7 @@ func testRunKeyAddNewKey(t testing.TB, newPassword string, gopts GlobalOptions) testKeyNewPassword = "" }() - rtest.OK(t, runKey(context.TODO(), gopts, KeyOptions{}, []string{"add"})) + rtest.OK(t, runKeyAdd(context.TODO(), gopts, KeyAddOptions{}, []string{})) } func testRunKeyAddNewKeyUserHost(t testing.TB, gopts GlobalOptions) { @@ -46,10 +46,10 @@ func testRunKeyAddNewKeyUserHost(t testing.TB, gopts GlobalOptions) { }() t.Log("adding key for john@example.com") - rtest.OK(t, runKey(context.TODO(), gopts, KeyOptions{ + rtest.OK(t, runKeyAdd(context.TODO(), gopts, KeyAddOptions{ Username: "john", Hostname: "example.com", - }, []string{"add"})) + }, []string{})) repo, err := OpenRepository(context.TODO(), gopts) rtest.OK(t, err) @@ -66,13 +66,13 @@ func testRunKeyPasswd(t testing.TB, newPassword string, gopts GlobalOptions) { testKeyNewPassword = "" }() - rtest.OK(t, runKey(context.TODO(), gopts, KeyOptions{}, []string{"passwd"})) + rtest.OK(t, runKeyPasswd(context.TODO(), gopts, KeyPasswdOptions{}, []string{})) } func testRunKeyRemove(t testing.TB, gopts GlobalOptions, IDs []string) { t.Logf("remove %d keys: %q\n", len(IDs), IDs) for _, id := range IDs { - rtest.OK(t, runKey(context.TODO(), gopts, KeyOptions{}, []string{"remove", id})) + rtest.OK(t, runKeyRemove(context.TODO(), gopts, []string{id})) } } @@ -102,7 +102,7 @@ func TestKeyAddRemove(t *testing.T) { env.gopts.password = passwordList[len(passwordList)-1] t.Logf("testing access with last password %q\n", env.gopts.password) - rtest.OK(t, runKey(context.TODO(), env.gopts, KeyOptions{}, []string{"list"})) + rtest.OK(t, runKeyList(context.TODO(), env.gopts, []string{})) testRunCheck(t, env.gopts) testRunKeyAddNewKeyUserHost(t, env.gopts) @@ -130,15 +130,50 @@ func TestKeyProblems(t *testing.T) { testKeyNewPassword = "" }() - err := runKey(context.TODO(), env.gopts, KeyOptions{}, []string{"passwd"}) + err := runKeyPasswd(context.TODO(), env.gopts, KeyPasswdOptions{}, []string{}) t.Log(err) rtest.Assert(t, err != nil, "expected passwd change to fail") - err = runKey(context.TODO(), env.gopts, KeyOptions{}, []string{"add"}) + err = runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{}, []string{}) t.Log(err) rtest.Assert(t, err != nil, "expected key adding to fail") t.Logf("testing access with initial password %q\n", env.gopts.password) - rtest.OK(t, runKey(context.TODO(), env.gopts, KeyOptions{}, []string{"list"})) + rtest.OK(t, runKeyList(context.TODO(), env.gopts, []string{})) testRunCheck(t, env.gopts) } + +func TestKeyCommandInvalidArguments(t *testing.T) { + env, cleanup := withTestEnvironment(t) + defer cleanup() + + testRunInit(t, env.gopts) + env.gopts.backendTestHook = func(r backend.Backend) (backend.Backend, error) { + return &emptySaveBackend{r}, nil + } + + err := runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{}, []string{"johndoe"}) + t.Log(err) + rtest.Assert(t, err != nil, "expected key add to fail") + + testKeyNewPassword = "johndoe" + defer func() { + testKeyNewPassword = "" + }() + err = runKeyPasswd(context.TODO(), env.gopts, KeyPasswdOptions{}, []string{"johndoe"}) + t.Log(err) + rtest.Assert(t, err != nil, "expected key passwd to fail") + + env.gopts.password = "johndoe" + err = runKeyList(context.TODO(), env.gopts, []string{}) + t.Log(err) + rtest.Assert(t, err != nil, "expected key list to fail") + + err = runKeyRemove(context.TODO(), env.gopts, []string{}) + t.Log(err) + rtest.Assert(t, err != nil, "expected key remove to fail") + + err = runKeyRemove(context.TODO(), env.gopts, []string{"john", "doe"}) + t.Log(err) + rtest.Assert(t, err != nil, "expected key remove to fail") +} diff --git a/cmd/restic/cmd_key_list.go b/cmd/restic/cmd_key_list.go new file mode 100644 index 00000000000..517b7c84b7b --- /dev/null +++ b/cmd/restic/cmd_key_list.go @@ -0,0 +1,112 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "sync" + + "github.com/restic/restic/internal/repository" + "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui/table" + "github.com/spf13/cobra" +) + +var cmdKeyList = &cobra.Command{ + Use: "list", + Short: "List keys (passwords)", + Long: ` +The "list" sub-command lists all the keys (passwords) associated with the repository. +Returns the key ID, username, hostname, created time and if it's the current key being +used to access the repository. + +EXIT STATUS +=========== + +Exit status is 0 if the command is successful, and non-zero if there was any error. + `, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + return runKeyList(cmd.Context(), globalOptions, args) + }, +} + +func init() { + cmdKey.AddCommand(cmdKeyList) +} + +func runKeyList(ctx context.Context, gopts GlobalOptions, args []string) error { + if len(args) > 0 { + return fmt.Errorf("the key list command expects no arguments, only options - please see `restic help key list` for usage and flags") + } + + repo, err := OpenRepository(ctx, gopts) + if err != nil { + return err + } + + if !gopts.NoLock { + var lock *restic.Lock + lock, ctx, err = lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON) + defer unlockRepo(lock) + if err != nil { + return err + } + } + + return listKeys(ctx, repo, gopts) +} + +func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions) error { + type keyInfo struct { + Current bool `json:"current"` + ID string `json:"id"` + UserName string `json:"userName"` + HostName string `json:"hostName"` + Created string `json:"created"` + } + + var m sync.Mutex + var keys []keyInfo + + err := restic.ParallelList(ctx, s, restic.KeyFile, s.Connections(), func(ctx context.Context, id restic.ID, size int64) error { + k, err := repository.LoadKey(ctx, s, id) + if err != nil { + Warnf("LoadKey() failed: %v\n", err) + return nil + } + + key := keyInfo{ + Current: id == s.KeyID(), + ID: id.Str(), + UserName: k.Username, + HostName: k.Hostname, + Created: k.Created.Local().Format(TimeFormat), + } + + m.Lock() + defer m.Unlock() + keys = append(keys, key) + return nil + }) + + if err != nil { + return err + } + + if gopts.JSON { + return json.NewEncoder(globalOptions.stdout).Encode(keys) + } + + tab := table.New() + tab.AddColumn(" ID", "{{if .Current}}*{{else}} {{end}}{{ .ID }}") + tab.AddColumn("User", "{{ .UserName }}") + tab.AddColumn("Host", "{{ .HostName }}") + tab.AddColumn("Created", "{{ .Created }}") + + for _, key := range keys { + tab.AddRow(key) + } + + return tab.Write(globalOptions.stdout) +} diff --git a/cmd/restic/cmd_key_passwd.go b/cmd/restic/cmd_key_passwd.go new file mode 100644 index 00000000000..cb916274cc1 --- /dev/null +++ b/cmd/restic/cmd_key_passwd.go @@ -0,0 +1,89 @@ +package main + +import ( + "context" + "fmt" + + "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/repository" + "github.com/spf13/cobra" +) + +var cmdKeyPasswd = &cobra.Command{ + Use: "passwd", + Short: "Change key (password); creates a new key ID and removes the old key ID, returns new key ID", + Long: ` +The "passwd" sub-command creates a new key, validates the key and remove the old key ID. +Returns the new key ID. + +EXIT STATUS +=========== + +Exit status is 0 if the command is successful, and non-zero if there was any error. + `, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + return runKeyPasswd(cmd.Context(), globalOptions, keyPasswdOpts, args) + }, +} + +type KeyPasswdOptions struct { + KeyAddOptions +} + +var keyPasswdOpts KeyPasswdOptions + +func init() { + cmdKey.AddCommand(cmdKeyPasswd) + + flags := cmdKeyPasswd.Flags() + flags.StringVarP(&keyPasswdOpts.NewPasswordFile, "new-password-file", "", "", "`file` from which to read the new password") + flags.StringVarP(&keyPasswdOpts.Username, "user", "", "", "the username for new key") + flags.StringVarP(&keyPasswdOpts.Hostname, "host", "", "", "the hostname for new key") +} + +func runKeyPasswd(ctx context.Context, gopts GlobalOptions, opts KeyPasswdOptions, args []string) error { + if len(args) > 0 { + return fmt.Errorf("the key passwd command expects no arguments, only options - please see `restic help key passwd` for usage and flags") + } + + repo, err := OpenRepository(ctx, gopts) + if err != nil { + return err + } + + lock, ctx, err := lockRepoExclusive(ctx, repo, gopts.RetryLock, gopts.JSON) + defer unlockRepo(lock) + if err != nil { + return err + } + + return changePassword(ctx, repo, gopts, opts) +} + +func changePassword(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, opts KeyPasswdOptions) error { + pw, err := getNewPassword(gopts, opts.NewPasswordFile) + if err != nil { + return err + } + + id, err := repository.AddKey(ctx, repo, pw, "", "", repo.Key()) + if err != nil { + return errors.Fatalf("creating new key failed: %v\n", err) + } + oldID := repo.KeyID() + + err = switchToNewKeyAndRemoveIfBroken(ctx, repo, id, pw) + if err != nil { + return err + } + + err = repository.RemoveKey(ctx, repo, oldID) + if err != nil { + return err + } + + Verbosef("saved new key as %s\n", id) + + return nil +} diff --git a/cmd/restic/cmd_key_remove.go b/cmd/restic/cmd_key_remove.go new file mode 100644 index 00000000000..c8e303ffc80 --- /dev/null +++ b/cmd/restic/cmd_key_remove.go @@ -0,0 +1,73 @@ +package main + +import ( + "context" + "fmt" + + "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/repository" + "github.com/restic/restic/internal/restic" + "github.com/spf13/cobra" +) + +var cmdKeyRemove = &cobra.Command{ + Use: "remove [ID]", + Short: "Remove key ID (password) from the repository.", + Long: ` +The "remove" sub-command removes the selected key ID. The "remove" command does not allow +removing the current key being used to access the repository. + +EXIT STATUS +=========== + +Exit status is 0 if the command is successful, and non-zero if there was any error. + `, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + return runKeyRemove(cmd.Context(), globalOptions, args) + }, +} + +func init() { + cmdKey.AddCommand(cmdKeyRemove) +} + +func runKeyRemove(ctx context.Context, gopts GlobalOptions, args []string) error { + if len(args) != 1 { + return fmt.Errorf("key remove expects one argument as the key id") + } + + repo, err := OpenRepository(ctx, gopts) + if err != nil { + return err + } + + lock, ctx, err := lockRepoExclusive(ctx, repo, gopts.RetryLock, gopts.JSON) + defer unlockRepo(lock) + if err != nil { + return err + } + + idPrefix := args[0] + + return deleteKey(ctx, repo, idPrefix) +} + +func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string) error { + id, err := restic.Find(ctx, repo, restic.KeyFile, idPrefix) + if err != nil { + return err + } + + if id == repo.KeyID() { + return errors.Fatal("refusing to remove key currently used to access repository") + } + + err = repository.RemoveKey(ctx, repo, id) + if err != nil { + return err + } + + Verbosef("removed key %v\n", id) + return nil +} From e46b21ab80b97cf4dc8780f11fcf9e94e07076ab Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Thu, 8 Feb 2024 20:52:30 +0100 Subject: [PATCH 2/2] key: fix integration test for invalid arguments --- cmd/restic/cmd_key_integration_test.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/cmd/restic/cmd_key_integration_test.go b/cmd/restic/cmd_key_integration_test.go index 72fa914daed..16cc1bdad7f 100644 --- a/cmd/restic/cmd_key_integration_test.go +++ b/cmd/restic/cmd_key_integration_test.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "regexp" + "strings" "testing" "github.com/restic/restic/internal/backend" @@ -154,26 +155,21 @@ func TestKeyCommandInvalidArguments(t *testing.T) { err := runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{}, []string{"johndoe"}) t.Log(err) - rtest.Assert(t, err != nil, "expected key add to fail") + rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key add: %v", err) - testKeyNewPassword = "johndoe" - defer func() { - testKeyNewPassword = "" - }() err = runKeyPasswd(context.TODO(), env.gopts, KeyPasswdOptions{}, []string{"johndoe"}) t.Log(err) - rtest.Assert(t, err != nil, "expected key passwd to fail") + rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key passwd: %v", err) - env.gopts.password = "johndoe" - err = runKeyList(context.TODO(), env.gopts, []string{}) + err = runKeyList(context.TODO(), env.gopts, []string{"johndoe"}) t.Log(err) - rtest.Assert(t, err != nil, "expected key list to fail") + rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key list: %v", err) err = runKeyRemove(context.TODO(), env.gopts, []string{}) t.Log(err) - rtest.Assert(t, err != nil, "expected key remove to fail") + rtest.Assert(t, err != nil && strings.Contains(err.Error(), "one argument"), "unexpected error for key remove: %v", err) err = runKeyRemove(context.TODO(), env.gopts, []string{"john", "doe"}) t.Log(err) - rtest.Assert(t, err != nil, "expected key remove to fail") + rtest.Assert(t, err != nil && strings.Contains(err.Error(), "one argument"), "unexpected error for key remove: %v", err) }