From ea234d9cbf88e89eff9c26dcac0782bae54e3fd2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 31 Mar 2015 19:21:02 -0700 Subject: [PATCH] command/revoke: revoke --- api/sys_lease.go | 4 +- command/command_test.go | 18 +++++++++ command/revoke.go | 87 +++++++++++++++++++++++++++++++++++++++++ command/revoke_test.go | 45 +++++++++++++++++++++ commands.go | 6 +++ http/handler.go | 1 + http/sys_lease.go | 41 +++++++++++++++++++ http/sys_lease_test.go | 17 ++++++++ 8 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 command/revoke.go create mode 100644 command/revoke_test.go create mode 100644 http/sys_lease.go create mode 100644 http/sys_lease_test.go diff --git a/api/sys_lease.go b/api/sys_lease.go index 1ffa95b86e..1583df95ef 100644 --- a/api/sys_lease.go +++ b/api/sys_lease.go @@ -14,6 +14,8 @@ func (c *Sys) Renew(id string) (*Secret, error) { func (c *Sys) Revoke(id string) error { r := c.c.NewRequest("PUT", "/v1/sys/revoke/"+id) resp, err := c.c.RawRequest(r) - defer resp.Body.Close() + if err == nil { + defer resp.Body.Close() + } return err } diff --git a/command/command_test.go b/command/command_test.go index 7e1c7de497..4aa33ad589 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -1,3 +1,21 @@ package command +import ( + "testing" + + "github.com/hashicorp/vault/api" +) + const FixturePath = "./test-fixtures" + +func testClient(t *testing.T, addr string, token string) *api.Client { + config := api.DefaultConfig() + config.Address = addr + client, err := api.NewClient(config) + if err != nil { + t.Fatalf("err: %s", err) + } + client.SetToken(token) + + return client +} diff --git a/command/revoke.go b/command/revoke.go new file mode 100644 index 0000000000..06b9bc5dbd --- /dev/null +++ b/command/revoke.go @@ -0,0 +1,87 @@ +package command + +import ( + "fmt" + "strings" +) + +// RevokeCommand is a Command that mounts a new mount. +type RevokeCommand struct { + Meta +} + +func (c *RevokeCommand) Run(args []string) int { + var prefix bool + flags := c.Meta.FlagSet("revoke", FlagSetDefault) + flags.BoolVar(&prefix, "prefix", false, "") + flags.Usage = func() { c.Ui.Error(c.Help()) } + if err := flags.Parse(args); err != nil { + return 1 + } + + args = flags.Args() + if len(args) != 1 { + flags.Usage() + c.Ui.Error(fmt.Sprintf( + "\nRevoke expects one argument: the ID to revoke")) + return 1 + } + vaultId := args[0] + + client, err := c.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Error initializing client: %s", err)) + return 2 + } + + if err := client.Sys().Revoke(vaultId); err != nil { + c.Ui.Error(fmt.Sprintf( + "Revoke error: %s", err)) + return 1 + } + + c.Ui.Output(fmt.Sprintf("Key revoked with ID '%s'.", vaultId)) + return 0 +} + +func (c *RevokeCommand) Synopsis() string { + return "Revoke a secret." +} + +func (c *RevokeCommand) Help() string { + helpText := ` +Usage: vault revoke [options] id + + Revoke a secret by its Vault ID. + + This command revokes a secret by its Vault ID that was returned + with it. Once the key is revoked, it is no longer valid. + + With the -prefix flag, the revoke is done by prefix: any secret prefixed + with the given partial ID is revoked. Vault IDs are structured in such + a way to make revocation of prefixes useful. + +General Options: + + -address=TODO The address of the Vault server. + + -ca-cert=path Path to a PEM encoded CA cert file to use to + verify the Vault server SSL certificate. + + -ca-path=path Path to a directory of PEM encoded CA cert files + to verify the Vault server SSL certificate. If both + -ca-cert and -ca-path are specified, -ca-path is used. + + -insecure Do not verify TLS certificate. This is highly + not recommended. This is especially not recommended + for unsealing a vault. + +Revoke Options: + + -prefix=true Revoke all secrets with the matching prefix. This + defaults to false: an exact revocation. + +` + return strings.TrimSpace(helpText) +} diff --git a/command/revoke_test.go b/command/revoke_test.go new file mode 100644 index 0000000000..909c25b26f --- /dev/null +++ b/command/revoke_test.go @@ -0,0 +1,45 @@ +package command + +import ( + "testing" + + "github.com/hashicorp/vault/http" + "github.com/hashicorp/vault/vault" + "github.com/mitchellh/cli" +) + +func TestRevoke(t *testing.T) { + core, _, token := vault.TestCoreUnsealed(t) + ln, addr := http.TestServer(t, core) + defer ln.Close() + + ui := new(cli.MockUi) + c := &RevokeCommand{ + Meta: Meta{ + ClientToken: token, + Ui: ui, + }, + } + + client := testClient(t, addr, token) + err := client.Logical().Write("secret/foo", map[string]interface{}{ + "key": "value", + "lease": "1m", + }) + if err != nil { + t.Fatalf("err: %s", err) + } + + secret, err := client.Logical().Read("secret/foo") + if err != nil { + t.Fatalf("err: %s", err) + } + + args := []string{ + "-address", addr, + secret.VaultId, + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } +} diff --git a/commands.go b/commands.go index 3a8c65f573..5b61b93597 100644 --- a/commands.go +++ b/commands.go @@ -42,6 +42,12 @@ func init() { }, nil }, + "revoke": func() (cli.Command, error) { + return &command.RevokeCommand{ + Meta: meta, + }, nil + }, + "seal": func() (cli.Command, error) { return &command.SealCommand{ Meta: meta, diff --git a/http/handler.go b/http/handler.go index 60367acd4e..2171a81ad1 100644 --- a/http/handler.go +++ b/http/handler.go @@ -20,6 +20,7 @@ func Handler(core *vault.Core) http.Handler { mux.Handle("/v1/sys/seal", handleSysSeal(core)) mux.Handle("/v1/sys/unseal", handleSysUnseal(core)) mux.Handle("/v1/sys/mounts/", handleSysMounts(core)) + mux.Handle("/v1/sys/revoke/", handleSysRevoke(core)) mux.Handle("/v1/", handleLogical(core)) return mux } diff --git a/http/sys_lease.go b/http/sys_lease.go new file mode 100644 index 0000000000..fb1157af94 --- /dev/null +++ b/http/sys_lease.go @@ -0,0 +1,41 @@ +package http + +import ( + "net/http" + "strings" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/vault" +) + +func handleSysRevoke(core *vault.Core) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "PUT" { + respondError(w, http.StatusMethodNotAllowed, nil) + return + } + + // Determine the path... + prefix := "/v1/sys/revoke/" + if !strings.HasPrefix(r.URL.Path, prefix) { + respondError(w, http.StatusNotFound, nil) + return + } + path := r.URL.Path[len(prefix):] + if path == "" { + respondError(w, http.StatusNotFound, nil) + return + } + + _, err := core.HandleRequest(requestAuth(r, &logical.Request{ + Operation: logical.WriteOperation, + Path: "sys/revoke/" + path, + })) + if err != nil { + respondError(w, http.StatusBadRequest, err) + return + } + + respondOk(w, nil) + }) +} diff --git a/http/sys_lease_test.go b/http/sys_lease_test.go new file mode 100644 index 0000000000..57e9deb8ef --- /dev/null +++ b/http/sys_lease_test.go @@ -0,0 +1,17 @@ +package http + +import ( + "testing" + + "github.com/hashicorp/vault/vault" +) + +func TestSysRevoke(t *testing.T) { + core, _, token := vault.TestCoreUnsealed(t) + ln, addr := TestServer(t, core) + defer ln.Close() + TestServerAuth(t, addr, token) + + resp := testHttpPut(t, addr+"/v1/sys/revoke/secret/foo/1234", nil) + testResponseStatus(t, resp, 204) +}