cli: add new acl role subcommands for CRUD role actions. (#14087)

This commit is contained in:
James Rasell
2022-08-12 09:52:32 +02:00
committed by GitHub
parent b8fe43a372
commit ff798dc560
13 changed files with 1338 additions and 0 deletions

102
command/acl_role.go Normal file
View File

@@ -0,0 +1,102 @@
package command
import (
"fmt"
"sort"
"strings"
"github.com/hashicorp/nomad/api"
"github.com/mitchellh/cli"
)
// Ensure ACLRoleCommand satisfies the cli.Command interface.
var _ cli.Command = &ACLRoleCommand{}
// ACLRoleCommand implements cli.Command.
type ACLRoleCommand struct {
Meta
}
// Help satisfies the cli.Command Help function.
func (a *ACLRoleCommand) Help() string {
helpText := `
Usage: nomad acl role <subcommand> [options] [args]
This command groups subcommands for interacting with ACL roles. Nomad's ACL
system can be used to control access to data and APIs. ACL roles are
associated with one or more ACL policies which grant specific capabilities.
For a full guide see: https://www.nomadproject.io/guides/acl.html
Create an ACL role:
$ nomad acl role create -name="name" -policy-name="policy-name"
List all ACL roles:
$ nomad acl role list
Lookup a specific ACL role:
$ nomad acl role info <acl_role_id>
Update an ACL role:
$ nomad acl role update -name="updated-name" <acl_role_id>
Delete an ACL role:
$ nomad acl role delete <acl_role_id>
Please see the individual subcommand help for detailed usage information.
`
return strings.TrimSpace(helpText)
}
// Synopsis satisfies the cli.Command Synopsis function.
func (a *ACLRoleCommand) Synopsis() string { return "Interact with ACL roles" }
// Name returns the name of this command.
func (a *ACLRoleCommand) Name() string { return "acl role" }
// Run satisfies the cli.Command Run function.
func (a *ACLRoleCommand) Run(_ []string) int { return cli.RunResultHelp }
// formatACLRole formats and converts the ACL role API object into a string KV
// representation suitable for console output.
func formatACLRole(aclRole *api.ACLRole) string {
return formatKV([]string{
fmt.Sprintf("ID|%s", aclRole.ID),
fmt.Sprintf("Name|%s", aclRole.Name),
fmt.Sprintf("Description|%s", aclRole.Description),
fmt.Sprintf("Policies|%s", strings.Join(aclRolePolicyLinkToStringList(aclRole.Policies), ",")),
fmt.Sprintf("Create Index|%d", aclRole.CreateIndex),
fmt.Sprintf("Modify Index|%d", aclRole.ModifyIndex),
})
}
// aclRolePolicyLinkToStringList converts an array of ACL role policy links to
// an array of string policy names. The returned array will be sorted.
func aclRolePolicyLinkToStringList(policyLinks []*api.ACLRolePolicyLink) []string {
policies := make([]string, len(policyLinks))
for i, policy := range policyLinks {
policies[i] = policy.Name
}
sort.Strings(policies)
return policies
}
// aclRolePolicyNamesToPolicyLinks takes a list of policy names as a string
// array and converts this to an array of ACL role policy links. Any duplicate
// names are removed.
func aclRolePolicyNamesToPolicyLinks(policyNames []string) []*api.ACLRolePolicyLink {
var policyLinks []*api.ACLRolePolicyLink
keys := make(map[string]struct{})
for _, policyName := range policyNames {
if _, ok := keys[policyName]; !ok {
policyLinks = append(policyLinks, &api.ACLRolePolicyLink{Name: policyName})
keys[policyName] = struct{}{}
}
}
return policyLinks
}

148
command/acl_role_create.go Normal file
View File

@@ -0,0 +1,148 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/nomad/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
// Ensure ACLRoleCreateCommand satisfies the cli.Command interface.
var _ cli.Command = &ACLRoleCreateCommand{}
// ACLRoleCreateCommand implements cli.Command.
type ACLRoleCreateCommand struct {
Meta
name string
description string
policyNames []string
json bool
tmpl string
}
// Help satisfies the cli.Command Help function.
func (a *ACLRoleCreateCommand) Help() string {
helpText := `
Usage: nomad acl token create [options]
Create is used to create new ACL roles. Use requires a management token.
General Options:
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
ACL Create Options:
-name
Sets the human readable name for the ACL role. The name must be between
1-128 characters and is a required parameter.
-description
A free form text description of the role that must not exceed 256
characters.
-policy-name
Specifies a policy to associate with the role identified by their name. This
flag can be specified multiple times and must be specified at least once.
-json
Output the ACL role in a JSON format.
-t
Format and display the ACL role using a Go template.
`
return strings.TrimSpace(helpText)
}
func (a *ACLRoleCreateCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(a.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-name": complete.PredictAnything,
"-description": complete.PredictAnything,
"-policy-name": complete.PredictAnything,
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
})
}
func (a *ACLRoleCreateCommand) AutocompleteArgs() complete.Predictor { return complete.PredictNothing }
// Synopsis satisfies the cli.Command Synopsis function.
func (a *ACLRoleCreateCommand) Synopsis() string { return "Create a new ACL role" }
// Name returns the name of this command.
func (a *ACLRoleCreateCommand) Name() string { return "acl role create" }
// Run satisfies the cli.Command Run function.
func (a *ACLRoleCreateCommand) Run(args []string) int {
flags := a.Meta.FlagSet(a.Name(), FlagSetClient)
flags.Usage = func() { a.Ui.Output(a.Help()) }
flags.StringVar(&a.name, "name", "", "")
flags.StringVar(&a.description, "description", "", "")
flags.Var((funcVar)(func(s string) error {
a.policyNames = append(a.policyNames, s)
return nil
}), "policy-name", "")
flags.BoolVar(&a.json, "json", false, "")
flags.StringVar(&a.tmpl, "t", "", "")
if err := flags.Parse(args); err != nil {
return 1
}
// Check that we got no arguments.
if len(flags.Args()) != 0 {
a.Ui.Error("This command takes no arguments")
a.Ui.Error(commandErrorText(a))
return 1
}
// Perform some basic validation on the submitted role information to avoid
// sending API and RPC requests which will fail basic validation.
if a.name == "" {
a.Ui.Error("ACL role name must be specified using the -name flag")
return 1
}
if len(a.policyNames) < 1 {
a.Ui.Error("At least one policy name must be specified using the -policy-name flag")
return 1
}
// Set up the ACL with the passed parameters.
aclRole := api.ACLRole{
Name: a.name,
Description: a.description,
Policies: aclRolePolicyNamesToPolicyLinks(a.policyNames),
}
// Get the HTTP client.
client, err := a.Meta.Client()
if err != nil {
a.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}
// Create the ACL role via the API.
role, _, err := client.ACLRoles().Create(&aclRole, nil)
if err != nil {
a.Ui.Error(fmt.Sprintf("Error creating ACL role: %s", err))
return 1
}
if a.json || len(a.tmpl) > 0 {
out, err := Format(a.json, a.tmpl, role)
if err != nil {
a.Ui.Error(err.Error())
return 1
}
a.Ui.Output(out)
return 0
}
a.Ui.Output(formatACLRole(role))
return 0
}

