Skip to content

Commit

Permalink
cli: add ability to create and view tokens with ACL role links.
Browse files Browse the repository at this point in the history
  • Loading branch information
jrasell committed Aug 15, 2022
1 parent 2e8e73f commit 6a6cddf
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 27 deletions.
59 changes: 42 additions & 17 deletions command/acl_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/hashicorp/nomad/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)

Expand Down Expand Up @@ -127,7 +128,7 @@ func (c *ACLBootstrapCommand) Run(args []string) int {
}

// Format the output
c.Ui.Output(formatKVACLToken(token))
outputACLToken(c.Ui, token)
return 0
}

Expand All @@ -143,32 +144,56 @@ func formatKVPolicy(policy *api.ACLPolicy) string {
return formatKV(output)
}

// formatKVACLToken returns a K/V formatted ACL token
func formatKVACLToken(token *api.ACLToken) string {
// Add the fixed preamble
output := []string{
// outputACLToken formats and outputs the ACL token via the UI in the correct
// format.
func outputACLToken(ui cli.Ui, token *api.ACLToken) {

// Build the initial KV output which is always the same not matter whether
// the token is a management or client type.
kvOutput := []string{
fmt.Sprintf("Accessor ID|%s", token.AccessorID),
fmt.Sprintf("Secret ID|%s", token.SecretID),
fmt.Sprintf("Name|%s", token.Name),
fmt.Sprintf("Type|%s", token.Type),
fmt.Sprintf("Global|%v", token.Global),
fmt.Sprintf("Create Time|%v", token.CreateTime),
fmt.Sprintf("Expiry Time |%s", expiryTimeString(token.ExpirationTime)),
fmt.Sprintf("Create Index|%d", token.CreateIndex),
fmt.Sprintf("Modify Index|%d", token.ModifyIndex),
}

// Special case the policy output
// If the token is a management type, make it obvious that it is not
// possible to have policies or roles assigned to it and just output the
// KV data.
if token.Type == "management" {
output = append(output, "Policies|n/a")
kvOutput = append(kvOutput, "Policies|n/a", "Roles|n/a")
ui.Output(formatKV(kvOutput))
} else {
output = append(output, fmt.Sprintf("Policies|%v", token.Policies))
}

// Add the generic output
output = append(output,
fmt.Sprintf("Create Time|%v", token.CreateTime),
fmt.Sprintf("Expiry Time |%s", expiryTimeString(token.ExpirationTime)),
fmt.Sprintf("Create Index|%d", token.CreateIndex),
fmt.Sprintf("Modify Index|%d", token.ModifyIndex),
)
return formatKV(output)
// Policies are only currently referenced by name, so keep the previous
// format. When/if policies gain an ID alongside name like roles, this
// output should follow that of the roles.
kvOutput = append(kvOutput, fmt.Sprintf("Policies|%v", token.Policies))

var roleOutput []string

// If we have linked roles, add the ID and name in a list format to the
// output. Otherwise, make it clear there are no linked roles.
if len(token.Roles) > 0 {
roleOutput = append(roleOutput, "ID|Name")
for _, roleLink := range token.Roles {
roleOutput = append(roleOutput, roleLink.ID+"|"+roleLink.Name)
}
} else {
roleOutput = append(roleOutput, "<none>")
}

// Output the mixed formats of data, ensuring there is a space between
// the KV and list data.
ui.Output(formatKV(kvOutput))
ui.Output("")
ui.Output(fmt.Sprintf("Roles\n%s", formatList(roleOutput)))
}
}

func expiryTimeString(t *time.Time) string {
Expand Down
60 changes: 53 additions & 7 deletions command/acl_token_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import (

type ACLTokenCreateCommand struct {
Meta

roleNames []string
roleIDs []string
}

func (c *ACLTokenCreateCommand) Help() string {
Expand Down Expand Up @@ -38,6 +41,12 @@ Create Options:
Specifies a policy to associate with the token. Can be specified multiple times,
but only with client type tokens.
-role-id
ID of a role to use for this token. May be specified multiple times.
-role-name
Name of a role to use for this token. May be specified multiple times.
-ttl
Specifies the time-to-live of the created ACL token. This takes the form of
a time duration such as "5m" and "1h". By default, tokens will be created
Expand All @@ -49,11 +58,13 @@ Create Options:
func (c *ACLTokenCreateCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"name": complete.PredictAnything,
"type": complete.PredictAnything,
"global": complete.PredictNothing,
"policy": complete.PredictAnything,
"ttl": complete.PredictAnything,
"name": complete.PredictAnything,
"type": complete.PredictAnything,
"global": complete.PredictNothing,
"policy": complete.PredictAnything,
"role-id": complete.PredictAnything,
"role-name": complete.PredictAnything,
"ttl": complete.PredictAnything,
})
}

Expand Down Expand Up @@ -81,6 +92,14 @@ func (c *ACLTokenCreateCommand) Run(args []string) int {
policies = append(policies, s)
return nil
}), "policy", "")
flags.Var((funcVar)(func(s string) error {
c.roleNames = append(c.roleNames, s)
return nil
}), "role-name", "")
flags.Var((funcVar)(func(s string) error {
c.roleIDs = append(c.roleIDs, s)
return nil
}), "role-id", "")
if err := flags.Parse(args); err != nil {
return 1
}
Expand All @@ -93,11 +112,12 @@ func (c *ACLTokenCreateCommand) Run(args []string) int {
return 1
}

