Skip to content

Commit

Permalink
Merge pull request #14110 from hashicorp/f-gh-13120-acl-role-token-su…
Browse files Browse the repository at this point in the history
…pport

ACL: add ACL role functionality to ACL tokens
  • Loading branch information
jrasell authored Aug 17, 2022
2 parents ff798dc + 9953159 commit 37a4c32
Show file tree
Hide file tree
Showing 20 changed files with 945 additions and 59 deletions.
20 changes: 20 additions & 0 deletions api/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,11 @@ type ACLToken struct {
Name string
Type string
Policies []string

// Roles represents the ACL roles that this token is tied to. The token
// will inherit the permissions of all policies detailed within the role.
Roles []*ACLTokenRoleLink

Global bool
CreateTime time.Time

Expand All @@ -335,11 +340,26 @@ type ACLToken struct {
ModifyIndex uint64
}

// ACLTokenRoleLink is used to link an ACL token to an ACL role. The ACL token
// can therefore inherit all the ACL policy permissions that the ACL role
// contains.
type ACLTokenRoleLink struct {

// ID is the ACLRole.ID UUID. This field is immutable and represents the
// absolute truth for the link.
ID string

// Name is the human friendly identifier for the ACL role and is a
// convenience field for operators.
Name string
}

type ACLTokenListStub struct {
AccessorID string
Name string
Type string
Policies []string
Roles []*ACLTokenRoleLink
Global bool
CreateTime time.Time

Expand Down
143 changes: 143 additions & 0 deletions api/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,39 @@ func TestACLTokens_CreateUpdate(t *testing.T) {
out3, _, err := at.Update(out2, nil)
require.Error(t, err)
require.Nil(t, out3)

// Try adding a role link to our token, which should be possible. For this
// we need to create a policy and link to this from a role.
aclPolicy := ACLPolicy{
Name: "acl-role-api-test",
Rules: `namespace "default" { policy = "read" }`,
}
writeMeta, err := c.ACLPolicies().Upsert(&aclPolicy, nil)
require.NoError(t, err)
assertWriteMeta(t, writeMeta)

// Create an ACL role referencing the previously created
// policy.
role := ACLRole{
Name: "acl-role-api-test",
Policies: []*ACLRolePolicyLink{{Name: aclPolicy.Name}},
}
aclRoleCreateResp, writeMeta, err := c.ACLRoles().Create(&role, nil)
require.NoError(t, err)
assertWriteMeta(t, writeMeta)
require.NotEmpty(t, aclRoleCreateResp.ID)
require.Equal(t, role.Name, aclRoleCreateResp.Name)

out2.Roles = []*ACLTokenRoleLink{{Name: aclRoleCreateResp.Name}}
out2.ExpirationTTL = 0

out3, writeMeta, err = at.Update(out2, nil)
require.NoError(t, err)
require.NotNil(t, out3)
require.Len(t, out3.Policies, 1)
require.Equal(t, out3.Policies[0], "foo1")
require.Len(t, out3.Roles, 1)
require.Equal(t, out3.Roles[0].Name, role.Name)
}

func TestACLTokens_Info(t *testing.T) {
Expand Down Expand Up @@ -221,6 +254,116 @@ func TestACLTokens_Info(t *testing.T) {
require.NotNil(t, out2.ExpirationTime)
},
},
{
name: "token with role link",
testFn: func(client *Client) {

// Create an ACL policy that can be referenced within the ACL
// role.
aclPolicy := ACLPolicy{
Name: "acl-role-api-test",
Rules: `namespace "default" { policy = "read" }`,
}
writeMeta, err := testClient.ACLPolicies().Upsert(&aclPolicy, nil)
require.NoError(t, err)
assertWriteMeta(t, writeMeta)

// Create an ACL role referencing the previously created
// policy.
role := ACLRole{
Name: "acl-role-api-test",
Policies: []*ACLRolePolicyLink{{Name: aclPolicy.Name}},
}
aclRoleCreateResp, writeMeta, err := testClient.ACLRoles().Create(&role, nil)
require.NoError(t, err)
assertWriteMeta(t, writeMeta)
require.NotEmpty(t, aclRoleCreateResp.ID)
require.Equal(t, role.Name, aclRoleCreateResp.Name)

// Create a token with a role linking.
token := &ACLToken{
Name: "token-with-role-link",
Type: "client",
Roles: []*ACLTokenRoleLink{{Name: role.Name}},
}

out, wm, err := client.ACLTokens().Create(token, nil)
require.Nil(t, err)
assertWriteMeta(t, wm)
require.NotNil(t, out)

// Query the token and ensure it matches what was returned
// during the creation.
out2, qm, err := client.ACLTokens().Info(out.AccessorID, nil)
require.Nil(t, err)
assertQueryMeta(t, qm)
require.Equal(t, out, out2)
require.Len(t, out.Roles, 1)
require.Equal(t, out.Roles[0].Name, aclPolicy.Name)
},
},

{
name: "token with role and policy link",
testFn: func(client *Client) {

// Create an ACL policy that can be referenced within the ACL
// role.
aclPolicy1 := ACLPolicy{
Name: "acl-role-api-test-1",
Rules: `namespace "default" { policy = "read" }`,
}
writeMeta, err := testClient.ACLPolicies().Upsert(&aclPolicy1, nil)
require.NoError(t, err)
assertWriteMeta(t, writeMeta)

// Create another that can be referenced within the ACL token
// directly.
aclPolicy2 := ACLPolicy{
Name: "acl-role-api-test-2",
Rules: `namespace "fawlty" { policy = "read" }`,
}
writeMeta, err = testClient.ACLPolicies().Upsert(&aclPolicy2, nil)
require.NoError(t, err)
assertWriteMeta(t, writeMeta)

// Create an ACL role referencing the previously created
// policy.
role := ACLRole{
Name: "acl-role-api-test",
Policies: []*ACLRolePolicyLink{{Name: aclPolicy1.Name}},
}
aclRoleCreateResp, writeMeta, err := testClient.ACLRoles().Create(&role, nil)
require.NoError(t, err)
assertWriteMeta(t, writeMeta)
require.NotEmpty(t, aclRoleCreateResp.ID)
require.Equal(t, role.Name, aclRoleCreateResp.Name)

// Create a token with a role linking.
token := &ACLToken{
Name: "token-with-role-link",
Type: "client",
Policies: []string{aclPolicy2.Name},
Roles: []*ACLTokenRoleLink{{Name: role.Name}},
}

out, wm, err := client.ACLTokens().Create(token, nil)
require.Nil(t, err)
assertWriteMeta(t, wm)
require.NotNil(t, out)
require.Len(t, out.Policies, 1)
require.Equal(t, out.Policies[0], aclPolicy2.Name)
require.Len(t, out.Roles, 1)
require.Equal(t, out.Roles[0].Name, role.Name)

// Query the token and ensure it matches what was returned
// during the creation.
out2, qm, err := client.ACLTokens().Info(out.AccessorID, nil)
require.Nil(t, err)
assertQueryMeta(t, qm)
require.Equal(t, out, out2)
},
},
}

for _, tc := range testCases {
Expand Down
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
54 changes: 47 additions & 7 deletions command/acl_token_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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,
})
}

Expand Down Expand Up @@ -81,6 +94,14 @@ func (c *ACLTokenCreateCommand) Run(args []string) int {