Command: token capabilities using accessor (#24479)

* Command: token capabilities using accessor

* release note

* Apply suggestions from code review

Co-authored-by: Marc Boudreau <marc.boudreau@hashicorp.com>

---------

Co-authored-by: Marc Boudreau <marc.boudreau@hashicorp.com>
This commit is contained in:
Mattias Fjellström
2023-12-13 17:15:21 +01:00
committed by GitHub
parent dc5c3e8d97
commit e4ffe8979c
4 changed files with 160 additions and 12 deletions

View File

@@ -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
}

3
changelog/24479.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
command/token-capabilities: allow using accessor when listing token capabilities on a path
```

View File

@@ -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

View File

@@ -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()