View File

@@ -0,0 +1,80 @@
package command
import (
"testing"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/testutil"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestACLRoleCreateCommand_Run(t *testing.T) {
ci.Parallel(t)
// Build a test server with ACLs enabled.
srv, _, url := testServer(t, false, func(c *agent.Config) {
c.ACL.Enabled = true
})
defer srv.Shutdown()
// Wait for the server to start fully and ensure we have a bootstrap token.
testutil.WaitForLeader(t, srv.Agent.RPC)
rootACLToken := srv.RootToken
require.NotNil(t, rootACLToken)
ui := cli.NewMockUi()
cmd := &ACLRoleCreateCommand{
Meta: Meta{
Ui: ui,
flagAddress: url,
},
}
// Test the basic validation on the command.
require.Equal(t, 1, cmd.Run([]string{"-address=" + url, "this-command-does-not-take-args"}))
require.Contains(t, ui.ErrorWriter.String(), "This command takes no arguments")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
require.Equal(t, 1, cmd.Run([]string{"-address=" + url}))
require.Contains(t, ui.ErrorWriter.String(), "ACL role name must be specified using the -name flag")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
require.Equal(t, 1, cmd.Run([]string{"-address=" + url, `-name="foobar"`}))
require.Contains(t, ui.ErrorWriter.String(), "At least one policy name must be specified using the -policy-name flag")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
// Create an ACL policy that can be referenced within the ACL role.
aclPolicy := structs.ACLPolicy{
Name: "acl-role-cli-test-policy",
Rules: `namespace "default" {
policy = "read"
}
`,
}
err := srv.Agent.Server().State().UpsertACLPolicies(
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{&aclPolicy})
require.NoError(t, err)
// Create an ACL role.
args := []string{
"-address=" + url, "-token=" + rootACLToken.SecretID, "-name=acl-role-cli-test",
"-policy-name=acl-role-cli-test-policy", "-description=acl-role-all-the-things",
}
require.Equal(t, 0, cmd.Run(args))
s := ui.OutputWriter.String()
require.Contains(t, s, "Name = acl-role-cli-test")
require.Contains(t, s, "Description = acl-role-all-the-things")
require.Contains(t, s, "Policies = acl-role-cli-test-policy")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
}

