cli: add ability to create and view tokens with ACL role links.
This commit is contained in:
parent
f5d8cb2d90
commit
51a7df50bb
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue