Add an interactive command to delete a key from any keystore.

This lists any matching keys, and requires the user to pick which one
to choose, if there is more than 1 matching key.  Also requires the
user to confirm before deleting.

Signed-off-by: Ying Li <ying.li@docker.com>
This commit is contained in:
Ying Li 2015-11-14 01:44:20 -08:00
parent daa844079f
commit 0d7df87805
3 changed files with 310 additions and 0 deletions

View File

@ -2,11 +2,13 @@ package main
import ( import (
"archive/zip" "archive/zip"
"bufio"
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"strconv"
"strings" "strings"
notaryclient "github.com/docker/notary/client" notaryclient "github.com/docker/notary/client"
@ -30,6 +32,7 @@ func init() {
cmdKey.AddCommand(cmdKeyImport) cmdKey.AddCommand(cmdKeyImport)
cmdKey.AddCommand(cmdKeyImportRoot) cmdKey.AddCommand(cmdKeyImportRoot)
cmdKey.AddCommand(cmdRotateKey) cmdKey.AddCommand(cmdRotateKey)
cmdKey.AddCommand(cmdKeyRemove)
} }
var cmdKey = &cobra.Command{ var cmdKey = &cobra.Command{
@ -91,6 +94,13 @@ var cmdKeyImportRoot = &cobra.Command{
Run: keysImportRoot, Run: keysImportRoot,
} }
var cmdKeyRemove = &cobra.Command{
Use: "remove [ keyID ]",
Short: "Removes the key with the given keyID.",
Long: "Removes the key with the given keyID. If the key is stored in more than one location, you will be asked which one to remove.",
Run: keyRemove,
}
func truncateWithEllipsis(str string, maxWidth int, leftTruncate bool) string { func truncateWithEllipsis(str string, maxWidth int, leftTruncate bool) string {
if len(str) <= maxWidth { if len(str) <= maxWidth {
return str return str
@ -411,6 +421,101 @@ func keysRotate(cmd *cobra.Command, args []string) {
} }
} }
func removeKeyInteractively(keyStores []trustmanager.KeyStore, keyID string,
in io.Reader, out io.Writer) error {
var foundKeys [][]string
var storesByIndex []trustmanager.KeyStore
for _, store := range keyStores {
for keypath, role := range store.ListKeys() {
if filepath.Base(keypath) == keyID {
foundKeys = append(foundKeys,
[]string{keypath, role, store.Name()})
storesByIndex = append(storesByIndex, store)
}
}
}
if len(foundKeys) == 0 {
return fmt.Errorf("No key with ID %s found.", keyID)
}
readIn := bufio.NewReader(in)
if len(foundKeys) > 1 {
for {
// ask the user for which key to delete
fmt.Fprintf(out, "Found the following matching keys:\n")
for i, info := range foundKeys {
fmt.Fprintf(out, "\t%d. %s: %s (%s)\n", i+1, info[0], info[1], info[2])
}
fmt.Fprint(out, "Which would you like to delete? Please enter a number: ")
result, err := readIn.ReadBytes('\n')
if err != nil {
return err
}
index, err := strconv.Atoi(strings.TrimSpace(string(result)))
if err != nil || index > len(foundKeys) || index < 1 {
fmt.Fprintf(out, "\nInvalid choice: %s\n", string(result))
continue
}
foundKeys = [][]string{foundKeys[index-1]}
storesByIndex = []trustmanager.KeyStore{storesByIndex[index-1]}
fmt.Fprintln(out, "")
break
}
}
// Now the length must be 1 - ask for confirmation.
keyDescription := fmt.Sprintf("%s (role %s) from %s", foundKeys[0][0],
foundKeys[0][1], foundKeys[0][2])
fmt.Fprintf(out, "Are you sure you want to remove %s? [Y/n] ",
keyDescription)
result, err := readIn.ReadBytes('\n')
if err != nil {
return err
}
yesno := strings.ToLower(strings.TrimSpace(string(result)))
if !strings.HasPrefix("yes", yesno) && yesno != "" {
fmt.Fprintln(out, "\nAborting action.")
return nil
}
err = storesByIndex[0].RemoveKey(foundKeys[0][0])
if err != nil {
return err
}
fmt.Fprintf(out, "\nDeleted %s.\n", keyDescription)
return nil
}
// keyRemove deletes a private key based on ID
func keyRemove(cmd *cobra.Command, args []string) {
if len(args) < 1 {
cmd.Usage()
fatalf("must specify the key ID of the key to remove")
}
parseConfig()
keyID := args[0]
// This is an invalid ID
if len(keyID) != idSize {
fatalf("invalid key ID provided: %s", keyID)
}
stores := getKeyStores(cmd, mainViper.GetString("trust_dir"), retriever, true)
cmd.Println("")
err := removeKeyInteractively(stores, keyID, os.Stdin, cmd.Out())
cmd.Println("")
if err != nil {
fatalf(err.Error())
}
}
func getKeyStores(cmd *cobra.Command, directory string, func getKeyStores(cmd *cobra.Command, directory string,
ret passphrase.Retriever, withHardware bool) []trustmanager.KeyStore { ret passphrase.Retriever, withHardware bool) []trustmanager.KeyStore {

View File

@ -16,6 +16,8 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
var ret = passphrase.ConstantRetriever("pass")
func TestTruncateWithEllipsis(t *testing.T) { func TestTruncateWithEllipsis(t *testing.T) {
digits := "1234567890" digits := "1234567890"
// do not truncate // do not truncate
@ -133,3 +135,206 @@ func TestPrettyPrintZeroKeys(t *testing.T) {
assert.Len(t, lines, 1) assert.Len(t, lines, 1)
assert.Equal(t, "No signing keys found.", lines[0]) assert.Equal(t, "No signing keys found.", lines[0])
} }
// If there are no keys, removeKeyInteractively will just return an error about
// there not being any key
func TestRemoveIfNoKey(t *testing.T) {
var buf bytes.Buffer
stores := []trustmanager.KeyStore{trustmanager.NewKeyMemoryStore(nil)}
err := removeKeyInteractively(stores, "12345", &buf, &buf)
assert.Error(t, err)
assert.Contains(t, err.Error(), "No key with ID")
}
// If there is one key, asking to remove it will ask for confirmation. Passing
// anything other than 'yes'/'y'/'' response will abort the deletion and
// not delete the key.
func TestRemoveOneKeyAbort(t *testing.T) {
nos := []string{"no", "NO", "AAAARGH", " N "}
store := trustmanager.NewKeyMemoryStore(ret)
key, err := trustmanager.GenerateED25519Key(rand.Reader)
assert.NoError(t, err)
err = store.AddKey(key.ID(), "root", key)
assert.NoError(t, err)
stores := []trustmanager.KeyStore{store}
for _, noAnswer := range nos {
var out bytes.Buffer
in := bytes.NewBuffer([]byte(noAnswer + "\n"))
err := removeKeyInteractively(stores, key.ID(), in, &out)
assert.NoError(t, err)
text, err := ioutil.ReadAll(&out)
assert.NoError(t, err)
output := string(text)
assert.Contains(t, output, "Are you sure")
assert.Contains(t, output, "Aborting action")
assert.Len(t, store.ListKeys(), 1)
}
}
// If there is one key, asking to remove it will ask for confirmation. Passing
// 'yes'/'y'/'' response will continue the deletion.
func TestRemoveOneKeyConfirm(t *testing.T) {
yesses := []string{"yes", " Y ", "yE", " ", ""}
for _, yesAnswer := range yesses {
store := trustmanager.NewKeyMemoryStore(ret)
key, err := trustmanager.GenerateED25519Key(rand.Reader)
assert.NoError(t, err)
err = store.AddKey(key.ID(), "root", key)
assert.NoError(t, err)
var out bytes.Buffer
in := bytes.NewBuffer([]byte(yesAnswer + "\n"))
err = removeKeyInteractively(
[]trustmanager.KeyStore{store}, key.ID(), in, &out)
assert.NoError(t, err)
text, err := ioutil.ReadAll(&out)
assert.NoError(t, err)
output := string(text)
assert.Contains(t, output, "Are you sure")
assert.Contains(t, output, "Deleted "+key.ID())
assert.Len(t, store.ListKeys(), 0)
}
}
// If there is more than one key, removeKeyInteractively will ask which key to
// delete and will do so over and over until the user quits if the answer is
// invalid.
func TestRemoveMultikeysInvalidInput(t *testing.T) {
in := bytes.NewBuffer([]byte("nota number\n9999\n-3\n0"))
key, err := trustmanager.GenerateED25519Key(rand.Reader)
assert.NoError(t, err)
stores := []trustmanager.KeyStore{
trustmanager.NewKeyMemoryStore(ret),
trustmanager.NewKeyMemoryStore(ret),
}
err = stores[0].AddKey(key.ID(), "root", key)
assert.NoError(t, err)
err = stores[1].AddKey("gun/"+key.ID(), "target", key)
assert.NoError(t, err)
var out bytes.Buffer
err = removeKeyInteractively(stores, key.ID(), in, &out)
assert.Error(t, err)
text, err := ioutil.ReadAll(&out)
assert.NoError(t, err)
assert.Len(t, stores[0].ListKeys(), 1)
assert.Len(t, stores[1].ListKeys(), 1)
// It should have listed the keys over and over, asking which key the user
// wanted to delete
output := string(text)
assert.Contains(t, output, "Found the following matching keys")
var rootCount, targetCount int
for _, line := range strings.Split(output, "\n") {
if strings.Contains(line, key.ID()) {
if strings.Contains(line, "target") {
targetCount++
} else {
rootCount++
}
}
}
assert.Equal(t, rootCount, targetCount)
assert.Equal(t, 4, rootCount) // for each of the 4 invalid inputs
}
// If there is more than one key, removeKeyInteractively will ask which key to
// delete. Then it will confirm whether they want to delete, and the user can
// abort at that confirmation.
func TestRemoveMultikeysAbortChoice(t *testing.T) {
in := bytes.NewBuffer([]byte("1\nn\n"))
key, err := trustmanager.GenerateED25519Key(rand.Reader)
assert.NoError(t, err)
stores := []trustmanager.KeyStore{
trustmanager.NewKeyMemoryStore(ret),
trustmanager.NewKeyMemoryStore(ret),
}
err = stores[0].AddKey(key.ID(), "root", key)
assert.NoError(t, err)
err = stores[1].AddKey("gun/"+key.ID(), "target", key)
assert.NoError(t, err)
var out bytes.Buffer
err = removeKeyInteractively(stores, key.ID(), in, &out)
assert.NoError(t, err) // no error to abort deleting
text, err := ioutil.ReadAll(&out)
assert.NoError(t, err)
assert.Len(t, stores[0].ListKeys(), 1)
assert.Len(t, stores[1].ListKeys(), 1)
// It should have listed the keys, asked whether the user really wanted to
// delete, and then aborted.
output := string(text)
assert.Contains(t, output, "Found the following matching keys")
assert.Contains(t, output, "Are you sure")
assert.Contains(t, output, "Aborting action")
}
// If there is more than one key, removeKeyInteractively will ask which key to
// delete. Then it will confirm whether they want to delete, and if the user
// confirms, will remove it from the correct key store.
func TestRemoveMultikeysRemoveOnlyChosenKey(t *testing.T) {
in := bytes.NewBuffer([]byte("1\ny\n"))
key, err := trustmanager.GenerateED25519Key(rand.Reader)
assert.NoError(t, err)
stores := []trustmanager.KeyStore{
trustmanager.NewKeyMemoryStore(ret),
trustmanager.NewKeyMemoryStore(ret),
}
err = stores[0].AddKey(key.ID(), "root", key)
assert.NoError(t, err)
err = stores[1].AddKey("gun/"+key.ID(), "target", key)
assert.NoError(t, err)
var out bytes.Buffer
err = removeKeyInteractively(stores, key.ID(), in, &out)
assert.NoError(t, err)
text, err := ioutil.ReadAll(&out)
assert.NoError(t, err)
// It should have listed the keys, asked whether the user really wanted to
// delete, and then deleted.
output := string(text)
assert.Contains(t, output, "Found the following matching keys")
assert.Contains(t, output, "Are you sure")
assert.Contains(t, output, "Deleted "+key.ID())
// figure out which one we picked to delete, and assert it was deleted
for _, line := range strings.Split(output, "\n") {
if strings.HasPrefix(line, "\t1.") { // we picked the first item
if strings.Contains(line, "root") { // first key store
assert.Len(t, stores[0].ListKeys(), 0)
assert.Len(t, stores[1].ListKeys(), 1)
} else {
assert.Len(t, stores[0].ListKeys(), 1)
assert.Len(t, stores[1].ListKeys(), 0)
}
}
}
}