Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Aug 12, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions command/acl_role.go
Original file line number Diff line number Diff line change
@@ -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 changes: 148 additions & 0 deletions command/acl_role_create.go
Original file line number Diff line number Diff line change
@@ -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 should be specified at least once.
jrasell marked this conversation as resolved.
Show resolved Hide resolved

-json
Output the ACL role in a JSON format.

-t
jrasell marked this conversation as resolved.
Show resolved Hide resolved
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
}
80 changes: 80 additions & 0 deletions command/acl_role_create_test.go
Original file line number Diff line number Diff line change
@@ -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()
}
Loading