diff --git a/api/sys_leases.go b/api/sys_leases.go index 40826a7d2..e018015de 100644 --- a/api/sys_leases.go +++ b/api/sys_leases.go @@ -27,6 +27,27 @@ func (c *Sys) Renew(id string, increment int) (*Secret, error) { return ParseSecret(resp.Body) } +func (c *Sys) Lookup(id string) (*Secret, error) { + r := c.c.NewRequest("PUT", "/v1/sys/leases/lookup") + + body := map[string]interface{}{ + "lease_id": id, + } + if err := r.SetJSONBody(body); err != nil { + return nil, err + } + + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + resp, err := c.c.RawRequestWithContext(ctx, r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return ParseSecret(resp.Body) +} + func (c *Sys) Revoke(id string) error { r := c.c.NewRequest("PUT", "/v1/sys/leases/revoke") body := map[string]interface{}{ diff --git a/changelog/11129.txt b/changelog/11129.txt new file mode 100644 index 000000000..8faedf23a --- /dev/null +++ b/changelog/11129.txt @@ -0,0 +1,3 @@ +```release-note:feature +cli/api: Add lease lookup command +``` diff --git a/command/commands.go b/command/commands.go index 0635cf1b6..6fb676b64 100644 --- a/command/commands.go +++ b/command/commands.go @@ -286,6 +286,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { BaseCommand: getBaseCommand(), }, nil }, + "lease lookup": func() (cli.Command, error) { + return &LeaseLookupCommand{ + BaseCommand: getBaseCommand(), + }, nil + }, "lease revoke": func() (cli.Command, error) { return &LeaseRevokeCommand{ BaseCommand: getBaseCommand(), diff --git a/command/lease_lookup.go b/command/lease_lookup.go new file mode 100644 index 000000000..4d5aa6da3 --- /dev/null +++ b/command/lease_lookup.go @@ -0,0 +1,89 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*LeaseLookupCommand)(nil) +var _ cli.CommandAutocomplete = (*LeaseLookupCommand)(nil) + +type LeaseLookupCommand struct { + *BaseCommand +} + +func (c *LeaseLookupCommand) Synopsis() string { + return "Lookup the lease of a secret" +} + +func (c *LeaseLookupCommand) Help() string { + helpText := ` +Usage: vault lease lookup ID + + Lookup the lease information of a secret. + + Every secret in Vault has a lease associated with it. Users can look up + information on the lease by referencing the lease ID. + + Lookup lease of a secret: + + $ vault lease lookup database/creds/readonly/2f6a614c... + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *LeaseLookupCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + + return set +} + +func (c *LeaseLookupCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictAnything +} + +func (c *LeaseLookupCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *LeaseLookupCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + leaseID := "" + + args = f.Args() + switch len(args) { + case 0: + c.UI.Error("Missing ID!") + return 1 + case 1: + leaseID = strings.TrimSpace(args[0]) + default: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + secret, err := client.Sys().Lookup(leaseID) + if err != nil { + c.UI.Error(fmt.Sprintf("error looking up lease id %s: %s", leaseID, err)) + return 2 + } + + return OutputSecret(c.UI, secret) +} diff --git a/command/lease_lookup_test.go b/command/lease_lookup_test.go new file mode 100644 index 000000000..4de63200f --- /dev/null +++ b/command/lease_lookup_test.go @@ -0,0 +1,100 @@ +package command + +import ( + "strings" + "testing" + + "github.com/hashicorp/vault/api" + "github.com/mitchellh/cli" +) + +func testLeaseLookupCommand(tb testing.TB) (*cli.MockUi, *LeaseLookupCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &LeaseLookupCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +// testLeaseLookupCommandMountAndLease mounts a leased secret backend and returns +// the leaseID of an item. +func testLeaseLookupCommandMountAndLease(tb testing.TB, client *api.Client) string { + if err := client.Sys().Mount("testing", &api.MountInput{ + Type: "generic-leased", + }); err != nil { + tb.Fatal(err) + } + + if _, err := client.Logical().Write("testing/foo", map[string]interface{}{ + "key": "value", + "lease": "5m", + }); err != nil { + tb.Fatal(err) + } + + // Read the secret back to get the leaseID + secret, err := client.Logical().Read("testing/foo") + if err != nil { + tb.Fatal(err) + } + if secret == nil || secret.LeaseID == "" { + tb.Fatalf("missing secret or lease: %#v", secret) + } + + return secret.LeaseID +} + +// TestLeaseLookupCommand_Run tests basic lookup +func TestLeaseLookupCommand_Run(t *testing.T) { + t.Parallel() + + t.Run("empty", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + _ = testLeaseLookupCommandMountAndLease(t, client) + + ui, cmd := testLeaseLookupCommand(t) + cmd.client = client + + code := cmd.Run(nil) + if exp := 1; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + expectedMsg := "Missing ID!" + if !strings.Contains(combined, expectedMsg) { + t.Errorf("expected %q to contain %q", combined, expectedMsg) + } + }) + + t.Run("integration", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + leaseID := testLeaseLookupCommandMountAndLease(t, client) + + _, cmd := testLeaseLookupCommand(t) + cmd.client = client + + code := cmd.Run([]string{leaseID}) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testLeaseLookupCommand(t) + assertNoTabs(t, cmd) + }) +} diff --git a/vendor/github.com/hashicorp/vault/api/sys_leases.go b/vendor/github.com/hashicorp/vault/api/sys_leases.go index 40826a7d2..e018015de 100644 --- a/vendor/github.com/hashicorp/vault/api/sys_leases.go +++ b/vendor/github.com/hashicorp/vault/api/sys_leases.go @@ -27,6 +27,27 @@ func (c *Sys) Renew(id string, increment int) (*Secret, error) { return ParseSecret(resp.Body) } +func (c *Sys) Lookup(id string) (*Secret, error) { + r := c.c.NewRequest("PUT", "/v1/sys/leases/lookup") + + body := map[string]interface{}{ + "lease_id": id, + } + if err := r.SetJSONBody(body); err != nil { + return nil, err + } + + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + resp, err := c.c.RawRequestWithContext(ctx, r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return ParseSecret(resp.Body) +} + func (c *Sys) Revoke(id string) error { r := c.c.NewRequest("PUT", "/v1/sys/leases/revoke") body := map[string]interface{}{ diff --git a/website/content/docs/commands/lease.mdx b/website/content/docs/commands/lease.mdx index a67671daa..f76e197e8 100644 --- a/website/content/docs/commands/lease.mdx +++ b/website/content/docs/commands/lease.mdx @@ -14,6 +14,20 @@ secrets. For leases attached to tokens, use the [`vault token`](/docs/commands/t ## Examples +Lookup a lease: + +```shell-session +$ vault lease lookup database/creds/readonly/27e1b9a1-27b8-83d9-9fe0-d99d786bdc83 +Key Value +--- ----- +expire_time 2021-03-17T11:55:50.755313-05:00 +id database/creds/readonly/27e1b9a1-27b8-83d9-9fe0-d99d786bdc83 +issue_time 2021-03-17T11:45:50.755312-05:00 +last_renewal +renewable true +ttl 9m52s +``` + Renew a lease: ```shell-session @@ -40,6 +54,7 @@ Usage: vault lease [options] [args] # ... Subcommands: + lookup Lookup lease information by lease id renew Renews the lease of a secret revoke Revokes leases and secrets ``` diff --git a/website/content/docs/commands/lease/index.mdx b/website/content/docs/commands/lease/index.mdx index a67671daa..f76e197e8 100644 --- a/website/content/docs/commands/lease/index.mdx +++ b/website/content/docs/commands/lease/index.mdx @@ -14,6 +14,20 @@ secrets. For leases attached to tokens, use the [`vault token`](/docs/commands/t ## Examples +Lookup a lease: + +```shell-session +$ vault lease lookup database/creds/readonly/27e1b9a1-27b8-83d9-9fe0-d99d786bdc83 +Key Value +--- ----- +expire_time 2021-03-17T11:55:50.755313-05:00 +id database/creds/readonly/27e1b9a1-27b8-83d9-9fe0-d99d786bdc83 +issue_time 2021-03-17T11:45:50.755312-05:00 +last_renewal +renewable true +ttl 9m52s +``` + Renew a lease: ```shell-session @@ -40,6 +54,7 @@ Usage: vault lease [options] [args] # ... Subcommands: + lookup Lookup lease information by lease id renew Renews the lease of a secret revoke Revokes leases and secrets ``` diff --git a/website/content/docs/commands/lease/lookup.mdx b/website/content/docs/commands/lease/lookup.mdx new file mode 100644 index 000000000..62f16cbaa --- /dev/null +++ b/website/content/docs/commands/lease/lookup.mdx @@ -0,0 +1,35 @@ +--- +layout: docs +page_title: lease lookup - Command +sidebar_title: lookup +description: |- + The "lease lookup" command retrieves information about a lease. +--- + +# lease lookup + +The `lease lookup` command retrieves information on the lease of a secret. + +Every secret in Vault has a lease associated with it. Users can look up +information on the lease by referencing the lease ID. + +## Examples + +Lookup a lease: + +```shell-session +$ vault lease lookup database/creds/readonly/27e1b9a1-27b8-83d9-9fe0-d99d786bdc83 +Key Value +--- ----- +expire_time 2021-03-17T11:55:50.755313-05:00 +id database/creds/readonly/27e1b9a1-27b8-83d9-9fe0-d99d786bdc83 +issue_time 2021-03-17T11:45:50.755312-05:00 +last_renewal +renewable true +ttl 9m52s +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands) +included on all commands.