View File

@@ -0,0 +1,83 @@
package command
import (
"fmt"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
// Ensure ACLRoleDeleteCommand satisfies the cli.Command interface.
var _ cli.Command = &ACLRoleDeleteCommand{}
// ACLRoleDeleteCommand implements cli.Command.
type ACLRoleDeleteCommand struct {
Meta
}
// Help satisfies the cli.Command Help function.
func (a *ACLRoleDeleteCommand) Help() string {
helpText := `
Usage: nomad acl role delete <acl_role_id>
Delete is used to delete an existing ACL role. Use requires a management
token.
General Options:
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace)
return strings.TrimSpace(helpText)
}
func (a *ACLRoleDeleteCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(a.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{})
}
func (a *ACLRoleDeleteCommand) AutocompleteArgs() complete.Predictor { return complete.PredictNothing }
// Synopsis satisfies the cli.Command Synopsis function.
func (a *ACLRoleDeleteCommand) Synopsis() string { return "Delete an existing ACL role" }
// Name returns the name of this command.
func (a *ACLRoleDeleteCommand) Name() string { return "acl token delete" }
// Run satisfies the cli.Command Run function.
func (a *ACLRoleDeleteCommand) Run(args []string) int {
flags := a.Meta.FlagSet(a.Name(), FlagSetClient)
flags.Usage = func() { a.Ui.Output(a.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
// Check that the last argument is the role ID to delete.
if len(flags.Args()) != 1 {
a.Ui.Error("This command takes one argument: <acl_role_id>")
a.Ui.Error(commandErrorText(a))
return 1
}
aclRoleID := flags.Args()[0]
// Get the HTTP client.
client, err := a.Meta.Client()
if err != nil {
a.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}
// Delete the specified ACL role.
_, err = client.ACLRoles().Delete(aclRoleID, nil)
if err != nil {
a.Ui.Error(fmt.Sprintf("Error deleting ACL role: %s", err))
return 1
}
// Give some feedback to indicate the deletion was successful.
a.Ui.Output(fmt.Sprintf("ACL role %s successfully deleted", aclRoleID))
return 0
}

View File

@@ -0,0 +1,77 @@
package command
import (
"testing"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/testutil"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestACLRoleDeleteCommand_Run(t *testing.T) {
ci.Parallel(t)
// Build a test server with ACLs enabled.
srv, _, url := testServer(t, false, func(c *agent.Config) {
c.ACL.Enabled = true
})
defer srv.Shutdown()
// Wait for the server to start fully and ensure we have a bootstrap token.
testutil.WaitForLeader(t, srv.Agent.RPC)
rootACLToken := srv.RootToken
require.NotNil(t, rootACLToken)
ui := cli.NewMockUi()
cmd := &ACLRoleDeleteCommand{
Meta: Meta{
Ui: ui,
flagAddress: url,
},
}
// Try and delete more than one ACL role.
code := cmd.Run([]string{"-address=" + url, "acl-role-1", "acl-role-2"})
require.Equal(t, 1, code)
require.Contains(t, ui.ErrorWriter.String(), "This command takes one argument")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
// Try deleting a role that does not exist.
require.Equal(t, 1, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, "acl-role-1"}))
require.Contains(t, ui.ErrorWriter.String(), "ACL role not found")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
// Create an ACL policy that can be referenced within the ACL role.
aclPolicy := structs.ACLPolicy{
Name: "acl-role-cli-test",
Rules: `namespace "default" {
policy = "read"
}
`,
}
err := srv.Agent.Server().State().UpsertACLPolicies(
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{&aclPolicy})
require.NoError(t, err)
// Create an ACL role referencing the previously created policy.
aclRole := structs.ACLRole{
ID: uuid.Generate(),
Name: "acl-role-cli-test",
Policies: []*structs.ACLRolePolicyLink{{Name: aclPolicy.Name}},
}
err = srv.Agent.Server().State().UpsertACLRoles(
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{&aclRole})
require.NoError(t, err)
// Delete the existing ACL role.
require.Equal(t, 0, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, aclRole.ID}))
require.Contains(t, ui.OutputWriter.String(), "successfully deleted")
}

121
command/acl_role_info.go Normal file
View File

@@ -0,0 +1,121 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/nomad/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
// Ensure ACLRoleInfoCommand satisfies the cli.Command interface.
var _ cli.Command = &ACLRoleInfoCommand{}
// ACLRoleInfoCommand implements cli.Command.
type ACLRoleInfoCommand struct {
Meta
byName bool
json bool
tmpl string
}
// Help satisfies the cli.Command Help function.
func (a *ACLRoleInfoCommand) Help() string {
helpText := `
Usage: nomad acl role info [options] <acl_role_id>
Info is used to fetch information on an existing ACL roles. Requires a
management token.
General Options:
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
ACL Info Options:
-by-name
Look up the ACL role using its name as the identifier. The command defaults
to expecting the ACL ID as the argument.
-json
Output the ACL role in a JSON format.
-t
Format and display the ACL role using a Go template.
`
return strings.TrimSpace(helpText)
}
func (a *ACLRoleInfoCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(a.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-by-name": complete.PredictNothing,
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
})
}
func (a *ACLRoleInfoCommand) AutocompleteArgs() complete.Predictor { return complete.PredictNothing }
// Synopsis satisfies the cli.Command Synopsis function.
func (a *ACLRoleInfoCommand) Synopsis() string { return "Fetch information on an existing ACL role" }
// Name returns the name of this command.
func (a *ACLRoleInfoCommand) Name() string { return "acl role info" }
// Run satisfies the cli.Command Run function.
func (a *ACLRoleInfoCommand) Run(args []string) int {
flags := a.Meta.FlagSet(a.Name(), FlagSetClient)
flags.Usage = func() { a.Ui.Output(a.Help()) }
flags.BoolVar(&a.byName, "by-name", false, "")
flags.BoolVar(&a.json, "json", false, "")
flags.StringVar(&a.tmpl, "t", "", "")
if err := flags.Parse(args); err != nil {
return 1
}
// Check that we have exactly one argument.
if len(flags.Args()) != 1 {
a.Ui.Error("This command takes one argument: <acl_role_id>")
a.Ui.Error(commandErrorText(a))
return 1
}
// Get the HTTP client.
client, err := a.Meta.Client()
if err != nil {
a.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}
var (
aclRole *api.ACLRole
apiErr error
)
aclRoleID := flags.Args()[0]
// Use the correct API call depending on whether the lookup is by the name
// or the ID.
switch a.byName {
case true:
aclRole, _, apiErr = client.ACLRoles().GetByName(aclRoleID, nil)
default:
aclRole, _, apiErr = client.ACLRoles().Get(aclRoleID, nil)
}
// Handle any error from the API.
if apiErr != nil {
a.Ui.Error(fmt.Sprintf("Error reading ACL role: %s", apiErr))
return 1
}
// Format the output.
a.Ui.Output(formatACLRole(aclRole))
return 0
}

View File

@@ -0,0 +1,95 @@
package command
import (
"fmt"
"testing"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/testutil"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestACLRoleInfoCommand_Run(t *testing.T) {
ci.Parallel(t)
// Build a test server with ACLs enabled.
srv, _, url := testServer(t, false, func(c *agent.Config) {
c.ACL.Enabled = true
})
defer srv.Shutdown()
// Wait for the server to start fully and ensure we have a bootstrap token.
testutil.WaitForLeader(t, srv.Agent.RPC)
rootACLToken := srv.RootToken
require.NotNil(t, rootACLToken)
ui := cli.NewMockUi()
cmd := &ACLRoleInfoCommand{
Meta: Meta{
Ui: ui,
flagAddress: url,
},
}
// Perform a lookup without specifying an ID.
require.Equal(t, 1, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID}))
require.Contains(t, ui.ErrorWriter.String(), "This command takes one argument: <acl_role_id>")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
// Perform a lookup specifying a random ID.
require.Equal(t, 1, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, uuid.Generate()}))
require.Contains(t, ui.ErrorWriter.String(), "ACL role not found")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
// Create an ACL policy that can be referenced within the ACL role.
aclPolicy := structs.ACLPolicy{
Name: "acl-role-policy-cli-test",
Rules: `namespace "default" {
policy = "read"
}
`,
}
err := srv.Agent.Server().State().UpsertACLPolicies(
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{&aclPolicy})
require.NoError(t, err)
// Create an ACL role referencing the previously created policy.
aclRole := structs.ACLRole{
ID: uuid.Generate(),
Name: "acl-role-cli-test",
Policies: []*structs.ACLRolePolicyLink{{Name: aclPolicy.Name}},
}
err = srv.Agent.Server().State().UpsertACLRoles(
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{&aclRole})
require.NoError(t, err)
// Look up the ACL role using its ID.
require.Equal(t, 0, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, aclRole.ID}))
s := ui.OutputWriter.String()
require.Contains(t, s, fmt.Sprintf("ID = %s", aclRole.ID))
require.Contains(t, s, fmt.Sprintf("Name = %s", aclRole.Name))
require.Contains(t, s, "Description = <none>")
require.Contains(t, s, fmt.Sprintf("Policies = %s", aclPolicy.Name))
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
// Look up the ACL role using its Name.
require.Equal(t, 0, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, "-by-name", aclRole.Name}))
s = ui.OutputWriter.String()
require.Contains(t, s, fmt.Sprintf("ID = %s", aclRole.ID))
require.Contains(t, s, fmt.Sprintf("Name = %s", aclRole.Name))
require.Contains(t, s, "Description = <none>")
require.Contains(t, s, fmt.Sprintf("Policies = %s", aclPolicy.Name))
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
}

123
command/acl_role_list.go Normal file
View File

@@ -0,0 +1,123 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/nomad/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
// Ensure ACLRoleListCommand satisfies the cli.Command interface.
var _ cli.Command = &ACLRoleListCommand{}
// ACLRoleListCommand implements cli.Command.
type ACLRoleListCommand struct {
Meta
}
// Help satisfies the cli.Command Help function.
func (a *ACLRoleListCommand) Help() string {
helpText := `
Usage: nomad acl role list [options]
List is used to list existing ACL roles.
General Options:
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
ACL List Options:
-json
Output the ACL roles in a JSON format.
-t
Format and display the ACL roles using a Go template.
`
return strings.TrimSpace(helpText)
}
func (a *ACLRoleListCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(a.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
})
}
func (a *ACLRoleListCommand) AutocompleteArgs() complete.Predictor { return complete.PredictNothing }
// Synopsis satisfies the cli.Command Synopsis function.
func (a *ACLRoleListCommand) Synopsis() string { return "List ACL roles" }
// Name returns the name of this command.
func (a *ACLRoleListCommand) Name() string { return "acl role list" }
// Run satisfies the cli.Command Run function.
func (a *ACLRoleListCommand) Run(args []string) int {
var json bool
var tmpl string
flags := a.Meta.FlagSet(a.Name(), FlagSetClient)
flags.Usage = func() { a.Ui.Output(a.Help()) }
flags.BoolVar(&json, "json", false, "")
flags.StringVar(&tmpl, "t", "", "")
if err := flags.Parse(args); err != nil {
return 1
}
// Check that we got no arguments
if len(flags.Args()) != 0 {
a.Ui.Error("This command takes no arguments")
a.Ui.Error(commandErrorText(a))
return 1
}
// Get the HTTP client
client, err := a.Meta.Client()
if err != nil {
a.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}
// Fetch info on the policy
roles, _, err := client.ACLRoles().List(nil)
if err != nil {
a.Ui.Error(fmt.Sprintf("Error listing ACL roles: %s", err))
return 1
}
if json || len(tmpl) > 0 {
out, err := Format(json, tmpl, roles)
if err != nil {
a.Ui.Error(err.Error())
return 1
}
a.Ui.Output(out)
return 0
}
a.Ui.Output(formatACLRoles(roles))
return 0
}
func formatACLRoles(roles []*api.ACLRole) string {
if len(roles) == 0 {
return "No ACL roles found"
}
output := make([]string, 0, len(roles)+1)
output = append(output, "ID|Name|Description|Policies")
for _, role := range roles {
output = append(output, fmt.Sprintf(
"%s|%s|%s|%s",
role.ID, role.Name, role.Description, strings.Join(aclRolePolicyLinkToStringList(role.Policies), ",")))
}
return formatList(output)
}

View File

@@ -0,0 +1,77 @@
package command
import (
"testing"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/testutil"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestACLRoleListCommand_Run(t *testing.T) {
ci.Parallel(t)
// Build a test server with ACLs enabled.
srv, _, url := testServer(t, false, func(c *agent.Config) {
c.ACL.Enabled = true
})
defer srv.Shutdown()
// Wait for the server to start fully and ensure we have a bootstrap token.
testutil.WaitForLeader(t, srv.Agent.RPC)
rootACLToken := srv.RootToken
require.NotNil(t, rootACLToken)
ui := cli.NewMockUi()
cmd := &ACLRoleListCommand{
Meta: Meta{
Ui: ui,
flagAddress: url,
},
}
// Perform a list straight away without any roles held in state.
require.Equal(t, 0, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID}))
require.Contains(t, ui.OutputWriter.String(), "No ACL roles found")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
// Create an ACL policy that can be referenced within the ACL role.
aclPolicy := structs.ACLPolicy{
Name: "acl-role-policy-cli-test",
Rules: `namespace "default" {
policy = "read"
}
`,
}
err := srv.Agent.Server().State().UpsertACLPolicies(
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{&aclPolicy})
require.NoError(t, err)
// Create an ACL role referencing the previously created policy.
aclRole := structs.ACLRole{
ID: uuid.Generate(),
Name: "acl-role-cli-test",
Policies: []*structs.ACLRolePolicyLink{{Name: aclPolicy.Name}},
}
err = srv.Agent.Server().State().UpsertACLRoles(
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{&aclRole})
require.NoError(t, err)
// Perform a listing to get the created role.
require.Equal(t, 0, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID}))
s := ui.OutputWriter.String()
require.Contains(t, s, "ID")
require.Contains(t, s, "Name")
require.Contains(t, s, "Policies")
require.Contains(t, s, "acl-role-cli-test")
require.Contains(t, s, "acl-role-policy-cli-test")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
}

61
command/acl_role_test.go Normal file
View File

@@ -0,0 +1,61 @@
package command
import (
"testing"
"github.com/hashicorp/nomad/api"
"github.com/stretchr/testify/require"
)
func Test_formatACLRole(t *testing.T) {
inputACLRole := api.ACLRole{
ID: "this-is-usually-a-uuid",
Name: "this-is-my-friendly-name",
Description: "this-is-my-friendly-name",
Policies: []*api.ACLRolePolicyLink{
{Name: "policy-link-1"},
{Name: "policy-link-2"},
{Name: "policy-link-3"},
{Name: "policy-link-4"},
},
CreateIndex: 13,
ModifyIndex: 1313,
}
expectedOutput := "ID = this-is-usually-a-uuid\nName = this-is-my-friendly-name\nDescription = this-is-my-friendly-name\nPolicies = policy-link-1,policy-link-2,policy-link-3,policy-link-4\nCreate Index = 13\nModify Index = 1313"
actualOutput := formatACLRole(&inputACLRole)
require.Equal(t, expectedOutput, actualOutput)
}
func Test_aclRolePolicyLinkToStringList(t *testing.T) {
inputPolicyLinks := []*api.ACLRolePolicyLink{
{Name: "z-policy-link-1"},
{Name: "a-policy-link-2"},
{Name: "policy-link-3"},
{Name: "b-policy-link-4"},
}
expectedOutput := []string{
"a-policy-link-2",
"b-policy-link-4",
"policy-link-3",
"z-policy-link-1",
}
actualOutput := aclRolePolicyLinkToStringList(inputPolicyLinks)
require.Equal(t, expectedOutput, actualOutput)
}
func Test_aclRolePolicyNamesToPolicyLinks(t *testing.T) {
inputPolicyNames := []string{
"policy-link-1", "policy-link-2", "policy-link-3", "policy-link-4",
"policy-link-1", "policy-link-2", "policy-link-3", "policy-link-4",
"policy-link-1", "policy-link-2", "policy-link-3", "policy-link-4",
"policy-link-1", "policy-link-2", "policy-link-3", "policy-link-4",
}
expectedOutput := []*api.ACLRolePolicyLink{
{Name: "policy-link-1"},
{Name: "policy-link-2"},
{Name: "policy-link-3"},
{Name: "policy-link-4"},
}
actualOutput := aclRolePolicyNamesToPolicyLinks(inputPolicyNames)
require.ElementsMatch(t, expectedOutput, actualOutput)
}

217
command/acl_role_update.go Normal file
View File

@@ -0,0 +1,217 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/nomad/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
// Ensure ACLRoleUpdateCommand satisfies the cli.Command interface.
var _ cli.Command = &ACLRoleUpdateCommand{}
// ACLRoleUpdateCommand implements cli.Command.
type ACLRoleUpdateCommand struct {
Meta
name string
description string
policyNames []string
noMerge bool
json bool
tmpl string
}
// Help satisfies the cli.Command Help function.
func (a *ACLRoleUpdateCommand) Help() string {
helpText := `
Usage: nomad acl role update [options] <acl_role_id>
Update is used to update an existing ACL token. Requires a management token.
General Options:
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
Update Options:
-name
Sets the human readable name for the ACL role. The name must be between
1-128 characters.
-description
A free form text description of the role that must not exceed 256
characters.
-policy-name
Specifies a policy to associate with the role identified by their name. This
flag can be specified multiple times.
-no-merge
Do not merge the current role information with what is provided to the
command. Instead overwrite all fields with the exception of the role ID
which is immutable.
-json
Output the ACL role in a JSON format.
-t
Format and display the ACL role using a Go template.
`
return strings.TrimSpace(helpText)
}
func (a *ACLRoleUpdateCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(a.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-name": complete.PredictAnything,
"-description": complete.PredictAnything,
"-no-merge": complete.PredictNothing,
"-policy-name": complete.PredictAnything,
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
})
}
func (a *ACLRoleUpdateCommand) AutocompleteArgs() complete.Predictor { return complete.PredictNothing }
// Synopsis satisfies the cli.Command Synopsis function.
func (a *ACLRoleUpdateCommand) Synopsis() string { return "Update an existing ACL role" }
// Name returns the name of this command.
func (*ACLRoleUpdateCommand) Name() string { return "acl role update" }
// Run satisfies the cli.Command Run function.
func (a *ACLRoleUpdateCommand) Run(args []string) int {
flags := a.Meta.FlagSet(a.Name(), FlagSetClient)
flags.Usage = func() { a.Ui.Output(a.Help()) }
flags.StringVar(&a.name, "name", "", "")
flags.StringVar(&a.description, "description", "", "")
flags.Var((funcVar)(func(s string) error {
a.policyNames = append(a.policyNames, s)
return nil
}), "policy-name", "")
flags.BoolVar(&a.noMerge, "no-merge", false, "")
flags.BoolVar(&a.json, "json", false, "")
flags.StringVar(&a.tmpl, "t", "", "")
if err := flags.Parse(args); err != nil {
return 1
}
// Check that we got exactly one argument which is expected to be the ACL
// role ID.
if len(flags.Args()) != 1 {
a.Ui.Error("This command takes one argument: <acl_role_id>")
a.Ui.Error(commandErrorText(a))
return 1
}
// Get the HTTP client.
client, err := a.Meta.Client()
if err != nil {
a.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}
aclRoleID := flags.Args()[0]
// Read the current role in both cases, so we can fail better if not found.
currentRole, _, err := client.ACLRoles().Get(aclRoleID, nil)
if err != nil {
a.Ui.Error(fmt.Sprintf("Error when retrieving ACL role: %v", err))
return 1
}
var updatedRole api.ACLRole
// Depending on whether we are merging or not, we need to take a different
// approach.
switch a.noMerge {
case true:
// Perform some basic validation on the submitted role information to
// avoid sending API and RPC requests which will fail basic validation.
if a.name == "" {
a.Ui.Error("ACL role name must be specified using the -name flag")
return 1
}
if len(a.policyNames) < 1 {
a.Ui.Error("At least one policy name must be specified using the -policy-name flag")
return 1
}
updatedRole = api.ACLRole{
ID: aclRoleID,
Name: a.name,
Description: a.description,
Policies: aclRolePolicyNamesToPolicyLinks(a.policyNames),
}
default:
// Check that the operator specified at least one flag to update the ACL
// role with.
if len(a.policyNames) == 0 && a.name == "" && a.description == "" {
a.Ui.Error("Please provide at least one flag to update the ACL role")
a.Ui.Error(commandErrorText(a))
return 1
}
updatedRole = *currentRole
// If the operator specified a name or description, overwrite the
// existing value as these are simple strings.
if a.name != "" {
updatedRole.Name = a.name
}
if a.description != "" {
updatedRole.Description = a.description
}
// In order to merge the policy updates, we need to identify if the
// specified policy names already exist within the ACL role linking.
for _, policyName := range a.policyNames {
// Track whether we found the policy name already in the ACL role
// linking.
var found bool
for _, existingLinkedPolicy := range currentRole.Policies {
if policyName == existingLinkedPolicy.Name {
found = true
break
}
}
// If the policy name was not found, append this new link to the
// updated role.
if !found {
updatedRole.Policies = append(updatedRole.Policies, &api.ACLRolePolicyLink{Name: policyName})
}
}
}
// Update the ACL role with the new information via the API.
updatedACLRoleRead, _, err := client.ACLRoles().Update(&updatedRole, nil)
if err != nil {
a.Ui.Error(fmt.Sprintf("Error updating ACL role: %s", err))
return 1
}
if a.json || len(a.tmpl) > 0 {
out, err := Format(a.json, a.tmpl, updatedACLRoleRead)
if err != nil {
a.Ui.Error(err.Error())
return 1
}
a.Ui.Output(out)
return 0
}
// Format the output
a.Ui.Output(formatACLRole(updatedACLRoleRead))
return 0
}

View File

@@ -0,0 +1,124 @@
package command
import (
"fmt"
"testing"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/testutil"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestACLRoleUpdateCommand_Run(t *testing.T) {
ci.Parallel(t)
// Build a test server with ACLs enabled.
srv, _, url := testServer(t, false, func(c *agent.Config) {
c.ACL.Enabled = true
})
defer srv.Shutdown()
// Wait for the server to start fully and ensure we have a bootstrap token.
testutil.WaitForLeader(t, srv.Agent.RPC)
rootACLToken := srv.RootToken
require.NotNil(t, rootACLToken)
ui := cli.NewMockUi()
cmd := &ACLRoleUpdateCommand{
Meta: Meta{
Ui: ui,
flagAddress: url,
},
}
// Try calling the command without setting an ACL Role ID arg.
require.Equal(t, 1, cmd.Run([]string{"-address=" + url}))
require.Contains(t, ui.ErrorWriter.String(), "This command takes one argument")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
// Try calling the command with an ACL role ID that does not exist.
code := cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, "catch-me-if-you-can"})
require.Equal(t, 1, code)
require.Contains(t, ui.ErrorWriter.String(), "ACL role not found")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
// Create an ACL policy that can be referenced within the ACL role.
aclPolicy := structs.ACLPolicy{
Name: "acl-role-cli-test-policy",
Rules: `namespace "default" {
policy = "read"
}
`,
}
err := srv.Agent.Server().State().UpsertACLPolicies(
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{&aclPolicy})
require.NoError(t, err)
// Create an ACL role that can be used for updating.
aclRole := structs.ACLRole{
ID: uuid.Generate(),
Name: "acl-role-cli-test",
Description: "my-lovely-role",
Policies: []*structs.ACLRolePolicyLink{{Name: aclPolicy.Name}},
}
err = srv.Agent.Server().State().UpsertACLRoles(
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{&aclRole})
require.NoError(t, err)
// Try a merge update without setting any parameters to update.
code = cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, aclRole.ID})
require.Equal(t, 1, code)
require.Contains(t, ui.ErrorWriter.String(), "Please provide at least one flag to update the ACL role")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
// Update the description using the merge method.
code = cmd.Run([]string{
"-address=" + url, "-token=" + rootACLToken.SecretID, "-description=badger-badger-badger", aclRole.ID})
require.Equal(t, 0, code)
s := ui.OutputWriter.String()
require.Contains(t, s, fmt.Sprintf("ID = %s", aclRole.ID))
require.Contains(t, s, "Name = acl-role-cli-test")
require.Contains(t, s, "Description = badger-badger-badger")
require.Contains(t, s, "Policies = acl-role-cli-test-policy")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
// Try updating the role using no-merge without setting the required flags.
code = cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, "-no-merge", aclRole.ID})
require.Equal(t, 1, code)
require.Contains(t, ui.ErrorWriter.String(), "ACL role name must be specified using the -name flag")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
code = cmd.Run([]string{
"-address=" + url, "-token=" + rootACLToken.SecretID, "-no-merge", "-name=update-role-name", aclRole.ID})
require.Equal(t, 1, code)
require.Contains(t, ui.ErrorWriter.String(), "At least one policy name must be specified using the -policy-name flag")
// Update the role using no-merge with all required flags set.
code = cmd.Run([]string{
"-address=" + url, "-token=" + rootACLToken.SecretID, "-no-merge", "-name=update-role-name",
"-description=updated-description", "-policy-name=acl-role-cli-test-policy", aclRole.ID})
require.Equal(t, 0, code)
s = ui.OutputWriter.String()
require.Contains(t, s, fmt.Sprintf("ID = %s", aclRole.ID))
require.Contains(t, s, "Name = update-role-name")
require.Contains(t, s, "Description = updated-description")
require.Contains(t, s, "Policies = acl-role-cli-test-policy")
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
}

View File

@@ -107,6 +107,36 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
Meta: meta,
}, nil
},
"acl role": func() (cli.Command, error) {
return &ACLRoleCommand{
Meta: meta,
}, nil
},
"acl role create": func() (cli.Command, error) {
return &ACLRoleCreateCommand{
Meta: meta,
}, nil
},
"acl role delete": func() (cli.Command, error) {
return &ACLRoleDeleteCommand{
Meta: meta,
}, nil
},
"acl role info": func() (cli.Command, error) {
return &ACLRoleInfoCommand{
Meta: meta,
}, nil
},
"acl role list": func() (cli.Command, error) {
return &ACLRoleListCommand{
Meta: meta,
}, nil
},
"acl role update": func() (cli.Command, error) {
return &ACLRoleUpdateCommand{
Meta: meta,
}, nil
},
"acl token": func() (cli.Command, error) {
return &ACLTokenCommand{
Meta: meta,