mirror of https://github.com/docker/docs.git
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:
parent
daa844079f
commit
0d7df87805
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue