Add "policy" subcommand

This commit is contained in:
Seth Vargo 2017-09-07 22:00:21 -04:00
parent 0800385283
commit 2f8bf3c71f
No known key found for this signature in database
GPG Key ID: C921994F9C27E0FF
11 changed files with 761 additions and 97 deletions

View File

@ -0,0 +1,52 @@
package command
import (
"github.com/mitchellh/cli"
)
// Deprecation
// TODO: remove in 0.9.0
var _ cli.Command = (*PoliciesDeprecatedCommand)(nil)
type PoliciesDeprecatedCommand struct {
*BaseCommand
}
func (c *PoliciesDeprecatedCommand) Synopsis() string { return "" }
func (c *PoliciesDeprecatedCommand) Help() string {
return (&PolicyListCommand{
BaseCommand: c.BaseCommand,
}).Help()
}
func (c *PoliciesDeprecatedCommand) Run(args []string) int {
oargs := args
f := c.flagSet(FlagSetHTTP)
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
// Got an arg, this is trying to read a policy
if len(args) > 0 {
return (&PolicyReadCommand{
BaseCommand: &BaseCommand{
UI: c.UI,
client: c.client,
},
}).Run(oargs)
}
// No args, probably ran "vault policies" and we want to translate that to
// "vault policy list"
return (&PolicyListCommand{
BaseCommand: &BaseCommand{
UI: c.UI,
client: c.client,
},
}).Run(oargs)
}

View File

