diff --git a/api/sys_capabilities.go b/api/sys_capabilities.go index 6310d42fcf..d57b757117 100644 --- a/api/sys_capabilities.go +++ b/api/sys_capabilities.go @@ -78,3 +78,56 @@ func (c *Sys) CapabilitiesWithContext(ctx context.Context, token, path string) ( return res, nil } + +func (c *Sys) CapabilitiesAccessor(accessor, path string) ([]string, error) { + return c.CapabilitiesAccessorWithContext(context.Background(), accessor, path) +} + +func (c *Sys) CapabilitiesAccessorWithContext(ctx context.Context, accessor, path string) ([]string, error) { + ctx, cancelFunc := c.c.withConfiguredTimeout(ctx) + defer cancelFunc() + + body := map[string]string{ + "accessor": accessor, + "path": path, + } + + reqPath := "/v1/sys/capabilities-accessor" + + r := c.c.NewRequest(http.MethodPost, reqPath) + if err := r.SetJSONBody(body); err != nil { + return nil, err + } + + resp, err := c.c.rawRequestWithContext(ctx, r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + secret, err := ParseSecret(resp.Body) + if err != nil { + return nil, err + } + if secret == nil || secret.Data == nil { + return nil, errors.New("data from server response is empty") + } + + var res []string + err = mapstructure.Decode(secret.Data[path], &res) + if err != nil { + return nil, err + } + + if len(res) == 0 { + _, ok := secret.Data["capabilities"] + if ok { + err = mapstructure.Decode(secret.Data["capabilities"], &res) + if err != nil { + return nil, err + } + } + } + + return res, nil +} diff --git a/changelog/24479.txt b/changelog/24479.txt new file mode 100644 index 0000000000..e053e74d67 --- /dev/null +++ b/changelog/24479.txt @@ -0,0 +1,3 @@ +```release-note:improvement +command/token-capabilities: allow using accessor when listing token capabilities on a path +``` diff --git a/command/token_capabilities.go b/command/token_capabilities.go index fc149b9611..239793658b 100644 --- a/command/token_capabilities.go +++ b/command/token_capabilities.go @@ -19,6 +19,8 @@ var ( type TokenCapabilitiesCommand struct { *BaseCommand + + flagAccessor bool } func (c *TokenCapabilitiesCommand) Synopsis() string { @@ -27,12 +29,15 @@ func (c *TokenCapabilitiesCommand) Synopsis() string { func (c *TokenCapabilitiesCommand) Help() string { helpText := ` -Usage: vault token capabilities [options] [TOKEN] PATH +Usage: vault token capabilities [options] [TOKEN | ACCESSOR] PATH - Fetches the capabilities of a token for a given path. If a TOKEN is provided - as an argument, the "/sys/capabilities" endpoint and permission is used. If - no TOKEN is provided, the "/sys/capabilities-self" endpoint and permission - is used with the locally authenticated token. + Fetches the capabilities of a token or accessor for a given path. If a TOKEN + is provided as an argument, the "/sys/capabilities" endpoint is used, which + returns the capabilities of the provided TOKEN. If an ACCESSOR is provided + as an argument along with the -accessor option, the "/sys/capabilities-accessor" + endpoint is used, which returns the capabilities of the token referenced by + ACCESSOR. If no TOKEN is provided, the "/sys/capabilities-self" endpoint + is used, which returns the capabilities of the locally authenticated token. List capabilities for the local token on the "secret/foo" path: @@ -42,6 +47,10 @@ Usage: vault token capabilities [options] [TOKEN] PATH $ vault token capabilities 96ddf4bc-d217-f3ba-f9bd-017055595017 cubbyhole/foo + List capabilities for a token on the "cubbyhole/foo" path via its accessor: + + $ vault token capabilities -accessor 9793c9b3-e04a-46f3-e7b8-748d7da248da cubbyhole/foo + For a full list of examples, please see the documentation. ` + c.Flags().Help() @@ -50,7 +59,20 @@ Usage: vault token capabilities [options] [TOKEN] PATH } func (c *TokenCapabilitiesCommand) Flags() *FlagSets { - return c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + + f := set.NewFlagSet("Command Options") + + f.BoolVar(&BoolVar{ + Name: "accessor", + Target: &c.flagAccessor, + Default: false, + EnvVar: "", + Completion: complete.PredictNothing, + Usage: "Treat the argument as an accessor instead of a token.", + }) + + return set } func (c *TokenCapabilitiesCommand) AutocompleteArgs() complete.Predictor { @@ -72,13 +94,19 @@ func (c *TokenCapabilitiesCommand) Run(args []string) int { token := "" path := "" args = f.Args() - switch len(args) { - case 0: + switch { + case c.flagAccessor && len(args) < 2: + c.UI.Error(fmt.Sprintf("Not enough arguments with -accessor (expected 2, got %d)", len(args))) + return 1 + case c.flagAccessor && len(args) > 2: + c.UI.Error(fmt.Sprintf("Too many arguments with -accessor (expected 2, got %d)", len(args))) + return 1 + case len(args) == 0: c.UI.Error("Not enough arguments (expected 1-2, got 0)") return 1 - case 1: + case len(args) == 1: path = args[0] - case 2: + case len(args) == 2: token, path = args[0], args[1] default: c.UI.Error(fmt.Sprintf("Too many arguments (expected 1-2, got %d)", len(args))) @@ -92,11 +120,15 @@ func (c *TokenCapabilitiesCommand) Run(args []string) int { } var capabilities []string - if token == "" { + switch { + case token == "": capabilities, err = client.Sys().CapabilitiesSelf(path) - } else { + case c.flagAccessor: + capabilities, err = client.Sys().CapabilitiesAccessor(token, path) + default: capabilities, err = client.Sys().Capabilities(token, path) } + if err != nil { c.UI.Error(fmt.Sprintf("Error listing capabilities: %s", err)) return 2 diff --git a/command/token_capabilities_test.go b/command/token_capabilities_test.go index a3e5b9525a..1588b14a33 100644 --- a/command/token_capabilities_test.go +++ b/command/token_capabilities_test.go @@ -31,6 +31,24 @@ func TestTokenCapabilitiesCommand_Run(t *testing.T) { out string code int }{ + { + "accessor_no_args", + []string{"-accessor"}, + "Not enough arguments", + 1, + }, + { + "accessor_too_few_args", + []string{"-accessor", "abcd1234"}, + "Not enough arguments", + 1, + }, + { + "accessor_too_many_args", + []string{"-accessor", "abcd1234", "efgh5678", "ijkl9012"}, + "Too many arguments", + 1, + }, { "too_many_args", []string{"foo", "bar", "zip"}, @@ -103,6 +121,48 @@ func TestTokenCapabilitiesCommand_Run(t *testing.T) { } }) + t.Run("accessor", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + policy := `path "secret/foo" { capabilities = ["read"] }` + if err := client.Sys().PutPolicy("policy", policy); err != nil { + t.Error(err) + } + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"policy"}, + TTL: "30m", + }) + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { + t.Fatalf("missing auth data: %#v", secret) + } + accessor := secret.Auth.Accessor + + ui, cmd := testTokenCapabilitiesCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-accessor", + accessor, + "secret/foo", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "read" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + t.Run("local", func(t *testing.T) { t.Parallel()