cli: add ability to create and view tokens with ACL role links.

This commit is contained in:
James Rasell 2022-08-17 14:49:52 +01:00
parent f5d8cb2d90
commit 51a7df50bb
No known key found for this signature in database
GPG Key ID: AA7D460F5C8377AA
8 changed files with 154 additions and 28 deletions

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/hashicorp/nomad/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
@ -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
}
@ -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),
}
// Special case the policy output
if token.Type == "management" {
output = append(output, "Policies|n/a")
} 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)
}
// 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" {
kvOutput = append(kvOutput, "Policies|n/a", "Roles|n/a")
ui.Output(formatKV(kvOutput))
} else {
// 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 {

View File

@ -5,12 +5,17 @@ import (
"strings"
"time"
"github.com/hashicorp/go-set"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/helper"
"github.com/posener/complete"
)
type ACLTokenCreateCommand struct {
Meta
roleNames []string
roleIDs []string
}
func (c *ACLTokenCreateCommand) Help() string {
@ -38,6 +43,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
@ -49,11 +60,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,
})
}
@ -81,6 +94,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
}
@ -93,11 +114,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,
}
@ -127,6 +149,24 @@ 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
roleNameSet := set.From[string](roleNames).List()
roleNameFn := func(name string) *api.ACLTokenRoleLink { return &api.ACLTokenRoleLink{Name: name} }
roleIDsSet := set.From[string](roleIDs).List()
roleIDFn := func(id string) *api.ACLTokenRoleLink { return &api.ACLTokenRoleLink{ID: id} }
tokenLinks = append(tokenLinks, helper.ConvertSlice(roleNameSet, roleNameFn)...)
tokenLinks = append(tokenLinks, helper.ConvertSlice(roleIDsSet, roleIDFn)...)
return tokenLinks
}

View File

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

View File

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

View File

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

View File

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

View File

@ -718,3 +718,14 @@ func NewSafeTimer(duration time.Duration) (*time.Timer, StopFunc) {
return t, cancel
}
// ConvertSlice takes the input slice and generates a new one using the
// supplied conversion function to covert the element. This is useful when
// converting a slice of strings to a slice of structs which wraps the string.
func ConvertSlice[A, B any](original []A, conversion func(a A) B) []B {
result := make([]B, len(original))
for i, element := range original {
result[i] = conversion(element)
}
return result
}

View File

@ -546,3 +546,27 @@ func Test_NewSafeTimer(t *testing.T) {
<-timer.C
})
}
func Test_ConvertSlice(t *testing.T) {
t.Run("string wrapper", func(t *testing.T) {
type wrapper struct{ id string }
input := []string{"foo", "bar", "bad", "had"}
cFn := func(id string) *wrapper { return &wrapper{id: id} }
expectedOutput := []*wrapper{{id: "foo"}, {id: "bar"}, {id: "bad"}, {id: "had"}}
actualOutput := ConvertSlice(input, cFn)
require.ElementsMatch(t, expectedOutput, actualOutput)
})
t.Run("int wrapper", func(t *testing.T) {
type wrapper struct{ id int }
input := []int{10, 13, 1987, 2020}
cFn := func(id int) *wrapper { return &wrapper{id: id} }
expectedOutput := []*wrapper{{id: 10}, {id: 13}, {id: 1987}, {id: 2020}}
actualOutput := ConvertSlice(input, cFn)
require.ElementsMatch(t, expectedOutput, actualOutput)
})
}