@ -0,0 +1,96 @@
package command
import (
"strings"
"testing"
"github.com/mitchellh/cli"
)
func testPoliciesDeprecatedCommand(tb testing.TB) (*cli.MockUi, *PoliciesDeprecatedCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &PoliciesDeprecatedCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestPoliciesDeprecatedCommand_Run(t *testing.T) {
t.Parallel()
// TODO: remove in 0.9.0
t.Run("deprecated_arg", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPoliciesDeprecatedCommand(t)
cmd.client = client
// vault policies ARG -> vault policy read ARG
code := cmd.Run([]string{"default"})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
stdout := ui.OutputWriter.String()
if expected := "token/"; !strings.Contains(stdout, expected) {
t.Errorf("expected %q to contain %q", stdout, expected)
}
})
t.Run("deprecated_no_args", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPoliciesDeprecatedCommand(t)
cmd.client = client
// vault policies -> vault policy list
code := cmd.Run([]string{})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
stdout := ui.OutputWriter.String()
if expected := "root"; !strings.Contains(stdout, expected) {
t.Errorf("expected %q to contain %q", stdout, expected)
}
})
t.Run("deprecated_with_flags", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPoliciesDeprecatedCommand(t)
cmd.client = client
// vault policies -flag -> vault policy list
code := cmd.Run([]string{
"-address", client.Address(),
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
stdout := ui.OutputWriter.String()
if expected := "root"; !strings.Contains(stdout, expected) {
t.Errorf("expected %q to contain %q", stdout, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testPoliciesDeprecatedCommand(t)
assertNoTabs(t, cmd)
})
}

47
command/policy.go Normal file
View File

@ -0,0 +1,47 @@
package command
import (
"strings"
"github.com/mitchellh/cli"
)
var _ cli.Command = (*PolicyCommand)(nil)
// PolicyCommand is a Command that holds the audit commands
type PolicyCommand struct {
*BaseCommand
}
func (c *PolicyCommand) Synopsis() string {
return "Interact with policies"
}
func (c *PolicyCommand) Help() string {
helpText := `
Usage: vault policy <subcommand> [options] [args]
This command groups subcommands for interacting with policies. Users can
Users can write, read, and list policies in Vault.
List all enabled policies:
$ vault policy list
Create a policy named "my-policy" from contents on local disk:
$ vault policy write my-policy ./my-policy.hcl
Delete the policy named my-policy:
$ vault policy delete my-policy
Please see the individual subcommand help for detailed usage information.
`
return strings.TrimSpace(helpText)
}
func (c *PolicyCommand) Run(args []string) int {
return cli.RunResultHelp
}

View File

@ -8,11 +8,9 @@ import (
"github.com/posener/complete"
)
// Ensure we are implementing the right interfaces.
var _ cli.Command = (*PolicyDeleteCommand)(nil)
var _ cli.CommandAutocomplete = (*PolicyDeleteCommand)(nil)
// PolicyDeleteCommand is a Command that enables a new endpoint.
type PolicyDeleteCommand struct {
*BaseCommand
}
@ -23,17 +21,17 @@ func (c *PolicyDeleteCommand) Synopsis() string {
func (c *PolicyDeleteCommand) Help() string {
helpText := `
Usage: vault policy-delete [options] NAME
Usage: vault policy delete [options] NAME
Deletes a policy in the Vault server with the given name. Once the policy
is deleted, all tokens associated with the policy will be affected
immediately.
Deletes the policy named NAME in the Vault server. Once the policy is deleted,
all tokens associated with the policy are affected immediately.
Delete the policy named "my-policy":
$ vault policy-delete my-policy
$ vault policy delete my-policy
For a full list of examples, please see the documentation.
Note that it is not possible to delete the "default" or "root" policies.
These are built-in policies.
` + c.Flags().Help()

109
command/policy_fmt.go Normal file
View File

@ -0,0 +1,109 @@
package command
import (
"fmt"
"io/ioutil"
"strings"
"github.com/hashicorp/hcl/hcl/printer"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
homedir "github.com/mitchellh/go-homedir"
"github.com/posener/complete"
)
var _ cli.Command = (*PolicyFmtCommand)(nil)
var _ cli.CommandAutocomplete = (*PolicyFmtCommand)(nil)
type PolicyFmtCommand struct {
*BaseCommand
}
func (c *PolicyFmtCommand) Synopsis() string {
return "Formats a policy on disk"
}
func (c *PolicyFmtCommand) Help() string {
helpText := `
Usage: vault policy fmt [options] PATH
Formats a local policy file to the policy specification. This command will
overwrite the file at the given PATH with the properly-formatted policy
file contents.
Format the local file "my-policy.hcl" as a policy file:
$ vault policy fmt my-policy.hcl
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *PolicyFmtCommand) Flags() *FlagSets {
return c.flagSet(FlagSetNone)
}
func (c *PolicyFmtCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictFiles("*.hcl")
}
func (c *PolicyFmtCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *PolicyFmtCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
// Get the filepath, accounting for ~ and stuff
path, err := homedir.Expand(strings.TrimSpace(args[0]))
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to expand path: %s", err))
return 1
}
// Read the entire contents into memory - it would be nice if we could use
// a buffer, but hcl wants the full contents.
b, err := ioutil.ReadFile(path)
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading source file: %s", err))
return 1
}
// Actually parse the policy
if _, err := vault.Parse(string(b)); err != nil {
c.UI.Error(err.Error())
return 1
}
// Generate final contents
result, err := printer.Format(b)
if err != nil {
c.UI.Error(fmt.Sprintf("Error printing result: %s", err))
return 1
}
// Write them back out
if err := ioutil.WriteFile(path, result, 0644); err != nil {
c.UI.Error(fmt.Sprintf("Error writing result: %s", err))
return 1
}
c.UI.Output(fmt.Sprintf("Success! Formatted policy: %s", path))
return 0
}

213
command/policy_fmt_test.go Normal file
View File

@ -0,0 +1,213 @@
package command
import (
"io/ioutil"
"os"
"strings"
"testing"
"github.com/mitchellh/cli"
)
func testPolicyFmtCommand(tb testing.TB) (*cli.MockUi, *PolicyFmtCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &PolicyFmtCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestPolicyFmtCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"not_enough_args",
[]string{},
"Not enough arguments",
1,
},
{
"too_many_args",
[]string{"foo", "bar"},
"Too many arguments",
1,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ui, cmd := testPolicyFmtCommand(t)
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("default", func(t *testing.T) {
t.Parallel()
policy := strings.TrimSpace(`
path "secret" {
capabilities = ["create", "update","delete"]
}
`)
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())
if _, err := f.Write([]byte(policy)); err != nil {
t.Fatal(err)
}
f.Close()
_, cmd := testPolicyFmtCommand(t)
code := cmd.Run([]string{
f.Name(),
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := strings.TrimSpace(`
path "secret" {
capabilities = ["create", "update", "delete"]
}
`) + "\n"
contents, err := ioutil.ReadFile(f.Name())
if err != nil {
t.Fatal(err)
}
if string(contents) != expected {
t.Errorf("expected %q to be %q", string(contents), expected)
}
})
t.Run("bad_hcl", func(t *testing.T) {
t.Parallel()
policy := `dafdaf`
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())
if _, err := f.Write([]byte(policy)); err != nil {
t.Fatal(err)
}
f.Close()
ui, cmd := testPolicyFmtCommand(t)
code := cmd.Run([]string{
f.Name(),
})
if exp := 1; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
stderr := ui.ErrorWriter.String()
expected := "Failed to parse policy"
if !strings.Contains(stderr, expected) {
t.Errorf("expected %q to include %q", stderr, expected)
}
})
t.Run("bad_policy", func(t *testing.T) {
t.Parallel()
policy := `banana "foo" {}`
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())
if _, err := f.Write([]byte(policy)); err != nil {
t.Fatal(err)
}
f.Close()
ui, cmd := testPolicyFmtCommand(t)
code := cmd.Run([]string{
f.Name(),
})
if exp := 1; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
stderr := ui.ErrorWriter.String()
expected := "Failed to parse policy"
if !strings.Contains(stderr, expected) {
t.Errorf("expected %q to include %q", stderr, expected)
}
})
t.Run("bad_policy", func(t *testing.T) {
t.Parallel()
policy := `path "secret/" { capabilities = ["bogus"] }`
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())
if _, err := f.Write([]byte(policy)); err != nil {
t.Fatal(err)
}
f.Close()
ui, cmd := testPolicyFmtCommand(t)
code := cmd.Run([]string{
f.Name(),
})
if exp := 1; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
stderr := ui.ErrorWriter.String()
expected := "Failed to parse policy"
if !strings.Contains(stderr, expected) {
t.Errorf("expected %q to include %q", stderr, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testPolicyFmtCommand(t)
assertNoTabs(t, cmd)
})
}

View File

@ -8,11 +8,9 @@ import (
"github.com/posener/complete"
)
// Ensure we are implementing the right interfaces.
var _ cli.Command = (*PolicyListCommand)(nil)
var _ cli.CommandAutocomplete = (*PolicyListCommand)(nil)
// PolicyListCommand is a Command that enables a new endpoint.
type PolicyListCommand struct {
*BaseCommand
}
@ -23,20 +21,9 @@ func (c *PolicyListCommand) Synopsis() string {
func (c *PolicyListCommand) Help() string {
helpText := `
Usage: vault policies [options] [NAME]
Usage: vault policy list [options]
Lists the policies that are installed on the Vault server. If the optional
argument is given, this command returns the policy's contents.
List all policies stored in Vault:
$ vault policies
Read the contents of the policy named "my-policy":
$ vault policies my-policy
For a full list of examples, please see the documentation.
Lists the names of the policies that are installed on the Vault server.
` + c.Flags().Help()
@ -48,7 +35,7 @@ func (c *PolicyListCommand) Flags() *FlagSets {
}
func (c *PolicyListCommand) AutocompleteArgs() complete.Predictor {
return c.PredictVaultPolicies()
return nil
}
func (c *PolicyListCommand) AutocompleteFlags() complete.Flags {
@ -64,10 +51,9 @@ func (c *PolicyListCommand) Run(args []string) int {
}
args = f.Args()
switch len(args) {
case 0, 1:
default:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0-2, got %d)", len(args)))
switch {
case len(args) > 0:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args)))
return 1
}
@ -77,28 +63,13 @@ func (c *PolicyListCommand) Run(args []string) int {
return 2
}
switch len(args) {
case 0:
policies, err := client.Sys().ListPolicies()
if err != nil {
c.UI.Error(fmt.Sprintf("Error listing policies: %s", err))
return 2
}
for _, p := range policies {
c.UI.Output(p)
}
case 1:
name := strings.ToLower(strings.TrimSpace(args[0]))
rules, err := client.Sys().GetPolicy(name)
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading policy %s: %s", name, err))
return 2
}
if rules == "" {
c.UI.Error(fmt.Sprintf("Error reading policy: no policy named: %s", name))
return 2
}
c.UI.Output(strings.TrimSpace(rules))
policies, err := client.Sys().ListPolicies()
if err != nil {
c.UI.Error(fmt.Sprintf("Error listing policies: %s", err))
return 2
}
for _, p := range policies {
c.UI.Output(p)
}
return 0

View File

@ -29,16 +29,10 @@ func TestPolicyListCommand_Run(t *testing.T) {
}{
{
"too_many_args",
[]string{"foo", "bar"},
[]string{"foo"},
"Too many arguments",
1,
},
{
"no_policy_exists",
[]string{"not-a-real-policy"},
"no policy named",
2,
},
}
t.Run("validations", func(t *testing.T) {
@ -69,7 +63,7 @@ func TestPolicyListCommand_Run(t *testing.T) {
}
})
t.Run("list", func(t *testing.T) {
t.Run("default", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
@ -90,33 +84,6 @@ func TestPolicyListCommand_Run(t *testing.T) {
}
})
t.Run("read", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
policy := `path "secret/" {}`
if err := client.Sys().PutPolicy("my-policy", policy); err != nil {
t.Fatal(err)
}
ui, cmd := testPolicyListCommand(t)
cmd.client = client
code := cmd.Run([]string{
"my-policy",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, policy) {
t.Errorf("expected %q to contain %q", combined, policy)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()

87
command/policy_read.go Normal file
View File

@ -0,0 +1,87 @@
package command
import (
"fmt"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*PolicyReadCommand)(nil)
var _ cli.CommandAutocomplete = (*PolicyReadCommand)(nil)
type PolicyReadCommand struct {
*BaseCommand
}
func (c *PolicyReadCommand) Synopsis() string {
return "Prints the contents of a policy"
}
func (c *PolicyReadCommand) Help() string {
helpText := `
Usage: vault policy read [options] [NAME]
Prints the contents and metadata of the Vault policy named NAME. If the policy
does not exist, an error is returned.
Read the policy named "my-policy":
$ vault policy read my-policy
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *PolicyReadCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP)
}
func (c *PolicyReadCommand) AutocompleteArgs() complete.Predictor {
return c.PredictVaultPolicies()
}
func (c *PolicyReadCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *PolicyReadCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
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
}
name := strings.ToLower(strings.TrimSpace(args[0]))
rules, err := client.Sys().GetPolicy(name)
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading policy named %s: %s", name, err))
return 2
}
if rules == "" {
c.UI.Error(fmt.Sprintf("No policy named: %s", name))
return 2
}
c.UI.Output(strings.TrimSpace(rules))
return 0
}

128
command/policy_read_test.go Normal file
View File

@ -0,0 +1,128 @@
package command
import (
"strings"
"testing"
"github.com/mitchellh/cli"
)
func testPolicyReadCommand(tb testing.TB) (*cli.MockUi, *PolicyReadCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &PolicyReadCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestPolicyReadCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"too_many_args",
[]string{"foo", "bar"},
"Too many arguments",
1,
},
{
"no_policy_exists",
[]string{"not-a-real-policy"},
"No policy named",
2,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPolicyReadCommand(t)
cmd.client = client
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("default", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
policy := `path "secret/" {}`
if err := client.Sys().PutPolicy("my-policy", policy); err != nil {
t.Fatal(err)
}
ui, cmd := testPolicyReadCommand(t)
cmd.client = client
code := cmd.Run([]string{
"my-policy",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, policy) {
t.Errorf("expected %q to contain %q", combined, policy)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testPolicyReadCommand(t)
cmd.client = client
code := cmd.Run([]string{
"my-policy",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error reading policy named my-policy: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testPolicyReadCommand(t)
assertNoTabs(t, cmd)
})
}

View File

@ -11,11 +11,9 @@ import (
"github.com/posener/complete"
)
// Ensure we are implementing the right interfaces.
var _ cli.Command = (*PolicyWriteCommand)(nil)
var _ cli.CommandAutocomplete = (*PolicyWriteCommand)(nil)
// PolicyWriteCommand is a Command uploads a policy
type PolicyWriteCommand struct {
*BaseCommand
@ -23,26 +21,24 @@ type PolicyWriteCommand struct {
}
func (c *PolicyWriteCommand) Synopsis() string {
return "Uploads a policy file"
return "Uploads a named policy from a file"
}
func (c *PolicyWriteCommand) Help() string {
helpText := `
Usage: vault policy-write [options] NAME PATH
Usage: vault policy write [options] NAME PATH
Uploads a policy with the given name from the contents of a local file or
stdin. If the path is "-", the policy is read from stdin. Otherwise, it is
loaded from the file at the given path.
Uploads a policy with name NAME from the contents of a local file PATH or
stdin. If PATH is "-", the policy is read from stdin. Otherwise, it is
loaded from the file at the given path on the local disk.
Upload a policy named "my-policy" from /tmp/policy.hcl on the local disk:
Upload a policy named "my-policy" from "/tmp/policy.hcl" on the local disk:
$ vault policy-write my-policy /tmp/policy.hcl
$ vault policy write my-policy /tmp/policy.hcl
Upload a policy from stdin:
$ cat my-policy.hcl | vault policy-write my-policy -
For a full list of examples, please see the documentation.
$ cat my-policy.hcl | vault policy write my-policy -
` + c.Flags().Help()