// Setup the token
// Set up the token.
tk := &api.ACLToken{
Name: name,
Type: tokenType,
Policies: policies,
Roles: generateACLTokenRoleLinks(c.roleNames, c.roleIDs),
Global: global,
}

Expand Down Expand Up @@ -127,6 +147,32 @@ func (c *ACLTokenCreateCommand) Run(args []string) int {
}

// Format the output
c.Ui.Output(formatKVACLToken(token))
outputACLToken(c.Ui, token)
return 0
}

// generateACLTokenRoleLinks takes the command input role links by ID and name
// and coverts this to the relevant API object. It handles de-duplicating
// entries to the best effort, so this doesn't need to be done on the leader.
func generateACLTokenRoleLinks(roleNames, roleIDs []string) []*api.ACLTokenRoleLink {
var tokenLinks []*api.ACLTokenRoleLink

roleNameKeys := make(map[string]struct{})
roleIDKeys := make(map[string]struct{})

for _, roleName := range roleNames {
if _, ok := roleNameKeys[roleName]; !ok {
tokenLinks = append(tokenLinks, &api.ACLTokenRoleLink{Name: roleName})
roleNameKeys[roleName] = struct{}{}
}
}

for _, roleID := range roleIDs {
if _, ok := roleIDKeys[roleID]; !ok {
tokenLinks = append(tokenLinks, &api.ACLTokenRoleLink{ID: roleID})
roleIDKeys[roleID] = struct{}{}
}
}

return tokenLinks
}
26 changes: 26 additions & 0 deletions command/acl_token_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package command
import (
"testing"

"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/mitchellh/cli"
Expand Down Expand Up @@ -50,3 +51,28 @@ func TestACLTokenCreateCommand(t *testing.T) {
out = ui.OutputWriter.String()
require.NotContains(t, out, "Expiry Time = <never>")
}

func Test_generateACLTokenRoleLinks(t *testing.T) {
ci.Parallel(t)

inputRoleNames := []string{
"duplicate",
"policy1",
"policy2",
"duplicate",
}
inputRoleIDs := []string{
"77a780d8-2dee-7c7f-7822-6f5471c5cbb2",
"56850b06-a343-a772-1a5c-ad083fd8a50e",
"77a780d8-2dee-7c7f-7822-6f5471c5cbb2",
"77a780d8-2dee-7c7f-7822-6f5471c5cbb2",
}
expectedOutput := []*api.ACLTokenRoleLink{
{Name: "duplicate"},
{Name: "policy1"},
{Name: "policy2"},
{ID: "77a780d8-2dee-7c7f-7822-6f5471c5cbb2"},
{ID: "56850b06-a343-a772-1a5c-ad083fd8a50e"},
}
require.ElementsMatch(t, generateACLTokenRoleLinks(inputRoleNames, inputRoleIDs), expectedOutput)
}
2 changes: 1 addition & 1 deletion command/acl_token_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,6 @@ func (c *ACLTokenInfoCommand) Run(args []string) int {
}

// Format the output
c.Ui.Output(formatKVACLToken(token))
outputACLToken(c.Ui, token)
return 0
}
2 changes: 1 addition & 1 deletion command/acl_token_self.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,6 @@ func (c *ACLTokenSelfCommand) Run(args []string) int {
}

// Format the output
c.Ui.Output(formatKVACLToken(token))
outputACLToken(c.Ui, token)
return 0
}
2 changes: 1 addition & 1 deletion command/acl_token_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,6 @@ func (c *ACLTokenUpdateCommand) Run(args []string) int {
}

// Format the output
c.Ui.Output(formatKVACLToken(updatedToken))
outputACLToken(c.Ui, updatedToken)
return 0
}

0 comments on commit 6a6cddf

Please sign in to comment.