Merge pull request #14320 from hashicorp/f-gh-13120-sso-umbrella-merged-main
acl: add token expiration and ACL role functionality
This commit is contained in:
commit
1ed17ada46
224
api/acl.go
224
api/acl.go
|
@ -202,6 +202,96 @@ func (a *ACLTokens) ExchangeOneTimeToken(secret string, q *WriteOptions) (*ACLTo
|
|||
return resp.Token, wm, nil
|
||||
}
|
||||
|
||||
var (
|
||||
// errMissingACLRoleID is the generic errors to use when a call is missing
|
||||
// the required ACL Role ID parameter.
|
||||
errMissingACLRoleID = errors.New("missing ACL role ID")
|
||||
)
|
||||
|
||||
// ACLRoles is used to query the ACL Role endpoints.
|
||||
type ACLRoles struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// ACLRoles returns a new handle on the ACL roles API client.
|
||||
func (c *Client) ACLRoles() *ACLRoles {
|
||||
return &ACLRoles{client: c}
|
||||
}
|
||||
|
||||
// List is used to detail all the ACL roles currently stored within state.
|
||||
func (a *ACLRoles) List(q *QueryOptions) ([]*ACLRoleListStub, *QueryMeta, error) {
|
||||
var resp []*ACLRoleListStub
|
||||
qm, err := a.client.query("/v1/acl/roles", &resp, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return resp, qm, nil
|
||||
}
|
||||
|
||||
// Create is used to create an ACL role.
|
||||
func (a *ACLRoles) Create(role *ACLRole, w *WriteOptions) (*ACLRole, *WriteMeta, error) {
|
||||
if role.ID != "" {
|
||||
return nil, nil, errors.New("cannot specify ACL role ID")
|
||||
}
|
||||
var resp ACLRole
|
||||
wm, err := a.client.write("/v1/acl/role", role, &resp, w)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &resp, wm, nil
|
||||
}
|
||||
|
||||
// Update is used to update an existing ACL role.
|
||||
func (a *ACLRoles) Update(role *ACLRole, w *WriteOptions) (*ACLRole, *WriteMeta, error) {
|
||||
if role.ID == "" {
|
||||
return nil, nil, errMissingACLRoleID
|
||||
}
|
||||
var resp ACLRole
|
||||
wm, err := a.client.write("/v1/acl/role/"+role.ID, role, &resp, w)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &resp, wm, nil
|
||||
}
|
||||
|
||||
// Delete is used to delete an ACL role.
|
||||
func (a *ACLRoles) Delete(roleID string, w *WriteOptions) (*WriteMeta, error) {
|
||||
if roleID == "" {
|
||||
return nil, errMissingACLRoleID
|
||||
}
|
||||
wm, err := a.client.delete("/v1/acl/role/"+roleID, nil, nil, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wm, nil
|
||||
}
|
||||
|
||||
// Get is used to look up an ACL role.
|
||||
func (a *ACLRoles) Get(roleID string, q *QueryOptions) (*ACLRole, *QueryMeta, error) {
|
||||
if roleID == "" {
|
||||
return nil, nil, errMissingACLRoleID
|
||||
}
|
||||
var resp ACLRole
|
||||
qm, err := a.client.query("/v1/acl/role/"+roleID, &resp, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &resp, qm, nil
|
||||
}
|
||||
|
||||
// GetByName is used to look up an ACL role using its name.
|
||||
func (a *ACLRoles) GetByName(roleName string, q *QueryOptions) (*ACLRole, *QueryMeta, error) {
|
||||
if roleName == "" {
|
||||
return nil, nil, errors.New("missing ACL role name")
|
||||
}
|
||||
var resp ACLRole
|
||||
qm, err := a.client.query("/v1/acl/role/name/"+roleName, &resp, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &resp, qm, nil
|
||||
}
|
||||
|
||||
// ACLPolicyListStub is used to for listing ACL policies
|
||||
type ACLPolicyListStub struct {
|
||||
Name string
|
||||
|
@ -231,24 +321,62 @@ type JobACL struct {
|
|||
|
||||
// ACLToken represents a client token which is used to Authenticate
|
||||
type ACLToken struct {
|
||||
AccessorID string
|
||||
SecretID string
|
||||
Name string
|
||||
Type string
|
||||
Policies []string
|
||||
Global bool
|
||||
CreateTime time.Time
|
||||
AccessorID string
|
||||
SecretID string
|
||||
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
|
||||
|
||||
// ExpirationTime represents the point after which a token should be
|
||||
// considered revoked and is eligible for destruction. The zero value of
|
||||
// time.Time does not respect json omitempty directives, so we must use a
|
||||
// pointer.
|
||||
ExpirationTime *time.Time `json:",omitempty"`
|
||||
|
||||
// ExpirationTTL is a convenience field for helping set ExpirationTime to a
|
||||
// value of CreateTime+ExpirationTTL. This can only be set during token
|
||||
// creation. This is a string version of a time.Duration like "2m".
|
||||
ExpirationTTL time.Duration `json:",omitempty"`
|
||||
|
||||
CreateIndex uint64
|
||||
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
|
||||
Global bool
|
||||
CreateTime time.Time
|
||||
AccessorID string
|
||||
Name string
|
||||
Type string
|
||||
Policies []string
|
||||
Roles []*ACLTokenRoleLink
|
||||
Global bool
|
||||
CreateTime time.Time
|
||||
|
||||
// ExpirationTime represents the point after which a token should be
|
||||
// considered revoked and is eligible for destruction. A nil value
|
||||
// indicates no expiration has been set on the token.
|
||||
ExpirationTime *time.Time `json:"expiration_time,omitempty"`
|
||||
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
@ -277,3 +405,73 @@ type OneTimeTokenExchangeResponse struct {
|
|||
type BootstrapRequest struct {
|
||||
BootstrapSecret string
|
||||
}
|
||||
|
||||
// ACLRole is an abstraction for the ACL system which allows the grouping of
|
||||
// ACL policies into a single object. ACL tokens can be created and linked to
|
||||
// a role; the token then inherits all the permissions granted by the policies.
|
||||
type ACLRole struct {
|
||||
|
||||
// ID is an internally generated UUID for this role and is controlled by
|
||||
// Nomad. It can be used after role creation to update the existing role.
|
||||
ID string
|
||||
|
||||
// Name is unique across the entire set of federated clusters and is
|
||||
// supplied by the operator on role creation. The name can be modified by
|
||||
// updating the role and including the Nomad generated ID. This update will
|
||||
// not affect tokens created and linked to this role. This is a required
|
||||
// field.
|
||||
Name string
|
||||
|
||||
// Description is a human-readable, operator set description that can
|
||||
// provide additional context about the role. This is an optional field.
|
||||
Description string
|
||||
|
||||
// Policies is an array of ACL policy links. Although currently policies
|
||||
// can only be linked using their name, in the future we will want to add
|
||||
// IDs also and thus allow operators to specify either a name, an ID, or
|
||||
// both. At least one entry is required.
|
||||
Policies []*ACLRolePolicyLink
|
||||
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
// ACLRolePolicyLink is used to link a policy to an ACL role. We use a struct
|
||||
// rather than a list of strings as in the future we will want to add IDs to
|
||||
// policies and then link via these.
|
||||
type ACLRolePolicyLink struct {
|
||||
|
||||
// Name is the ACLPolicy.Name value which will be linked to the ACL role.
|
||||
Name string
|
||||
}
|
||||
|
||||
// ACLRoleListStub is the stub object returned when performing a listing of ACL
|
||||
// roles. While it might not currently be different to the full response
|
||||
// object, it allows us to future-proof the RPC in the event the ACLRole object
|
||||
// grows over time.
|
||||
type ACLRoleListStub struct {
|
||||
|
||||
// ID is an internally generated UUID for this role and is controlled by
|
||||
// Nomad.
|
||||
ID string
|
||||
|
||||
// Name is unique across the entire set of federated clusters and is
|
||||
// supplied by the operator on role creation. The name can be modified by
|
||||
// updating the role and including the Nomad generated ID. This update will
|
||||
// not affect tokens created and linked to this role. This is a required
|
||||
// field.
|
||||
Name string
|
||||
|
||||
// Description is a human-readable, operator set description that can
|
||||
// provide additional context about the role. This is an operational field.
|
||||
Description string
|
||||
|
||||
// Policies is an array of ACL policy links. Although currently policies
|
||||
// can only be linked using their name, in the future we will want to add
|
||||
// IDs also and thus allow operators to specify either a name, an ID, or
|
||||
// both.
|
||||
Policies []*ACLRolePolicyLink
|
||||
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
|
318
api/acl_test.go
318
api/acl_test.go
|
@ -2,9 +2,11 @@ package api
|
|||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/nomad/api/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestACLPolicies_ListUpsert(t *testing.T) {
|
||||
|
@ -118,15 +120,10 @@ func TestACLTokens_List(t *testing.T) {
|
|||
|
||||
// Expect out bootstrap token
|
||||
result, qm, err := at.List(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
if qm.LastIndex == 0 {
|
||||
t.Fatalf("bad index: %d", qm.LastIndex)
|
||||
}
|
||||
if n := len(result); n != 1 {
|
||||
t.Fatalf("expected 1 token, got: %d", n)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, 0, qm.LastIndex)
|
||||
require.Len(t, result, 1)
|
||||
require.Nil(t, result[0].ExpirationTime)
|
||||
}
|
||||
|
||||
func TestACLTokens_CreateUpdate(t *testing.T) {
|
||||
|
@ -156,31 +153,224 @@ func TestACLTokens_CreateUpdate(t *testing.T) {
|
|||
|
||||
// Verify the change took hold
|
||||
assert.Equal(t, out.Name, out2.Name)
|
||||
|
||||
// Try updating the token to include a TTL which is not allowed.
|
||||
out2.ExpirationTTL = 10 * time.Minute
|
||||
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) {
|
||||
testutil.Parallel(t)
|
||||
c, s, _ := makeACLClient(t, nil, nil)
|
||||
defer s.Stop()
|
||||
at := c.ACLTokens()
|
||||
|
||||
token := &ACLToken{
|
||||
Name: "foo",
|
||||
Type: "client",
|
||||
Policies: []string{"foo1"},
|
||||
testClient, testServer, _ := makeACLClient(t, nil, nil)
|
||||
defer testServer.Stop()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
testFn func(client *Client)
|
||||
}{
|
||||
{
|
||||
name: "token without expiry",
|
||||
testFn: func(client *Client) {
|
||||
|
||||
token := &ACLToken{
|
||||
Name: "foo",
|
||||
Type: "client",
|
||||
Policies: []string{"foo1"},
|
||||
}
|
||||
|
||||
// Create the token
|
||||
out, wm, err := client.ACLTokens().Create(token, nil)
|
||||
require.Nil(t, err)
|
||||
assertWriteMeta(t, wm)
|
||||
require.NotNil(t, out)
|
||||
|
||||
// Query the token
|
||||
out2, qm, err := client.ACLTokens().Info(out.AccessorID, nil)
|
||||
require.Nil(t, err)
|
||||
assertQueryMeta(t, qm)
|
||||
require.Equal(t, out, out2)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "token with expiry",
|
||||
testFn: func(client *Client) {
|
||||
|
||||
token := &ACLToken{
|
||||
Name: "token-with-expiry",
|
||||
Type: "client",
|
||||
Policies: []string{"foo1"},
|
||||
ExpirationTTL: 10 * time.Minute,
|
||||
}
|
||||
|
||||
// Create the token
|
||||
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 as well as ensuring the expiration time
|
||||
// is set.
|
||||
out2, qm, err := client.ACLTokens().Info(out.AccessorID, nil)
|
||||
require.Nil(t, err)
|
||||
assertQueryMeta(t, qm)
|
||||
require.Equal(t, out, out2)
|
||||
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-role-and-policy",
|
||||
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-and-policy-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)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create the token
|
||||
out, wm, err := at.Create(token, nil)
|
||||
assert.Nil(t, err)
|
||||
assertWriteMeta(t, wm)
|
||||
assert.NotNil(t, out)
|
||||
|
||||
// Query the token
|
||||
out2, qm, err := at.Info(out.AccessorID, nil)
|
||||
assert.Nil(t, err)
|
||||
assertQueryMeta(t, qm)
|
||||
assert.Equal(t, out, out2)
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.testFn(testClient)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLTokens_Self(t *testing.T) {
|
||||
|
@ -299,3 +489,77 @@ func TestACLTokens_BootstrapValidToken(t *testing.T) {
|
|||
assertWriteMeta(t, wm)
|
||||
assert.Equal(t, bootkn, out.SecretID)
|
||||
}
|
||||
|
||||
func TestACLRoles(t *testing.T) {
|
||||
testutil.Parallel(t)
|
||||
|
||||
testClient, testServer, _ := makeACLClient(t, nil, nil)
|
||||
defer testServer.Stop()
|
||||
|
||||
// An initial listing shouldn't return any results.
|
||||
aclRoleListResp, queryMeta, err := testClient.ACLRoles().List(nil)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, aclRoleListResp)
|
||||
assertQueryMeta(t, queryMeta)
|
||||
|
||||
// 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)
|
||||
|
||||
// Another listing should return one result.
|
||||
aclRoleListResp, queryMeta, err = testClient.ACLRoles().List(nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, aclRoleListResp, 1)
|
||||
assertQueryMeta(t, queryMeta)
|
||||
|
||||
// Read the role using its ID.
|
||||
aclRoleReadResp, queryMeta, err := testClient.ACLRoles().Get(aclRoleCreateResp.ID, nil)
|
||||
require.NoError(t, err)
|
||||
assertQueryMeta(t, queryMeta)
|
||||
require.Equal(t, aclRoleCreateResp, aclRoleReadResp)
|
||||
|
||||
// Read the role using its name.
|
||||
aclRoleReadResp, queryMeta, err = testClient.ACLRoles().GetByName(aclRoleCreateResp.Name, nil)
|
||||
require.NoError(t, err)
|
||||
assertQueryMeta(t, queryMeta)
|
||||
require.Equal(t, aclRoleCreateResp, aclRoleReadResp)
|
||||
|
||||
// Update the role name.
|
||||
role.Name = "acl-role-api-test-badger-badger-badger"
|
||||
role.ID = aclRoleCreateResp.ID
|
||||
aclRoleUpdateResp, writeMeta, err := testClient.ACLRoles().Update(&role, nil)
|
||||
require.NoError(t, err)
|
||||
assertWriteMeta(t, writeMeta)
|
||||
require.Equal(t, role.Name, aclRoleUpdateResp.Name)
|
||||
require.Equal(t, role.ID, aclRoleUpdateResp.ID)
|
||||
|
||||
// Delete the role.
|
||||
writeMeta, err = testClient.ACLRoles().Delete(aclRoleCreateResp.ID, nil)
|
||||
require.NoError(t, err)
|
||||
assertWriteMeta(t, writeMeta)
|
||||
|
||||
// Make sure there are no ACL roles now present.
|
||||
aclRoleListResp, queryMeta, err = testClient.ACLRoles().List(nil)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, aclRoleListResp)
|
||||
assertQueryMeta(t, queryMeta)
|
||||
}
|
||||
|
|
|
@ -5,8 +5,10 @@ import (
|
|||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
|
@ -126,7 +128,7 @@ func (c *ACLBootstrapCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
// Format the output
|
||||
c.Ui.Output(formatKVACLToken(token))
|
||||
outputACLToken(c.Ui, token)
|
||||
return 0
|
||||
}
|
||||
|
||||
|
@ -159,29 +161,61 @@ func formatACLPolicy(policy *api.ACLPolicy) string {
|
|||
return formattedOut
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if t == nil || t.IsZero() {
|
||||
return "<none>"
|
||||
}
|
||||
return t.String()
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestACLBootstrapCommand(t *testing.T) {
|
||||
|
@ -33,6 +34,7 @@ func TestACLBootstrapCommand(t *testing.T) {
|
|||
|
||||
out := ui.OutputWriter.String()
|
||||
must.StrContains(t, out, "Secret ID")
|
||||
require.Contains(t, out, "Expiry Time = <none>")
|
||||
}
|
||||
|
||||
// If a bootstrap token has already been created, attempts to create more should
|
||||
|
@ -110,6 +112,7 @@ func TestACLBootstrapCommand_WithOperatorFileBootstrapToken(t *testing.T) {
|
|||
|
||||
out := ui.OutputWriter.String()
|
||||
must.StrContains(t, out, mockToken.SecretID)
|
||||
require.Contains(t, out, "Expiry Time = <none>")
|
||||
}
|
||||
|
||||
// Attempting to bootstrap the server with an invalid operator provided token in a file should
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
Specifies a policy to associate with the role identified by their name. This
|
||||
flag can be specified multiple times and must be specified at least once.
|
||||
|
||||
-json
|
||||
Output the ACL role in a JSON format.
|
||||
|
||||
-t
|
||||
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": 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", "")
|
||||
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 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
|
||||
}
|
|
@ -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 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=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()
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
// Ensure ACLRoleDeleteCommand satisfies the cli.Command interface.
|
||||
var _ cli.Command = &ACLRoleDeleteCommand{}
|
||||
|
||||
// ACLRoleDeleteCommand implements cli.Command.
|
||||
type ACLRoleDeleteCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
// Help satisfies the cli.Command Help function.
|
||||
func (a *ACLRoleDeleteCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad acl role delete <acl_role_id>
|
||||
|
||||
Delete is used to delete an existing ACL role. Use requires a management
|
||||
token.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace)
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (a *ACLRoleDeleteCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(a.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{})
|
||||
}
|
||||
|
||||
func (a *ACLRoleDeleteCommand) AutocompleteArgs() complete.Predictor { return complete.PredictNothing }
|
||||
|
||||
// Synopsis satisfies the cli.Command Synopsis function.
|
||||
func (a *ACLRoleDeleteCommand) Synopsis() string { return "Delete an existing ACL role" }
|
||||
|
||||
// Name returns the name of this command.
|
||||
func (a *ACLRoleDeleteCommand) Name() string { return "acl token delete" }
|
||||
|
||||
// Run satisfies the cli.Command Run function.
|
||||
func (a *ACLRoleDeleteCommand) Run(args []string) int {
|
||||
|
||||
flags := a.Meta.FlagSet(a.Name(), FlagSetClient)
|
||||
flags.Usage = func() { a.Ui.Output(a.Help()) }
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that the last argument is the role ID to delete.
|
||||
if len(flags.Args()) != 1 {
|
||||
a.Ui.Error("This command takes one argument: <acl_role_id>")
|
||||
a.Ui.Error(commandErrorText(a))
|
||||
return 1
|
||||
}
|
||||
|
||||
aclRoleID := flags.Args()[0]
|
||||
|
||||
// Get the HTTP client.
|
||||
client, err := a.Meta.Client()
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Delete the specified ACL role.
|
||||
_, err = client.ACLRoles().Delete(aclRoleID, nil)
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error deleting ACL role: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Give some feedback to indicate the deletion was successful.
|
||||
a.Ui.Output(fmt.Sprintf("ACL role %s successfully deleted", aclRoleID))
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestACLRoleDeleteCommand_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 := &ACLRoleDeleteCommand{
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
flagAddress: url,
|
||||
},
|
||||
}
|
||||
|
||||
// Try and delete more than one ACL role.
|
||||
code := cmd.Run([]string{"-address=" + url, "acl-role-1", "acl-role-2"})
|
||||
require.Equal(t, 1, code)
|
||||
require.Contains(t, ui.ErrorWriter.String(), "This command takes one argument")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Try deleting a role that does not exist.
|
||||
require.Equal(t, 1, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, "acl-role-1"}))
|
||||
require.Contains(t, ui.ErrorWriter.String(), "ACL role not found")
|
||||
|
||||
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",
|
||||
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 referencing the previously created policy.
|
||||
aclRole := structs.ACLRole{
|
||||
ID: uuid.Generate(),
|
||||
Name: "acl-role-cli-test",
|
||||
Policies: []*structs.ACLRolePolicyLink{{Name: aclPolicy.Name}},
|
||||
}
|
||||
err = srv.Agent.Server().State().UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{&aclRole}, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Delete the existing ACL role.
|
||||
require.Equal(t, 0, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, aclRole.ID}))
|
||||
require.Contains(t, ui.OutputWriter.String(), "successfully deleted")
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
// Ensure ACLRoleInfoCommand satisfies the cli.Command interface.
|
||||
var _ cli.Command = &ACLRoleInfoCommand{}
|
||||
|
||||
// ACLRoleInfoCommand implements cli.Command.
|
||||
type ACLRoleInfoCommand struct {
|
||||
Meta
|
||||
|
||||
byName bool
|
||||
json bool
|
||||
tmpl string
|
||||
}
|
||||
|
||||
// Help satisfies the cli.Command Help function.
|
||||
func (a *ACLRoleInfoCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad acl role info [options] <acl_role_id>
|
||||
|
||||
Info is used to fetch information on an existing ACL roles. Requires a
|
||||
management token.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
|
||||
|
||||
ACL Info Options:
|
||||
|
||||
-by-name
|
||||
Look up the ACL role using its name as the identifier. The command defaults
|
||||
to expecting the ACL ID as the argument.
|
||||
|
||||
-json
|
||||
Output the ACL role in a JSON format.
|
||||
|
||||
-t
|
||||
Format and display the ACL role using a Go template.
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (a *ACLRoleInfoCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(a.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{
|
||||
"-by-name": complete.PredictNothing,
|
||||
"-json": complete.PredictNothing,
|
||||
"-t": complete.PredictAnything,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *ACLRoleInfoCommand) AutocompleteArgs() complete.Predictor { return complete.PredictNothing }
|
||||
|
||||
// Synopsis satisfies the cli.Command Synopsis function.
|
||||
func (a *ACLRoleInfoCommand) Synopsis() string { return "Fetch information on an existing ACL role" }
|
||||
|
||||
// Name returns the name of this command.
|
||||
func (a *ACLRoleInfoCommand) Name() string { return "acl role info" }
|
||||
|
||||
// Run satisfies the cli.Command Run function.
|
||||
func (a *ACLRoleInfoCommand) Run(args []string) int {
|
||||
|
||||
flags := a.Meta.FlagSet(a.Name(), FlagSetClient)
|
||||
flags.Usage = func() { a.Ui.Output(a.Help()) }
|
||||
flags.BoolVar(&a.byName, "by-name", false, "")
|
||||
flags.BoolVar(&a.json, "json", false, "")
|
||||
flags.StringVar(&a.tmpl, "t", "", "")
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we have exactly one argument.
|
||||
if len(flags.Args()) != 1 {
|
||||
a.Ui.Error("This command takes one argument: <acl_role_id>")
|
||||
a.Ui.Error(commandErrorText(a))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Get the HTTP client.
|
||||
client, err := a.Meta.Client()
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
var (
|
||||
aclRole *api.ACLRole
|
||||
apiErr error
|
||||
)
|
||||
|
||||
aclRoleID := flags.Args()[0]
|
||||
|
||||
// Use the correct API call depending on whether the lookup is by the name
|
||||
// or the ID.
|
||||
switch a.byName {
|
||||
case true:
|
||||
aclRole, _, apiErr = client.ACLRoles().GetByName(aclRoleID, nil)
|
||||
default:
|
||||
aclRole, _, apiErr = client.ACLRoles().Get(aclRoleID, nil)
|
||||
}
|
||||
|
||||
// Handle any error from the API.
|
||||
if apiErr != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error reading ACL role: %s", apiErr))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Format the output.
|
||||
a.Ui.Output(formatACLRole(aclRole))
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestACLRoleInfoCommand_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 := &ACLRoleInfoCommand{
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
flagAddress: url,
|
||||
},
|
||||
}
|
||||
|
||||
// Perform a lookup without specifying an ID.
|
||||
require.Equal(t, 1, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID}))
|
||||
require.Contains(t, ui.ErrorWriter.String(), "This command takes one argument: <acl_role_id>")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Perform a lookup specifying a random ID.
|
||||
require.Equal(t, 1, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, uuid.Generate()}))
|
||||
require.Contains(t, ui.ErrorWriter.String(), "ACL role not found")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Create an ACL policy that can be referenced within the ACL role.
|
||||
aclPolicy := structs.ACLPolicy{
|
||||
Name: "acl-role-policy-cli-test",
|
||||
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 referencing the previously created policy.
|
||||
aclRole := structs.ACLRole{
|
||||
ID: uuid.Generate(),
|
||||
Name: "acl-role-cli-test",
|
||||
Policies: []*structs.ACLRolePolicyLink{{Name: aclPolicy.Name}},
|
||||
}
|
||||
err = srv.Agent.Server().State().UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{&aclRole}, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Look up the ACL role using its ID.
|
||||
require.Equal(t, 0, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, aclRole.ID}))
|
||||
s := ui.OutputWriter.String()
|
||||
require.Contains(t, s, fmt.Sprintf("ID = %s", aclRole.ID))
|
||||
require.Contains(t, s, fmt.Sprintf("Name = %s", aclRole.Name))
|
||||
require.Contains(t, s, "Description = <none>")
|
||||
require.Contains(t, s, fmt.Sprintf("Policies = %s", aclPolicy.Name))
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Look up the ACL role using its Name.
|
||||
require.Equal(t, 0, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, "-by-name", aclRole.Name}))
|
||||
s = ui.OutputWriter.String()
|
||||
require.Contains(t, s, fmt.Sprintf("ID = %s", aclRole.ID))
|
||||
require.Contains(t, s, fmt.Sprintf("Name = %s", aclRole.Name))
|
||||
require.Contains(t, s, "Description = <none>")
|
||||
require.Contains(t, s, fmt.Sprintf("Policies = %s", aclPolicy.Name))
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
// Ensure ACLRoleListCommand satisfies the cli.Command interface.
|
||||
var _ cli.Command = &ACLRoleListCommand{}
|
||||
|
||||
// ACLRoleListCommand implements cli.Command.
|
||||
type ACLRoleListCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
// Help satisfies the cli.Command Help function.
|
||||
func (a *ACLRoleListCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad acl role list [options]
|
||||
|
||||
List is used to list existing ACL roles.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
|
||||
|
||||
ACL List Options:
|
||||
|
||||
-json
|
||||
Output the ACL roles in a JSON format.
|
||||
|
||||
-t
|
||||
Format and display the ACL roles using a Go template.
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (a *ACLRoleListCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(a.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{
|
||||
"-json": complete.PredictNothing,
|
||||
"-t": complete.PredictAnything,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *ACLRoleListCommand) AutocompleteArgs() complete.Predictor { return complete.PredictNothing }
|
||||
|
||||
// Synopsis satisfies the cli.Command Synopsis function.
|
||||
func (a *ACLRoleListCommand) Synopsis() string { return "List ACL roles" }
|
||||
|
||||
// Name returns the name of this command.
|
||||
func (a *ACLRoleListCommand) Name() string { return "acl role list" }
|
||||
|
||||
// Run satisfies the cli.Command Run function.
|
||||
func (a *ACLRoleListCommand) Run(args []string) int {
|
||||
var json bool
|
||||
var tmpl string
|
||||
|
||||
flags := a.Meta.FlagSet(a.Name(), FlagSetClient)
|
||||
flags.Usage = func() { a.Ui.Output(a.Help()) }
|
||||
flags.BoolVar(&json, "json", false, "")
|
||||
flags.StringVar(&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
|
||||
}
|
||||
|
||||
// Get the HTTP client
|
||||
client, err := a.Meta.Client()
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Fetch info on the policy
|
||||
roles, _, err := client.ACLRoles().List(nil)
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error listing ACL roles: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
if json || len(tmpl) > 0 {
|
||||
out, err := Format(json, tmpl, roles)
|
||||
if err != nil {
|
||||
a.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
a.Ui.Output(out)
|
||||
return 0
|
||||
}
|
||||
|
||||
a.Ui.Output(formatACLRoles(roles))
|
||||
return 0
|
||||
}
|
||||
|
||||
func formatACLRoles(roles []*api.ACLRoleListStub) string {
|
||||
if len(roles) == 0 {
|
||||
return "No ACL roles found"
|
||||
}
|
||||
|
||||
output := make([]string, 0, len(roles)+1)
|
||||
output = append(output, "ID|Name|Description|Policies")
|
||||
for _, role := range roles {
|
||||
output = append(output, fmt.Sprintf(
|
||||
"%s|%s|%s|%s",
|
||||
role.ID, role.Name, role.Description, strings.Join(aclRolePolicyLinkToStringList(role.Policies), ",")))
|
||||
}
|
||||
|
||||
return formatList(output)
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestACLRoleListCommand_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 := &ACLRoleListCommand{
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
flagAddress: url,
|
||||
},
|
||||
}
|
||||
|
||||
// Perform a list straight away without any roles held in state.
|
||||
require.Equal(t, 0, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID}))
|
||||
require.Contains(t, ui.OutputWriter.String(), "No ACL roles found")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Create an ACL policy that can be referenced within the ACL role.
|
||||
aclPolicy := structs.ACLPolicy{
|
||||
Name: "acl-role-policy-cli-test",
|
||||
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 referencing the previously created policy.
|
||||
aclRole := structs.ACLRole{
|
||||
ID: uuid.Generate(),
|
||||
Name: "acl-role-cli-test",
|
||||
Policies: []*structs.ACLRolePolicyLink{{Name: aclPolicy.Name}},
|
||||
}
|
||||
err = srv.Agent.Server().State().UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{&aclRole}, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Perform a listing to get the created role.
|
||||
require.Equal(t, 0, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID}))
|
||||
s := ui.OutputWriter.String()
|
||||
require.Contains(t, s, "ID")
|
||||
require.Contains(t, s, "Name")
|
||||
require.Contains(t, s, "Policies")
|
||||
require.Contains(t, s, "acl-role-cli-test")
|
||||
require.Contains(t, s, "acl-role-policy-cli-test")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_formatACLRole(t *testing.T) {
|
||||
inputACLRole := api.ACLRole{
|
||||
ID: "this-is-usually-a-uuid",
|
||||
Name: "this-is-my-friendly-name",
|
||||
Description: "this-is-my-friendly-name",
|
||||
Policies: []*api.ACLRolePolicyLink{
|
||||
{Name: "policy-link-1"},
|
||||
{Name: "policy-link-2"},
|
||||
{Name: "policy-link-3"},
|
||||
{Name: "policy-link-4"},
|
||||
},
|
||||
CreateIndex: 13,
|
||||
ModifyIndex: 1313,
|
||||
}
|
||||
expectedOutput := "ID = this-is-usually-a-uuid\nName = this-is-my-friendly-name\nDescription = this-is-my-friendly-name\nPolicies = policy-link-1,policy-link-2,policy-link-3,policy-link-4\nCreate Index = 13\nModify Index = 1313"
|
||||
actualOutput := formatACLRole(&inputACLRole)
|
||||
require.Equal(t, expectedOutput, actualOutput)
|
||||
}
|
||||
|
||||
func Test_aclRolePolicyLinkToStringList(t *testing.T) {
|
||||
inputPolicyLinks := []*api.ACLRolePolicyLink{
|
||||
{Name: "z-policy-link-1"},
|
||||
{Name: "a-policy-link-2"},
|
||||
{Name: "policy-link-3"},
|
||||
{Name: "b-policy-link-4"},
|
||||
}
|
||||
expectedOutput := []string{
|
||||
"a-policy-link-2",
|
||||
"b-policy-link-4",
|
||||
"policy-link-3",
|
||||
"z-policy-link-1",
|
||||
}
|
||||
actualOutput := aclRolePolicyLinkToStringList(inputPolicyLinks)
|
||||
require.Equal(t, expectedOutput, actualOutput)
|
||||
}
|
||||
|
||||
func Test_aclRolePolicyNamesToPolicyLinks(t *testing.T) {
|
||||
inputPolicyNames := []string{
|
||||
"policy-link-1", "policy-link-2", "policy-link-3", "policy-link-4",
|
||||
"policy-link-1", "policy-link-2", "policy-link-3", "policy-link-4",
|
||||
"policy-link-1", "policy-link-2", "policy-link-3", "policy-link-4",
|
||||
"policy-link-1", "policy-link-2", "policy-link-3", "policy-link-4",
|
||||
}
|
||||
expectedOutput := []*api.ACLRolePolicyLink{
|
||||
{Name: "policy-link-1"},
|
||||
{Name: "policy-link-2"},
|
||||
{Name: "policy-link-3"},
|
||||
{Name: "policy-link-4"},
|
||||
}
|
||||
actualOutput := aclRolePolicyNamesToPolicyLinks(inputPolicyNames)
|
||||
require.ElementsMatch(t, expectedOutput, actualOutput)
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
// Ensure ACLRoleUpdateCommand satisfies the cli.Command interface.
|
||||
var _ cli.Command = &ACLRoleUpdateCommand{}
|
||||
|
||||
// ACLRoleUpdateCommand implements cli.Command.
|
||||
type ACLRoleUpdateCommand struct {
|
||||
Meta
|
||||
|
||||
name string
|
||||
description string
|
||||
policyNames []string
|
||||
noMerge bool
|
||||
json bool
|
||||
tmpl string
|
||||
}
|
||||
|
||||
// Help satisfies the cli.Command Help function.
|
||||
func (a *ACLRoleUpdateCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad acl role update [options] <acl_role_id>
|
||||
|
||||
Update is used to update an existing ACL token. Requires a management token.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
|
||||
|
||||
Update Options:
|
||||
|
||||
-name
|
||||
Sets the human readable name for the ACL role. The name must be between
|
||||
1-128 characters.
|
||||
|
||||
-description
|
||||
A free form text description of the role that must not exceed 256
|
||||
characters.
|
||||
|
||||
-policy
|
||||
Specifies a policy to associate with the role identified by their name. This
|
||||
flag can be specified multiple times.
|
||||
|
||||
-no-merge
|
||||
Do not merge the current role information with what is provided to the
|
||||
command. Instead overwrite all fields with the exception of the role ID
|
||||
which is immutable.
|
||||
|
||||
-json
|
||||
Output the ACL role in a JSON format.
|
||||
|
||||
-t
|
||||
Format and display the ACL role using a Go template.
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (a *ACLRoleUpdateCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(a.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{
|
||||
"-name": complete.PredictAnything,
|
||||
"-description": complete.PredictAnything,
|
||||
"-no-merge": complete.PredictNothing,
|
||||
"-policy": complete.PredictAnything,
|
||||
"-json": complete.PredictNothing,
|
||||
"-t": complete.PredictAnything,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *ACLRoleUpdateCommand) AutocompleteArgs() complete.Predictor { return complete.PredictNothing }
|
||||
|
||||
// Synopsis satisfies the cli.Command Synopsis function.
|
||||
func (a *ACLRoleUpdateCommand) Synopsis() string { return "Update an existing ACL role" }
|
||||
|
||||
// Name returns the name of this command.
|
||||
func (*ACLRoleUpdateCommand) Name() string { return "acl role update" }
|
||||
|
||||
// Run satisfies the cli.Command Run function.
|
||||
func (a *ACLRoleUpdateCommand) 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", "")
|
||||
flags.BoolVar(&a.noMerge, "no-merge", false, "")
|
||||
flags.BoolVar(&a.json, "json", false, "")
|
||||
flags.StringVar(&a.tmpl, "t", "", "")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we got exactly one argument which is expected to be the ACL
|
||||
// role ID.
|
||||
if len(flags.Args()) != 1 {
|
||||
a.Ui.Error("This command takes one argument: <acl_role_id>")
|
||||
a.Ui.Error(commandErrorText(a))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Get the HTTP client.
|
||||
client, err := a.Meta.Client()
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
aclRoleID := flags.Args()[0]
|
||||
|
||||
// Read the current role in both cases, so we can fail better if not found.
|
||||
currentRole, _, err := client.ACLRoles().Get(aclRoleID, nil)
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error when retrieving ACL role: %v", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
var updatedRole api.ACLRole
|
||||
|
||||
// Depending on whether we are merging or not, we need to take a different
|
||||
// approach.
|
||||
switch a.noMerge {
|
||||
case true:
|
||||
|
||||
// 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 flag")
|
||||
return 1
|
||||
}
|
||||
|
||||
updatedRole = api.ACLRole{
|
||||
ID: aclRoleID,
|
||||
Name: a.name,
|
||||
Description: a.description,
|
||||
Policies: aclRolePolicyNamesToPolicyLinks(a.policyNames),
|
||||
}
|
||||
default:
|
||||
// Check that the operator specified at least one flag to update the ACL
|
||||
// role with.
|
||||
if len(a.policyNames) == 0 && a.name == "" && a.description == "" {
|
||||
a.Ui.Error("Please provide at least one flag to update the ACL role")
|
||||
a.Ui.Error(commandErrorText(a))
|
||||
return 1
|
||||
}
|
||||
|
||||
updatedRole = *currentRole
|
||||
|
||||
// If the operator specified a name or description, overwrite the
|
||||
// existing value as these are simple strings.
|
||||
if a.name != "" {
|
||||
updatedRole.Name = a.name
|
||||
}
|
||||
if a.description != "" {
|
||||
updatedRole.Description = a.description
|
||||
}
|
||||
|
||||
// In order to merge the policy updates, we need to identify if the
|
||||
// specified policy names already exist within the ACL role linking.
|
||||
for _, policyName := range a.policyNames {
|
||||
|
||||
// Track whether we found the policy name already in the ACL role
|
||||
// linking.
|
||||
var found bool
|
||||
|
||||
for _, existingLinkedPolicy := range currentRole.Policies {
|
||||
if policyName == existingLinkedPolicy.Name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If the policy name was not found, append this new link to the
|
||||
// updated role.
|
||||
if !found {
|
||||
updatedRole.Policies = append(updatedRole.Policies, &api.ACLRolePolicyLink{Name: policyName})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the ACL role with the new information via the API.
|
||||
updatedACLRoleRead, _, err := client.ACLRoles().Update(&updatedRole, nil)
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error updating ACL role: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
if a.json || len(a.tmpl) > 0 {
|
||||
out, err := Format(a.json, a.tmpl, updatedACLRoleRead)
|
||||
if err != nil {
|
||||
a.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
a.Ui.Output(out)
|
||||
return 0
|
||||
}
|
||||
|
||||
// Format the output
|
||||
a.Ui.Output(formatACLRole(updatedACLRoleRead))
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestACLRoleUpdateCommand_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 := &ACLRoleUpdateCommand{
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
flagAddress: url,
|
||||
},
|
||||
}
|
||||
|
||||
// Try calling the command without setting an ACL Role ID arg.
|
||||
require.Equal(t, 1, cmd.Run([]string{"-address=" + url}))
|
||||
require.Contains(t, ui.ErrorWriter.String(), "This command takes one argument")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Try calling the command with an ACL role ID that does not exist.
|
||||
code := cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, "catch-me-if-you-can"})
|
||||
require.Equal(t, 1, code)
|
||||
require.Contains(t, ui.ErrorWriter.String(), "ACL role not found")
|
||||
|
||||
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 that can be used for updating.
|
||||
aclRole := structs.ACLRole{
|
||||
ID: uuid.Generate(),
|
||||
Name: "acl-role-cli-test",
|
||||
Description: "my-lovely-role",
|
||||
Policies: []*structs.ACLRolePolicyLink{{Name: aclPolicy.Name}},
|
||||
}
|
||||
|
||||
err = srv.Agent.Server().State().UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{&aclRole}, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try a merge update without setting any parameters to update.
|
||||
code = cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, aclRole.ID})
|
||||
require.Equal(t, 1, code)
|
||||
require.Contains(t, ui.ErrorWriter.String(), "Please provide at least one flag to update the ACL role")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Update the description using the merge method.
|
||||
code = cmd.Run([]string{
|
||||
"-address=" + url, "-token=" + rootACLToken.SecretID, "-description=badger-badger-badger", aclRole.ID})
|
||||
require.Equal(t, 0, code)
|
||||
s := ui.OutputWriter.String()
|
||||
require.Contains(t, s, fmt.Sprintf("ID = %s", aclRole.ID))
|
||||
require.Contains(t, s, "Name = acl-role-cli-test")
|
||||
require.Contains(t, s, "Description = badger-badger-badger")
|
||||
require.Contains(t, s, "Policies = acl-role-cli-test-policy")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Try updating the role using no-merge without setting the required flags.
|
||||
code = cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, "-no-merge", aclRole.ID})
|
||||
require.Equal(t, 1, code)
|
||||
require.Contains(t, ui.ErrorWriter.String(), "ACL role name must be specified using the -name flag")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
code = cmd.Run([]string{
|
||||
"-address=" + url, "-token=" + rootACLToken.SecretID, "-no-merge", "-name=update-role-name", aclRole.ID})
|
||||
require.Equal(t, 1, code)
|
||||
require.Contains(t, ui.ErrorWriter.String(), "At least one policy name must be specified using the -policy flag")
|
||||
|
||||
// Update the role using no-merge with all required flags set.
|
||||
code = cmd.Run([]string{
|
||||
"-address=" + url, "-token=" + rootACLToken.SecretID, "-no-merge", "-name=update-role-name",
|
||||
"-description=updated-description", "-policy=acl-role-cli-test-policy", aclRole.ID})
|
||||
require.Equal(t, 0, code)
|
||||
s = ui.OutputWriter.String()
|
||||
require.Contains(t, s, fmt.Sprintf("ID = %s", aclRole.ID))
|
||||
require.Contains(t, s, "Name = update-role-name")
|
||||
require.Contains(t, s, "Description = updated-description")
|
||||
require.Contains(t, s, "Policies = acl-role-cli-test-policy")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
}
|
|
@ -3,13 +3,19 @@ package command
|
|||
import (
|
||||
"fmt"
|
||||
"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 {
|
||||
|
@ -36,6 +42,17 @@ Create Options:
|
|||
-policy=""
|
||||
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
|
||||
without a TTL and therefore never expire.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
@ -43,10 +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,
|
||||
"name": complete.PredictAnything,
|
||||
"type": complete.PredictAnything,
|
||||
"global": complete.PredictNothing,
|
||||
"policy": complete.PredictAnything,
|
||||
"role-id": complete.PredictAnything,
|
||||
"role-name": complete.PredictAnything,
|
||||
"ttl": complete.PredictAnything,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -61,7 +81,7 @@ func (c *ACLTokenCreateCommand) Synopsis() string {
|
|||
func (c *ACLTokenCreateCommand) Name() string { return "acl token create" }
|
||||
|
||||
func (c *ACLTokenCreateCommand) Run(args []string) int {
|
||||
var name, tokenType string
|
||||
var name, tokenType, ttl string
|
||||
var global bool
|
||||
var policies []string
|
||||
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
|
||||
|
@ -69,10 +89,19 @@ func (c *ACLTokenCreateCommand) Run(args []string) int {
|
|||
flags.StringVar(&name, "name", "", "")
|
||||
flags.StringVar(&tokenType, "type", "client", "")
|
||||
flags.BoolVar(&global, "global", false, "")
|
||||
flags.StringVar(&ttl, "ttl", "", "")
|
||||
flags.Var((funcVar)(func(s string) error {
|
||||
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
|
||||
}
|
||||
|
@ -85,14 +114,26 @@ 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,
|
||||
}
|
||||
|
||||
// If the user set a TTL flag value, convert this to a time duration and
|
||||
// add it to our token request object.
|
||||
if ttl != "" {
|
||||
ttlDuration, err := time.ParseDuration(ttl)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to parse TTL as time duration: %s", err))
|
||||
return 1
|
||||
}
|
||||
tk.ExpirationTTL = ttlDuration
|
||||
}
|
||||
|
||||
// Get the HTTP client
|
||||
client, err := c.Meta.Client()
|
||||
if err != nil {
|
||||
|
@ -108,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,10 +3,12 @@ package command
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestACLTokenCreateCommand(t *testing.T) {
|
||||
|
@ -30,11 +32,48 @@ func TestACLTokenCreateCommand(t *testing.T) {
|
|||
code := cmd.Run([]string{"-address=" + url, "-token=foo", "-policy=foo", "-type=client"})
|
||||
must.One(t, code)
|
||||
|
||||
// Request to create a new token with a valid management token
|
||||
// Request to create a new token with a valid management token that does
|
||||
// not have an expiry set.
|
||||
code = cmd.Run([]string{"-address=" + url, "-token=" + token.SecretID, "-policy=foo", "-type=client"})
|
||||
must.Zero(t, code)
|
||||
require.Equal(t, 0, code)
|
||||
|
||||
// Check the output
|
||||
out := ui.OutputWriter.String()
|
||||
must.StrContains(t, out, "[foo]")
|
||||
require.Contains(t, out, "[foo]")
|
||||
require.Contains(t, out, "Expiry Time = <none>")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Create a new token that has an expiry TTL set and check the response.
|
||||
code = cmd.Run([]string{"-address=" + url, "-token=" + token.SecretID, "-type=management", "-ttl=10m"})
|
||||
require.Equal(t, 0, code)
|
||||
|
||||
out = ui.OutputWriter.String()
|
||||
require.NotContains(t, out, "Expiry Time = <none>")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package command
|
|||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/posener/complete"
|
||||
|
@ -108,9 +109,17 @@ func formatTokens(tokens []*api.ACLTokenListStub) string {
|
|||
}
|
||||
|
||||
output := make([]string, 0, len(tokens)+1)
|
||||
output = append(output, "Name|Type|Global|Accessor ID")
|
||||
output = append(output, "Name|Type|Global|Accessor ID|Expired")
|
||||
for _, p := range tokens {
|
||||
output = append(output, fmt.Sprintf("%s|%s|%t|%s", p.Name, p.Type, p.Global, p.AccessorID))
|
||||
expired := false
|
||||
if p.ExpirationTime != nil && !p.ExpirationTime.IsZero() {
|
||||
if p.ExpirationTime.Before(time.Now().UTC()) {
|
||||
expired = true
|
||||
}
|
||||
}
|
||||
|
||||
output = append(output, fmt.Sprintf(
|
||||
"%s|%s|%t|%s|%v", p.Name, p.Type, p.Global, p.AccessorID, expired))
|
||||
}
|
||||
|
||||
return formatList(output)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -322,3 +322,206 @@ func (s *HTTPServer) ExchangeOneTimeToken(resp http.ResponseWriter, req *http.Re
|
|||
setIndex(resp, out.Index)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ACLRoleListRequest performs a listing of ACL roles and is callable via the
|
||||
// /v1/acl/roles HTTP API.
|
||||
func (s *HTTPServer) ACLRoleListRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
|
||||
// The endpoint only supports GET requests.
|
||||
if req.Method != http.MethodGet {
|
||||
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
||||
}
|
||||
|
||||
// Set up the request args and parse this to ensure the query options are
|
||||
// set.
|
||||
args := structs.ACLRolesListRequest{}
|
||||
|
||||
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Perform the RPC request.
|
||||
var reply structs.ACLRolesListResponse
|
||||
if err := s.agent.RPC(structs.ACLListRolesRPCMethod, &args, &reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setMeta(resp, &reply.QueryMeta)
|
||||
|
||||
if reply.ACLRoles == nil {
|
||||
reply.ACLRoles = make([]*structs.ACLRoleListStub, 0)
|
||||
}
|
||||
return reply.ACLRoles, nil
|
||||
}
|
||||
|
||||
// ACLRoleRequest creates a new ACL role and is callable via the
|
||||
// /v1/acl/role HTTP API.
|
||||
func (s *HTTPServer) ACLRoleRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
|
||||
// // The endpoint only supports PUT or POST requests.
|
||||
if !(req.Method == http.MethodPut || req.Method == http.MethodPost) {
|
||||
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
||||
}
|
||||
|
||||
// Use the generic upsert function without setting an ID as this will be
|
||||
// handled by the Nomad leader.
|
||||
return s.aclRoleUpsertRequest(resp, req, "")
|
||||
}
|
||||
|
||||
// ACLRoleSpecificRequest is callable via the /v1/acl/role/ HTTP API and
|
||||
// handles read via both the role name and ID, updates, and deletions.
|
||||
func (s *HTTPServer) ACLRoleSpecificRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
|
||||
// Grab the suffix of the request, so we can further understand it.
|
||||
reqSuffix := strings.TrimPrefix(req.URL.Path, "/v1/acl/role/")
|
||||
|
||||
// Split the request suffix in order to identify whether this is a lookup
|
||||
// of a service, or whether this includes a service and service identifier.
|
||||
suffixParts := strings.Split(reqSuffix, "/")
|
||||
|
||||
switch len(suffixParts) {
|
||||
case 1:
|
||||
// Ensure the role ID is not an empty string which is possible if the
|
||||
// caller requested "/v1/acl/role/"
|
||||
if suffixParts[0] == "" {
|
||||
return nil, CodedError(http.StatusBadRequest, "missing ACL role ID")
|
||||
}
|
||||
return s.aclRoleRequest(resp, req, suffixParts[0])
|
||||
case 2:
|
||||
// This endpoint only supports GET.
|
||||
if req.Method != http.MethodGet {
|
||||
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
||||
}
|
||||
|
||||
// Ensure that the path is correct, otherwise the call could use
|
||||
// "/v1/acl/role/foobar/role-name" and successfully pass through here.
|
||||
if suffixParts[0] != "name" {
|
||||
return nil, CodedError(http.StatusBadRequest, "invalid URI")
|
||||
}
|
||||
|
||||
// Ensure the role name is not an empty string which is possible if the
|
||||
// caller requested "/v1/acl/role/name/"
|
||||
if suffixParts[1] == "" {
|
||||
return nil, CodedError(http.StatusBadRequest, "missing ACL role name")
|
||||
}
|
||||
|
||||
return s.aclRoleGetByNameRequest(resp, req, suffixParts[1])
|
||||
|
||||
default:
|
||||
return nil, CodedError(http.StatusBadRequest, "invalid URI")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTPServer) aclRoleRequest(
|
||||
resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
|
||||
|
||||
// Identify the method which indicates which downstream function should be
|
||||
// called.
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
return s.aclRoleGetByIDRequest(resp, req, roleID)
|
||||
case http.MethodDelete:
|
||||
return s.aclRoleDeleteRequest(resp, req, roleID)
|
||||
case http.MethodPost, http.MethodPut:
|
||||
return s.aclRoleUpsertRequest(resp, req, roleID)
|
||||
default:
|
||||
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTPServer) aclRoleGetByIDRequest(
|
||||
resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
|
||||
|
||||
args := structs.ACLRoleByIDRequest{
|
||||
RoleID: roleID,
|
||||
}
|
||||
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var reply structs.ACLRoleByIDResponse
|
||||
if err := s.agent.RPC(structs.ACLGetRoleByIDRPCMethod, &args, &reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setMeta(resp, &reply.QueryMeta)
|
||||
|
||||
if reply.ACLRole == nil {
|
||||
return nil, CodedError(http.StatusNotFound, "ACL role not found")
|
||||
}
|
||||
return reply.ACLRole, nil
|
||||
}
|
||||
|
||||
func (s *HTTPServer) aclRoleDeleteRequest(
|
||||
resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
|
||||
|
||||
args := structs.ACLRolesDeleteByIDRequest{
|
||||
ACLRoleIDs: []string{roleID},
|
||||
}
|
||||
s.parseWriteRequest(req, &args.WriteRequest)
|
||||
|
||||
var reply structs.ACLRolesDeleteByIDResponse
|
||||
if err := s.agent.RPC(structs.ACLDeleteRolesByIDRPCMethod, &args, &reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setIndex(resp, reply.Index)
|
||||
return nil, nil
|
||||
|
||||
}
|
||||
|
||||
// aclRoleUpsertRequest handles upserting an ACL to the Nomad servers. It can
|
||||
// handle both new creations, and updates to existing roles.
|
||||
func (s *HTTPServer) aclRoleUpsertRequest(
|
||||
resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
|
||||
|
||||
// Decode the ACL role.
|
||||
var aclRole structs.ACLRole
|
||||
if err := decodeBody(req, &aclRole); err != nil {
|
||||
return nil, CodedError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
// Ensure the request path ID matches the ACL role ID that was decoded.
|
||||
// Only perform this check on updates as a generic error on creation might
|
||||
// be confusing to operators as there is no specific role request path.
|
||||
if roleID != "" && roleID != aclRole.ID {
|
||||
return nil, CodedError(http.StatusBadRequest, "ACL role ID does not match request path")
|
||||
}
|
||||
|
||||
args := structs.ACLRolesUpsertRequest{
|
||||
ACLRoles: []*structs.ACLRole{&aclRole},
|
||||
}
|
||||
s.parseWriteRequest(req, &args.WriteRequest)
|
||||
|
||||
var out structs.ACLRolesUpsertResponse
|
||||
if err := s.agent.RPC(structs.ACLUpsertRolesRPCMethod, &args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setIndex(resp, out.Index)
|
||||
|
||||
if len(out.ACLRoles) > 0 {
|
||||
return out.ACLRoles[0], nil
|
||||
}
|
||||
return nil, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *HTTPServer) aclRoleGetByNameRequest(
|
||||
resp http.ResponseWriter, req *http.Request, roleName string) (interface{}, error) {
|
||||
|
||||
args := structs.ACLRoleByNameRequest{
|
||||
RoleName: roleName,
|
||||
}
|
||||
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var reply structs.ACLRoleByNameResponse
|
||||
if err := s.agent.RPC(structs.ACLGetRoleByNameRPCMethod, &args, &reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setMeta(resp, &reply.QueryMeta)
|
||||
|
||||
if reply.ACLRole == nil {
|
||||
return nil, CodedError(http.StatusNotFound, "ACL role not found")
|
||||
}
|
||||
return reply.ACLRole, nil
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -558,3 +560,437 @@ func TestHTTP_OneTimeToken(t *testing.T) {
|
|||
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTPServer_ACLRoleListRequest(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
testFn func(srv *TestAgent)
|
||||
}{
|
||||
{
|
||||
name: "no auth token set",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, "/v1/acl/roles", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleListRequest(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid method",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodConnect, "/v1/acl/roles", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleListRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Invalid method")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no roles in state",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, "/v1/acl/roles", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleListRequest(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, obj.([]*structs.ACLRoleListStub))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "roles in state",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, srv.server.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Create two ACL roles and put these directly into state.
|
||||
aclRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
require.NoError(t, srv.server.State().UpsertACLRoles(structs.MsgTypeTestSetup, 20, aclRoles, false))
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, "/v1/acl/roles", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleListRequest(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, obj.([]*structs.ACLRoleListStub), 2)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "roles in state using prefix",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, srv.server.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Create two ACL roles and put these directly into state, one
|
||||
// using a custom prefix.
|
||||
aclRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
aclRoles[1].ID = "badger-badger-badger-" + uuid.Generate()
|
||||
require.NoError(t, srv.server.State().UpsertACLRoles(structs.MsgTypeTestSetup, 20, aclRoles, false))
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, "/v1/acl/roles?prefix=badger-badger-badger", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleListRequest(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, obj.([]*structs.ACLRoleListStub), 1)
|
||||
require.Contains(t, obj.([]*structs.ACLRoleListStub)[0].ID, "badger-badger-badger")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
httpACLTest(t, nil, tc.testFn)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPServer_ACLRoleRequest(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
testFn func(srv *TestAgent)
|
||||
}{
|
||||
{
|
||||
name: "no auth token set",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Create a mock role to use in the request body.
|
||||
mockACLRole := mock.ACLRole()
|
||||
mockACLRole.ID = ""
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodPut, "/v1/acl/role", encodeReq(mockACLRole))
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Permission denied")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid method",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodConnect, "/v1/acl/role", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Invalid method")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "successful upsert",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, srv.server.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Create a mock role to use in the request body.
|
||||
mockACLRole := mock.ACLRole()
|
||||
mockACLRole.ID = ""
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodPut, "/v1/acl/role", encodeReq(mockACLRole))
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleRequest(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, obj)
|
||||
require.Equal(t, obj.(*structs.ACLRole).Hash, mockACLRole.Hash)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
httpACLTest(t, nil, tc.testFn)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPServer_ACLRoleSpecificRequest(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
testFn func(srv *TestAgent)
|
||||
}{
|
||||
{
|
||||
name: "invalid URI",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, "/v1/acl/role/name/this/is/will/not/work", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "invalid URI")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid role name lookalike URI",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, "/v1/acl/role/foobar/rolename", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "invalid URI")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing role name",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, "/v1/acl/role/name/", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "missing ACL role name")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing role ID",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, "/v1/acl/role/", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "missing ACL role ID")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "role name incorrect method",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodConnect, "/v1/acl/role/name/foobar", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Invalid method")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "role ID incorrect method",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodConnect, "/v1/acl/role/foobar", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Invalid method")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get role by name",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, srv.server.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Create a mock role and put directly into state.
|
||||
mockACLRole := mock.ACLRole()
|
||||
require.NoError(t, srv.server.State().UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{mockACLRole}, false))
|
||||
|
||||
url := fmt.Sprintf("/v1/acl/role/name/%s", mockACLRole.Name)
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, obj.(*structs.ACLRole).Hash, mockACLRole.Hash)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get, update, and delete role by ID",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, srv.server.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Create a mock role and put directly into state.
|
||||
mockACLRole := mock.ACLRole()
|
||||
require.NoError(t, srv.server.State().UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{mockACLRole}, false))
|
||||
|
||||
url := fmt.Sprintf("/v1/acl/role/%s", mockACLRole.ID)
|
||||
|
||||
// Build the HTTP request to read the role using its ID.
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, obj.(*structs.ACLRole).Hash, mockACLRole.Hash)
|
||||
|
||||
// Update the role policy list and make the request via the
|
||||
// HTTP API.
|
||||
mockACLRole.Policies = []*structs.ACLRolePolicyLink{{Name: "mocked-test-policy-1"}}
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, url, encodeReq(mockACLRole))
|
||||
require.NoError(t, err)
|
||||
respW = httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err = srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, obj.(*structs.ACLRole).Policies, mockACLRole.Policies)
|
||||
|
||||
// Delete the ACL role using its ID.
|
||||
req, err = http.NewRequest(http.MethodDelete, url, nil)
|
||||
require.NoError(t, err)
|
||||
respW = httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err = srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, obj)
|
||||
|
||||
// Ensure the ACL role is no longer stored within state.
|
||||
aclRole, err := srv.server.State().GetACLRoleByID(nil, mockACLRole.ID)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, aclRole)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
httpACLTest(t, nil, tc.testFn)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -241,6 +241,12 @@ func convertServerConfig(agentConfig *Config) (*nomad.Config, error) {
|
|||
if agentConfig.ACL.ReplicationToken != "" {
|
||||
conf.ReplicationToken = agentConfig.ACL.ReplicationToken
|
||||
}
|
||||
if agentConfig.ACL.TokenMinExpirationTTL != 0 {
|
||||
conf.ACLTokenMinExpirationTTL = agentConfig.ACL.TokenMinExpirationTTL
|
||||
}
|
||||
if agentConfig.ACL.TokenMaxExpirationTTL != 0 {
|
||||
conf.ACLTokenMaxExpirationTTL = agentConfig.ACL.TokenMaxExpirationTTL
|
||||
}
|
||||
if agentConfig.Sentinel != nil {
|
||||
conf.SentinelConfig = agentConfig.Sentinel
|
||||
}
|
||||
|
@ -377,6 +383,13 @@ func convertServerConfig(agentConfig *Config) (*nomad.Config, error) {
|
|||
}
|
||||
conf.CSIPluginGCThreshold = dur
|
||||
}
|
||||
if gcThreshold := agentConfig.Server.ACLTokenGCThreshold; gcThreshold != "" {
|
||||
dur, err := time.ParseDuration(gcThreshold)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conf.ACLTokenExpirationGCThreshold = dur
|
||||
}
|
||||
|
||||
if heartbeatGrace := agentConfig.Server.HeartbeatGrace; heartbeatGrace != 0 {
|
||||
conf.HeartbeatGrace = heartbeatGrace
|
||||
|
|
|
@ -380,6 +380,18 @@ type ACLConfig struct {
|
|||
// within the authoritative region.
|
||||
ReplicationToken string `hcl:"replication_token"`
|
||||
|
||||
// TokenMinExpirationTTL is used to enforce the lowest acceptable value for
|
||||
// ACL token expiration. This is used by the Nomad servers to validate ACL
|
||||
// tokens with an expiration value set upon creation.
|
||||
TokenMinExpirationTTL time.Duration
|
||||
TokenMinExpirationTTLHCL string `hcl:"token_min_expiration_ttl" json:"-"`
|
||||
|
||||
// TokenMaxExpirationTTL is used to enforce the highest acceptable value
|
||||
// for ACL token expiration. This is used by the Nomad servers to validate
|
||||
// ACL tokens with an expiration value set upon creation.
|
||||
TokenMaxExpirationTTL time.Duration
|
||||
TokenMaxExpirationTTLHCL string `hcl:"token_max_expiration_ttl" json:"-"`
|
||||
|
||||
// ExtraKeysHCL is used by hcl to surface unexpected keys
|
||||
ExtraKeysHCL []string `hcl:",unusedKeys" json:"-"`
|
||||
}
|
||||
|
@ -452,7 +464,7 @@ type ServerConfig struct {
|
|||
EvalGCThreshold string `hcl:"eval_gc_threshold"`
|
||||
|
||||
// DeploymentGCThreshold controls how "old" a deployment must be to be
|
||||
// collected by GC. Age is not the only requirement for a deployment to be
|
||||
// collected by GC. Age is not the only requirement for a deployment to be
|
||||
// GCed but the threshold can be used to filter by age.
|
||||
DeploymentGCThreshold string `hcl:"deployment_gc_threshold"`
|
||||
|
||||
|
@ -466,6 +478,10 @@ type ServerConfig struct {
|
|||
// GCed but the threshold can be used to filter by age.
|
||||
CSIPluginGCThreshold string `hcl:"csi_plugin_gc_threshold"`
|
||||
|
||||
// ACLTokenGCThreshold controls how "old" an expired ACL token must be to
|
||||
// be collected by GC.
|
||||
ACLTokenGCThreshold string `hcl:"acl_token_gc_threshold"`
|
||||
|
||||
// RootKeyGCInterval is how often we dispatch a job to GC
|
||||
// encryption key metadata
|
||||
RootKeyGCInterval string `hcl:"root_key_gc_interval"`
|
||||
|
@ -1159,7 +1175,7 @@ func DevConfig(mode *devModeConfig) *Config {
|
|||
return conf
|
||||
}
|
||||
|
||||
// DefaultConfig is a the baseline configuration for Nomad
|
||||
// DefaultConfig is the baseline configuration for Nomad.
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
LogLevel: "INFO",
|
||||
|
@ -1738,6 +1754,18 @@ func (a *ACLConfig) Merge(b *ACLConfig) *ACLConfig {
|
|||
if b.PolicyTTLHCL != "" {
|
||||
result.PolicyTTLHCL = b.PolicyTTLHCL
|
||||
}
|
||||
if b.TokenMinExpirationTTL != 0 {
|
||||
result.TokenMinExpirationTTL = b.TokenMinExpirationTTL
|
||||
}
|
||||
if b.TokenMinExpirationTTLHCL != "" {
|
||||
result.TokenMinExpirationTTLHCL = b.TokenMinExpirationTTLHCL
|
||||
}
|
||||
if b.TokenMaxExpirationTTL != 0 {
|
||||
result.TokenMaxExpirationTTL = b.TokenMaxExpirationTTL
|
||||
}
|
||||
if b.TokenMaxExpirationTTLHCL != "" {
|
||||
result.TokenMaxExpirationTTLHCL = b.TokenMaxExpirationTTLHCL
|
||||
}
|
||||
if b.ReplicationToken != "" {
|
||||
result.ReplicationToken = b.ReplicationToken
|
||||
}
|
||||
|
@ -1794,6 +1822,9 @@ func (s *ServerConfig) Merge(b *ServerConfig) *ServerConfig {
|
|||
if b.CSIPluginGCThreshold != "" {
|
||||
result.CSIPluginGCThreshold = b.CSIPluginGCThreshold
|
||||
}
|
||||
if b.ACLTokenGCThreshold != "" {
|
||||
result.ACLTokenGCThreshold = b.ACLTokenGCThreshold
|
||||
}
|
||||
if b.RootKeyGCInterval != "" {
|
||||
result.RootKeyGCInterval = b.RootKeyGCInterval
|
||||
}
|
||||
|
|
|
@ -66,6 +66,8 @@ func ParseConfigFile(path string) (*Config, error) {
|
|||
{"gc_interval", &c.Client.GCInterval, &c.Client.GCIntervalHCL, nil},
|
||||
{"acl.token_ttl", &c.ACL.TokenTTL, &c.ACL.TokenTTLHCL, nil},
|
||||
{"acl.policy_ttl", &c.ACL.PolicyTTL, &c.ACL.PolicyTTLHCL, nil},
|
||||
{"acl.token_min_expiration_ttl", &c.ACL.TokenMinExpirationTTL, &c.ACL.TokenMinExpirationTTLHCL, nil},
|
||||
{"acl.token_max_expiration_ttl", &c.ACL.TokenMaxExpirationTTL, &c.ACL.TokenMaxExpirationTTLHCL, nil},
|
||||
{"client.server_join.retry_interval", &c.Client.ServerJoin.RetryInterval, &c.Client.ServerJoin.RetryIntervalHCL, nil},
|
||||
{"server.heartbeat_grace", &c.Server.HeartbeatGrace, &c.Server.HeartbeatGraceHCL, nil},
|
||||
{"server.min_heartbeat_ttl", &c.Server.MinHeartbeatTTL, &c.Server.MinHeartbeatTTLHCL, nil},
|
||||
|
|
|
@ -107,6 +107,7 @@ var basicConfig = &Config{
|
|||
DeploymentGCThreshold: "12h",
|
||||
CSIVolumeClaimGCThreshold: "12h",
|
||||
CSIPluginGCThreshold: "12h",
|
||||
ACLTokenGCThreshold: "12h",
|
||||
HeartbeatGrace: 30 * time.Second,
|
||||
HeartbeatGraceHCL: "30s",
|
||||
MinHeartbeatTTL: 33 * time.Second,
|
||||
|
@ -149,12 +150,16 @@ var basicConfig = &Config{
|
|||
LicensePath: "/tmp/nomad.hclic",
|
||||
},
|
||||
ACL: &ACLConfig{
|
||||
Enabled: true,
|
||||
TokenTTL: 60 * time.Second,
|
||||
TokenTTLHCL: "60s",
|
||||
PolicyTTL: 60 * time.Second,
|
||||
PolicyTTLHCL: "60s",
|
||||
ReplicationToken: "foobar",
|
||||
Enabled: true,
|
||||
TokenTTL: 60 * time.Second,
|
||||
TokenTTLHCL: "60s",
|
||||
PolicyTTL: 60 * time.Second,
|
||||
PolicyTTLHCL: "60s",
|
||||
TokenMinExpirationTTLHCL: "1h",
|
||||
TokenMinExpirationTTL: 1 * time.Hour,
|
||||
TokenMaxExpirationTTLHCL: "100h",
|
||||
TokenMaxExpirationTTL: 100 * time.Hour,
|
||||
ReplicationToken: "foobar",
|
||||
},
|
||||
Audit: &config.AuditConfig{
|
||||
Enabled: pointer.Of(true),
|
||||
|
|
|
@ -155,10 +155,12 @@ func TestConfig_Merge(t *testing.T) {
|
|||
},
|
||||
},
|
||||
ACL: &ACLConfig{
|
||||
Enabled: true,
|
||||
TokenTTL: 60 * time.Second,
|
||||
PolicyTTL: 60 * time.Second,
|
||||
ReplicationToken: "foo",
|
||||
Enabled: true,
|
||||
TokenTTL: 60 * time.Second,
|
||||
PolicyTTL: 60 * time.Second,
|
||||
TokenMinExpirationTTL: 60 * time.Second,
|
||||
TokenMaxExpirationTTL: 60 * time.Second,
|
||||
ReplicationToken: "foo",
|
||||
},
|
||||
Ports: &Ports{
|
||||
HTTP: 4646,
|
||||
|
@ -355,10 +357,12 @@ func TestConfig_Merge(t *testing.T) {
|
|||
},
|
||||
},
|
||||
ACL: &ACLConfig{
|
||||
Enabled: true,
|
||||
TokenTTL: 20 * time.Second,
|
||||
PolicyTTL: 20 * time.Second,
|
||||
ReplicationToken: "foobar",
|
||||
Enabled: true,
|
||||
TokenTTL: 20 * time.Second,
|
||||
PolicyTTL: 20 * time.Second,
|
||||
TokenMinExpirationTTL: 20 * time.Second,
|
||||
TokenMaxExpirationTTL: 20 * time.Second,
|
||||
ReplicationToken: "foobar",
|
||||
},
|
||||
Ports: &Ports{
|
||||
HTTP: 20000,
|
||||
|
|
|
@ -381,6 +381,11 @@ func (s HTTPServer) registerHandlers(enableDebug bool) {
|
|||
s.mux.HandleFunc("/v1/acl/token", s.wrap(s.ACLTokenSpecificRequest))
|
||||
s.mux.HandleFunc("/v1/acl/token/", s.wrap(s.ACLTokenSpecificRequest))
|
||||
|
||||
// Register our ACL role handlers.
|
||||
s.mux.HandleFunc("/v1/acl/roles", s.wrap(s.ACLRoleListRequest))
|
||||
s.mux.HandleFunc("/v1/acl/role", s.wrap(s.ACLRoleRequest))
|
||||
s.mux.HandleFunc("/v1/acl/role/", s.wrap(s.ACLRoleSpecificRequest))
|
||||
|
||||
s.mux.Handle("/v1/client/fs/", wrapCORS(s.wrap(s.FsRequest)))
|
||||
s.mux.HandleFunc("/v1/client/gc", s.wrap(s.ClientGCRequest))
|
||||
s.mux.Handle("/v1/client/stats", wrapCORS(s.wrap(s.ClientStatsRequest)))
|
||||
|
|
|
@ -116,6 +116,7 @@ server {
|
|||
deployment_gc_threshold = "12h"
|
||||
csi_volume_claim_gc_threshold = "12h"
|
||||
csi_plugin_gc_threshold = "12h"
|
||||
acl_token_gc_threshold = "12h"
|
||||
heartbeat_grace = "30s"
|
||||
min_heartbeat_ttl = "33s"
|
||||
max_heartbeats_per_second = 11.0
|
||||
|
@ -159,10 +160,12 @@ server {
|
|||
}
|
||||
|
||||
acl {
|
||||
enabled = true
|
||||
token_ttl = "60s"
|
||||
policy_ttl = "60s"
|
||||
replication_token = "foobar"
|
||||
enabled = true
|
||||
token_ttl = "60s"
|
||||
policy_ttl = "60s"
|
||||
token_min_expiration_ttl = "1h"
|
||||
token_max_expiration_ttl = "100h"
|
||||
replication_token = "foobar"
|
||||
}
|
||||
|
||||
audit {
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
"enabled": true,
|
||||
"policy_ttl": "60s",
|
||||
"replication_token": "foobar",
|
||||
"token_ttl": "60s"
|
||||
"token_ttl": "60s",
|
||||
"token_min_expiration_ttl": "1h",
|
||||
"token_max_expiration_ttl": "100h"
|
||||
}
|
||||
],
|
||||
"audit": {
|
||||
|
@ -255,6 +257,7 @@
|
|||
],
|
||||
"server": [
|
||||
{
|
||||
"acl_token_gc_threshold": "12h",
|
||||
"authoritative_region": "foobar",
|
||||
"bootstrap_expect": 5,
|
||||
"csi_plugin_gc_threshold": "12h",
|
||||
|
|
|
@ -107,6 +107,36 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
|
|||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"acl role": func() (cli.Command, error) {
|
||||
return &ACLRoleCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"acl role create": func() (cli.Command, error) {
|
||||
return &ACLRoleCreateCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"acl role delete": func() (cli.Command, error) {
|
||||
return &ACLRoleDeleteCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"acl role info": func() (cli.Command, error) {
|
||||
return &ACLRoleInfoCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"acl role list": func() (cli.Command, error) {
|
||||
return &ACLRoleListCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"acl role update": func() (cli.Command, error) {
|
||||
return &ACLRoleUpdateCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"acl token": func() (cli.Command, error) {
|
||||
return &ACLTokenCommand{
|
||||
Meta: meta,
|
||||
|
|
|
@ -643,6 +643,17 @@ 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
|
||||
}
|
||||
|
||||
// IsMethodHTTP returns whether s is a known HTTP method, ignoring case.
|
||||
func IsMethodHTTP(s string) bool {
|
||||
switch strings.ToUpper(s) {
|
||||
|
|
|
@ -527,6 +527,31 @@ func Test_NewSafeTimer(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func Test_IsMethodHTTP(t *testing.T) {
|
||||
t.Run("is method", func(t *testing.T) {
|
||||
cases := []string{
|
||||
|
|
|
@ -57,6 +57,8 @@ var msgTypeNames = map[structs.MessageType]string{
|
|||
structs.VarApplyStateRequestType: "VarApplyStateRequestType",
|
||||
structs.RootKeyMetaUpsertRequestType: "RootKeyMetaUpsertRequestType",
|
||||
structs.RootKeyMetaDeleteRequestType: "RootKeyMetaDeleteRequestType",
|
||||
structs.ACLRolesUpsertRequestType: "ACLRolesUpsertRequestType",
|
||||
structs.ACLRolesDeleteByIDRequestType: "ACLRolesDeleteByIDRequestType",
|
||||
structs.NamespaceUpsertRequestType: "NamespaceUpsertRequestType",
|
||||
structs.NamespaceDeleteRequestType: "NamespaceDeleteRequestType",
|
||||
}
|
||||
|
|
59
nomad/acl.go
59
nomad/acl.go
|
@ -82,9 +82,9 @@ func (s *Server) ResolveClaims(claims *structs.IdentityClaims) (*acl.ACL, error)
|
|||
return aclObj, nil
|
||||
}
|
||||
|
||||
// resolveTokenFromSnapshotCache is used to resolve an ACL object from a snapshot of state,
|
||||
// using a cache to avoid parsing and ACL construction when possible. It is split from resolveToken
|
||||
// to simplify testing.
|
||||
// resolveTokenFromSnapshotCache is used to resolve an ACL object from a
|
||||
// snapshot of state, using a cache to avoid parsing and ACL construction when
|
||||
// possible. It is split from resolveToken to simplify testing.
|
||||
func resolveTokenFromSnapshotCache(snap *state.StateSnapshot, cache *lru.TwoQueueCache, secretID string) (*acl.ACL, error) {
|
||||
// Lookup the ACL Token
|
||||
var token *structs.ACLToken
|
||||
|
@ -101,6 +101,9 @@ func resolveTokenFromSnapshotCache(snap *state.StateSnapshot, cache *lru.TwoQueu
|
|||
if token == nil {
|
||||
return nil, structs.ErrTokenNotFound
|
||||
}
|
||||
if token.IsExpired(time.Now().UTC()) {
|
||||
return nil, structs.ErrTokenExpired
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a management token
|
||||
|
@ -108,22 +111,61 @@ func resolveTokenFromSnapshotCache(snap *state.StateSnapshot, cache *lru.TwoQueu
|
|||
return acl.ManagementACL, nil
|
||||
}
|
||||
|
||||
// Get all associated policies
|
||||
policies := make([]*structs.ACLPolicy, 0, len(token.Policies))
|
||||
// Store all policies detailed in the token request, this includes the
|
||||
// named policies and those referenced within the role link.
|
||||
policies := make([]*structs.ACLPolicy, 0, len(token.Policies)+len(token.Roles))
|
||||
|
||||
// Iterate all the token policies and add these to our policy tracking
|
||||
// array.
|
||||
for _, policyName := range token.Policies {
|
||||
policy, err := snap.ACLPolicyByName(nil, policyName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if policy == nil {
|
||||
// Ignore policies that don't exist, since they don't grant any more privilege
|
||||
// Ignore policies that don't exist, since they don't grant any
|
||||
// more privilege.
|
||||
continue
|
||||
}
|
||||
|
||||
// Save the policy and update the cache key
|
||||
// Add the policy to the tracking array.
|
||||
policies = append(policies, policy)
|
||||
}
|
||||
|
||||
// Iterate all the token role links, so we can unpack these and identify
|
||||
// the ACL policies.
|
||||
for _, roleLink := range token.Roles {
|
||||
|
||||
// Any error reading the role means we cannot move forward. We just
|
||||
// ignore any roles that have been detailed but are not within our
|
||||
// state.
|
||||
role, err := snap.GetACLRoleByID(nil, roleLink.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if role == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Unpack the policies held within the ACL role to form a single list
|
||||
// of ACL policies that this token has available.
|
||||
for _, policyLink := range role.Policies {
|
||||
policy, err := snap.ACLPolicyByName(nil, policyLink.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ignore policies that don't exist, since they don't grant any
|
||||
// more privilege.
|
||||
if policy == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add the policy to the tracking array.
|
||||
policies = append(policies, policy)
|
||||
}
|
||||
}
|
||||
|
||||
// Compile and cache the ACL object
|
||||
aclObj, err := structs.CompileACLObject(cache, policies)
|
||||
if err != nil {
|
||||
|
@ -161,6 +203,9 @@ func (s *Server) ResolveSecretToken(secretID string) (*structs.ACLToken, error)
|
|||
if token == nil {
|
||||
return nil, structs.ErrTokenNotFound
|
||||
}
|
||||
if token.IsExpired(time.Now().UTC()) {
|
||||
return nil, structs.ErrTokenExpired
|
||||
}
|
||||
}
|
||||
|
||||
return token, nil
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
metrics "github.com/armon/go-metrics"
|
||||
log "github.com/hashicorp/go-hclog"
|
||||
memdb "github.com/hashicorp/go-memdb"
|
||||
"github.com/hashicorp/go-set"
|
||||
policy "github.com/hashicorp/nomad/acl"
|
||||
"github.com/hashicorp/nomad/helper"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
|
@ -468,7 +469,7 @@ func (a *ACL) UpsertTokens(args *structs.ACLTokenUpsertRequest, reply *structs.A
|
|||
|
||||
// Validate non-zero set of tokens
|
||||
if len(args.Tokens) == 0 {
|
||||
return structs.NewErrRPCCoded(400, "must specify as least one token")
|
||||
return structs.NewErrRPCCoded(http.StatusBadRequest, "must specify as least one token")
|
||||
}
|
||||
|
||||
// Force the request to the authoritative region if we are creating global tokens
|
||||
|
@ -486,14 +487,15 @@ func (a *ACL) UpsertTokens(args *structs.ACLTokenUpsertRequest, reply *structs.A
|
|||
// the entire request as a single batch.
|
||||
if hasGlobal {
|
||||
if !allGlobal {
|
||||
return structs.NewErrRPCCoded(400, "cannot upsert mixed global and non-global tokens")
|
||||
return structs.NewErrRPCCoded(http.StatusBadRequest,
|
||||
"cannot upsert mixed global and non-global tokens")
|
||||
}
|
||||
|
||||
// Force the request to the authoritative region if it has global
|
||||
args.Region = a.srv.config.AuthoritativeRegion
|
||||
}
|
||||
|
||||
if done, err := a.srv.forward("ACL.UpsertTokens", args, args, reply); done {
|
||||
if done, err := a.srv.forward(structs.ACLUpsertTokensRPCMethod, args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "upsert_tokens"}, time.Now())
|
||||
|
@ -505,40 +507,91 @@ func (a *ACL) UpsertTokens(args *structs.ACLTokenUpsertRequest, reply *structs.A
|
|||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// Snapshot the state
|
||||
state, err := a.srv.State().Snapshot()
|
||||
// Snapshot the state so we can perform lookups against the accessor ID if
|
||||
// needed. Do it here, so we only need to do this once no matter how many
|
||||
// tokens we are upserting.
|
||||
stateSnapshot, err := a.srv.State().Snapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate each token
|
||||
for idx, token := range args.Tokens {
|
||||
if err := token.Validate(); err != nil {
|
||||
return structs.NewErrRPCCodedf(400, "token %d invalid: %v", idx, err)
|
||||
}
|
||||
|
||||
// Generate an accessor and secret ID if new
|
||||
if token.AccessorID == "" {
|
||||
token.AccessorID = uuid.Generate()
|
||||
token.SecretID = uuid.Generate()
|
||||
token.CreateTime = time.Now().UTC()
|
||||
// Store any existing token found, so we can perform the correct update
|
||||
// validation.
|
||||
var existingToken *structs.ACLToken
|
||||
|
||||
} else {
|
||||
// Verify the token exists
|
||||
out, err := state.ACLTokenByAccessorID(nil, token.AccessorID)
|
||||
// If the token is being updated, perform a lookup so can can validate
|
||||
// the new changes against the old.
|
||||
if token.AccessorID != "" {
|
||||
out, err := stateSnapshot.ACLTokenByAccessorID(nil, token.AccessorID)
|
||||
if err != nil {
|
||||
return structs.NewErrRPCCodedf(400, "token lookup failed: %v", err)
|
||||
return structs.NewErrRPCCodedf(http.StatusInternalServerError, "token lookup failed: %v", err)
|
||||
}
|
||||
if out == nil {
|
||||
return structs.NewErrRPCCodedf(404, "cannot find token %s", token.AccessorID)
|
||||
return structs.NewErrRPCCodedf(http.StatusBadRequest, "cannot find token %s", token.AccessorID)
|
||||
}
|
||||
existingToken = out
|
||||
}
|
||||
|
||||
// Canonicalize sets information needed by the validation function, so
|
||||
// this order must be maintained.
|
||||
token.Canonicalize()
|
||||
|
||||
if err := token.Validate(a.srv.config.ACLTokenMinExpirationTTL,
|
||||
a.srv.config.ACLTokenMaxExpirationTTL, existingToken); err != nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusBadRequest, "token %d invalid: %v", idx, err)
|
||||
}
|
||||
|
||||
var normalizedRoleLinks []*structs.ACLTokenRoleLink
|
||||
uniqueRoleIDs := make(map[string]struct{})
|
||||
|
||||
// Iterate, check, and normalize the ACL role links that the token has.
|
||||
for _, roleLink := range token.Roles {
|
||||
|
||||
var (
|
||||
existing *structs.ACLRole
|
||||
roleIdentifier string
|
||||
lookupErr error
|
||||
)
|
||||
|
||||
// In the event the caller specified the role name, we need to
|
||||
// identify the immutable ID. In either case, we need to ensure the
|
||||
// role exists.
|
||||
switch roleLink.ID {
|
||||
case "":
|
||||
roleIdentifier = roleLink.Name
|
||||
existing, lookupErr = stateSnapshot.GetACLRoleByName(nil, roleIdentifier)
|
||||
default:
|
||||
roleIdentifier = roleLink.ID
|
||||
existing, lookupErr = stateSnapshot.GetACLRoleByID(nil, roleIdentifier)
|
||||
}
|
||||
|
||||
// Cannot toggle the "Global" mode
|
||||
if token.Global != out.Global {
|
||||
return structs.NewErrRPCCodedf(400, "cannot toggle global mode of %s", token.AccessorID)
|
||||
// Handle any state lookup error or inability to locate the role
|
||||
// within state.
|
||||
if lookupErr != nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusInternalServerError, "role lookup failed: %v", lookupErr)
|
||||
}
|
||||
if existing == nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusBadRequest, "cannot find role %s", roleIdentifier)
|
||||
}
|
||||
|
||||
// Ensure the role ID is written to the object and that the name is
|
||||
// emptied as it is possible the role name is updated in the future.
|
||||
roleLink.ID = existing.ID
|
||||
roleLink.Name = ""
|
||||
|
||||
// Deduplicate role links by their ID.
|
||||
if _, ok := uniqueRoleIDs[roleLink.ID]; !ok {
|
||||
normalizedRoleLinks = append(normalizedRoleLinks, roleLink)
|
||||
uniqueRoleIDs[roleLink.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the normalized array of ACL role links back to the token.
|
||||
token.Roles = normalizedRoleLinks
|
||||
|
||||
// Compute the token hash
|
||||
token.SetHash()
|
||||
}
|
||||
|
@ -549,14 +602,14 @@ func (a *ACL) UpsertTokens(args *structs.ACLTokenUpsertRequest, reply *structs.A
|
|||
return err
|
||||
}
|
||||
|
||||
// Populate the response. We do a lookup against the state to
|
||||
// pickup the proper create / modify times.
|
||||
state, err = a.srv.State().Snapshot()
|
||||
// Populate the response. We do a lookup against the state to pick up the
|
||||
// proper create / modify times.
|
||||
stateSnapshot, err = a.srv.State().Snapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, token := range args.Tokens {
|
||||
out, err := state.ACLTokenByAccessorID(nil, token.AccessorID)
|
||||
out, err := stateSnapshot.ACLTokenByAccessorID(nil, token.AccessorID)
|
||||
if err != nil {
|
||||
return structs.NewErrRPCCodedf(400, "token lookup failed: %v", err)
|
||||
}
|
||||
|
@ -1024,3 +1077,499 @@ func (a *ACL) ExpireOneTimeTokens(args *structs.OneTimeTokenExpireRequest, reply
|
|||
reply.Index = index
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpsertRoles creates or updates ACL roles held within Nomad.
|
||||
func (a *ACL) UpsertRoles(
|
||||
args *structs.ACLRolesUpsertRequest,
|
||||
reply *structs.ACLRolesUpsertResponse) error {
|
||||
|
||||
// Only allow operators to upsert ACL roles when ACLs are enabled.
|
||||
if !a.srv.config.ACLEnabled {
|
||||
return aclDisabled
|
||||
}
|
||||
|
||||
// This endpoint always forwards to the authoritative region as ACL roles
|
||||
// are global.
|
||||
args.Region = a.srv.config.AuthoritativeRegion
|
||||
|
||||
if done, err := a.srv.forward(structs.ACLUpsertRolesRPCMethod, args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "upsert_roles"}, time.Now())
|
||||
|
||||
// Only tokens with management level permissions can create ACL roles.
|
||||
if acl, err := a.srv.ResolveToken(args.AuthToken); err != nil {
|
||||
return err
|
||||
} else if acl == nil || !acl.IsManagement() {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// Snapshot the state so we can perform lookups against the ID and policy
|
||||
// links if needed. Do it here, so we only need to do this once no matter
|
||||
// how many roles we are upserting.
|
||||
stateSnapshot, err := a.srv.State().Snapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate each role.
|
||||
for idx, role := range args.ACLRoles {
|
||||
|
||||
// Perform all the static validation of the ACL role object. Use the
|
||||
// array index as we cannot be sure the error was caused by a missing
|
||||
// name.
|
||||
if err := role.Validate(); err != nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusBadRequest, "role %d invalid: %v", idx, err)
|
||||
}
|
||||
|
||||
// If the caller has passed a role ID, this call is considered an
|
||||
// update to an existing role. We should therefore ensure it is found
|
||||
// within state. Otherwise, the call is considered a new creation, and
|
||||
// we must ensure a role of the same name does not exist.
|
||||
if role.ID == "" {
|
||||
existingRole, err := stateSnapshot.GetACLRoleByName(nil, role.Name)
|
||||
if err != nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusInternalServerError, "role lookup failed: %v", err)
|
||||
}
|
||||
if existingRole != nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusBadRequest, "role with name %s already exists", role.Name)
|
||||
}
|
||||
} else {
|
||||
existingRole, err := stateSnapshot.GetACLRoleByID(nil, role.ID)
|
||||
if err != nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusInternalServerError, "role lookup failed: %v", err)
|
||||
}
|
||||
if existingRole == nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusBadRequest, "cannot find role %s", role.ID)
|
||||
}
|
||||
}
|
||||
|
||||
policyNames := make(map[string]struct{})
|
||||
var policiesLinks []*structs.ACLRolePolicyLink
|
||||
|
||||
// We need to deduplicate the ACL policy links within this role as well
|
||||
// as ensure the policies exist within state.
|
||||
for _, policyLink := range role.Policies {
|
||||
|
||||
// If the RPC does not allow for missing policies, perform a state
|
||||
// look up for the policy. An error or not being able to find the
|
||||
// policy is terminal. We can include the name in the error message
|
||||
// as it has previously been validated.
|
||||
if !args.AllowMissingPolicies {
|
||||
existing, err := stateSnapshot.ACLPolicyByName(nil, policyLink.Name)
|
||||
if err != nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusInternalServerError, "policy lookup failed: %v", err)
|
||||
}
|
||||
if existing == nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusBadRequest, "cannot find policy %s", policyLink.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// If the policy name is not found within our map, this means we
|
||||
// have not seen it previously. We need to add this to our
|
||||
// deduplicated array and also mark the policy name as seen, so we
|
||||
// skip any future policies of the same name.
|
||||
if _, ok := policyNames[policyLink.Name]; !ok {
|
||||
policiesLinks = append(policiesLinks, policyLink)
|
||||
policyNames[policyLink.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Stored the potentially updated policy links within our role.
|
||||
role.Policies = policiesLinks
|
||||
|
||||
role.Canonicalize()
|
||||
role.SetHash()
|
||||
}
|
||||
|
||||
// Update via Raft.
|
||||
out, index, err := a.srv.raftApply(structs.ACLRolesUpsertRequestType, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the FSM response, which is an interface, contains an error.
|
||||
if err, ok := out.(error); ok && err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Populate the response. We do a lookup against the state to pick up the
|
||||
// proper create / modify times.
|
||||
stateSnapshot, err = a.srv.State().Snapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, role := range args.ACLRoles {
|
||||
lookupACLRole, err := stateSnapshot.GetACLRoleByName(nil, role.Name)
|
||||
if err != nil {
|
||||
return structs.NewErrRPCCodedf(400, "ACL role lookup failed: %v", err)
|
||||
}
|
||||
reply.ACLRoles = append(reply.ACLRoles, lookupACLRole)
|
||||
}
|
||||
|
||||
// Update the index. There is no need to floor this as we are writing to
|
||||
// state and therefore will get a non-zero index response.
|
||||
reply.Index = index
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteRolesByID is used to batch delete ACL roles using the ID as the
|
||||
// deletion key.
|
||||
func (a *ACL) DeleteRolesByID(
|
||||
args *structs.ACLRolesDeleteByIDRequest,
|
||||
reply *structs.ACLRolesDeleteByIDResponse) error {
|
||||
|
||||
// Only allow operators to delete ACL roles when ACLs are enabled.
|
||||
if !a.srv.config.ACLEnabled {
|
||||
return aclDisabled
|
||||
}
|
||||
|
||||
// This endpoint always forwards to the authoritative region as ACL roles
|
||||
// are global.
|
||||
args.Region = a.srv.config.AuthoritativeRegion
|
||||
|
||||
if done, err := a.srv.forward(structs.ACLDeleteRolesByIDRPCMethod, args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "delete_roles"}, time.Now())
|
||||
|
||||
// Only tokens with management level permissions can create ACL roles.
|
||||
if acl, err := a.srv.ResolveToken(args.AuthToken); err != nil {
|
||||
return err
|
||||
} else if acl == nil || !acl.IsManagement() {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// Update via Raft.
|
||||
out, index, err := a.srv.raftApply(structs.ACLRolesDeleteByIDRequestType, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the FSM response, which is an interface, contains an error.
|
||||
if err, ok := out.(error); ok && err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the index. There is no need to floor this as we are writing to
|
||||
// state and therefore will get a non-zero index response.
|
||||
reply.Index = index
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListRoles is used to list ACL roles within state. If not prefix is supplied,
|
||||
// all ACL roles are listed, otherwise a prefix search is performed on the ACL
|
||||
// role name.
|
||||
func (a *ACL) ListRoles(
|
||||
args *structs.ACLRolesListRequest,
|
||||
reply *structs.ACLRolesListResponse) error {
|
||||
|
||||
// Only allow operators to list ACL roles when ACLs are enabled.
|
||||
if !a.srv.config.ACLEnabled {
|
||||
return aclDisabled
|
||||
}
|
||||
|
||||
if done, err := a.srv.forward(structs.ACLListRolesRPCMethod, args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "list_roles"}, time.Now())
|
||||
|
||||
// Resolve the token and ensure it has some form of permissions.
|
||||
acl, err := a.srv.ResolveToken(args.AuthToken)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if acl == nil {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// If the token is a management token, they can list all tokens. If not,
|
||||
// the role set tracks which role links the token has and therefore which
|
||||
// ones the caller can list.
|
||||
isManagement := acl.IsManagement()
|
||||
roleSet := &set.Set[string]{}
|
||||
|
||||
// If the token is not a management token, we determine which roles are
|
||||
// linked to the token and therefore can be listed by the caller.
|
||||
if !isManagement {
|
||||
token, err := a.requestACLToken(args.AuthToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if token == nil {
|
||||
return structs.ErrTokenNotFound
|
||||
}
|
||||
|
||||
// Generate a set of Role IDs from the token role links.
|
||||
roleSet = set.FromFunc(token.Roles, func(roleLink *structs.ACLTokenRoleLink) string { return roleLink.ID })
|
||||
}
|
||||
|
||||
// Set up and return the blocking query.
|
||||
return a.srv.blockingRPC(&blockingOptions{
|
||||
queryOpts: &args.QueryOptions,
|
||||
queryMeta: &reply.QueryMeta,
|
||||
run: func(ws memdb.WatchSet, stateStore *state.StateStore) error {
|
||||
|
||||
// The iteration below appends directly to the reply object, so in
|
||||
// order for blocking queries to work properly we must ensure the
|
||||
// ACLRoles are reset. This allows the blocking query run function
|
||||
// to work as expected.
|
||||
reply.ACLRoles = nil
|
||||
|
||||
var (
|
||||
err error
|
||||
iter memdb.ResultIterator
|
||||
)
|
||||
|
||||
// If the operator supplied a prefix, perform a prefix search.
|
||||
// Otherwise, list all ACL roles in state.
|
||||
switch args.QueryOptions.Prefix {
|
||||
case "":
|
||||
iter, err = stateStore.GetACLRoles(ws)
|
||||
default:
|
||||
iter, err = stateStore.GetACLRoleByIDPrefix(ws, args.QueryOptions.Prefix)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Iterate all the results and add these to our reply object. Check
|
||||
// before appending to the reply that the caller is allowed to view
|
||||
// the role.
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
|
||||
role := raw.(*structs.ACLRole)
|
||||
|
||||
if roleSet.Contains(role.ID) || isManagement {
|
||||
reply.ACLRoles = append(reply.ACLRoles, role.Stub())
|
||||
}
|
||||
}
|
||||
|
||||
// Use the index table to populate the query meta as we have no way
|
||||
// of tracking the max index on deletes.
|
||||
return a.srv.setReplyQueryMeta(stateStore, state.TableACLRoles, &reply.QueryMeta)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetRolesByID is used to get a set of ACL Roles as defined by their ID. This
|
||||
// endpoint is used by the replication process and uses a specific response in
|
||||
// order to make that process easier.
|
||||
func (a *ACL) GetRolesByID(args *structs.ACLRolesByIDRequest, reply *structs.ACLRolesByIDResponse) error {
|
||||
|
||||
// This endpoint is only used by the replication process which is only
|
||||
// running on ACL enabled clusters, so this check should never be
|
||||
// triggered.
|
||||
if !a.srv.config.ACLEnabled {
|
||||
return aclDisabled
|
||||
}
|
||||
|
||||
if done, err := a.srv.forward(structs.ACLGetRolesByIDRPCMethod, args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "get_roles_id"}, time.Now())
|
||||
|
||||
// Check that the caller has a management token and that ACLs are enabled
|
||||
// properly.
|
||||
if acl, err := a.srv.ResolveToken(args.AuthToken); err != nil {
|
||||
return err
|
||||
} else if acl == nil || !acl.IsManagement() {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// Set up and return the blocking query
|
||||
return a.srv.blockingRPC(&blockingOptions{
|
||||
queryOpts: &args.QueryOptions,
|
||||
queryMeta: &reply.QueryMeta,
|
||||
run: func(ws memdb.WatchSet, stateStore *state.StateStore) error {
|
||||
|
||||
// Instantiate the output map to the correct maximum length.
|
||||
reply.ACLRoles = make(map[string]*structs.ACLRole, len(args.ACLRoleIDs))
|
||||
|
||||
// Look for the ACL role and add this to our mapping if we have
|
||||
// found it.
|
||||
for _, roleID := range args.ACLRoleIDs {
|
||||
out, err := stateStore.GetACLRoleByID(ws, roleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if out != nil {
|
||||
reply.ACLRoles[out.ID] = out
|
||||
}
|
||||
}
|
||||
|
||||
// Use the index table to populate the query meta as we have no way
|
||||
// of tracking the max index on deletes.
|
||||
return a.srv.setReplyQueryMeta(stateStore, state.TableACLRoles, &reply.QueryMeta)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetRoleByID is used to look up an individual ACL role using its ID.
|
||||
func (a *ACL) GetRoleByID(
|
||||
args *structs.ACLRoleByIDRequest,
|
||||
reply *structs.ACLRoleByIDResponse) error {
|
||||
|
||||
// Only allow operators to read an ACL role when ACLs are enabled.
|
||||
if !a.srv.config.ACLEnabled {
|
||||
return aclDisabled
|
||||
}
|
||||
|
||||
if done, err := a.srv.forward(structs.ACLGetRoleByIDRPCMethod, args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "get_role_id"}, time.Now())
|
||||
|
||||
// Resolve the token and ensure it has some form of permissions.
|
||||
acl, err := a.srv.ResolveToken(args.AuthToken)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if acl == nil {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// If the token is a management token, they can detail any token they so
|
||||
// desire.
|
||||
isManagement := acl.IsManagement()
|
||||
|
||||
// If the token is not a management token, we determine if the caller wants
|
||||
// to detail a role linked to their token.
|
||||
if !isManagement {
|
||||
aclToken, err := a.requestACLToken(args.AuthToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if aclToken == nil {
|
||||
return structs.ErrTokenNotFound
|
||||
}
|
||||
|
||||
found := false
|
||||
|
||||
for _, roleLink := range aclToken.Roles {
|
||||
if roleLink.ID == args.RoleID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
|
||||
// Set up and return the blocking query.
|
||||
return a.srv.blockingRPC(&blockingOptions{
|
||||
queryOpts: &args.QueryOptions,
|
||||
queryMeta: &reply.QueryMeta,
|
||||
run: func(ws memdb.WatchSet, stateStore *state.StateStore) error {
|
||||
|
||||
// Perform a lookup for the ACL role.
|
||||
out, err := stateStore.GetACLRoleByID(ws, args.RoleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the index correctly depending on whether the ACL role was
|
||||
// found.
|
||||
switch out {
|
||||
case nil:
|
||||
index, err := stateStore.Index(state.TableACLRoles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply.Index = index
|
||||
default:
|
||||
reply.Index = out.ModifyIndex
|
||||
}
|
||||
|
||||
// We didn't encounter an error looking up the index; set the ACL
|
||||
// role on the reply and exit successfully.
|
||||
reply.ACLRole = out
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetRoleByName is used to look up an individual ACL role using its name.
|
||||
func (a *ACL) GetRoleByName(
|
||||
args *structs.ACLRoleByNameRequest,
|
||||
reply *structs.ACLRoleByNameResponse) error {
|
||||
|
||||
// Only allow operators to read an ACL role when ACLs are enabled.
|
||||
if !a.srv.config.ACLEnabled {
|
||||
return aclDisabled
|
||||
}
|
||||
|
||||
if done, err := a.srv.forward(structs.ACLGetRoleByNameRPCMethod, args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "get_role_name"}, time.Now())
|
||||
|
||||
// Resolve the token and ensure it has some form of permissions.
|
||||
acl, err := a.srv.ResolveToken(args.AuthToken)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if acl == nil {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// If the token is a management token, they can detail any token they so
|
||||
// desire.
|
||||
isManagement := acl.IsManagement()
|
||||
|
||||
// If the token is not a management token, we determine if the caller wants
|
||||
// to detail a role linked to their token.
|
||||
if !isManagement {
|
||||
aclToken, err := a.requestACLToken(args.AuthToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if aclToken == nil {
|
||||
return structs.ErrTokenNotFound
|
||||
}
|
||||
|
||||
found := false
|
||||
|
||||
for _, roleLink := range aclToken.Roles {
|
||||
if roleLink.Name == args.RoleName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
|
||||
// Set up and return the blocking query.
|
||||
return a.srv.blockingRPC(&blockingOptions{
|
||||
queryOpts: &args.QueryOptions,
|
||||
queryMeta: &reply.QueryMeta,
|
||||
run: func(ws memdb.WatchSet, stateStore *state.StateStore) error {
|
||||
|
||||
// Perform a lookup for the ACL role.
|
||||
out, err := stateStore.GetACLRoleByName(ws, args.RoleName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the index correctly depending on whether the ACL role was
|
||||
// found.
|
||||
switch out {
|
||||
case nil:
|
||||
index, err := stateStore.Index(state.TableACLRoles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply.Index = index
|
||||
default:
|
||||
reply.Index = out.ModifyIndex
|
||||
}
|
||||
|
||||
// We didn't encounter an error looking up the index; set the ACL
|
||||
// role on the reply and exit successfully.
|
||||
reply.ACLRole = out
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,137 +2,383 @@ package nomad
|
|||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/nomad/acl"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/state"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResolveACLToken(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Create mock state store and cache
|
||||
state := state.TestStateStore(t)
|
||||
cache, err := lru.New2Q(16)
|
||||
assert.Nil(t, err)
|
||||
testCases := []struct {
|
||||
name string
|
||||
testFn func()
|
||||
}{
|
||||
{
|
||||
name: "leader token",
|
||||
testFn: func() {
|
||||
|
||||
// Create a policy / token
|
||||
policy := mock.ACLPolicy()
|
||||
policy2 := mock.ACLPolicy()
|
||||
token := mock.ACLToken()
|
||||
token.Policies = []string{policy.Name, policy2.Name}
|
||||
token2 := mock.ACLToken()
|
||||
token2.Type = structs.ACLManagementToken
|
||||
token2.Policies = nil
|
||||
err = state.UpsertACLPolicies(structs.MsgTypeTestSetup, 100, []*structs.ACLPolicy{policy, policy2})
|
||||
assert.Nil(t, err)
|
||||
err = state.UpsertACLTokens(structs.MsgTypeTestSetup, 110, []*structs.ACLToken{token, token2})
|
||||
assert.Nil(t, err)
|
||||
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
||||
defer testServerCleanup()
|
||||
testutil.WaitForLeader(t, testServer.RPC)
|
||||
|
||||
snap, err := state.Snapshot()
|
||||
assert.Nil(t, err)
|
||||
// Check the leader ACL token is correctly set.
|
||||
leaderACL := testServer.getLeaderAcl()
|
||||
require.NotEmpty(t, leaderACL)
|
||||
|
||||
// Attempt resolution of blank token. Should return anonymous policy
|
||||
aclObj, err := resolveTokenFromSnapshotCache(snap, cache, "")
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, aclObj)
|
||||
// Resolve the token and ensure it's a management token.
|
||||
aclResp, err := testServer.ResolveToken(leaderACL)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, aclResp)
|
||||
require.True(t, aclResp.IsManagement())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "anonymous token",
|
||||
testFn: func() {
|
||||
|
||||
// Attempt resolution of unknown token. Should fail.
|
||||
randID := uuid.Generate()
|
||||
aclObj, err = resolveTokenFromSnapshotCache(snap, cache, randID)
|
||||
assert.Equal(t, structs.ErrTokenNotFound, err)
|
||||
assert.Nil(t, aclObj)
|
||||
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
||||
defer testServerCleanup()
|
||||
testutil.WaitForLeader(t, testServer.RPC)
|
||||
|
||||
// Attempt resolution of management token. Should get singleton.
|
||||
aclObj, err = resolveTokenFromSnapshotCache(snap, cache, token2.SecretID)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, aclObj)
|
||||
assert.Equal(t, true, aclObj.IsManagement())
|
||||
if aclObj != acl.ManagementACL {
|
||||
t.Fatalf("expected singleton")
|
||||
// Call the function with an empty input secret ID which is
|
||||
// classed as representing anonymous access in clusters with
|
||||
// ACLs enabled.
|
||||
aclResp, err := testServer.ResolveToken("")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, aclResp)
|
||||
require.False(t, aclResp.IsManagement())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "token not found",
|
||||
testFn: func() {
|
||||
|
||||
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
||||
defer testServerCleanup()
|
||||
testutil.WaitForLeader(t, testServer.RPC)
|
||||
|
||||
// Call the function with randomly generated secret ID which
|
||||
// does not exist within state.
|
||||
aclResp, err := testServer.ResolveToken(uuid.Generate())
|
||||
require.Equal(t, structs.ErrTokenNotFound, err)
|
||||
require.Nil(t, aclResp)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "token expired",
|
||||
testFn: func() {
|
||||
|
||||
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
||||
defer testServerCleanup()
|
||||
testutil.WaitForLeader(t, testServer.RPC)
|
||||
|
||||
// Create a mock token with an expiration time long in the
|
||||
// past, and upsert.
|
||||
token := mock.ACLToken()
|
||||
token.ExpirationTime = pointer.Of(time.Date(
|
||||
1970, time.January, 1, 0, 0, 0, 0, time.UTC))
|
||||
|
||||
err := testServer.State().UpsertACLTokens(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLToken{token})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Perform the function call which should result in finding the
|
||||
// token has expired.
|
||||
aclResp, err := testServer.ResolveToken(uuid.Generate())
|
||||
require.Equal(t, structs.ErrTokenNotFound, err)
|
||||
require.Nil(t, aclResp)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "management token",
|
||||
testFn: func() {
|
||||
|
||||
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
||||
defer testServerCleanup()
|
||||
testutil.WaitForLeader(t, testServer.RPC)
|
||||
|
||||
// Generate a management token and upsert this.
|
||||
managementToken := mock.ACLToken()
|
||||
managementToken.Type = structs.ACLManagementToken
|
||||
managementToken.Policies = nil
|
||||
|
||||
err := testServer.State().UpsertACLTokens(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLToken{managementToken})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Resolve the token and check that we received a management
|
||||
// ACL.
|
||||
aclResp, err := testServer.ResolveToken(managementToken.SecretID)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, aclResp)
|
||||
require.True(t, aclResp.IsManagement())
|
||||
require.Equal(t, acl.ManagementACL, aclResp)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client token with policies only",
|
||||
testFn: func() {
|
||||
|
||||
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
||||
defer testServerCleanup()
|
||||
testutil.WaitForLeader(t, testServer.RPC)
|
||||
|
||||
// Generate a client token with associated policies and upsert
|
||||
// these.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy2 := mock.ACLPolicy()
|
||||
err := testServer.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2})
|
||||
|
||||
clientToken := mock.ACLToken()
|
||||
clientToken.Policies = []string{policy1.Name, policy2.Name}
|
||||
err = testServer.State().UpsertACLTokens(
|
||||
structs.MsgTypeTestSetup, 20, []*structs.ACLToken{clientToken})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Resolve the token and check that we received a client
|
||||
// ACL with appropriate permissions.
|
||||
aclResp, err := testServer.ResolveToken(clientToken.SecretID)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, aclResp)
|
||||
require.False(t, aclResp.IsManagement())
|
||||
|
||||
allowed := aclResp.AllowNamespaceOperation("default", acl.NamespaceCapabilityListJobs)
|
||||
require.True(t, allowed)
|
||||
allowed = aclResp.AllowNamespaceOperation("other", acl.NamespaceCapabilityListJobs)
|
||||
require.False(t, allowed)
|
||||
|
||||
// Resolve the same token again and ensure we get the same
|
||||
// result.
|
||||
aclResp2, err := testServer.ResolveToken(clientToken.SecretID)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, aclResp2)
|
||||
require.Equal(t, aclResp, aclResp2)
|
||||
|
||||
// Bust the cache by upserting the policy
|
||||
err = testServer.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 30, []*structs.ACLPolicy{policy1})
|
||||
require.Nil(t, err)
|
||||
|
||||
// Resolve the same token again, should get different value
|
||||
aclResp3, err := testServer.ResolveToken(clientToken.SecretID)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, aclResp3)
|
||||
require.NotEqual(t, aclResp2, aclResp3)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client token with roles only",
|
||||
testFn: func() {
|
||||
|
||||
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
||||
defer testServerCleanup()
|
||||
testutil.WaitForLeader(t, testServer.RPC)
|
||||
|
||||
// Create a client token that only has a link to a role.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy2 := mock.ACLPolicy()
|
||||
err := testServer.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2})
|
||||
|
||||
aclRole := mock.ACLRole()
|
||||
aclRole.Policies = []*structs.ACLRolePolicyLink{
|
||||
{Name: policy1.Name},
|
||||
{Name: policy2.Name},
|
||||
}
|
||||
err = testServer.State().UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 30, []*structs.ACLRole{aclRole}, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
clientToken := mock.ACLToken()
|
||||
clientToken.Policies = []string{}
|
||||
clientToken.Roles = []*structs.ACLTokenRoleLink{{ID: aclRole.ID}}
|
||||
err = testServer.State().UpsertACLTokens(
|
||||
structs.MsgTypeTestSetup, 30, []*structs.ACLToken{clientToken})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Resolve the token and check that we received a client
|
||||
// ACL with appropriate permissions.
|
||||
aclResp, err := testServer.ResolveToken(clientToken.SecretID)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, aclResp)
|
||||
require.False(t, aclResp.IsManagement())
|
||||
|
||||
allowed := aclResp.AllowNamespaceOperation("default", acl.NamespaceCapabilityListJobs)
|
||||
require.True(t, allowed)
|
||||
allowed = aclResp.AllowNamespaceOperation("other", acl.NamespaceCapabilityListJobs)
|
||||
require.False(t, allowed)
|
||||
|
||||
// Remove the policies from the ACL role and ensure the resolution
|
||||
// permissions are updated.
|
||||
aclRole.Policies = []*structs.ACLRolePolicyLink{}
|
||||
err = testServer.State().UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 40, []*structs.ACLRole{aclRole}, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
aclResp, err = testServer.ResolveToken(clientToken.SecretID)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, aclResp)
|
||||
require.False(t, aclResp.IsManagement())
|
||||
require.False(t, aclResp.AllowNamespaceOperation("default", acl.NamespaceCapabilityListJobs))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client with roles and policies",
|
||||
testFn: func() {
|
||||
|
||||
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
||||
defer testServerCleanup()
|
||||
testutil.WaitForLeader(t, testServer.RPC)
|
||||
|
||||
// Generate two policies, each with a different namespace
|
||||
// permission set.
|
||||
policy1 := &structs.ACLPolicy{
|
||||
Name: "policy-" + uuid.Generate(),
|
||||
Rules: `namespace "platform" { policy = "write"}`,
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 10,
|
||||
}
|
||||
policy1.SetHash()
|
||||
policy2 := &structs.ACLPolicy{
|
||||
Name: "policy-" + uuid.Generate(),
|
||||
Rules: `namespace "web" { policy = "write"}`,
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 10,
|
||||
}
|
||||
policy2.SetHash()
|
||||
|
||||
err := testServer.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a role which references the policy that has access to
|
||||
// the web namespace.
|
||||
aclRole := mock.ACLRole()
|
||||
aclRole.Policies = []*structs.ACLRolePolicyLink{{Name: policy2.Name}}
|
||||
err = testServer.State().UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{aclRole}, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a token which references the policy and role.
|
||||
clientToken := mock.ACLToken()
|
||||
clientToken.Policies = []string{policy1.Name}
|
||||
clientToken.Roles = []*structs.ACLTokenRoleLink{{ID: aclRole.ID}}
|
||||
err = testServer.State().UpsertACLTokens(
|
||||
structs.MsgTypeTestSetup, 30, []*structs.ACLToken{clientToken})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Resolve the token and check that we received a client
|
||||
// ACL with appropriate permissions.
|
||||
aclResp, err := testServer.ResolveToken(clientToken.SecretID)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, aclResp)
|
||||
require.False(t, aclResp.IsManagement())
|
||||
|
||||
allowed := aclResp.AllowNamespaceOperation("platform", acl.NamespaceCapabilityListJobs)
|
||||
require.True(t, allowed)
|
||||
allowed = aclResp.AllowNamespaceOperation("web", acl.NamespaceCapabilityListJobs)
|
||||
require.True(t, allowed)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Attempt resolution of client token
|
||||
aclObj, err = resolveTokenFromSnapshotCache(snap, cache, token.SecretID)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, aclObj)
|
||||
|
||||
// Check that the ACL object looks reasonable
|
||||
assert.Equal(t, false, aclObj.IsManagement())
|
||||
allowed := aclObj.AllowNamespaceOperation("default", acl.NamespaceCapabilityListJobs)
|
||||
assert.Equal(t, true, allowed)
|
||||
allowed = aclObj.AllowNamespaceOperation("other", acl.NamespaceCapabilityListJobs)
|
||||
assert.Equal(t, false, allowed)
|
||||
|
||||
// Resolve the same token again, should get cache value
|
||||
aclObj2, err := resolveTokenFromSnapshotCache(snap, cache, token.SecretID)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, aclObj2)
|
||||
if aclObj != aclObj2 {
|
||||
t.Fatalf("expected cached value")
|
||||
}
|
||||
|
||||
// Bust the cache by upserting the policy
|
||||
err = state.UpsertACLPolicies(structs.MsgTypeTestSetup, 120, []*structs.ACLPolicy{policy})
|
||||
assert.Nil(t, err)
|
||||
snap, err = state.Snapshot()
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Resolve the same token again, should get different value
|
||||
aclObj3, err := resolveTokenFromSnapshotCache(snap, cache, token.SecretID)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, aclObj3)
|
||||
if aclObj == aclObj3 {
|
||||
t.Fatalf("unexpected cached value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveACLToken_LeaderToken(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
assert := assert.New(t)
|
||||
s1, _, cleanupS1 := TestACLServer(t, nil)
|
||||
defer cleanupS1()
|
||||
testutil.WaitForLeader(t, s1.RPC)
|
||||
|
||||
leaderAcl := s1.getLeaderAcl()
|
||||
assert.NotEmpty(leaderAcl)
|
||||
token, err := s1.ResolveToken(leaderAcl)
|
||||
assert.Nil(err)
|
||||
if assert.NotNil(token) {
|
||||
assert.True(token.IsManagement())
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.testFn()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveSecretToken(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
s1, _, cleanupS1 := TestACLServer(t, nil)
|
||||
defer cleanupS1()
|
||||
testutil.WaitForLeader(t, s1.RPC)
|
||||
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
||||
defer testServerCleanup()
|
||||
testutil.WaitForLeader(t, testServer.RPC)
|
||||
|
||||
state := s1.State()
|
||||
leaderToken := s1.getLeaderAcl()
|
||||
assert.NotEmpty(t, leaderToken)
|
||||
testCases := []struct {
|
||||
name string
|
||||
testFn func(testServer *Server)
|
||||
}{
|
||||
{
|
||||
name: "valid token",
|
||||
testFn: func(testServer *Server) {
|
||||
|
||||
token := mock.ACLToken()
|
||||
// Generate and upsert a token.
|
||||
token := mock.ACLToken()
|
||||
err := testServer.State().UpsertACLTokens(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLToken{token})
|
||||
require.NoError(t, err)
|
||||
|
||||
err := state.UpsertACLTokens(structs.MsgTypeTestSetup, 110, []*structs.ACLToken{token})
|
||||
assert.Nil(t, err)
|
||||
// Attempt to look up the token and perform checks.
|
||||
tokenResp, err := testServer.ResolveSecretToken(token.SecretID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tokenResp)
|
||||
require.Equal(t, token, tokenResp)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "anonymous token",
|
||||
testFn: func(testServer *Server) {
|
||||
|
||||
respToken, err := s1.ResolveSecretToken(token.SecretID)
|
||||
assert.Nil(t, err)
|
||||
if assert.NotNil(t, respToken) {
|
||||
assert.NotEmpty(t, respToken.AccessorID)
|
||||
// Call the function with an empty input secret ID which is
|
||||
// classed as representing anonymous access in clusters with
|
||||
// ACLs enabled.
|
||||
tokenResp, err := testServer.ResolveSecretToken("")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tokenResp)
|
||||
require.Equal(t, structs.AnonymousACLToken, tokenResp)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "token not found",
|
||||
testFn: func(testServer *Server) {
|
||||
|
||||
// Call the function with randomly generated secret ID which
|
||||
// does not exist within state.
|
||||
tokenResp, err := testServer.ResolveSecretToken(uuid.Generate())
|
||||
require.Equal(t, structs.ErrTokenNotFound, err)
|
||||
require.Nil(t, tokenResp)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "token expired",
|
||||
testFn: func(testServer *Server) {
|
||||
|
||||
// Create a mock token with an expiration time long in the
|
||||
// past, and upsert.
|
||||
token := mock.ACLToken()
|
||||
token.ExpirationTime = pointer.Of(time.Date(
|
||||
1970, time.January, 1, 0, 0, 0, 0, time.UTC))
|
||||
|
||||
err := testServer.State().UpsertACLTokens(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLToken{token})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Perform the function call which should result in finding the
|
||||
// token has expired.
|
||||
tokenResp, err := testServer.ResolveSecretToken(uuid.Generate())
|
||||
require.Equal(t, structs.ErrTokenNotFound, err)
|
||||
require.Nil(t, tokenResp)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.testFn(testServer)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveClaims(t *testing.T) {
|
||||
|
|
|
@ -193,6 +193,14 @@ type Config struct {
|
|||
// one-time tokens.
|
||||
OneTimeTokenGCInterval time.Duration
|
||||
|
||||
// ACLTokenExpirationGCInterval is how often we dispatch a job to GC
|
||||
// expired ACL tokens.
|
||||
ACLTokenExpirationGCInterval time.Duration
|
||||
|
||||
// ACLTokenExpirationGCThreshold controls how "old" an expired ACL token
|
||||
// must be to be collected by GC.
|
||||
ACLTokenExpirationGCThreshold time.Duration
|
||||
|
||||
// RootKeyGCInterval is how often we dispatch a job to GC
|
||||
// encryption key metadata
|
||||
RootKeyGCInterval time.Duration
|
||||
|
@ -302,6 +310,14 @@ type Config struct {
|
|||
// the Authoritative Region.
|
||||
ReplicationToken string
|
||||
|
||||
// TokenMinExpirationTTL is used to enforce the lowest acceptable value for
|
||||
// ACL token expiration.
|
||||
ACLTokenMinExpirationTTL time.Duration
|
||||
|
||||
// TokenMaxExpirationTTL is used to enforce the highest acceptable value
|
||||
// for ACL token expiration.
|
||||
ACLTokenMaxExpirationTTL time.Duration
|
||||
|
||||
// SentinelGCInterval is the interval that we GC unused policies.
|
||||
SentinelGCInterval time.Duration
|
||||
|
||||
|
@ -439,6 +455,8 @@ func DefaultConfig() *Config {
|
|||
CSIVolumeClaimGCInterval: 5 * time.Minute,
|
||||
CSIVolumeClaimGCThreshold: 5 * time.Minute,
|
||||
OneTimeTokenGCInterval: 10 * time.Minute,
|
||||
ACLTokenExpirationGCInterval: 5 * time.Minute,
|
||||
ACLTokenExpirationGCThreshold: 1 * time.Hour,
|
||||
RootKeyGCInterval: 10 * time.Minute,
|
||||
RootKeyGCThreshold: 1 * time.Hour,
|
||||
RootKeyRotationThreshold: 720 * time.Hour, // 30 days
|
||||
|
@ -466,6 +484,8 @@ func DefaultConfig() *Config {
|
|||
LicenseConfig: &LicenseConfig{},
|
||||
EnableEventBroker: true,
|
||||
EventBufferSize: 100,
|
||||
ACLTokenMinExpirationTTL: 1 * time.Minute,
|
||||
ACLTokenMaxExpirationTTL: 24 * time.Hour,
|
||||
AutopilotConfig: &structs.AutopilotConfig{
|
||||
CleanupDeadServers: true,
|
||||
LastContactThreshold: 200 * time.Millisecond,
|
||||
|
|
|
@ -55,6 +55,10 @@ func (c *CoreScheduler) Process(eval *structs.Evaluation) error {
|
|||
return c.csiPluginGC(eval)
|
||||
case structs.CoreJobOneTimeTokenGC:
|
||||
return c.expiredOneTimeTokenGC(eval)
|
||||
case structs.CoreJobLocalTokenExpiredGC:
|
||||
return c.expiredACLTokenGC(eval, false)
|
||||
case structs.CoreJobGlobalTokenExpiredGC:
|
||||
return c.expiredACLTokenGC(eval, true)
|
||||
case structs.CoreJobRootKeyRotateOrGC:
|
||||
return c.rootKeyRotateOrGC(eval)
|
||||
case structs.CoreJobVariablesRekey:
|
||||
|
@ -86,6 +90,12 @@ func (c *CoreScheduler) forceGC(eval *structs.Evaluation) error {
|
|||
if err := c.expiredOneTimeTokenGC(eval); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.expiredACLTokenGC(eval, false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.expiredACLTokenGC(eval, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.rootKeyRotateOrGC(eval); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -784,6 +794,100 @@ func (c *CoreScheduler) expiredOneTimeTokenGC(eval *structs.Evaluation) error {
|
|||
return c.srv.RPC("ACL.ExpireOneTimeTokens", req, &structs.GenericResponse{})
|
||||
}
|
||||
|
||||
// expiredACLTokenGC handles running the garbage collector for expired ACL
|
||||
// tokens. It can be used for both local and global tokens and includes
|
||||
// behaviour to account for periodic and user actioned garbage collection
|
||||
// invocations.
|
||||
func (c *CoreScheduler) expiredACLTokenGC(eval *structs.Evaluation, global bool) error {
|
||||
|
||||
// If ACLs are not enabled, we do not need to continue and should exit
|
||||
// early. This is not an error condition as callers can blindly call this
|
||||
// function without checking the configuration. If the caller wants this to
|
||||
// be an error, they should check this config value themselves.
|
||||
if !c.srv.config.ACLEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the function has been triggered for global tokens, but we are not the
|
||||
// authoritative region, we should exit. This is not an error condition as
|
||||
// callers can blindly call this function without checking the
|
||||
// configuration. If the caller wants this to be an error, they should
|
||||
// check this config value themselves.
|
||||
if global && c.srv.config.AuthoritativeRegion != c.srv.Region() {
|
||||
return nil
|
||||
}
|
||||
|
||||
expiryThresholdIdx := c.getThreshold(eval, "expired_acl_token",
|
||||
"acl_token_expiration_gc_threshold", c.srv.config.ACLTokenExpirationGCThreshold)
|
||||
|
||||
expiredIter, err := c.snap.ACLTokensByExpired(global)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
expiredAccessorIDs []string
|
||||
num int
|
||||
)
|
||||
|
||||
// The memdb iterator contains all tokens which include an expiration time,
|
||||
// however, as the caller, we do not know at which point in the array the
|
||||
// tokens are no longer expired. This time therefore forms the basis at
|
||||
// which we draw the line in the iteration loop and find the final expired
|
||||
// token that is eligible for deletion.
|
||||
now := time.Now().UTC()
|
||||
|
||||
for raw := expiredIter.Next(); raw != nil; raw = expiredIter.Next() {
|
||||
token := raw.(*structs.ACLToken)
|
||||
|
||||
// The iteration order of the indexes mean if we come across an
|
||||
// unexpired token, we can exit as we have found all currently expired
|
||||
// tokens.
|
||||
if !token.IsExpired(now) {
|
||||
break
|
||||
}
|
||||
|
||||
// Check if the token is recent enough to skip, otherwise we'll delete
|
||||
// it.
|
||||
if token.CreateIndex > expiryThresholdIdx {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add the token accessor ID to the tracking array, thus marking it
|
||||
// ready for deletion.
|
||||
expiredAccessorIDs = append(expiredAccessorIDs, token.AccessorID)
|
||||
|
||||
// Increment the counter. If this is at or above our limit, we return
|
||||
// what we have so far.
|
||||
if num++; num >= structs.ACLMaxExpiredBatchSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// There is no need to call the RPC endpoint if we do not have any tokens
|
||||
// to delete.
|
||||
if len(expiredAccessorIDs) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Log a nice, friendly debug message which could be useful when debugging
|
||||
// garbage collection in environments with a high rate of token creation
|
||||
// and expiration.
|
||||
c.logger.Debug("expired ACL token GC found eligible tokens",
|
||||
"num", len(expiredAccessorIDs))
|
||||
|
||||
// Set up and make the RPC request which will return any error performing
|
||||
// the deletion.
|
||||
req := structs.ACLTokenDeleteRequest{
|
||||
AccessorIDs: expiredAccessorIDs,
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Region: c.srv.Region(),
|
||||
AuthToken: eval.LeaderACL,
|
||||
},
|
||||
}
|
||||
return c.srv.RPC(structs.ACLDeleteTokensRPCMethod, req, &structs.GenericResponse{})
|
||||
}
|
||||
|
||||
// rootKeyRotateOrGC is used to rotate or garbage collect root keys
|
||||
func (c *CoreScheduler) rootKeyRotateOrGC(eval *structs.Evaluation) error {
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
memdb "github.com/hashicorp/go-memdb"
|
||||
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/state"
|
||||
|
@ -2675,3 +2676,165 @@ func TestCoreScheduler_FailLoop(t *testing.T) {
|
|||
out.TriggeredBy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreScheduler_ExpiredACLTokenGC(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testServer, rootACLToken, testServerShutdown := TestACLServer(t, func(c *Config) {
|
||||
c.NumSchedulers = 0
|
||||
})
|
||||
defer testServerShutdown()
|
||||
testutil.WaitForLeader(t, testServer.RPC)
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
// Craft some specific local and global tokens. For each type, one is
|
||||
// expired, one is not.
|
||||
expiredGlobal := mock.ACLToken()
|
||||
expiredGlobal.Global = true
|
||||
expiredGlobal.ExpirationTime = pointer.Of(now.Add(-2 * time.Hour))
|
||||
|
||||
unexpiredGlobal := mock.ACLToken()
|
||||
unexpiredGlobal.Global = true
|
||||
unexpiredGlobal.ExpirationTime = pointer.Of(now.Add(2 * time.Hour))
|
||||
|
||||
expiredLocal := mock.ACLToken()
|
||||
expiredLocal.ExpirationTime = pointer.Of(now.Add(-2 * time.Hour))
|
||||
|
||||
unexpiredLocal := mock.ACLToken()
|
||||
unexpiredLocal.ExpirationTime = pointer.Of(now.Add(2 * time.Hour))
|
||||
|
||||
// Upsert these into state.
|
||||
err := testServer.State().UpsertACLTokens(structs.MsgTypeTestSetup, 10, []*structs.ACLToken{
|
||||
expiredGlobal, unexpiredGlobal, expiredLocal, unexpiredLocal,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Overwrite the timetable. The existing timetable has an entry due to the
|
||||
// ACL bootstrapping which makes witnessing a new index at a timestamp in
|
||||
// the past impossible.
|
||||
tt := NewTimeTable(timeTableGranularity, timeTableLimit)
|
||||
tt.Witness(20, time.Now().UTC().Add(-1*testServer.config.ACLTokenExpirationGCThreshold))
|
||||
testServer.fsm.timetable = tt
|
||||
|
||||
// Generate the core scheduler.
|
||||
snap, err := testServer.State().Snapshot()
|
||||
require.NoError(t, err)
|
||||
coreScheduler := NewCoreScheduler(testServer, snap)
|
||||
|
||||
// Trigger global and local periodic garbage collection runs.
|
||||
index, err := testServer.State().LatestIndex()
|
||||
require.NoError(t, err)
|
||||
index++
|
||||
|
||||
globalGCEval := testServer.coreJobEval(structs.CoreJobGlobalTokenExpiredGC, index)
|
||||
require.NoError(t, coreScheduler.Process(globalGCEval))
|
||||
|
||||
localGCEval := testServer.coreJobEval(structs.CoreJobLocalTokenExpiredGC, index)
|
||||
require.NoError(t, coreScheduler.Process(localGCEval))
|
||||
|
||||
// Ensure the ACL tokens stored within state are as expected.
|
||||
iter, err := testServer.State().ACLTokens(nil, state.SortDefault)
|
||||
require.NoError(t, err)
|
||||
|
||||
var tokens []*structs.ACLToken
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
tokens = append(tokens, raw.(*structs.ACLToken))
|
||||
}
|
||||
require.ElementsMatch(t, []*structs.ACLToken{rootACLToken, unexpiredGlobal, unexpiredLocal}, tokens)
|
||||
}
|
||||
|
||||
func TestCoreScheduler_ExpiredACLTokenGC_Force(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testServer, rootACLToken, testServerShutdown := TestACLServer(t, func(c *Config) {
|
||||
c.NumSchedulers = 0
|
||||
})
|
||||
defer testServerShutdown()
|
||||
testutil.WaitForLeader(t, testServer.RPC)
|
||||
|
||||
// This time is the threshold for all expiry calls to be based on. All
|
||||
// tokens with expiry can use this as their base and use Add().
|
||||
expiryTimeThreshold := time.Now().UTC()
|
||||
|
||||
// Track expired and non-expired tokens for local and global tokens in
|
||||
// separate arrays, so we have a clear way to test state.
|
||||
var expiredGlobalTokens, nonExpiredGlobalTokens, expiredLocalTokens, nonExpiredLocalTokens []*structs.ACLToken
|
||||
|
||||
// Add the root ACL token to the appropriate array. This will be returned
|
||||
// from state so must be accounted for and tested.
|
||||
nonExpiredGlobalTokens = append(nonExpiredGlobalTokens, rootACLToken)
|
||||
|
||||
// Generate and upsert a number of mixed expired, non-expired global
|
||||
// tokens.
|
||||
for i := 0; i < 20; i++ {
|
||||
mockedToken := mock.ACLToken()
|
||||
mockedToken.Global = true
|
||||
if i%2 == 0 {
|
||||
expiredGlobalTokens = append(expiredGlobalTokens, mockedToken)
|
||||
mockedToken.ExpirationTime = pointer.Of(expiryTimeThreshold.Add(-24 * time.Hour))
|
||||
} else {
|
||||
nonExpiredGlobalTokens = append(nonExpiredGlobalTokens, mockedToken)
|
||||
mockedToken.ExpirationTime = pointer.Of(expiryTimeThreshold.Add(24 * time.Hour))
|
||||
}
|
||||
}
|
||||
|
||||
// Generate and upsert a number of mixed expired, non-expired local
|
||||
// tokens.
|
||||
for i := 0; i < 20; i++ {
|
||||
mockedToken := mock.ACLToken()
|
||||
mockedToken.Global = false
|
||||
if i%2 == 0 {
|
||||
expiredLocalTokens = append(expiredLocalTokens, mockedToken)
|
||||
mockedToken.ExpirationTime = pointer.Of(expiryTimeThreshold.Add(-24 * time.Hour))
|
||||
} else {
|
||||
nonExpiredLocalTokens = append(nonExpiredLocalTokens, mockedToken)
|
||||
mockedToken.ExpirationTime = pointer.Of(expiryTimeThreshold.Add(24 * time.Hour))
|
||||
}
|
||||
}
|
||||
|
||||
allTokens := append(expiredGlobalTokens, nonExpiredGlobalTokens...)
|
||||
allTokens = append(allTokens, expiredLocalTokens...)
|
||||
allTokens = append(allTokens, nonExpiredLocalTokens...)
|
||||
|
||||
// Upsert them all.
|
||||
err := testServer.State().UpsertACLTokens(structs.MsgTypeTestSetup, 10, allTokens)
|
||||
require.NoError(t, err)
|
||||
|
||||
// This function provides an easy way to get all tokens out of the
|
||||
// iterator.
|
||||
fromIteratorFunc := func(iter memdb.ResultIterator) []*structs.ACLToken {
|
||||
var tokens []*structs.ACLToken
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
tokens = append(tokens, raw.(*structs.ACLToken))
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
// Check all the tokens are correctly stored within state.
|
||||
iter, err := testServer.State().ACLTokens(nil, state.SortDefault)
|
||||
require.NoError(t, err)
|
||||
|
||||
tokens := fromIteratorFunc(iter)
|
||||
require.ElementsMatch(t, allTokens, tokens)
|
||||
|
||||
// Generate the core scheduler and trigger a forced garbage collection
|
||||
// which should delete all expired tokens.
|
||||
snap, err := testServer.State().Snapshot()
|
||||
require.NoError(t, err)
|
||||
coreScheduler := NewCoreScheduler(testServer, snap)
|
||||
|
||||
index, err := testServer.State().LatestIndex()
|
||||
require.NoError(t, err)
|
||||
index++
|
||||
|
||||
forceGCEval := testServer.coreJobEval(structs.CoreJobForceGC, index)
|
||||
require.NoError(t, coreScheduler.Process(forceGCEval))
|
||||
|
||||
// List all the remaining ACL tokens to be sure they are as expected.
|
||||
iter, err = testServer.State().ACLTokens(nil, state.SortDefault)
|
||||
require.NoError(t, err)
|
||||
|
||||
tokens = fromIteratorFunc(iter)
|
||||
require.ElementsMatch(t, append(nonExpiredGlobalTokens, nonExpiredLocalTokens...), tokens)
|
||||
}
|
||||
|
|
80
nomad/fsm.go
80
nomad/fsm.go
|
@ -58,6 +58,7 @@ const (
|
|||
VariablesSnapshot SnapshotType = 22
|
||||
VariablesQuotaSnapshot SnapshotType = 23
|
||||
RootKeyMetaSnapshot SnapshotType = 24
|
||||
ACLRoleSnapshot SnapshotType = 25
|
||||
|
||||
// Namespace appliers were moved from enterprise and therefore start at 64
|
||||
NamespaceSnapshot SnapshotType = 64
|
||||
|
@ -323,6 +324,10 @@ func (n *nomadFSM) Apply(log *raft.Log) interface{} {
|
|||
return n.applyRootKeyMetaUpsert(msgType, buf[1:], log.Index)
|
||||
case structs.RootKeyMetaDeleteRequestType:
|
||||
return n.applyRootKeyMetaDelete(msgType, buf[1:], log.Index)
|
||||
case structs.ACLRolesUpsertRequestType:
|
||||
return n.applyACLRolesUpsert(msgType, buf[1:], log.Index)
|
||||
case structs.ACLRolesDeleteByIDRequestType:
|
||||
return n.applyACLRolesDeleteByID(msgType, buf[1:], log.Index)
|
||||
}
|
||||
|
||||
// Check enterprise only message types.
|
||||
|
@ -1748,6 +1753,20 @@ func (n *nomadFSM) restoreImpl(old io.ReadCloser, filter *FSMFilter) error {
|
|||
if err := restore.RootKeyMetaRestore(keyMeta); err != nil {
|
||||
return err
|
||||
}
|
||||
case ACLRoleSnapshot:
|
||||
|
||||
// Create a new ACLRole object, so we can decode the message into
|
||||
// it.
|
||||
aclRole := new(structs.ACLRole)
|
||||
|
||||
if err := dec.Decode(aclRole); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Perform the restoration.
|
||||
if err := restore.ACLRoleRestore(aclRole); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
// Check if this is an enterprise only object being restored
|
||||
|
@ -2008,6 +2027,36 @@ func (n *nomadFSM) applyDeleteServiceRegistrationByNodeID(msgType structs.Messag
|
|||
return nil
|
||||
}
|
||||
|
||||
func (n *nomadFSM) applyACLRolesUpsert(msgType structs.MessageType, buf []byte, index uint64) interface{} {
|
||||
defer metrics.MeasureSince([]string{"nomad", "fsm", "apply_acl_role_upsert"}, time.Now())
|
||||
var req structs.ACLRolesUpsertRequest
|
||||
if err := structs.Decode(buf, &req); err != nil {
|
||||
panic(fmt.Errorf("failed to decode request: %v", err))
|
||||
}
|
||||
|
||||
if err := n.state.UpsertACLRoles(msgType, index, req.ACLRoles, req.AllowMissingPolicies); err != nil {
|
||||
n.logger.Error("UpsertACLRoles failed", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *nomadFSM) applyACLRolesDeleteByID(msgType structs.MessageType, buf []byte, index uint64) interface{} {
|
||||
defer metrics.MeasureSince([]string{"nomad", "fsm", "apply_acl_role_delete_by_id"}, time.Now())
|
||||
var req structs.ACLRolesDeleteByIDRequest
|
||||
if err := structs.Decode(buf, &req); err != nil {
|
||||
panic(fmt.Errorf("failed to decode request: %v", err))
|
||||
}
|
||||
|
||||
if err := n.state.DeleteACLRolesByID(msgType, index, req.ACLRoleIDs); err != nil {
|
||||
n.logger.Error("DeleteACLRolesByID failed", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type FSMFilter struct {
|
||||
evaluator *bexpr.Evaluator
|
||||
}
|
||||
|
@ -2209,6 +2258,10 @@ func (s *nomadSnapshot) Persist(sink raft.SnapshotSink) error {
|
|||
sink.Cancel()
|
||||
return err
|
||||
}
|
||||
if err := s.persistACLRoles(sink, encoder); err != nil {
|
||||
sink.Cancel()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -2836,6 +2889,33 @@ func (s *nomadSnapshot) persistRootKeyMeta(sink raft.SnapshotSink,
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *nomadSnapshot) persistACLRoles(sink raft.SnapshotSink,
|
||||
encoder *codec.Encoder) error {
|
||||
|
||||
// Get all the ACL roles.
|
||||
ws := memdb.NewWatchSet()
|
||||
aclRolesIter, err := s.snap.GetACLRoles(ws)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
// Get the next item.
|
||||
for raw := aclRolesIter.Next(); raw != nil; raw = aclRolesIter.Next() {
|
||||
|
||||
// Prepare the request struct.
|
||||
role := raw.(*structs.ACLRole)
|
||||
|
||||
// Write out an ACL role snapshot.
|
||||
sink.Write([]byte{byte(ACLRoleSnapshot)})
|
||||
if err := encoder.Encode(role); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Release is a no-op, as we just need to GC the pointer
|
||||
// to the state store snapshot. There is nothing to explicitly
|
||||
// cleanup.
|
||||
|
|
|
@ -2893,6 +2893,43 @@ func TestFSM_SnapshotRestore_ServiceRegistrations(t *testing.T) {
|
|||
require.ElementsMatch(t, restoredRegs, serviceRegs)
|
||||
}
|
||||
|
||||
func TestFSM_SnapshotRestore_ACLRoles(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Create our initial FSM which will be snapshotted.
|
||||
fsm := testFSM(t)
|
||||
testState := fsm.State()
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, testState.UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Generate and upsert some ACL roles.
|
||||
aclRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
require.NoError(t, testState.UpsertACLRoles(structs.MsgTypeTestSetup, 10, aclRoles, false))
|
||||
|
||||
// Perform a snapshot restore.
|
||||
restoredFSM := testSnapshotRestore(t, fsm)
|
||||
restoredState := restoredFSM.State()
|
||||
|
||||
// List the ACL roles from restored state and ensure everything is as
|
||||
// expected.
|
||||
iter, err := restoredState.GetACLRoles(memdb.NewWatchSet())
|
||||
require.NoError(t, err)
|
||||
|
||||
var restoredACLRoles []*structs.ACLRole
|
||||
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
restoredACLRoles = append(restoredACLRoles, raw.(*structs.ACLRole))
|
||||
}
|
||||
require.ElementsMatch(t, restoredACLRoles, aclRoles)
|
||||
}
|
||||
|
||||
func TestFSM_ReconcileSummaries(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
// Add some state
|
||||
|
@ -3420,6 +3457,73 @@ func TestFSM_SnapshotRestore_Variables(t *testing.T) {
|
|||
require.ElementsMatch(t, restoredSVs, svs)
|
||||
}
|
||||
|
||||
func TestFSM_ApplyACLRolesUpsert(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
fsm := testFSM(t)
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, fsm.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Generate the upsert request and apply the change.
|
||||
req := structs.ACLRolesUpsertRequest{
|
||||
ACLRoles: []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()},
|
||||
}
|
||||
buf, err := structs.Encode(structs.ACLRolesUpsertRequestType, req)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, fsm.Apply(makeLog(buf)))
|
||||
|
||||
// Read out both ACL roles and perform an equality check using the hash.
|
||||
ws := memdb.NewWatchSet()
|
||||
out, err := fsm.State().GetACLRoleByName(ws, req.ACLRoles[0].Name)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, req.ACLRoles[0].Hash, out.Hash)
|
||||
|
||||
out, err = fsm.State().GetACLRoleByName(ws, req.ACLRoles[1].Name)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, req.ACLRoles[1].Hash, out.Hash)
|
||||
}
|
||||
|
||||
func TestFSM_ApplyACLRolesDeleteByID(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
fsm := testFSM(t)
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, fsm.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Generate and upsert two ACL roles.
|
||||
aclRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
require.NoError(t, fsm.State().UpsertACLRoles(structs.MsgTypeTestSetup, 10, aclRoles, false))
|
||||
|
||||
// Build and apply our message.
|
||||
req := structs.ACLRolesDeleteByIDRequest{ACLRoleIDs: []string{aclRoles[0].ID, aclRoles[1].ID}}
|
||||
buf, err := structs.Encode(structs.ACLRolesDeleteByIDRequestType, req)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, fsm.Apply(makeLog(buf)))
|
||||
|
||||
// List all ACL roles within state to ensure both have been removed.
|
||||
ws := memdb.NewWatchSet()
|
||||
iter, err := fsm.State().GetACLRoles(ws)
|
||||
require.NoError(t, err)
|
||||
|
||||
var count int
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
count++
|
||||
}
|
||||
require.Equal(t, 0, count)
|
||||
}
|
||||
|
||||
func TestFSM_ACLEvents(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
|
|
323
nomad/leader.go
323
nomad/leader.go
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-memdb"
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/hashicorp/nomad/helper"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/state"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
|
@ -347,7 +348,8 @@ func (s *Server) establishLeadership(stopCh chan struct{}) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Scheduler periodic jobs
|
||||
// Schedule periodic jobs which include expired local ACL token garbage
|
||||
// collection.
|
||||
go s.schedulePeriodic(stopCh)
|
||||
|
||||
// Reap any failed evaluations
|
||||
|
@ -379,12 +381,23 @@ func (s *Server) establishLeadership(stopCh chan struct{}) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Start replication of ACLs and Policies if they are enabled,
|
||||
// and we are not the authoritative region.
|
||||
if s.config.ACLEnabled && s.config.Region != s.config.AuthoritativeRegion {
|
||||
go s.replicateACLPolicies(stopCh)
|
||||
go s.replicateACLTokens(stopCh)
|
||||
go s.replicateNamespaces(stopCh)
|
||||
// If ACLs are enabled, the leader needs to start a number of long-lived
|
||||
// routines. Exactly which routines, depends on whether this leader is
|
||||
// running within the authoritative region or not.
|
||||
if s.config.ACLEnabled {
|
||||
|
||||
// The authoritative region is responsible for garbage collecting
|
||||
// expired global tokens. Otherwise, non-authoritative regions need to
|
||||
// replicate policies, tokens, and namespaces.
|
||||
switch s.config.AuthoritativeRegion {
|
||||
case s.config.Region:
|
||||
go s.schedulePeriodicAuthoritative(stopCh)
|
||||
default:
|
||||
go s.replicateACLPolicies(stopCh)
|
||||
go s.replicateACLTokens(stopCh)
|
||||
go s.replicateACLRoles(stopCh)
|
||||
go s.replicateNamespaces(stopCh)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup any enterprise systems required.
|
||||
|
@ -772,43 +785,35 @@ func (s *Server) schedulePeriodic(stopCh chan struct{}) {
|
|||
variablesRekey := time.NewTicker(s.config.VariablesRekeyInterval)
|
||||
defer variablesRekey.Stop()
|
||||
|
||||
// getLatest grabs the latest index from the state store. It returns true if
|
||||
// the index was retrieved successfully.
|
||||
getLatest := func() (uint64, bool) {
|
||||
snapshotIndex, err := s.fsm.State().LatestIndex()
|
||||
if err != nil {
|
||||
s.logger.Error("failed to determine state store's index", "error", err)
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return snapshotIndex, true
|
||||
}
|
||||
// Set up the expired ACL local token garbage collection timer.
|
||||
localTokenExpiredGC, localTokenExpiredGCStop := helper.NewSafeTimer(s.config.ACLTokenExpirationGCInterval)
|
||||
defer localTokenExpiredGCStop()
|
||||
|
||||
for {
|
||||
|
||||
select {
|
||||
case <-evalGC.C:
|
||||
if index, ok := getLatest(); ok {
|
||||
if index, ok := s.getLatestIndex(); ok {
|
||||
s.evalBroker.Enqueue(s.coreJobEval(structs.CoreJobEvalGC, index))
|
||||
}
|
||||
case <-nodeGC.C:
|
||||
if index, ok := getLatest(); ok {
|
||||
if index, ok := s.getLatestIndex(); ok {
|
||||
s.evalBroker.Enqueue(s.coreJobEval(structs.CoreJobNodeGC, index))
|
||||
}
|
||||
case <-jobGC.C:
|
||||
if index, ok := getLatest(); ok {
|
||||
if index, ok := s.getLatestIndex(); ok {
|
||||
s.evalBroker.Enqueue(s.coreJobEval(structs.CoreJobJobGC, index))
|
||||
}
|
||||
case <-deploymentGC.C:
|
||||
if index, ok := getLatest(); ok {
|
||||
if index, ok := s.getLatestIndex(); ok {
|
||||
s.evalBroker.Enqueue(s.coreJobEval(structs.CoreJobDeploymentGC, index))
|
||||
}
|
||||
case <-csiPluginGC.C:
|
||||
if index, ok := getLatest(); ok {
|
||||
if index, ok := s.getLatestIndex(); ok {
|
||||
s.evalBroker.Enqueue(s.coreJobEval(structs.CoreJobCSIPluginGC, index))
|
||||
}
|
||||
case <-csiVolumeClaimGC.C:
|
||||
if index, ok := getLatest(); ok {
|
||||
if index, ok := s.getLatestIndex(); ok {
|
||||
s.evalBroker.Enqueue(s.coreJobEval(structs.CoreJobCSIVolumeClaimGC, index))
|
||||
}
|
||||
case <-oneTimeTokenGC.C:
|
||||
|
@ -816,24 +821,63 @@ func (s *Server) schedulePeriodic(stopCh chan struct{}) {
|
|||
continue
|
||||
}
|
||||
|
||||
if index, ok := getLatest(); ok {
|
||||
if index, ok := s.getLatestIndex(); ok {
|
||||
s.evalBroker.Enqueue(s.coreJobEval(structs.CoreJobOneTimeTokenGC, index))
|
||||
}
|
||||
case <-localTokenExpiredGC.C:
|
||||
if index, ok := s.getLatestIndex(); ok {
|
||||
s.evalBroker.Enqueue(s.coreJobEval(structs.CoreJobLocalTokenExpiredGC, index))
|
||||
}
|
||||
localTokenExpiredGC.Reset(s.config.ACLTokenExpirationGCInterval)
|
||||
case <-rootKeyGC.C:
|
||||
if index, ok := getLatest(); ok {
|
||||
if index, ok := s.getLatestIndex(); ok {
|
||||
s.evalBroker.Enqueue(s.coreJobEval(structs.CoreJobRootKeyRotateOrGC, index))
|
||||
}
|
||||
case <-variablesRekey.C:
|
||||
if index, ok := getLatest(); ok {
|
||||
if index, ok := s.getLatestIndex(); ok {
|
||||
s.evalBroker.Enqueue(s.coreJobEval(structs.CoreJobVariablesRekey, index))
|
||||
}
|
||||
|
||||
case <-stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// schedulePeriodicAuthoritative is a long-lived routine intended for use on
|
||||
// the leader within the authoritative region only. It periodically queues work
|
||||
// onto the _core scheduler for ACL based activities such as removing expired
|
||||
// global ACL tokens.
|
||||
func (s *Server) schedulePeriodicAuthoritative(stopCh chan struct{}) {
|
||||
|
||||
// Set up the expired ACL global token garbage collection timer.
|
||||
globalTokenExpiredGC, globalTokenExpiredGCStop := helper.NewSafeTimer(s.config.ACLTokenExpirationGCInterval)
|
||||
defer globalTokenExpiredGCStop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-globalTokenExpiredGC.C:
|
||||
if index, ok := s.getLatestIndex(); ok {
|
||||
s.evalBroker.Enqueue(s.coreJobEval(structs.CoreJobGlobalTokenExpiredGC, index))
|
||||
}
|
||||
globalTokenExpiredGC.Reset(s.config.ACLTokenExpirationGCInterval)
|
||||
case <-stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getLatestIndex is a helper function which returns the latest index from the
|
||||
// state store. The boolean return indicates whether the call has been
|
||||
// successful or not.
|
||||
func (s *Server) getLatestIndex() (uint64, bool) {
|
||||
snapshotIndex, err := s.fsm.State().LatestIndex()
|
||||
if err != nil {
|
||||
s.logger.Error("failed to determine state store's index", "error", err)
|
||||
return 0, false
|
||||
}
|
||||
return snapshotIndex, true
|
||||
}
|
||||
|
||||
// coreJobEval returns an evaluation for a core job
|
||||
func (s *Server) coreJobEval(job string, modifyIndex uint64) *structs.Evaluation {
|
||||
return &structs.Evaluation{
|
||||
|
@ -1646,6 +1690,229 @@ func diffACLTokens(store *state.StateStore, minIndex uint64, remoteList []*struc
|
|||
return
|
||||
}
|
||||
|
||||
// replicateACLRoles is used to replicate ACL Roles from the authoritative
|
||||
// region to this region. The loop should only be run on the leader within the
|
||||
// federated region.
|
||||
func (s *Server) replicateACLRoles(stopCh chan struct{}) {
|
||||
|
||||
// Generate our request object. We only need to do this once and reuse it
|
||||
// for every RPC request. The MinQueryIndex is updated after every
|
||||
// successful replication loop, so the next query acts as a blocking query
|
||||
// and only returns upon a change in the authoritative region.
|
||||
req := structs.ACLRolesListRequest{
|
||||
QueryOptions: structs.QueryOptions{
|
||||
AllowStale: true,
|
||||
Region: s.config.AuthoritativeRegion,
|
||||
},
|
||||
}
|
||||
|
||||
// Create our replication rate limiter for ACL roles and log a lovely
|
||||
// message to indicate the process is starting.
|
||||
limiter := rate.NewLimiter(replicationRateLimit, int(replicationRateLimit))
|
||||
s.logger.Debug("starting ACL Role replication from authoritative region",
|
||||
"authoritative_region", req.Region)
|
||||
|
||||
// Enter the main ACL Role replication loop that will only exit when the
|
||||
// stopCh is closed.
|
||||
//
|
||||
// Any error encountered will use the replicationBackoffContinue function
|
||||
// which handles replication backoff and shutdown coordination in the event
|
||||
// of an error inside the loop.
|
||||
for {
|
||||
select {
|
||||
case <-stopCh:
|
||||
return
|
||||
default:
|
||||
|
||||
// Rate limit how often we attempt replication. It is OK to ignore
|
||||
// the error as the context will never be cancelled and the limit
|
||||
// parameters are controlled internally.
|
||||
_ = limiter.Wait(context.Background())
|
||||
|
||||
// Set the replication token on each replication iteration so that
|
||||
// it is always current and can handle agent SIGHUP reloads.
|
||||
req.AuthToken = s.ReplicationToken()
|
||||
|
||||
var resp structs.ACLRolesListResponse
|
||||
|
||||
// Make the list RPC request to the authoritative region, so we
|
||||
// capture the latest ACL role listing.
|
||||
err := s.forwardRegion(s.config.AuthoritativeRegion, structs.ACLListRolesRPCMethod, &req, &resp)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to fetch ACL Roles from authoritative region", "error", err)
|
||||
if s.replicationBackoffContinue(stopCh) {
|
||||
continue
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Perform a two-way diff on the ACL roles.
|
||||
toDelete, toUpdate := diffACLRoles(s.State(), req.MinQueryIndex, resp.ACLRoles)
|
||||
|
||||
// A significant amount of time could pass between the last check
|
||||
// on whether we should stop the replication process. Therefore, do
|
||||
// a check here, before calling Raft.
|
||||
select {
|
||||
case <-stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// If we have ACL roles to delete, make this call directly to Raft.
|
||||
if len(toDelete) > 0 {
|
||||
args := structs.ACLRolesDeleteByIDRequest{ACLRoleIDs: toDelete}
|
||||
_, _, err := s.raftApply(structs.ACLRolesDeleteByIDRequestType, &args)
|
||||
|
||||
// If the error was because we lost leadership while calling
|
||||
// Raft, avoid logging as this can be confusing to operators.
|
||||
if err != nil {
|
||||
if err != raft.ErrLeadershipLost {
|
||||
s.logger.Error("failed to delete ACL roles", "error", err)
|
||||
}
|
||||
if s.replicationBackoffContinue(stopCh) {
|
||||
continue
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch any outdated policies.
|
||||
var fetched []*structs.ACLRole
|
||||
if len(toUpdate) > 0 {
|
||||
req := structs.ACLRolesByIDRequest{
|
||||
ACLRoleIDs: toUpdate,
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Region: s.config.AuthoritativeRegion,
|
||||
AuthToken: s.ReplicationToken(),
|
||||
AllowStale: true,
|
||||
MinQueryIndex: resp.Index - 1,
|
||||
},
|
||||
}
|
||||
var reply structs.ACLRolesByIDResponse
|
||||
if err := s.forwardRegion(s.config.AuthoritativeRegion, structs.ACLGetRolesByIDRPCMethod, &req, &reply); err != nil {
|
||||
s.logger.Error("failed to fetch ACL Roles from authoritative region", "error", err)
|
||||
if s.replicationBackoffContinue(stopCh) {
|
||||
continue
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, aclRole := range reply.ACLRoles {
|
||||
fetched = append(fetched, aclRole)
|
||||
}
|
||||
}
|
||||
|
||||
// Update local tokens
|
||||
if len(fetched) > 0 {
|
||||
|
||||
// The replication of ACL roles and policies are independent,
|
||||
// therefore we cannot ensure the policies linked within the
|
||||
// role are present. We must set allow missing to true.
|
||||
args := structs.ACLRolesUpsertRequest{
|
||||
ACLRoles: fetched,
|
||||
AllowMissingPolicies: true,
|
||||
}
|
||||
|
||||
// Perform the upsert directly via Raft.
|
||||
_, _, err := s.raftApply(structs.ACLRolesUpsertRequestType, &args)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to update ACL roles", "error", err)
|
||||
if s.replicationBackoffContinue(stopCh) {
|
||||
continue
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the minimum query index, blocks until there is a change.
|
||||
req.MinQueryIndex = resp.Index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// replicationBackoffContinue should be used when a replication loop encounters
|
||||
// an error and wants to wait until either the backoff time has been met, or
|
||||
// the stopCh has been closed. The boolean indicates whether the replication
|
||||
// process should continue.
|
||||
//
|
||||
// Typical use:
|
||||
//
|
||||
// if s.replicationBackoffContinue(stopCh) {
|
||||
// continue
|
||||
// } else {
|
||||
// return
|
||||
// }
|
||||
func (s *Server) replicationBackoffContinue(stopCh chan struct{}) bool {
|
||||
|
||||
timer, timerStopFn := helper.NewSafeTimer(s.config.ReplicationBackoff)
|
||||
defer timerStopFn()
|
||||
|
||||
select {
|
||||
case <-timer.C:
|
||||
return true
|
||||
case <-stopCh:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// diffACLRoles is used to perform a two-way diff between the local ACL Roles
|
||||
// and the remote Roles to determine which tokens need to be deleted or
|
||||
// updated. The returned array's contain ACL Role IDs.
|
||||
func diffACLRoles(
|
||||
store *state.StateStore, minIndex uint64, remoteList []*structs.ACLRoleListStub) (
|
||||
delete []string, update []string) {
|
||||
|
||||
// The local ACL role tracking is keyed by the role ID and the value is the
|
||||
// hash of the role.
|
||||
local := make(map[string][]byte)
|
||||
|
||||
// The remote ACL role tracking is keyed by the role ID; the value is an
|
||||
// empty struct as we already have the full object.
|
||||
remote := make(map[string]struct{})
|
||||
|
||||
// Read all the ACL role currently held within our local state. This panic
|
||||
// will only happen as a developer making a mistake with naming the index
|
||||
// to use.
|
||||
iter, err := store.GetACLRoles(nil)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to iterate local ACL roles: %v", err))
|
||||
}
|
||||
|
||||
// Iterate the local ACL roles and add them to our tracking of local roles.
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
aclRole := raw.(*structs.ACLRole)
|
||||
local[aclRole.ID] = aclRole.Hash
|
||||
}
|
||||
|
||||
// Iterate over the remote ACL roles.
|
||||
for _, remoteACLRole := range remoteList {
|
||||
remote[remoteACLRole.ID] = struct{}{}
|
||||
|
||||
// Identify whether the ACL role is within the local state. If it is
|
||||
// not, add this to our update list.
|
||||
if localHash, ok := local[remoteACLRole.ID]; !ok {
|
||||
update = append(update, remoteACLRole.ID)
|
||||
|
||||
// Check if ACL role is newer remotely and there is a hash
|
||||
// mismatch.
|
||||
} else if remoteACLRole.ModifyIndex > minIndex && !bytes.Equal(localHash, remoteACLRole.Hash) {
|
||||
update = append(update, remoteACLRole.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// If we have ACL roles within state which are no longer present in the
|
||||
// authoritative region we should delete them.
|
||||
for localACLRole := range local {
|
||||
if _, ok := remote[localACLRole]; !ok {
|
||||
delete = append(delete, localACLRole)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// getOrCreateAutopilotConfig is used to get the autopilot config, initializing it if necessary
|
||||
func (s *Server) getOrCreateAutopilotConfig() *structs.AutopilotConfig {
|
||||
state := s.fsm.State()
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/hashicorp/raft"
|
||||
"github.com/hashicorp/serf/serf"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -1025,6 +1026,114 @@ func TestLeader_DiffACLTokens(t *testing.T) {
|
|||
assert.Equal(t, []string{p3.AccessorID, p4.AccessorID}, update)
|
||||
}
|
||||
|
||||
func TestServer_replicationBackoffContinue(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
testFn func()
|
||||
}{
|
||||
{
|
||||
name: "leadership lost",
|
||||
testFn: func() {
|
||||
|
||||
// Create a test server with a long enough backoff that we will
|
||||
// be able to close the channel before it fires, but not too
|
||||
// long that the test having problems means CI will hang
|
||||
// forever.
|
||||
testServer, testServerCleanup := TestServer(t, func(c *Config) {
|
||||
c.ReplicationBackoff = 5 * time.Second
|
||||
})
|
||||
defer testServerCleanup()
|
||||
|
||||
// Create our stop channel which is used by the server to
|
||||
// indicate leadership loss.
|
||||
stopCh := make(chan struct{})
|
||||
|
||||
// The resultCh is used to block and collect the output from
|
||||
// the test routine.
|
||||
resultCh := make(chan bool, 1)
|
||||
|
||||
// Run a routine to collect the result and close the channel
|
||||
// straight away.
|
||||
go func() {
|
||||
output := testServer.replicationBackoffContinue(stopCh)
|
||||
resultCh <- output
|
||||
}()
|
||||
|
||||
close(stopCh)
|
||||
|
||||
actualResult := <-resultCh
|
||||
require.False(t, actualResult)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "backoff continue",
|
||||
testFn: func() {
|
||||
|
||||
// Create a test server with a short backoff.
|
||||
testServer, testServerCleanup := TestServer(t, func(c *Config) {
|
||||
c.ReplicationBackoff = 10 * time.Nanosecond
|
||||
})
|
||||
defer testServerCleanup()
|
||||
|
||||
// Create our stop channel which is used by the server to
|
||||
// indicate leadership loss.
|
||||
stopCh := make(chan struct{})
|
||||
|
||||
// The resultCh is used to block and collect the output from
|
||||
// the test routine.
|
||||
resultCh := make(chan bool, 1)
|
||||
|
||||
// Run a routine to collect the result without closing stopCh.
|
||||
go func() {
|
||||
output := testServer.replicationBackoffContinue(stopCh)
|
||||
resultCh <- output
|
||||
}()
|
||||
|
||||
actualResult := <-resultCh
|
||||
require.True(t, actualResult)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.testFn()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_diffACLRoles(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
stateStore := state.TestStateStore(t)
|
||||
|
||||
// Build an initial baseline of ACL Roles.
|
||||
aclRole0 := mock.ACLRole()
|
||||
aclRole1 := mock.ACLRole()
|
||||
aclRole2 := mock.ACLRole()
|
||||
aclRole3 := mock.ACLRole()
|
||||
|
||||
// Upsert these into our local state. Use copies, so we can alter the roles
|
||||
// directly and use within the diff func.
|
||||
err := stateStore.UpsertACLRoles(structs.MsgTypeTestSetup, 50,
|
||||
[]*structs.ACLRole{aclRole0.Copy(), aclRole1.Copy(), aclRole2.Copy(), aclRole3.Copy()}, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Modify the ACL roles to create a number of differences. These roles
|
||||
// represent the state of the authoritative region.
|
||||
aclRole2.ModifyIndex = 50
|
||||
aclRole3.ModifyIndex = 200
|
||||
aclRole3.Hash = []byte{0, 1, 2, 3}
|
||||
aclRole4 := mock.ACLRole()
|
||||
|
||||
// Run the diff function and test the output.
|
||||
toDelete, toUpdate := diffACLRoles(stateStore, 50, []*structs.ACLRoleListStub{
|
||||
aclRole2.Stub(), aclRole3.Stub(), aclRole4.Stub()})
|
||||
require.ElementsMatch(t, []string{aclRole0.ID, aclRole1.ID}, toDelete)
|
||||
require.ElementsMatch(t, []string{aclRole3.ID, aclRole4.ID}, toUpdate)
|
||||
}
|
||||
|
||||
func TestLeader_UpgradeRaftVersion(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
|
@ -1665,6 +1774,27 @@ func waitForStableLeadership(t *testing.T, servers []*Server) *Server {
|
|||
return leader
|
||||
}
|
||||
|
||||
func TestServer_getLatestIndex(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testServer, testServerCleanup := TestServer(t, nil)
|
||||
defer testServerCleanup()
|
||||
|
||||
// Test a new state store value.
|
||||
idx, success := testServer.getLatestIndex()
|
||||
require.True(t, success)
|
||||
must.Eq(t, 1, idx)
|
||||
|
||||
// Upsert something with a high index, and check again.
|
||||
err := testServer.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 1013, []*structs.ACLPolicy{mock.ACLPolicy()})
|
||||
require.NoError(t, err)
|
||||
|
||||
idx, success = testServer.getLatestIndex()
|
||||
require.True(t, success)
|
||||
must.Eq(t, 1013, idx)
|
||||
}
|
||||
|
||||
func TestServer_handleEvalBrokerStateChange(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
|
|
|
@ -2510,3 +2510,19 @@ func mockVariableMetadata() structs.VariableMetadata {
|
|||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ACLRole() *structs.ACLRole {
|
||||
role := structs.ACLRole{
|
||||
ID: uuid.Generate(),
|
||||
Name: fmt.Sprintf("acl-role-%s", uuid.Short()),
|
||||
Description: "mocked-test-acl-role",
|
||||
Policies: []*structs.ACLRolePolicyLink{
|
||||
{Name: "mocked-test-policy-1"},
|
||||
{Name: "mocked-test-policy-2"},
|
||||
},
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 10,
|
||||
}
|
||||
role.SetHash()
|
||||
return &role
|
||||
}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
package indexer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-memdb"
|
||||
)
|
||||
|
||||
var (
|
||||
// Ensure the required memdb interfaces are met at compile time.
|
||||
_ memdb.Indexer = SingleIndexer{}
|
||||
_ memdb.SingleIndexer = SingleIndexer{}
|
||||
)
|
||||
|
||||
// SingleIndexer implements both memdb.Indexer and memdb.SingleIndexer. It may
|
||||
// be used in a memdb.IndexSchema to specify functions that generate the index
|
||||
// value for memdb.Txn operations.
|
||||
type SingleIndexer struct {
|
||||
|
||||
// readIndex is used by memdb for Txn.Get, Txn.First, and other operations
|
||||
// that read data.
|
||||
ReadIndex
|
||||
|
||||
// writeIndex is used by memdb for Txn.Insert, Txn.Delete, and other
|
||||
// operations that write data to the index.
|
||||
WriteIndex
|
||||
}
|
||||
|
||||
// ReadIndex implements memdb.Indexer. It exists so that a function can be used
|
||||
// to provide the interface.
|
||||
//
|
||||
// Unlike memdb.Indexer, a readIndex function accepts only a single argument. To
|
||||
// generate an index from multiple values, use a struct type with multiple fields.
|
||||
type ReadIndex func(arg any) ([]byte, error)
|
||||
|
||||
func (f ReadIndex) FromArgs(args ...interface{}) ([]byte, error) {
|
||||
if len(args) != 1 {
|
||||
return nil, fmt.Errorf("index supports only a single arg")
|
||||
}
|
||||
return f(args[0])
|
||||
}
|
||||
|
||||
var ErrMissingValueForIndex = fmt.Errorf("object is missing a value for this index")
|
||||
|
||||
// WriteIndex implements memdb.SingleIndexer. It exists so that a function
|
||||
// can be used to provide this interface.
|
||||
//
|
||||
// Instead of a bool return value, writeIndex expects errMissingValueForIndex to
|
||||
// indicate that an index could not be build for the object. It will translate
|
||||
// this error into a false value to satisfy the memdb.SingleIndexer interface.
|
||||
type WriteIndex func(raw any) ([]byte, error)
|
||||
|
||||
func (f WriteIndex) FromObject(raw any) (bool, []byte, error) {
|
||||
v, err := f(raw)
|
||||
if errors.Is(err, ErrMissingValueForIndex) {
|
||||
return false, nil, nil
|
||||
}
|
||||
return err == nil, v, err
|
||||
}
|
||||
|
||||
// IndexBuilder is a buffer used to construct memdb index values.
|
||||
type IndexBuilder bytes.Buffer
|
||||
|
||||
// Bytes returns the stored IndexBuilder value as a byte array.
|
||||
func (b *IndexBuilder) Bytes() []byte { return (*bytes.Buffer)(b).Bytes() }
|
||||
|
||||
// Time is used to write the passed time into the IndexBuilder for use as a
|
||||
// memdb index value.
|
||||
func (b *IndexBuilder) Time(t time.Time) {
|
||||
val := t.Unix()
|
||||
buf := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(buf, uint64(val))
|
||||
(*bytes.Buffer)(b).Write(buf)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package indexer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_IndexBuilder_Time(t *testing.T) {
|
||||
builder := &IndexBuilder{}
|
||||
testTime := time.Date(1987, time.April, 13, 8, 3, 0, 0, time.UTC)
|
||||
builder.Time(testTime)
|
||||
require.Equal(t, []byte{0, 0, 0, 0, 32, 128, 155, 180}, builder.Bytes())
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package indexer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TimeQuery struct {
|
||||
Value time.Time
|
||||
}
|
||||
|
||||
// IndexFromTimeQuery can be used as a memdb.Indexer query via ReadIndex and
|
||||
// allows querying by time.
|
||||
func IndexFromTimeQuery(arg any) ([]byte, error) {
|
||||
p, ok := arg.(*TimeQuery)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type %T for TimeQuery index", arg)
|
||||
}
|
||||
|
||||
// Construct the index value and return the byte array representation of
|
||||
// the time value.
|
||||
var b IndexBuilder
|
||||
b.Time(p.Value)
|
||||
return b.Bytes(), nil
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package indexer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_IndexFromTimeQuery(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testCases := []struct {
|
||||
inputArg interface{}
|
||||
expectedOutputBytes []byte
|
||||
expectedOutputError error
|
||||
name string
|
||||
}{
|
||||
{
|
||||
inputArg: &TimeQuery{
|
||||
Value: time.Date(1987, time.April, 13, 8, 3, 0, 0, time.UTC),
|
||||
},
|
||||
expectedOutputBytes: []byte{0x0, 0x0, 0x0, 0x0, 0x20, 0x80, 0x9b, 0xb4},
|
||||
expectedOutputError: nil,
|
||||
name: "generic test 1",
|
||||
},
|
||||
{
|
||||
inputArg: &TimeQuery{
|
||||
Value: time.Date(2022, time.April, 27, 14, 12, 0, 0, time.UTC),
|
||||
},
|
||||
expectedOutputBytes: []byte{0x0, 0x0, 0x0, 0x0, 0x62, 0x69, 0x4f, 0x30},
|
||||
expectedOutputError: nil,
|
||||
name: "generic test 2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualOutput, actualError := IndexFromTimeQuery(tc.inputArg)
|
||||
require.Equal(t, tc.expectedOutputError, actualError)
|
||||
require.Equal(t, tc.expectedOutputBytes, actualOutput)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ import (
|
|||
"sync"
|
||||
|
||||
memdb "github.com/hashicorp/go-memdb"
|
||||
|
||||
"github.com/hashicorp/nomad/nomad/state/indexer"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
)
|
||||
|
||||
|
@ -17,16 +17,20 @@ const (
|
|||
TableVariables = "variables"
|
||||
TableVariablesQuotas = "variables_quota"
|
||||
TableRootKeyMeta = "root_key_meta"
|
||||
TableACLRoles = "acl_roles"
|
||||
)
|
||||
|
||||
const (
|
||||
indexID = "id"
|
||||
indexJob = "job"
|
||||
indexNodeID = "node_id"
|
||||
indexAllocID = "alloc_id"
|
||||
indexServiceName = "service_name"
|
||||
indexKeyID = "key_id"
|
||||
indexPath = "path"
|
||||
indexID = "id"
|
||||
indexJob = "job"
|
||||
indexNodeID = "node_id"
|
||||
indexAllocID = "alloc_id"
|
||||
indexServiceName = "service_name"
|
||||
indexExpiresGlobal = "expires-global"
|
||||
indexExpiresLocal = "expires-local"
|
||||
indexKeyID = "key_id"
|
||||
indexPath = "path"
|
||||
indexName = "name"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -78,6 +82,7 @@ func init() {
|
|||
variablesTableSchema,
|
||||
variablesQuotasTableSchema,
|
||||
variablesRootKeyMetaSchema,
|
||||
aclRolesTableSchema,
|
||||
}...)
|
||||
}
|
||||
|
||||
|
@ -894,10 +899,60 @@ func aclTokenTableSchema() *memdb.TableSchema {
|
|||
Field: "Global",
|
||||
},
|
||||
},
|
||||
indexExpiresGlobal: {
|
||||
Name: indexExpiresGlobal,
|
||||
AllowMissing: true,
|
||||
Unique: false,
|
||||
Indexer: indexer.SingleIndexer{
|
||||
ReadIndex: indexer.ReadIndex(indexer.IndexFromTimeQuery),
|
||||
WriteIndex: indexer.WriteIndex(indexExpiresGlobalFromACLToken),
|
||||
},
|
||||
},
|
||||
indexExpiresLocal: {
|
||||
Name: indexExpiresLocal,
|
||||
AllowMissing: true,
|
||||
Unique: false,
|
||||
Indexer: indexer.SingleIndexer{
|
||||
ReadIndex: indexer.ReadIndex(indexer.IndexFromTimeQuery),
|
||||
WriteIndex: indexer.WriteIndex(indexExpiresLocalFromACLToken),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func indexExpiresLocalFromACLToken(raw interface{}) ([]byte, error) {
|
||||
return indexExpiresFromACLToken(raw, false)
|
||||
}
|
||||
|
||||
func indexExpiresGlobalFromACLToken(raw interface{}) ([]byte, error) {
|
||||
return indexExpiresFromACLToken(raw, true)
|
||||
}
|
||||
|
||||
// indexExpiresFromACLToken implements the indexer.WriteIndex interface and
|
||||
// allows us to use an ACL tokens ExpirationTime as an index, if it is a
|
||||
// non-default value. This allows for efficient lookups when trying to deal
|
||||
// with removal of expired tokens from state.
|
||||
func indexExpiresFromACLToken(raw interface{}, global bool) ([]byte, error) {
|
||||
p, ok := raw.(*structs.ACLToken)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type %T for structs.ACLToken index", raw)
|
||||
}
|
||||
if p.Global != global {
|
||||
return nil, indexer.ErrMissingValueForIndex
|
||||
}
|
||||
if !p.HasExpirationTime() {
|
||||
return nil, indexer.ErrMissingValueForIndex
|
||||
}
|
||||
if p.ExpirationTime.Unix() < 0 {
|
||||
return nil, fmt.Errorf("token expiration time cannot be before the unix epoch: %s", p.ExpirationTime)
|
||||
}
|
||||
|
||||
var b indexer.IndexBuilder
|
||||
b.Time(*p.ExpirationTime)
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
// oneTimeTokenTableSchema returns the MemDB schema for the tokens table.
|
||||
// This table is used to store one-time tokens for ACL tokens
|
||||
func oneTimeTokenTableSchema() *memdb.TableSchema {
|
||||
|
@ -1406,3 +1461,27 @@ func variablesRootKeyMetaSchema() *memdb.TableSchema {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
func aclRolesTableSchema() *memdb.TableSchema {
|
||||
return &memdb.TableSchema{
|
||||
Name: TableACLRoles,
|
||||
Indexes: map[string]*memdb.IndexSchema{
|
||||
indexID: {
|
||||
Name: indexID,
|
||||
AllowMissing: false,
|
||||
Unique: true,
|
||||
Indexer: &memdb.StringFieldIndex{
|
||||
Field: "ID",
|
||||
},
|
||||
},
|
||||
indexName: {
|
||||
Name: indexName,
|
||||
AllowMissing: false,
|
||||
Unique: true,
|
||||
Indexer: &memdb.StringFieldIndex{
|
||||
Field: "Name",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5674,10 +5674,20 @@ func (s *StateStore) ACLTokenByAccessorID(ws memdb.WatchSet, id string) (*struct
|
|||
}
|
||||
ws.Add(watchCh)
|
||||
|
||||
if existing != nil {
|
||||
return existing.(*structs.ACLToken), nil
|
||||
// If the existing token is nil, this indicates it does not exist in state.
|
||||
if existing == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, nil
|
||||
|
||||
// Assert the token type which allows us to perform additional work on the
|
||||
// token that is needed before returning the call.
|
||||
token := existing.(*structs.ACLToken)
|
||||
|
||||
// Handle potential staleness of ACL role links.
|
||||
if token, err = s.fixTokenRoleLinks(txn, token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// ACLTokenBySecretID is used to lookup a token by secret ID
|
||||
|
@ -5694,10 +5704,20 @@ func (s *StateStore) ACLTokenBySecretID(ws memdb.WatchSet, secretID string) (*st
|
|||
}
|
||||
ws.Add(watchCh)
|
||||
|
||||
if existing != nil {
|
||||
return existing.(*structs.ACLToken), nil
|
||||
// If the existing token is nil, this indicates it does not exist in state.
|
||||
if existing == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, nil
|
||||
|
||||
// Assert the token type which allows us to perform additional work on the
|
||||
// token that is needed before returning the call.
|
||||
token := existing.(*structs.ACLToken)
|
||||
|
||||
// Handle potential staleness of ACL role links.
|
||||
if token, err = s.fixTokenRoleLinks(txn, token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// ACLTokenByAccessorIDPrefix is used to lookup tokens by prefix
|
||||
|
|
|
@ -0,0 +1,340 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-memdb"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// ACLTokensByExpired returns an array accessor IDs of expired ACL tokens.
|
||||
// Their expiration is determined against the passed time.Time value.
|
||||
//
|
||||
// The function handles global and local tokens independently as determined by
|
||||
// the global boolean argument. The number of returned IDs can be limited by
|
||||
// the max integer, which is useful to limit the number of tokens we attempt to
|
||||
// delete in a single transaction.
|
||||
func (s *StateStore) ACLTokensByExpired(global bool) (memdb.ResultIterator, error) {
|
||||
tnx := s.db.ReadTxn()
|
||||
|
||||
iter, err := tnx.Get("acl_token", expiresIndexName(global))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed acl token listing: %v", err)
|
||||
}
|
||||
return iter, nil
|
||||
}
|
||||
|
||||
// expiresIndexName is a helper function to identify the correct ACL token
|
||||
// table expiry index to use.
|
||||
func expiresIndexName(global bool) string {
|
||||
if global {
|
||||
return indexExpiresGlobal
|
||||
}
|
||||
return indexExpiresLocal
|
||||
}
|
||||
|
||||
// UpsertACLRoles is used to insert a number of ACL roles into the state store.
|
||||
// It uses a single write transaction for efficiency, however, any error means
|
||||
// no entries will be committed.
|
||||
func (s *StateStore) UpsertACLRoles(
|
||||
msgType structs.MessageType, index uint64, roles []*structs.ACLRole, allowMissingPolicies bool) error {
|
||||
|
||||
// Grab a write transaction.
|
||||
txn := s.db.WriteTxnMsgT(msgType, index)
|
||||
defer txn.Abort()
|
||||
|
||||
// updated tracks whether any inserts have been made. This allows us to
|
||||
// skip updating the index table if we do not need to.
|
||||
var updated bool
|
||||
|
||||
// Iterate the array of roles. In the event of a single error, all inserts
|
||||
// fail via the txn.Abort() defer.
|
||||
for _, role := range roles {
|
||||
|
||||
roleUpdated, err := s.upsertACLRoleTxn(index, txn, role, allowMissingPolicies)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure we track whether any inserts have been made.
|
||||
updated = updated || roleUpdated
|
||||
}
|
||||
|
||||
// If we did not perform any inserts, exit early.
|
||||
if !updated {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Perform the index table update to mark the new insert.
|
||||
if err := txn.Insert(tableIndex, &IndexEntry{TableACLRoles, index}); err != nil {
|
||||
return fmt.Errorf("index update failed: %v", err)
|
||||
}
|
||||
|
||||
return txn.Commit()
|
||||
}
|
||||
|
||||
// upsertACLRoleTxn inserts a single ACL role into the state store using the
|
||||
// provided write transaction. It is the responsibility of the caller to update
|
||||
// the index table.
|
||||
func (s *StateStore) upsertACLRoleTxn(
|
||||
index uint64, txn *txn, role *structs.ACLRole, allowMissingPolicies bool) (bool, error) {
|
||||
|
||||
// Ensure the role hash is not zero to provide defense in depth. This
|
||||
// should be done outside the state store, so we do not spend time here
|
||||
// and thus Raft, when it, can be avoided.
|
||||
if len(role.Hash) == 0 {
|
||||
role.SetHash()
|
||||
}
|
||||
|
||||
// This validation also happens within the RPC handler, but Raft latency
|
||||
// could mean that by the time the state call is invoked, another Raft
|
||||
// update has deleted policies detailed in role. Therefore, check again
|
||||
// while in our write txn.
|
||||
if !allowMissingPolicies {
|
||||
if err := s.validateACLRolePolicyLinksTxn(txn, role); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// This validation also happens within the RPC handler, but Raft latency
|
||||
// could mean that by the time the state call is invoked, another Raft
|
||||
// update has already written a role with the same name. We therefore need
|
||||
// to check we are not trying to create a role with an existing name.
|
||||
existingRaw, err := txn.First(TableACLRoles, indexName, role.Name)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("ACL role lookup failed: %v", err)
|
||||
}
|
||||
|
||||
// Track our type asserted role, so we only need to do this once.
|
||||
var existing *structs.ACLRole
|
||||
|
||||
// If we did not find an ACL Role within state with the same name, we need
|
||||
// to check using the ID index as the operator might be performing an
|
||||
// update on the role name.
|
||||
//
|
||||
// If we found an entry using the name index, we need to check that the ID
|
||||
// matches the object within the request.
|
||||
if existingRaw == nil {
|
||||
existingRaw, err = txn.First(TableACLRoles, indexID, role.ID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("ACL role lookup failed: %v", err)
|
||||
}
|
||||
if existingRaw != nil {
|
||||
existing = existingRaw.(*structs.ACLRole)
|
||||
}
|
||||
} else {
|
||||
existing = existingRaw.(*structs.ACLRole)
|
||||
if existing.ID != role.ID {
|
||||
return false, fmt.Errorf("ACL role with name %s already exists", role.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Depending on whether this is an initial create, or an update, we need to
|
||||
// check and set certain parameters. The most important is to ensure any
|
||||
// create index is carried over.
|
||||
if existing != nil {
|
||||
|
||||
// If the role already exists, check whether the update contains any
|
||||
// difference. If it doesn't, we can avoid a state update as wel as
|
||||
// updates to any blocking queries.
|
||||
if existing.Equals(role) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
role.CreateIndex = existing.CreateIndex
|
||||
role.ModifyIndex = index
|
||||
} else {
|
||||
role.CreateIndex = index
|
||||
role.ModifyIndex = index
|
||||
}
|
||||
|
||||
// Insert the role into the table.
|
||||
if err := txn.Insert(TableACLRoles, role); err != nil {
|
||||
return false, fmt.Errorf("ACL role insert failed: %v", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// validateACLRolePolicyLinksTxn is the same as ValidateACLRolePolicyLinks but
|
||||
// allows callers to pass their own transaction.
|
||||
func (s *StateStore) validateACLRolePolicyLinksTxn(txn *txn, role *structs.ACLRole) error {
|
||||
for _, policyLink := range role.Policies {
|
||||
_, existing, err := txn.FirstWatch("acl_policy", indexID, policyLink.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ACL policy lookup failed: %v", err)
|
||||
}
|
||||
if existing == nil {
|
||||
return errors.New("ACL policy not found")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteACLRolesByID is responsible for batch deleting ACL roles based on
|
||||
// their ID. It uses a single write transaction for efficiency, however, any
|
||||
// error means no entries will be committed. An error is produced if a role is
|
||||
// not found within state which has been passed within the array.
|
||||
func (s *StateStore) DeleteACLRolesByID(
|
||||
msgType structs.MessageType, index uint64, roleIDs []string) error {
|
||||
|
||||
txn := s.db.WriteTxnMsgT(msgType, index)
|
||||
defer txn.Abort()
|
||||
|
||||
for _, roleID := range roleIDs {
|
||||
|
||||
existing, err := txn.First(TableACLRoles, indexID, roleID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ACL role lookup failed: %v", err)
|
||||
}
|
||||
if existing == nil {
|
||||
return errors.New("ACL role not found")
|
||||
}
|
||||
|
||||
// Delete the existing entry from the table.
|
||||
if err := txn.Delete(TableACLRoles, existing); err != nil {
|
||||
return fmt.Errorf("ACL role deletion failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the index table to indicate an update has occurred.
|
||||
if err := txn.Insert(tableIndex, &IndexEntry{TableACLRoles, index}); err != nil {
|
||||
return fmt.Errorf("index update failed: %v", err)
|
||||
}
|
||||
|
||||
return txn.Commit()
|
||||
}
|
||||
|
||||
// GetACLRoles returns an iterator that contains all ACL roles stored within
|
||||
// state.
|
||||
func (s *StateStore) GetACLRoles(ws memdb.WatchSet) (memdb.ResultIterator, error) {
|
||||
txn := s.db.ReadTxn()
|
||||
|
||||
// Walk the entire table to get all ACL roles.
|
||||
iter, err := txn.Get(TableACLRoles, indexID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ACL role lookup failed: %v", err)
|
||||
}
|
||||
ws.Add(iter.WatchCh())
|
||||
|
||||
return iter, nil
|
||||
}
|
||||
|
||||
// GetACLRoleByID returns a single ACL role specified by the input ID. The role
|
||||
// object will be nil, if no matching entry was found; it is the responsibility
|
||||
// of the caller to check for this.
|
||||
func (s *StateStore) GetACLRoleByID(ws memdb.WatchSet, roleID string) (*structs.ACLRole, error) {
|
||||
txn := s.db.ReadTxn()
|
||||
return s.getACLRoleByIDTxn(txn, ws, roleID)
|
||||
}
|
||||
|
||||
// getACLRoleByIDTxn allows callers to pass a read transaction in order to read
|
||||
// a single ACL role specified by the input ID. The role object will be nil, if
|
||||
// no matching entry was found; it is the responsibility of the caller to check
|
||||
// for this.
|
||||
func (s *StateStore) getACLRoleByIDTxn(txn ReadTxn, ws memdb.WatchSet, roleID string) (*structs.ACLRole, error) {
|
||||
|
||||
// Perform the ACL role lookup using the "id" index.
|
||||
watchCh, existing, err := txn.FirstWatch(TableACLRoles, indexID, roleID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ACL role lookup failed: %v", err)
|
||||
}
|
||||
ws.Add(watchCh)
|
||||
|
||||
if existing != nil {
|
||||
return existing.(*structs.ACLRole), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetACLRoleByName returns a single ACL role specified by the input name. The
|
||||
// role object will be nil, if no matching entry was found; it is the
|
||||
// responsibility of the caller to check for this.
|
||||
func (s *StateStore) GetACLRoleByName(ws memdb.WatchSet, roleName string) (*structs.ACLRole, error) {
|
||||
txn := s.db.ReadTxn()
|
||||
|
||||
// Perform the ACL role lookup using the "name" index.
|
||||
watchCh, existing, err := txn.FirstWatch(TableACLRoles, indexName, roleName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ACL role lookup failed: %v", err)
|
||||
}
|
||||
ws.Add(watchCh)
|
||||
|
||||
if existing != nil {
|
||||
return existing.(*structs.ACLRole), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetACLRoleByIDPrefix is used to lookup ACL policies using a prefix to match
|
||||
// on the ID.
|
||||
func (s *StateStore) GetACLRoleByIDPrefix(ws memdb.WatchSet, idPrefix string) (memdb.ResultIterator, error) {
|
||||
txn := s.db.ReadTxn()
|
||||
|
||||
iter, err := txn.Get(TableACLRoles, indexID+"_prefix", idPrefix)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ACL role lookup failed: %v", err)
|
||||
}
|
||||
ws.Add(iter.WatchCh())
|
||||
|
||||
return iter, nil
|
||||
}
|
||||
|
||||
// fixTokenRoleLinks is a state helper that ensures the returned ACL token has
|
||||
// an accurate representation of ACL role links. The role links could have
|
||||
// become stale when a linked role was deleted or renamed. This will correct
|
||||
// them and generates a newly allocated token only when fixes are needed. If
|
||||
// the role links are still accurate, we just return the original token.
|
||||
func (s *StateStore) fixTokenRoleLinks(txn ReadTxn, original *structs.ACLToken) (*structs.ACLToken, error) {
|
||||
|
||||
// Track whether we have made an initial copy to ensure we are not
|
||||
// operating on the token directly from state.
|
||||
copied := false
|
||||
|
||||
token := original
|
||||
|
||||
// copyTokenFn is a helper function which copies the ACL token along with
|
||||
// a certain number of ACL role links.
|
||||
copyTokenFn := func(t *structs.ACLToken, numLinks int) *structs.ACLToken {
|
||||
clone := t.Copy()
|
||||
clone.Roles = slices.Clone(t.Roles[:numLinks])
|
||||
return clone
|
||||
}
|
||||
|
||||
for linkIndex, link := range original.Roles {
|
||||
|
||||
// This should never happen, but guard against it anyway, so we log an
|
||||
// error rather than panic.
|
||||
if link.ID == "" {
|
||||
return nil, errors.New("detected corrupted token within the state store: missing role link ID")
|
||||
}
|
||||
|
||||
role, err := s.getACLRoleByIDTxn(txn, nil, link.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if role == nil {
|
||||
if !copied {
|
||||
// clone the token as we cannot touch the original
|
||||
token = copyTokenFn(original, linkIndex)
|
||||
copied = true
|
||||
}
|
||||
// if already owned then we just don't append it.
|
||||
} else if role.Name != link.Name {
|
||||
if !copied {
|
||||
token = copyTokenFn(original, linkIndex)
|
||||
copied = true
|
||||
}
|
||||
|
||||
// append the corrected policy
|
||||
token.Roles = append(token.Roles, &structs.ACLTokenRoleLink{ID: link.ID, Name: role.Name})
|
||||
|
||||
} else if copied {
|
||||
token.Roles = append(token.Roles, link)
|
||||
}
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
|
@ -0,0 +1,643 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-memdb"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStateStore_ACLTokensByExpired(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
testState := testStateStore(t)
|
||||
|
||||
// This function provides an easy way to get all tokens out of the
|
||||
// iterator.
|
||||
fromIteratorFunc := func(iter memdb.ResultIterator) []*structs.ACLToken {
|
||||
var tokens []*structs.ACLToken
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
tokens = append(tokens, raw.(*structs.ACLToken))
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
// This time is the threshold for all expiry calls to be based on. All
|
||||
// tokens with expiry can use this as their base and use Add().
|
||||
expiryTimeThreshold := time.Date(2022, time.April, 27, 14, 50, 0, 0, time.UTC)
|
||||
|
||||
// Generate two tokens without an expiry time. These tokens should never
|
||||
// show up in calls to ACLTokensByExpired.
|
||||
neverExpireLocalToken := mock.ACLToken()
|
||||
neverExpireGlobalToken := mock.ACLToken()
|
||||
neverExpireLocalToken.Global = true
|
||||
|
||||
// Upsert the tokens into state and perform a global and local read of
|
||||
// the state.
|
||||
err := testState.UpsertACLTokens(structs.MsgTypeTestSetup, 10, []*structs.ACLToken{
|
||||
neverExpireLocalToken, neverExpireGlobalToken})
|
||||
require.NoError(t, err)
|
||||
|
||||
iter, err := testState.ACLTokensByExpired(true)
|
||||
require.NoError(t, err)
|
||||
tokens := fromIteratorFunc(iter)
|
||||
require.Len(t, tokens, 0)
|
||||
|
||||
iter, err = testState.ACLTokensByExpired(false)
|
||||
require.NoError(t, err)
|
||||
tokens = fromIteratorFunc(iter)
|
||||
require.Len(t, tokens, 0)
|
||||
|
||||
// Generate, upsert, and test an expired local token. This token expired
|
||||
// long ago and therefore before all others coming in the tests. It should
|
||||
// therefore always be the first out.
|
||||
expiredLocalToken := mock.ACLToken()
|
||||
expiredLocalToken.ExpirationTime = pointer.Of(expiryTimeThreshold.Add(-48 * time.Hour))
|
||||
|
||||
err = testState.UpsertACLTokens(structs.MsgTypeTestSetup, 20, []*structs.ACLToken{expiredLocalToken})
|
||||
require.NoError(t, err)
|
||||
|
||||
iter, err = testState.ACLTokensByExpired(false)
|
||||
require.NoError(t, err)
|
||||
tokens = fromIteratorFunc(iter)
|
||||
require.Len(t, tokens, 1)
|
||||
require.Equal(t, expiredLocalToken.AccessorID, tokens[0].AccessorID)
|
||||
|
||||
// Generate, upsert, and test an expired global token. This token expired
|
||||
// long ago and therefore before all others coming in the tests. It should
|
||||
// therefore always be the first out.
|
||||
expiredGlobalToken := mock.ACLToken()
|
||||
expiredGlobalToken.Global = true
|
||||
expiredGlobalToken.ExpirationTime = pointer.Of(expiryTimeThreshold.Add(-48 * time.Hour))
|
||||
|
||||
err = testState.UpsertACLTokens(structs.MsgTypeTestSetup, 30, []*structs.ACLToken{expiredGlobalToken})
|
||||
require.NoError(t, err)
|
||||
|
||||
iter, err = testState.ACLTokensByExpired(true)
|
||||
require.NoError(t, err)
|
||||
tokens = fromIteratorFunc(iter)
|
||||
require.Len(t, tokens, 1)
|
||||
require.Equal(t, expiredGlobalToken.AccessorID, tokens[0].AccessorID)
|
||||
|
||||
// This test function allows us to run the same test for local and global
|
||||
// tokens.
|
||||
testFn := func(oldToken *structs.ACLToken, global bool) {
|
||||
|
||||
// Track all the expected expired ACL tokens, including the long
|
||||
// expired token.
|
||||
var expiredTokens []*structs.ACLToken
|
||||
expiredTokens = append(expiredTokens, oldToken)
|
||||
|
||||
// Generate and upsert a number of mixed expired, non-expired tokens.
|
||||
mixedTokens := make([]*structs.ACLToken, 20)
|
||||
for i := 0; i < 20; i++ {
|
||||
mockedToken := mock.ACLToken()
|
||||
mockedToken.Global = global
|
||||
if i%2 == 0 {
|
||||
expiredTokens = append(expiredTokens, mockedToken)
|
||||
mockedToken.ExpirationTime = pointer.Of(expiryTimeThreshold.Add(-24 * time.Hour))
|
||||
} else {
|
||||
mockedToken.ExpirationTime = pointer.Of(expiryTimeThreshold.Add(24 * time.Hour))
|
||||
}
|
||||
mixedTokens[i] = mockedToken
|
||||
}
|
||||
|
||||
err = testState.UpsertACLTokens(structs.MsgTypeTestSetup, 40, mixedTokens)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check the full listing works as expected as the first 11 elements
|
||||
// should all be our expired tokens. Ensure our oldest expired token is
|
||||
// first in the list.
|
||||
iter, err = testState.ACLTokensByExpired(global)
|
||||
require.NoError(t, err)
|
||||
tokens = fromIteratorFunc(iter)
|
||||
require.ElementsMatch(t, expiredTokens, tokens[:11])
|
||||
require.Equal(t, tokens[0], oldToken)
|
||||
}
|
||||
|
||||
testFn(expiredLocalToken, false)
|
||||
testFn(expiredGlobalToken, true)
|
||||
}
|
||||
|
||||
func Test_expiresIndexName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
globalInput bool
|
||||
expectedOutput string
|
||||
name string
|
||||
}{
|
||||
{
|
||||
globalInput: false,
|
||||
expectedOutput: indexExpiresLocal,
|
||||
name: "local",
|
||||
},
|
||||
{
|
||||
globalInput: true,
|
||||
expectedOutput: indexExpiresGlobal,
|
||||
name: "global",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualOutput := expiresIndexName(tc.globalInput)
|
||||
require.Equal(t, tc.expectedOutput, actualOutput)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateStore_UpsertACLRoles(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
testState := testStateStore(t)
|
||||
|
||||
// Generate a mocked ACL role for testing and attempt to upsert this
|
||||
// straight into state. It should fail because the ACL policies do not
|
||||
// exist.
|
||||
mockedACLRoles := []*structs.ACLRole{mock.ACLRole()}
|
||||
err := testState.UpsertACLRoles(structs.MsgTypeTestSetup, 10, mockedACLRoles, false)
|
||||
require.ErrorContains(t, err, "policy not found")
|
||||
|
||||
// Create the policies our ACL roles wants to link to and then try the
|
||||
// upsert again.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, testState.UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
require.NoError(t, testState.UpsertACLRoles(structs.MsgTypeTestSetup, 20, mockedACLRoles, false))
|
||||
|
||||
// Check that the index for the table was modified as expected.
|
||||
initialIndex, err := testState.Index(TableACLRoles)
|
||||
require.NoError(t, err)
|
||||
must.Eq(t, 20, initialIndex)
|
||||
|
||||
// List all the ACL roles in the table, so we can perform a number of tests
|
||||
// on the return array.
|
||||
ws := memdb.NewWatchSet()
|
||||
iter, err := testState.GetACLRoles(ws)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Count how many table entries we have, to ensure it is the expected
|
||||
// number.
|
||||
var count int
|
||||
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
count++
|
||||
|
||||
// Ensure the create and modify indexes are populated correctly.
|
||||
aclRole := raw.(*structs.ACLRole)
|
||||
must.Eq(t, 20, aclRole.CreateIndex)
|
||||
must.Eq(t, 20, aclRole.ModifyIndex)
|
||||
}
|
||||
require.Equal(t, 1, count, "incorrect number of ACL roles found")
|
||||
|
||||
// Try writing the same ACL roles to state which should not result in an
|
||||
// update to the table index.
|
||||
require.NoError(t, testState.UpsertACLRoles(structs.MsgTypeTestSetup, 30, mockedACLRoles, false))
|
||||
reInsertActualIndex, err := testState.Index(TableACLRoles)
|
||||
require.NoError(t, err)
|
||||
must.Eq(t, 20, reInsertActualIndex)
|
||||
|
||||
// Make a change to one of the ACL roles and ensure this update is accepted
|
||||
// and the table index is updated.
|
||||
updatedMockedACLRole := mockedACLRoles[0].Copy()
|
||||
updatedMockedACLRole.Policies = []*structs.ACLRolePolicyLink{{Name: "mocked-test-policy-1"}}
|
||||
updatedMockedACLRole.SetHash()
|
||||
require.NoError(t, testState.UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 30, []*structs.ACLRole{updatedMockedACLRole}, false))
|
||||
|
||||
// Check that the index for the table was modified as expected.
|
||||
updatedIndex, err := testState.Index(TableACLRoles)
|
||||
require.NoError(t, err)
|
||||
must.Eq(t, 30, updatedIndex)
|
||||
|
||||
// List the ACL roles in state.
|
||||
iter, err = testState.GetACLRoles(ws)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Count how many table entries we have, to ensure it is the expected
|
||||
// number.
|
||||
count = 0
|
||||
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
count++
|
||||
|
||||
// Ensure the create and modify indexes are populated correctly.
|
||||
aclRole := raw.(*structs.ACLRole)
|
||||
must.Eq(t, 20, aclRole.CreateIndex)
|
||||
must.Eq(t, 30, aclRole.ModifyIndex)
|
||||
}
|
||||
require.Equal(t, 1, count, "incorrect number of ACL roles found")
|
||||
|
||||
// Now try inserting an ACL role using the missing policies' argument to
|
||||
// simulate replication.
|
||||
replicatedACLRole := mock.ACLRole()
|
||||
replicatedACLRole.Policies = []*structs.ACLRolePolicyLink{{Name: "nope"}}
|
||||
require.NoError(t, testState.UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 40, []*structs.ACLRole{replicatedACLRole}, true))
|
||||
|
||||
replicatedACLRoleResp, err := testState.GetACLRoleByName(ws, replicatedACLRole.Name)
|
||||
require.NoError(t, err)
|
||||
must.Eq(t, replicatedACLRole.Hash, replicatedACLRoleResp.Hash)
|
||||
|
||||
// Try adding a new ACL role, which has a name clash with an existing
|
||||
// entry.
|
||||
dupRoleName := mock.ACLRole()
|
||||
dupRoleName.Name = mockedACLRoles[0].Name
|
||||
|
||||
err = testState.UpsertACLRoles(structs.MsgTypeTestSetup, 50,
|
||||
[]*structs.ACLRole{dupRoleName}, false)
|
||||
require.ErrorContains(t, err, fmt.Sprintf("ACL role with name %s already exists", dupRoleName.Name))
|
||||
}
|
||||
|
||||
func TestStateStore_ValidateACLRolePolicyLinks(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
testState := testStateStore(t)
|
||||
|
||||
// Create our mocked role which includes two ACL policy links.
|
||||
mockedACLRoles := []*structs.ACLRole{mock.ACLRole()}
|
||||
|
||||
// This should error as no policies exist within state.
|
||||
err := testState.UpsertACLRoles(structs.MsgTypeTestSetup, 10, mockedACLRoles, false)
|
||||
require.ErrorContains(t, err, "ACL policy not found")
|
||||
|
||||
// Upsert one ACL policy and retry the role which should still fail.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
|
||||
require.NoError(t, testState.UpsertACLPolicies(structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1}))
|
||||
err = testState.UpsertACLRoles(structs.MsgTypeTestSetup, 20, mockedACLRoles, false)
|
||||
require.ErrorContains(t, err, "ACL policy not found")
|
||||
|
||||
// Upsert the second ACL policy. The ACL role should now upsert into state
|
||||
// without error.
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, testState.UpsertACLPolicies(structs.MsgTypeTestSetup, 20, []*structs.ACLPolicy{policy2}))
|
||||
require.NoError(t, testState.UpsertACLRoles(structs.MsgTypeTestSetup, 30, mockedACLRoles, false))
|
||||
}
|
||||
|
||||
func TestStateStore_DeleteACLRolesByID(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
testState := testStateStore(t)
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, testState.UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Generate a some mocked ACL roles for testing and upsert these straight
|
||||
// into state.
|
||||
mockedACLRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
require.NoError(t, testState.UpsertACLRoles(structs.MsgTypeTestSetup, 10, mockedACLRoles, false))
|
||||
|
||||
// Try and delete a role using a name that doesn't exist. This should
|
||||
// return an error and not change the index for the table.
|
||||
err := testState.DeleteACLRolesByID(structs.MsgTypeTestSetup, 20, []string{"not-a-role"})
|
||||
require.ErrorContains(t, err, "ACL role not found")
|
||||
|
||||
tableIndex, err := testState.Index(TableACLRoles)
|
||||
require.NoError(t, err)
|
||||
must.Eq(t, 10, tableIndex)
|
||||
|
||||
// Delete one of the previously upserted ACL roles. This should succeed
|
||||
// and modify the table index.
|
||||
err = testState.DeleteACLRolesByID(structs.MsgTypeTestSetup, 20, []string{mockedACLRoles[0].ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
tableIndex, err = testState.Index(TableACLRoles)
|
||||
require.NoError(t, err)
|
||||
must.Eq(t, 20, tableIndex)
|
||||
|
||||
// List the ACL roles and ensure we now only have one present and that it
|
||||
// is the one we expect.
|
||||
ws := memdb.NewWatchSet()
|
||||
iter, err := testState.GetACLRoles(ws)
|
||||
require.NoError(t, err)
|
||||
|
||||
var aclRoles []*structs.ACLRole
|
||||
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
aclRoles = append(aclRoles, raw.(*structs.ACLRole))
|
||||
}
|
||||
|
||||
require.Len(t, aclRoles, 1, "incorrect number of ACL roles found")
|
||||
require.True(t, aclRoles[0].Equals(mockedACLRoles[1]))
|
||||
|
||||
// Delete the final remaining ACL role. This should succeed and modify the
|
||||
// table index.
|
||||
err = testState.DeleteACLRolesByID(structs.MsgTypeTestSetup, 30, []string{mockedACLRoles[1].ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
tableIndex, err = testState.Index(TableACLRoles)
|
||||
require.NoError(t, err)
|
||||
must.Eq(t, 30, tableIndex)
|
||||
|
||||
// List the ACL roles and ensure we have zero entries.
|
||||
iter, err = testState.GetACLRoles(ws)
|
||||
require.NoError(t, err)
|
||||
|
||||
aclRoles = []*structs.ACLRole{}
|
||||
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
aclRoles = append(aclRoles, raw.(*structs.ACLRole))
|
||||
}
|
||||
require.Len(t, aclRoles, 0, "incorrect number of ACL roles found")
|
||||
}
|
||||
|
||||
func TestStateStore_GetACLRoles(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
testState := testStateStore(t)
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, testState.UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Generate a some mocked ACL roles for testing and upsert these straight
|
||||
// into state.
|
||||
mockedACLRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
require.NoError(t, testState.UpsertACLRoles(structs.MsgTypeTestSetup, 10, mockedACLRoles, false))
|
||||
|
||||
// List the ACL roles and ensure they are exactly as we expect.
|
||||
ws := memdb.NewWatchSet()
|
||||
iter, err := testState.GetACLRoles(ws)
|
||||
require.NoError(t, err)
|
||||
|
||||
var aclRoles []*structs.ACLRole
|
||||
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
aclRoles = append(aclRoles, raw.(*structs.ACLRole))
|
||||
}
|
||||
|
||||
expected := mockedACLRoles
|
||||
for i := range expected {
|
||||
expected[i].CreateIndex = 10
|
||||
expected[i].ModifyIndex = 10
|
||||
}
|
||||
|
||||
require.ElementsMatch(t, aclRoles, expected)
|
||||
}
|
||||
|
||||
func TestStateStore_GetACLRoleByID(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
testState := testStateStore(t)
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, testState.UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Generate a some mocked ACL roles for testing and upsert these straight
|
||||
// into state.
|
||||
mockedACLRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
require.NoError(t, testState.UpsertACLRoles(structs.MsgTypeTestSetup, 10, mockedACLRoles, false))
|
||||
|
||||
ws := memdb.NewWatchSet()
|
||||
|
||||
// Try reading an ACL role that does not exist.
|
||||
aclRole, err := testState.GetACLRoleByID(ws, "not-a-role")
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, aclRole)
|
||||
|
||||
// Read the two ACL roles that we should find.
|
||||
aclRole, err = testState.GetACLRoleByID(ws, mockedACLRoles[0].ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, mockedACLRoles[0], aclRole)
|
||||
|
||||
aclRole, err = testState.GetACLRoleByID(ws, mockedACLRoles[1].ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, mockedACLRoles[1], aclRole)
|
||||
}
|
||||
|
||||
func TestStateStore_GetACLRoleByName(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
testState := testStateStore(t)
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, testState.UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Generate a some mocked ACL roles for testing and upsert these straight
|
||||
// into state.
|
||||
mockedACLRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
require.NoError(t, testState.UpsertACLRoles(structs.MsgTypeTestSetup, 10, mockedACLRoles, false))
|
||||
|
||||
ws := memdb.NewWatchSet()
|
||||
|
||||
// Try reading an ACL role that does not exist.
|
||||
aclRole, err := testState.GetACLRoleByName(ws, "not-a-role")
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, aclRole)
|
||||
|
||||
// Read the two ACL roles that we should find.
|
||||
aclRole, err = testState.GetACLRoleByName(ws, mockedACLRoles[0].Name)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, mockedACLRoles[0], aclRole)
|
||||
|
||||
aclRole, err = testState.GetACLRoleByName(ws, mockedACLRoles[1].Name)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, mockedACLRoles[1], aclRole)
|
||||
}
|
||||
|
||||
func TestStateStore_GetACLRoleByIDPrefix(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
testState := testStateStore(t)
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, testState.UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Generate a some mocked ACL roles for testing and upsert these straight
|
||||
// into state. Set the ID to something with a prefix we know so it is easy
|
||||
// to test.
|
||||
mockedACLRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
mockedACLRoles[0].ID = "test-prefix-" + uuid.Generate()
|
||||
mockedACLRoles[1].ID = "test-prefix-" + uuid.Generate()
|
||||
require.NoError(t, testState.UpsertACLRoles(structs.MsgTypeTestSetup, 10, mockedACLRoles, false))
|
||||
|
||||
ws := memdb.NewWatchSet()
|
||||
|
||||
// Try using a prefix that doesn't match any entries.
|
||||
iter, err := testState.GetACLRoleByIDPrefix(ws, "nope")
|
||||
require.NoError(t, err)
|
||||
|
||||
var aclRoles []*structs.ACLRole
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
aclRoles = append(aclRoles, raw.(*structs.ACLRole))
|
||||
}
|
||||
require.Len(t, aclRoles, 0)
|
||||
|
||||
// Use a prefix which should match two entries in state.
|
||||
iter, err = testState.GetACLRoleByIDPrefix(ws, "test-prefix-")
|
||||
require.NoError(t, err)
|
||||
|
||||
aclRoles = []*structs.ACLRole{}
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
aclRoles = append(aclRoles, raw.(*structs.ACLRole))
|
||||
}
|
||||
require.Len(t, aclRoles, 2)
|
||||
}
|
||||
|
||||
func TestStateStore_fixTokenRoleLinks(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
testFn func()
|
||||
}{
|
||||
{
|
||||
name: "no fix needed",
|
||||
testFn: func() {
|
||||
testState := testStateStore(t)
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, testState.UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Generate a some mocked ACL roles for testing and upsert these straight
|
||||
// into state.
|
||||
mockedACLRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
require.NoError(t, testState.UpsertACLRoles(structs.MsgTypeTestSetup, 20, mockedACLRoles, false))
|
||||
|
||||
// Create an ACL token linking to the ACL role.
|
||||
token1 := mock.ACLToken()
|
||||
token1.Roles = []*structs.ACLTokenRoleLink{{ID: mockedACLRoles[0].ID}}
|
||||
require.NoError(t, testState.UpsertACLTokens(
|
||||
structs.MsgTypeTestSetup, 20, []*structs.ACLToken{token1}))
|
||||
|
||||
// Perform the fix and check the returned token contains the
|
||||
// correct roles.
|
||||
readTxn := testState.db.ReadTxn()
|
||||
outputToken, err := testState.fixTokenRoleLinks(readTxn, token1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, outputToken.Roles, []*structs.ACLTokenRoleLink{{
|
||||
Name: mockedACLRoles[0].Name, ID: mockedACLRoles[0].ID,
|
||||
}})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "acl role from link deleted",
|
||||
testFn: func() {
|
||||
testState := testStateStore(t)
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, testState.UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Generate a some mocked ACL roles for testing and upsert these straight
|
||||
// into state.
|
||||
mockedACLRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
require.NoError(t, testState.UpsertACLRoles(structs.MsgTypeTestSetup, 20, mockedACLRoles, false))
|
||||
|
||||
// Create an ACL token linking to the ACL roles.
|
||||
token1 := mock.ACLToken()
|
||||
token1.Roles = []*structs.ACLTokenRoleLink{{ID: mockedACLRoles[0].ID}, {ID: mockedACLRoles[1].ID}}
|
||||
require.NoError(t, testState.UpsertACLTokens(
|
||||
structs.MsgTypeTestSetup, 30, []*structs.ACLToken{token1}))
|
||||
|
||||
// Now delete one of the ACL roles from state.
|
||||
require.NoError(t, testState.DeleteACLRolesByID(
|
||||
structs.MsgTypeTestSetup, 40, []string{mockedACLRoles[0].ID}))
|
||||
|
||||
// Perform the fix and check the returned token contains the
|
||||
// correct roles.
|
||||
readTxn := testState.db.ReadTxn()
|
||||
outputToken, err := testState.fixTokenRoleLinks(readTxn, token1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, outputToken.Roles, 1)
|
||||
require.Equal(t, outputToken.Roles, []*structs.ACLTokenRoleLink{{
|
||||
Name: mockedACLRoles[1].Name, ID: mockedACLRoles[1].ID,
|
||||
}})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "acl role from link name changed",
|
||||
testFn: func() {
|
||||
testState := testStateStore(t)
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, testState.UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Generate a some mocked ACL roles for testing and upsert these straight
|
||||
// into state.
|
||||
mockedACLRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
require.NoError(t, testState.UpsertACLRoles(structs.MsgTypeTestSetup, 20, mockedACLRoles, false))
|
||||
|
||||
// Create an ACL token linking to the ACL roles.
|
||||
token1 := mock.ACLToken()
|
||||
token1.Roles = []*structs.ACLTokenRoleLink{{ID: mockedACLRoles[0].ID}, {ID: mockedACLRoles[1].ID}}
|
||||
require.NoError(t, testState.UpsertACLTokens(
|
||||
structs.MsgTypeTestSetup, 30, []*structs.ACLToken{token1}))
|
||||
|
||||
// Now change the name of one of the ACL roles.
|
||||
mockedACLRoles[0].Name = "badger-badger-badger"
|
||||
require.NoError(t, testState.UpsertACLRoles(structs.MsgTypeTestSetup, 40, mockedACLRoles, false))
|
||||
|
||||
// Perform the fix and check the returned token contains the
|
||||
// correct roles.
|
||||
readTxn := testState.db.ReadTxn()
|
||||
outputToken, err := testState.fixTokenRoleLinks(readTxn, token1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, outputToken.Roles, 2)
|
||||
require.ElementsMatch(t, outputToken.Roles, []*structs.ACLTokenRoleLink{
|
||||
{Name: mockedACLRoles[0].Name, ID: mockedACLRoles[0].ID},
|
||||
{Name: mockedACLRoles[1].Name, ID: mockedACLRoles[1].ID},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.testFn()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -224,3 +224,12 @@ func (r *StateRestore) RootKeyMetaRestore(quota *structs.RootKeyMeta) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ACLRoleRestore is used to restore a single ACL role into the acl_roles
|
||||
// table.
|
||||
func (r *StateRestore) ACLRoleRestore(aclRole *structs.ACLRole) error {
|
||||
if err := r.txn.Insert(TableACLRoles, aclRole); err != nil {
|
||||
return fmt.Errorf("ACL role insert failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -604,3 +604,26 @@ func TestStateStore_VariablesRestore(t *testing.T) {
|
|||
require.Equal(t, svs[i], out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateStore_ACLRoleRestore(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
testState := testStateStore(t)
|
||||
|
||||
// Set up our test registrations and index.
|
||||
expectedIndex := uint64(13)
|
||||
aclRole := mock.ACLRole()
|
||||
aclRole.CreateIndex = expectedIndex
|
||||
aclRole.ModifyIndex = expectedIndex
|
||||
|
||||
restore, err := testState.Restore()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, restore.ACLRoleRestore(aclRole))
|
||||
require.NoError(t, restore.Commit())
|
||||
|
||||
// Check the state is now populated as we expect and that we can find the
|
||||
// restored registrations.
|
||||
ws := memdb.NewWatchSet()
|
||||
out, err := testState.GetACLRoleByName(ws, aclRole.Name)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, aclRole, out)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,500 @@
|
|||
package structs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"golang.org/x/crypto/blake2b"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
// ACLUpsertPoliciesRPCMethod is the RPC method for batch creating or
|
||||
// modifying ACL policies.
|
||||
//
|
||||
// Args: ACLPolicyUpsertRequest
|
||||
// Reply: GenericResponse
|
||||
ACLUpsertPoliciesRPCMethod = "ACL.UpsertPolicies"
|
||||
|
||||
// ACLUpsertTokensRPCMethod is the RPC method for batch creating or
|
||||
// modifying ACL tokens.
|
||||
//
|
||||
// Args: ACLTokenUpsertRequest
|
||||
// Reply: ACLTokenUpsertResponse
|
||||
ACLUpsertTokensRPCMethod = "ACL.UpsertTokens"
|
||||
|
||||
// ACLDeleteTokensRPCMethod is the RPC method for batch deleting ACL
|
||||
// tokens.
|
||||
//
|
||||
// Args: ACLTokenDeleteRequest
|
||||
// Reply: GenericResponse
|
||||
ACLDeleteTokensRPCMethod = "ACL.DeleteTokens"
|
||||
|
||||
// ACLUpsertRolesRPCMethod is the RPC method for batch creating or
|
||||
// modifying ACL roles.
|
||||
//
|
||||
// Args: ACLRolesUpsertRequest
|
||||
// Reply: ACLRolesUpsertResponse
|
||||
ACLUpsertRolesRPCMethod = "ACL.UpsertRoles"
|
||||
|
||||
// ACLDeleteRolesByIDRPCMethod the RPC method for batch deleting ACL
|
||||
// roles by their ID.
|
||||
//
|
||||
// Args: ACLRolesDeleteByIDRequest
|
||||
// Reply: ACLRolesDeleteByIDResponse
|
||||
ACLDeleteRolesByIDRPCMethod = "ACL.DeleteRolesByID"
|
||||
|
||||
// ACLListRolesRPCMethod is the RPC method for listing ACL roles.
|
||||
//
|
||||
// Args: ACLRolesListRequest
|
||||
// Reply: ACLRolesListResponse
|
||||
ACLListRolesRPCMethod = "ACL.ListRoles"
|
||||
|
||||
// ACLGetRolesByIDRPCMethod is the RPC method for detailing a number of ACL
|
||||
// roles using their ID. This is an internal only RPC endpoint and used by
|
||||
// the ACL Role replication process.
|
||||
//
|
||||
// Args: ACLRolesByIDRequest
|
||||
// Reply: ACLRolesByIDResponse
|
||||
ACLGetRolesByIDRPCMethod = "ACL.GetRolesByID"
|
||||
|
||||
// ACLGetRoleByIDRPCMethod is the RPC method for detailing an individual
|
||||
// ACL role using its ID.
|
||||
//
|
||||
// Args: ACLRoleByIDRequest
|
||||
// Reply: ACLRoleByIDResponse
|
||||
ACLGetRoleByIDRPCMethod = "ACL.GetRoleByID"
|
||||
|
||||
// ACLGetRoleByNameRPCMethod is the RPC method for detailing an individual
|
||||
// ACL role using its name.
|
||||
//
|
||||
// Args: ACLRoleByNameRequest
|
||||
// Reply: ACLRoleByNameResponse
|
||||
ACLGetRoleByNameRPCMethod = "ACL.GetRoleByName"
|
||||
)
|
||||
|
||||
const (
|
||||
// ACLMaxExpiredBatchSize is the maximum number of expired ACL tokens that
|
||||
// will be garbage collected in a single trigger. This number helps limit
|
||||
// the replication pressure due to expired token deletion. If there are a
|
||||
// large number of expired tokens pending garbage collection, this value is
|
||||
// a potential limiting factor.
|
||||
ACLMaxExpiredBatchSize = 4096
|
||||
|
||||
// maxACLRoleDescriptionLength limits an ACL roles description length.
|
||||
maxACLRoleDescriptionLength = 256
|
||||
)
|
||||
|
||||
var (
|
||||
// validACLRoleName is used to validate an ACL role name.
|
||||
validACLRoleName = regexp.MustCompile("^[a-zA-Z0-9-]{1,128}$")
|
||||
)
|
||||
|
||||
// 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. This field is always resolved to the
|
||||
// ID and discarded before the token is stored in state. This is because
|
||||
// operators can change the name of an ACL role.
|
||||
Name string
|
||||
}
|
||||
|
||||
// Canonicalize performs basic canonicalization on the ACL token object. It is
|
||||
// important for callers to understand certain fields such as AccessorID are
|
||||
// set if it is empty, so copies should be taken if needed before calling this
|
||||
// function.
|
||||
func (a *ACLToken) Canonicalize() {
|
||||
|
||||
// If the accessor ID is empty, it means this is creation of a new token,
|
||||
// therefore we need to generate base information.
|
||||
if a.AccessorID == "" {
|
||||
|
||||
a.AccessorID = uuid.Generate()
|
||||
a.SecretID = uuid.Generate()
|
||||
a.CreateTime = time.Now().UTC()
|
||||
|
||||
// If the user has not set the expiration time, but has provided a TTL, we
|
||||
// calculate and populate the former filed.
|
||||
if a.ExpirationTime == nil && a.ExpirationTTL != 0 {
|
||||
a.ExpirationTime = pointer.Of(a.CreateTime.Add(a.ExpirationTTL))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate is used to check a token for reasonableness
|
||||
func (a *ACLToken) Validate(minTTL, maxTTL time.Duration, existing *ACLToken) error {
|
||||
var mErr multierror.Error
|
||||
|
||||
// The human friendly name of an ACL token cannot exceed 256 characters.
|
||||
if len(a.Name) > maxTokenNameLength {
|
||||
mErr.Errors = append(mErr.Errors, errors.New("token name too long"))
|
||||
}
|
||||
|
||||
// The type of an ACL token must be set. An ACL token of type client must
|
||||
// have associated policies or roles, whereas a management token cannot be
|
||||
// associated with policies.
|
||||
switch a.Type {
|
||||
case ACLClientToken:
|
||||
if len(a.Policies) == 0 && len(a.Roles) == 0 {
|
||||
mErr.Errors = append(mErr.Errors, errors.New("client token missing policies or roles"))
|
||||
}
|
||||
case ACLManagementToken:
|
||||
if len(a.Policies) != 0 || len(a.Roles) != 0 {
|
||||
mErr.Errors = append(mErr.Errors, errors.New("management token cannot be associated with policies or roles"))
|
||||
}
|
||||
default:
|
||||
mErr.Errors = append(mErr.Errors, errors.New("token type must be client or management"))
|
||||
}
|
||||
|
||||
// There are different validation rules depending on whether the ACL token
|
||||
// is being created or updated.
|
||||
switch existing {
|
||||
case nil:
|
||||
if a.ExpirationTTL < 0 {
|
||||
mErr.Errors = append(mErr.Errors,
|
||||
fmt.Errorf("token expiration TTL '%s' should not be negative", a.ExpirationTTL))
|
||||
}
|
||||
|
||||
if a.ExpirationTime != nil && !a.ExpirationTime.IsZero() {
|
||||
|
||||
if a.CreateTime.After(*a.ExpirationTime) {
|
||||
mErr.Errors = append(mErr.Errors, errors.New("expiration time cannot be before create time"))
|
||||
}
|
||||
|
||||
// Create a time duration which details the time-til-expiry, so we can
|
||||
// check this against the regions max and min values.
|
||||
expiresIn := a.ExpirationTime.Sub(a.CreateTime)
|
||||
if expiresIn > maxTTL {
|
||||
mErr.Errors = append(mErr.Errors,
|
||||
fmt.Errorf("expiration time cannot be more than %s in the future (was %s)",
|
||||
maxTTL, expiresIn))
|
||||
|
||||
} else if expiresIn < minTTL {
|
||||
mErr.Errors = append(mErr.Errors,
|
||||
fmt.Errorf("expiration time cannot be less than %s in the future (was %s)",
|
||||
minTTL, expiresIn))
|
||||
}
|
||||
}
|
||||
default:
|
||||
if existing.Global != a.Global {
|
||||
mErr.Errors = append(mErr.Errors, errors.New("cannot toggle global mode"))
|
||||
}
|
||||
if existing.ExpirationTTL != a.ExpirationTTL {
|
||||
mErr.Errors = append(mErr.Errors, errors.New("cannot update expiration TTL"))
|
||||
}
|
||||
if existing.ExpirationTime != a.ExpirationTime {
|
||||
mErr.Errors = append(mErr.Errors, errors.New("cannot update expiration time"))
|
||||
}
|
||||
}
|
||||
|
||||
return mErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
// HasExpirationTime checks whether the ACL token has an expiration time value
|
||||
// set.
|
||||
func (a *ACLToken) HasExpirationTime() bool {
|
||||
if a == nil || a.ExpirationTime == nil {
|
||||
return false
|
||||
}
|
||||
return !a.ExpirationTime.IsZero()
|
||||
}
|
||||
|
||||
// IsExpired compares the ACLToken.ExpirationTime against the passed t to
|
||||
// identify whether the token is considered expired. The function can be called
|
||||
// without checking whether the ACL token has an expiry time.
|
||||
func (a *ACLToken) IsExpired(t time.Time) bool {
|
||||
|
||||
// Check the token has an expiration time before potentially modifying the
|
||||
// supplied time. This allows us to avoid extra work, if it isn't needed.
|
||||
if !a.HasExpirationTime() {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check and ensure the time location is set to UTC. This is vital for
|
||||
// consistency with multi-region global tokens.
|
||||
if t.Location() != time.UTC {
|
||||
t = t.UTC()
|
||||
}
|
||||
|
||||
return a.ExpirationTime.Before(t) || t.IsZero()
|
||||
}
|
||||
|
||||
// ACLRole is an abstraction for the ACL system which allows the grouping of
|
||||
// ACL policies into a single object. ACL tokens can be created and linked to
|
||||
// a role; the token then inherits all the permissions granted by the policies.
|
||||
type ACLRole struct {
|
||||
|
||||
// ID is an internally generated UUID for this role and is controlled by
|
||||
// Nomad.
|
||||
ID string
|
||||
|
||||
// Name is unique across the entire set of federated clusters and is
|
||||
// supplied by the operator on role creation. The name can be modified by
|
||||
// updating the role and including the Nomad generated ID. This update will
|
||||
// not affect tokens created and linked to this role. This is a required
|
||||
// field.
|
||||
Name string
|
||||
|
||||
// Description is a human-readable, operator set description that can
|
||||
// provide additional context about the role. This is an operational field.
|
||||
Description string
|
||||
|
||||
// Policies is an array of ACL policy links. Although currently policies
|
||||
// can only be linked using their name, in the future we will want to add
|
||||
// IDs also and thus allow operators to specify either a name, an ID, or
|
||||
// both.
|
||||
Policies []*ACLRolePolicyLink
|
||||
|
||||
// Hash is the hashed value of the role and is generated using all fields
|
||||
// above this point.
|
||||
Hash []byte
|
||||
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
// ACLRolePolicyLink is used to link a policy to an ACL role. We use a struct
|
||||
// rather than a list of strings as in the future we will want to add IDs to
|
||||
// policies and then link via these.
|
||||
type ACLRolePolicyLink struct {
|
||||
|
||||
// Name is the ACLPolicy.Name value which will be linked to the ACL role.
|
||||
Name string
|
||||
}
|
||||
|
||||
// SetHash is used to compute and set the hash of the ACL role. This should be
|
||||
// called every and each time a user specified field on the role is changed
|
||||
// before updating the Nomad state store.
|
||||
func (a *ACLRole) SetHash() []byte {
|
||||
|
||||
// Initialize a 256bit Blake2 hash (32 bytes).
|
||||
hash, err := blake2b.New256(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Write all the user set fields.
|
||||
_, _ = hash.Write([]byte(a.Name))
|
||||
_, _ = hash.Write([]byte(a.Description))
|
||||
|
||||
for _, policyLink := range a.Policies {
|
||||
_, _ = hash.Write([]byte(policyLink.Name))
|
||||
}
|
||||
|
||||
// Finalize the hash.
|
||||
hashVal := hash.Sum(nil)
|
||||
|
||||
// Set and return the hash.
|
||||
a.Hash = hashVal
|
||||
return hashVal
|
||||
}
|
||||
|
||||
// Validate ensure the ACL role contains valid information which meets Nomad's
|
||||
// internal requirements. This does not include any state calls, such as
|
||||
// ensuring the linked policies exist.
|
||||
func (a *ACLRole) Validate() error {
|
||||
|
||||
var mErr multierror.Error
|
||||
|
||||
if !validACLRoleName.MatchString(a.Name) {
|
||||
mErr.Errors = append(mErr.Errors, fmt.Errorf("invalid name '%s'", a.Name))
|
||||
}
|
||||
|
||||
if len(a.Description) > maxACLRoleDescriptionLength {
|
||||
mErr.Errors = append(mErr.Errors, fmt.Errorf("description longer than %d", maxACLRoleDescriptionLength))
|
||||
}
|
||||
|
||||
if len(a.Policies) < 1 {
|
||||
mErr.Errors = append(mErr.Errors, errors.New("at least one policy should be specified"))
|
||||
}
|
||||
|
||||
return mErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
// Canonicalize performs basic canonicalization on the ACL role object. It is
|
||||
// important for callers to understand certain fields such as ID are set if it
|
||||
// is empty, so copies should be taken if needed before calling this function.
|
||||
func (a *ACLRole) Canonicalize() {
|
||||
if a.ID == "" {
|
||||
a.ID = uuid.Generate()
|
||||
}
|
||||
}
|
||||
|
||||
// Equals performs an equality check on the two service registrations. It
|
||||
// handles nil objects.
|
||||
func (a *ACLRole) Equals(o *ACLRole) bool {
|
||||
if a == nil || o == nil {
|
||||
return a == o
|
||||
}
|
||||
if len(a.Hash) == 0 {
|
||||
a.SetHash()
|
||||
}
|
||||
if len(o.Hash) == 0 {
|
||||
o.SetHash()
|
||||
}
|
||||
return bytes.Equal(a.Hash, o.Hash)
|
||||
}
|
||||
|
||||
// Copy creates a deep copy of the ACL role. This copy can then be safely
|
||||
// modified. It handles nil objects.
|
||||
func (a *ACLRole) Copy() *ACLRole {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := new(ACLRole)
|
||||
*c = *a
|
||||
|
||||
c.Policies = slices.Clone(a.Policies)
|
||||
c.Hash = slices.Clone(a.Hash)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Stub converts the ACLRole object into a ACLRoleListStub object.
|
||||
func (a *ACLRole) Stub() *ACLRoleListStub {
|
||||
return &ACLRoleListStub{
|
||||
ID: a.ID,
|
||||
Name: a.Name,
|
||||
Description: a.Description,
|
||||
Policies: a.Policies,
|
||||
Hash: a.Hash,
|
||||
CreateIndex: a.CreateIndex,
|
||||
ModifyIndex: a.ModifyIndex,
|
||||
}
|
||||
}
|
||||
|
||||
// ACLRoleListStub is the stub object returned when performing a listing of ACL
|
||||
// roles. While it might not currently be different to the full response
|
||||
// object, it allows us to future-proof the RPC in the event the ACLRole object
|
||||
// grows over time.
|
||||
type ACLRoleListStub struct {
|
||||
|
||||
// ID is an internally generated UUID for this role and is controlled by
|
||||
// Nomad.
|
||||
ID string
|
||||
|
||||
// Name is unique across the entire set of federated clusters and is
|
||||
// supplied by the operator on role creation. The name can be modified by
|
||||
// updating the role and including the Nomad generated ID. This update will
|
||||
// not affect tokens created and linked to this role. This is a required
|
||||
// field.
|
||||
Name string
|
||||
|
||||
// Description is a human-readable, operator set description that can
|
||||
// provide additional context about the role. This is an operational field.
|
||||
Description string
|
||||
|
||||
// Policies is an array of ACL policy links. Although currently policies
|
||||
// can only be linked using their name, in the future we will want to add
|
||||
// IDs also and thus allow operators to specify either a name, an ID, or
|
||||
// both.
|
||||
Policies []*ACLRolePolicyLink
|
||||
|
||||
// Hash is the hashed value of the role and is generated using all fields
|
||||
// above this point.
|
||||
Hash []byte
|
||||
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
// ACLRolesUpsertRequest is the request object used to upsert one or more ACL
|
||||
// roles.
|
||||
type ACLRolesUpsertRequest struct {
|
||||
ACLRoles []*ACLRole
|
||||
|
||||
// AllowMissingPolicies skips the ACL Role policy link verification and is
|
||||
// used by the replication process. The replication cannot ensure policies
|
||||
// are present before ACL Roles are replicated.
|
||||
AllowMissingPolicies bool
|
||||
|
||||
WriteRequest
|
||||
}
|
||||
|
||||
// ACLRolesUpsertResponse is the response object when one or more ACL roles
|
||||
// have been successfully upserted into state.
|
||||
type ACLRolesUpsertResponse struct {
|
||||
ACLRoles []*ACLRole
|
||||
WriteMeta
|
||||
}
|
||||
|
||||
// ACLRolesDeleteByIDRequest is the request object to delete one or more ACL
|
||||
// roles using the role ID.
|
||||
type ACLRolesDeleteByIDRequest struct {
|
||||
ACLRoleIDs []string
|
||||
WriteRequest
|
||||
}
|
||||
|
||||
// ACLRolesDeleteByIDResponse is the response object when performing a deletion
|
||||
// of one or more ACL roles using the role ID.
|
||||
type ACLRolesDeleteByIDResponse struct {
|
||||
WriteMeta
|
||||
}
|
||||
|
||||
// ACLRolesListRequest is the request object when performing ACL role listings.
|
||||
type ACLRolesListRequest struct {
|
||||
QueryOptions
|
||||
}
|
||||
|
||||
// ACLRolesListResponse is the response object when performing ACL role
|
||||
// listings.
|
||||
type ACLRolesListResponse struct {
|
||||
ACLRoles []*ACLRoleListStub
|
||||
QueryMeta
|
||||
}
|
||||
|
||||
// ACLRolesByIDRequest is the request object when performing a lookup of
|
||||
// multiple roles by the ID.
|
||||
type ACLRolesByIDRequest struct {
|
||||
ACLRoleIDs []string
|
||||
QueryOptions
|
||||
}
|
||||
|
||||
// ACLRolesByIDResponse is the response object when performing a lookup of
|
||||
// multiple roles by their IDs.
|
||||
type ACLRolesByIDResponse struct {
|
||||
ACLRoles map[string]*ACLRole
|
||||
QueryMeta
|
||||
}
|
||||
|
||||
// ACLRoleByIDRequest is the request object to perform a lookup of an ACL
|
||||
// role using a specific ID.
|
||||
type ACLRoleByIDRequest struct {
|
||||
RoleID string
|
||||
QueryOptions
|
||||
}
|
||||
|
||||
// ACLRoleByIDResponse is the response object when performing a lookup of an
|
||||
// ACL role matching a specific ID.
|
||||
type ACLRoleByIDResponse struct {
|
||||
ACLRole *ACLRole
|
||||
QueryMeta
|
||||
}
|
||||
|
||||
// ACLRoleByNameRequest is the request object to perform a lookup of an ACL
|
||||
// role using a specific name.
|
||||
type ACLRoleByNameRequest struct {
|
||||
RoleName string
|
||||
QueryOptions
|
||||
}
|
||||
|
||||
// ACLRoleByNameResponse is the response object when performing a lookup of an
|
||||
// ACL role matching a specific name.
|
||||
type ACLRoleByNameResponse struct {
|
||||
ACLRole *ACLRole
|
||||
QueryMeta
|
||||
}
|
|
@ -0,0 +1,764 @@
|
|||
package structs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestACLToken_Canonicalize(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
testFn func()
|
||||
}{
|
||||
{
|
||||
name: "token with accessor",
|
||||
testFn: func() {
|
||||
mockToken := &ACLToken{
|
||||
AccessorID: uuid.Generate(),
|
||||
SecretID: uuid.Generate(),
|
||||
Name: "my cool token " + uuid.Generate(),
|
||||
Type: "client",
|
||||
Policies: []string{"foo", "bar"},
|
||||
Roles: []*ACLTokenRoleLink{},
|
||||
Global: false,
|
||||
CreateTime: time.Now().UTC(),
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 20,
|
||||
}
|
||||
mockToken.SetHash()
|
||||
copiedMockToken := mockToken.Copy()
|
||||
|
||||
mockToken.Canonicalize()
|
||||
require.Equal(t, copiedMockToken, mockToken)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "token without accessor",
|
||||
testFn: func() {
|
||||
mockToken := &ACLToken{
|
||||
Name: "my cool token " + uuid.Generate(),
|
||||
Type: "client",
|
||||
Policies: []string{"foo", "bar"},
|
||||
Global: false,
|
||||
}
|
||||
|
||||
mockToken.Canonicalize()
|
||||
require.NotEmpty(t, mockToken.AccessorID)
|
||||
require.NotEmpty(t, mockToken.SecretID)
|
||||
require.NotEmpty(t, mockToken.CreateTime)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "token with ttl without accessor",
|
||||
testFn: func() {
|
||||
mockToken := &ACLToken{
|
||||
Name: "my cool token " + uuid.Generate(),
|
||||
Type: "client",
|
||||
Policies: []string{"foo", "bar"},
|
||||
Global: false,
|
||||
ExpirationTTL: 10 * time.Hour,
|
||||
}
|
||||
|
||||
mockToken.Canonicalize()
|
||||
require.NotEmpty(t, mockToken.AccessorID)
|
||||
require.NotEmpty(t, mockToken.SecretID)
|
||||
require.NotEmpty(t, mockToken.CreateTime)
|
||||
require.NotEmpty(t, mockToken.ExpirationTime)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.testFn()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLTokenValidate(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputACLToken *ACLToken
|
||||
inputExistingACLToken *ACLToken
|
||||
expectedErrorContains string
|
||||
}{
|
||||
{
|
||||
name: "missing type",
|
||||
inputACLToken: &ACLToken{},
|
||||
inputExistingACLToken: nil,
|
||||
expectedErrorContains: "client or management",
|
||||
},
|
||||
{
|
||||
name: "missing policies or roles",
|
||||
inputACLToken: &ACLToken{
|
||||
Type: ACLClientToken,
|
||||
},
|
||||
inputExistingACLToken: nil,
|
||||
expectedErrorContains: "missing policies or roles",
|
||||
},
|
||||
{
|
||||
name: "invalid policies",
|
||||
inputACLToken: &ACLToken{
|
||||
Type: ACLManagementToken,
|
||||
Policies: []string{"foo"},
|
||||
},
|
||||
inputExistingACLToken: nil,
|
||||
expectedErrorContains: "associated with policies or roles",
|
||||
},
|
||||
{
|
||||
name: "invalid roles",
|
||||
inputACLToken: &ACLToken{
|
||||
Type: ACLManagementToken,
|
||||
Roles: []*ACLTokenRoleLink{{Name: "foo"}},
|
||||
},
|
||||
inputExistingACLToken: nil,
|
||||
expectedErrorContains: "associated with policies or roles",
|
||||
},
|
||||
{
|
||||
name: "name too long",
|
||||
inputACLToken: &ACLToken{
|
||||
Type: ACLManagementToken,
|
||||
Name: uuid.Generate() + uuid.Generate() + uuid.Generate() + uuid.Generate() +
|
||||
uuid.Generate() + uuid.Generate() + uuid.Generate() + uuid.Generate(),
|
||||
},
|
||||
inputExistingACLToken: nil,
|
||||
expectedErrorContains: "name too long",
|
||||
},
|
||||
{
|
||||
name: "negative TTL",
|
||||
inputACLToken: &ACLToken{
|
||||
Type: ACLManagementToken,
|
||||
Name: "foo",
|
||||
ExpirationTTL: -1 * time.Hour,
|
||||
},
|
||||
inputExistingACLToken: nil,
|
||||
expectedErrorContains: "should not be negative",
|
||||
},
|
||||
{
|
||||
name: "TTL too small",
|
||||
inputACLToken: &ACLToken{
|
||||
Type: ACLManagementToken,
|
||||
Name: "foo",
|
||||
CreateTime: time.Date(2022, time.July, 11, 16, 23, 0, 0, time.UTC),
|
||||
ExpirationTime: pointer.Of(time.Date(2022, time.July, 11, 16, 23, 10, 0, time.UTC)),
|
||||
},
|
||||
inputExistingACLToken: nil,
|
||||
expectedErrorContains: "expiration time cannot be less than",
|
||||
},
|
||||
{
|
||||
name: "TTL too large",
|
||||
inputACLToken: &ACLToken{
|
||||
Type: ACLManagementToken,
|
||||
Name: "foo",
|
||||
CreateTime: time.Date(2022, time.July, 11, 16, 23, 0, 0, time.UTC),
|
||||
ExpirationTime: pointer.Of(time.Date(2042, time.July, 11, 16, 23, 0, 0, time.UTC)),
|
||||
},
|
||||
inputExistingACLToken: nil,
|
||||
expectedErrorContains: "expiration time cannot be more than",
|
||||
},
|
||||
{
|
||||
name: "valid management",
|
||||
inputACLToken: &ACLToken{
|
||||
Type: ACLManagementToken,
|
||||
Name: "foo",
|
||||
},
|
||||
inputExistingACLToken: nil,
|
||||
expectedErrorContains: "",
|
||||
},
|
||||
{
|
||||
name: "valid client",
|
||||
inputACLToken: &ACLToken{
|
||||
Type: ACLClientToken,
|
||||
Name: "foo",
|
||||
Policies: []string{"foo"},
|
||||
},
|
||||
inputExistingACLToken: nil,
|
||||
expectedErrorContains: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualOutputError := tc.inputACLToken.Validate(1*time.Minute, 24*time.Hour, tc.inputExistingACLToken)
|
||||
if tc.expectedErrorContains != "" {
|
||||
require.ErrorContains(t, actualOutputError, tc.expectedErrorContains)
|
||||
} else {
|
||||
require.NoError(t, actualOutputError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLToken_HasExpirationTime(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputACLToken *ACLToken
|
||||
expectedOutput bool ``
|
||||
}{
|
||||
{
|
||||
name: "nil acl token",
|
||||
inputACLToken: nil,
|
||||
expectedOutput: false,
|
||||
},
|
||||
{
|
||||
name: "default empty value",
|
||||
inputACLToken: &ACLToken{},
|
||||
expectedOutput: false,
|
||||
},
|
||||
{
|
||||
name: "expiration set to now",
|
||||
inputACLToken: &ACLToken{
|
||||
ExpirationTime: pointer.Of(time.Now().UTC()),
|
||||
},
|
||||
expectedOutput: true,
|
||||
},
|
||||
{
|
||||
name: "expiration set to past",
|
||||
inputACLToken: &ACLToken{
|
||||
ExpirationTime: pointer.Of(time.Date(2022, time.February, 21, 19, 35, 0, 0, time.UTC)),
|
||||
},
|
||||
expectedOutput: true,
|
||||
},
|
||||
{
|
||||
name: "expiration set to future",
|
||||
inputACLToken: &ACLToken{
|
||||
ExpirationTime: pointer.Of(time.Date(2087, time.April, 25, 12, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
expectedOutput: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualOutput := tc.inputACLToken.HasExpirationTime()
|
||||
require.Equal(t, tc.expectedOutput, actualOutput)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLToken_IsExpired(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputACLToken *ACLToken
|
||||
inputTime time.Time
|
||||
expectedOutput bool
|
||||
}{
|
||||
{
|
||||
name: "token without expiry",
|
||||
inputACLToken: &ACLToken{},
|
||||
inputTime: time.Now().UTC(),
|
||||
expectedOutput: false,
|
||||
},
|
||||
{
|
||||
name: "empty input time",
|
||||
inputACLToken: &ACLToken{},
|
||||
inputTime: time.Time{},
|
||||
expectedOutput: false,
|
||||
},
|
||||
{
|
||||
name: "token not expired",
|
||||
inputACLToken: &ACLToken{
|
||||
ExpirationTime: pointer.Of(time.Date(2022, time.May, 9, 10, 27, 0, 0, time.UTC)),
|
||||
},
|
||||
inputTime: time.Date(2022, time.May, 9, 10, 26, 0, 0, time.UTC),
|
||||
expectedOutput: false,
|
||||
},
|
||||
{
|
||||
name: "token expired",
|
||||
inputACLToken: &ACLToken{
|
||||
ExpirationTime: pointer.Of(time.Date(2022, time.May, 9, 10, 27, 0, 0, time.UTC)),
|
||||
},
|
||||
inputTime: time.Date(2022, time.May, 9, 10, 28, 0, 0, time.UTC),
|
||||
expectedOutput: true,
|
||||
},
|
||||
{
|
||||
name: "empty input time",
|
||||
inputACLToken: &ACLToken{
|
||||
ExpirationTime: pointer.Of(time.Date(2022, time.May, 9, 10, 27, 0, 0, time.UTC)),
|
||||
},
|
||||
inputTime: time.Time{},
|
||||
expectedOutput: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualOutput := tc.inputACLToken.IsExpired(tc.inputTime)
|
||||
require.Equal(t, tc.expectedOutput, actualOutput)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLRole_SetHash(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputACLRole *ACLRole
|
||||
expectedOutput []byte
|
||||
}{
|
||||
{
|
||||
name: "no hash set",
|
||||
inputACLRole: &ACLRole{
|
||||
Name: "acl-role",
|
||||
Description: "mocked-test-acl-role",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "mocked-test-policy-1"},
|
||||
{Name: "mocked-test-policy-2"},
|
||||
},
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 10,
|
||||
Hash: []byte{},
|
||||
},
|
||||
expectedOutput: []byte{
|
||||
122, 193, 189, 171, 197, 13, 37, 81, 141, 213, 188, 212, 179, 223, 148, 160,
|
||||
171, 141, 155, 136, 21, 128, 252, 100, 149, 195, 236, 148, 94, 70, 173, 102,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hash set with change",
|
||||
inputACLRole: &ACLRole{
|
||||
Name: "acl-role",
|
||||
Description: "mocked-test-acl-role",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "mocked-test-policy-1"},
|
||||
{Name: "mocked-test-policy-2"},
|
||||
},
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 10,
|
||||
Hash: []byte{
|
||||
137, 147, 2, 29, 53, 94, 78, 13, 45, 51, 127, 193, 21, 248, 230, 126, 34,
|
||||
106, 216, 73, 248, 219, 209, 146, 204, 107, 185, 2, 89, 255, 198, 5,
|
||||
},
|
||||
},
|
||||
expectedOutput: []byte{
|
||||
122, 193, 189, 171, 197, 13, 37, 81, 141, 213, 188, 212, 179, 223, 148, 160,
|
||||
171, 141, 155, 136, 21, 128, 252, 100, 149, 195, 236, 148, 94, 70, 173, 102,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualOutput := tc.inputACLRole.SetHash()
|
||||
require.Equal(t, tc.expectedOutput, actualOutput)
|
||||
require.Equal(t, tc.inputACLRole.Hash, actualOutput)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLRole_Validate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputACLRole *ACLRole
|
||||
expectedError bool
|
||||
expectedErrorContains string
|
||||
}{
|
||||
{
|
||||
name: "role name too long",
|
||||
inputACLRole: &ACLRole{
|
||||
Name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
expectedError: true,
|
||||
expectedErrorContains: "invalid name",
|
||||
},
|
||||
{
|
||||
name: "role name too short",
|
||||
inputACLRole: &ACLRole{
|
||||
Name: "",
|
||||
},
|
||||
expectedError: true,
|
||||
expectedErrorContains: "invalid name",
|
||||
},
|
||||
{
|
||||
name: "role name with invalid characters",
|
||||
inputACLRole: &ACLRole{
|
||||
Name: "--#$%$^%_%%_?>",
|
||||
},
|
||||
expectedError: true,
|
||||
expectedErrorContains: "invalid name",
|
||||
},
|
||||
{
|
||||
name: "description too long",
|
||||
inputACLRole: &ACLRole{
|
||||
Name: "acl-role",
|
||||
Description: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
expectedError: true,
|
||||
expectedErrorContains: "description longer than",
|
||||
},
|
||||
{
|
||||
name: "no policies",
|
||||
inputACLRole: &ACLRole{
|
||||
Name: "acl-role",
|
||||
Description: "",
|
||||
},
|
||||
expectedError: true,
|
||||
expectedErrorContains: "at least one policy should be specified",
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
inputACLRole: &ACLRole{
|
||||
Name: "acl-role",
|
||||
Description: "",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "policy-1"},
|
||||
},
|
||||
},
|
||||
expectedError: false,
|
||||
expectedErrorContains: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualOutput := tc.inputACLRole.Validate()
|
||||
if tc.expectedError {
|
||||
require.ErrorContains(t, actualOutput, tc.expectedErrorContains)
|
||||
} else {
|
||||
require.NoError(t, actualOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLRole_Canonicalize(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputACLRole *ACLRole
|
||||
}{
|
||||
{
|
||||
name: "no ID set",
|
||||
inputACLRole: &ACLRole{},
|
||||
},
|
||||
{
|
||||
name: "id set",
|
||||
inputACLRole: &ACLRole{ID: "some-random-uuid"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
existing := tc.inputACLRole.Copy()
|
||||
tc.inputACLRole.Canonicalize()
|
||||
if existing.ID == "" {
|
||||
require.NotEmpty(t, tc.inputACLRole.ID)
|
||||
} else {
|
||||
require.Equal(t, existing.ID, tc.inputACLRole.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLRole_Equals(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
composedACLRole *ACLRole
|
||||
inputACLRole *ACLRole
|
||||
expectedOutput bool
|
||||
}{
|
||||
{
|
||||
name: "equal with hash set",
|
||||
composedACLRole: &ACLRole{
|
||||
Name: "acl-role-",
|
||||
Description: "mocked-test-acl-role",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "mocked-test-policy-1"},
|
||||
{Name: "mocked-test-policy-2"},
|
||||
},
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 10,
|
||||
Hash: []byte{
|
||||
122, 193, 189, 171, 197, 13, 37, 81, 141, 213, 188, 212, 179, 223, 148, 160,
|
||||
171, 141, 155, 136, 21, 128, 252, 100, 149, 195, 236, 148, 94, 70, 173, 102,
|
||||
},
|
||||
},
|
||||
inputACLRole: &ACLRole{
|
||||
Name: "acl-role",
|
||||
Description: "mocked-test-acl-role",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "mocked-test-policy-1"},
|
||||
{Name: "mocked-test-policy-2"},
|
||||
},
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 10,
|
||||
Hash: []byte{
|
||||
122, 193, 189, 171, 197, 13, 37, 81, 141, 213, 188, 212, 179, 223, 148, 160,
|
||||
171, 141, 155, 136, 21, 128, 252, 100, 149, 195, 236, 148, 94, 70, 173, 102,
|
||||
},
|
||||
},
|
||||
expectedOutput: true,
|
||||
},
|
||||
{
|
||||
name: "equal without hash set",
|
||||
composedACLRole: &ACLRole{
|
||||
Name: "acl-role",
|
||||
Description: "mocked-test-acl-role",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "mocked-test-policy-1"},
|
||||
{Name: "mocked-test-policy-2"},
|
||||
},
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 10,
|
||||
Hash: []byte{},
|
||||
},
|
||||
inputACLRole: &ACLRole{
|
||||
Name: "acl-role",
|
||||
Description: "mocked-test-acl-role",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "mocked-test-policy-1"},
|
||||
{Name: "mocked-test-policy-2"},
|
||||
},
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 10,
|
||||
Hash: []byte{},
|
||||
},
|
||||
expectedOutput: true,
|
||||
},
|
||||
{
|
||||
name: "both nil",
|
||||
composedACLRole: nil,
|
||||
inputACLRole: nil,
|
||||
expectedOutput: true,
|
||||
},
|
||||
{
|
||||
name: "not equal composed nil",
|
||||
composedACLRole: nil,
|
||||
inputACLRole: &ACLRole{
|
||||
Name: "acl-role",
|
||||
Description: "mocked-test-acl-role",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "mocked-test-policy-1"},
|
||||
{Name: "mocked-test-policy-2"},
|
||||
},
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 10,
|
||||
Hash: []byte{
|
||||
122, 193, 189, 171, 197, 13, 37, 81, 141, 213, 188, 212, 179, 223, 148, 160,
|
||||
171, 141, 155, 136, 21, 128, 252, 100, 149, 195, 236, 148, 94, 70, 173, 102,
|
||||
},
|
||||
},
|
||||
expectedOutput: false,
|
||||
},
|
||||
{
|
||||
name: "not equal input nil",
|
||||
composedACLRole: &ACLRole{
|
||||
Name: "acl-role",
|
||||
Description: "mocked-test-acl-role",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "mocked-test-policy-1"},
|
||||
{Name: "mocked-test-policy-2"},
|
||||
},
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 10,
|
||||
Hash: []byte{
|
||||
122, 193, 189, 171, 197, 13, 37, 81, 141, 213, 188, 212, 179, 223, 148, 160,
|
||||
171, 141, 155, 136, 21, 128, 252, 100, 149, 195, 236, 148, 94, 70, 173, 102,
|
||||
},
|
||||
},
|
||||
inputACLRole: nil,
|
||||
expectedOutput: false,
|
||||
},
|
||||
{
|
||||
name: "not equal with hash set",
|
||||
composedACLRole: &ACLRole{
|
||||
Name: "acl-role",
|
||||
Description: "mocked-test-acl-role",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "mocked-test-policy-1"},
|
||||
{Name: "mocked-test-policy-2"},
|
||||
},
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 10,
|
||||
Hash: []byte{
|
||||
122, 193, 189, 171, 197, 13, 37, 81, 141, 213, 188, 212, 179, 223, 148, 160,
|
||||
171, 141, 155, 136, 21, 128, 252, 100, 149, 195, 236, 148, 94, 70, 173, 102,
|
||||
},
|
||||
},
|
||||
inputACLRole: &ACLRole{
|
||||
Name: "acl-role",
|
||||
Description: "mocked-test-acl-role",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "mocked-test-policy-1"},
|
||||
},
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 10,
|
||||
Hash: []byte{
|
||||
137, 147, 2, 29, 53, 94, 78, 13, 45, 51, 127, 193, 21, 248, 230, 126, 34,
|
||||
106, 216, 73, 248, 219, 209, 146, 204, 107, 185, 2, 89, 255, 198, 5,
|
||||
},
|
||||
},
|
||||
expectedOutput: false,
|
||||
},
|
||||
{
|
||||
name: "not equal without hash set",
|
||||
composedACLRole: &ACLRole{
|
||||
Name: "acl-role",
|
||||
Description: "mocked-test-acl-role",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "mocked-test-policy-1"},
|
||||
{Name: "mocked-test-policy-2"},
|
||||
},
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 10,
|
||||
Hash: []byte{},
|
||||
},
|
||||
inputACLRole: &ACLRole{
|
||||
Name: "acl-role",
|
||||
Description: "mocked-test-acl-role",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "mocked-test-policy-1"},
|
||||
},
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 10,
|
||||
Hash: []byte{},
|
||||
},
|
||||
expectedOutput: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualOutput := tc.composedACLRole.Equals(tc.inputACLRole)
|
||||
require.Equal(t, tc.expectedOutput, actualOutput)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLRole_Copy(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputACLRole *ACLRole
|
||||
}{
|
||||
{
|
||||
name: "nil input",
|
||||
inputACLRole: nil,
|
||||
},
|
||||
{
|
||||
name: "general 1",
|
||||
inputACLRole: &ACLRole{
|
||||
Name: fmt.Sprintf("acl-role"),
|
||||
Description: "mocked-test-acl-role",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "mocked-test-policy-1"},
|
||||
{Name: "mocked-test-policy-2"},
|
||||
},
|
||||
CreateIndex: 10,
|
||||
ModifyIndex: 10,
|
||||
Hash: []byte{
|
||||
122, 193, 189, 171, 197, 13, 37, 81, 141, 213, 188, 212, 179, 223, 148, 160,
|
||||
171, 141, 155, 136, 21, 128, 252, 100, 149, 195, 236, 148, 94, 70, 173, 102,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualOutput := tc.inputACLRole.Copy()
|
||||
require.Equal(t, tc.inputACLRole, actualOutput)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLRole_Stub(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputACLRole *ACLRole
|
||||
expectedOutput *ACLRoleListStub
|
||||
}{
|
||||
{
|
||||
name: "partially hydrated",
|
||||
inputACLRole: &ACLRole{
|
||||
ID: "1d6332c8-02d7-325e-f675-a9bb4aff0c51",
|
||||
Name: "my-lovely-role",
|
||||
Description: "",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "my-lovely-policy"},
|
||||
},
|
||||
Hash: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
CreateIndex: 24,
|
||||
ModifyIndex: 24,
|
||||
},
|
||||
expectedOutput: &ACLRoleListStub{
|
||||
ID: "1d6332c8-02d7-325e-f675-a9bb4aff0c51",
|
||||
Name: "my-lovely-role",
|
||||
Description: "",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "my-lovely-policy"},
|
||||
},
|
||||
Hash: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
CreateIndex: 24,
|
||||
ModifyIndex: 24,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hully hydrated",
|
||||
inputACLRole: &ACLRole{
|
||||
ID: "1d6332c8-02d7-325e-f675-a9bb4aff0c51",
|
||||
Name: "my-lovely-role",
|
||||
Description: "this-is-my-lovely-role",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "my-lovely-policy"},
|
||||
},
|
||||
Hash: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
CreateIndex: 24,
|
||||
ModifyIndex: 24,
|
||||
},
|
||||
expectedOutput: &ACLRoleListStub{
|
||||
ID: "1d6332c8-02d7-325e-f675-a9bb4aff0c51",
|
||||
Name: "my-lovely-role",
|
||||
Description: "this-is-my-lovely-role",
|
||||
Policies: []*ACLRolePolicyLink{
|
||||
{Name: "my-lovely-policy"},
|
||||
},
|
||||
Hash: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
CreateIndex: 24,
|
||||
ModifyIndex: 24,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualOutput := tc.inputACLRole.Stub()
|
||||
require.Equal(t, tc.expectedOutput, actualOutput)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ACLRolesUpsertRequest(t *testing.T) {
|
||||
req := ACLRolesUpsertRequest{}
|
||||
require.False(t, req.IsRead())
|
||||
}
|
||||
|
||||
func Test_ACLRolesDeleteByIDRequest(t *testing.T) {
|
||||
req := ACLRolesDeleteByIDRequest{}
|
||||
require.False(t, req.IsRead())
|
||||
}
|
||||
|
||||
func Test_ACLRolesListRequest(t *testing.T) {
|
||||
req := ACLRolesListRequest{}
|
||||
require.True(t, req.IsRead())
|
||||
}
|
||||
|
||||
func Test_ACLRolesByIDRequest(t *testing.T) {
|
||||
req := ACLRolesByIDRequest{}
|
||||
require.True(t, req.IsRead())
|
||||
}
|
||||
|
||||
func Test_ACLRoleByIDRequest(t *testing.T) {
|
||||
req := ACLRoleByIDRequest{}
|
||||
require.True(t, req.IsRead())
|
||||
}
|
||||
|
||||
func Test_ACLRoleByNameRequest(t *testing.T) {
|
||||
req := ACLRoleByNameRequest{}
|
||||
require.True(t, req.IsRead())
|
||||
}
|
|
@ -12,6 +12,7 @@ const (
|
|||
errNotReadyForConsistentReads = "Not ready to serve consistent reads"
|
||||
errNoRegionPath = "No path to region"
|
||||
errTokenNotFound = "ACL token not found"
|
||||
errTokenExpired = "ACL token expired"
|
||||
errPermissionDenied = "Permission denied"
|
||||
errJobRegistrationDisabled = "Job registration, dispatch, and scale are disabled by the scheduler configuration"
|
||||
errNoNodeConn = "No path to node"
|
||||
|
@ -48,6 +49,7 @@ var (
|
|||
ErrNotReadyForConsistentReads = errors.New(errNotReadyForConsistentReads)
|
||||
ErrNoRegionPath = errors.New(errNoRegionPath)
|
||||
ErrTokenNotFound = errors.New(errTokenNotFound)
|
||||
ErrTokenExpired = errors.New(errTokenExpired)
|
||||
ErrPermissionDenied = errors.New(errPermissionDenied)
|
||||
ErrJobRegistrationDisabled = errors.New(errJobRegistrationDisabled)
|
||||
ErrNoNodeConn = errors.New(errNoNodeConn)
|
||||
|
|
|
@ -114,6 +114,8 @@ const (
|
|||
VarApplyStateRequestType MessageType = 50
|
||||
RootKeyMetaUpsertRequestType MessageType = 51
|
||||
RootKeyMetaDeleteRequestType MessageType = 52
|
||||
ACLRolesUpsertRequestType MessageType = 53
|
||||
ACLRolesDeleteByIDRequestType MessageType = 54
|
||||
|
||||
// Namespace types were moved from enterprise and therefore start at 64
|
||||
NamespaceUpsertRequestType MessageType = 64
|
||||
|
@ -10905,6 +10907,16 @@ const (
|
|||
// tokens. We periodically scan for expired tokens and delete them.
|
||||
CoreJobOneTimeTokenGC = "one-time-token-gc"
|
||||
|
||||
// CoreJobLocalTokenExpiredGC is used for the garbage collection of
|
||||
// expired local ACL tokens. We periodically scan for expired tokens and
|
||||
// delete them.
|
||||
CoreJobLocalTokenExpiredGC = "local-token-expired-gc"
|
||||
|
||||
// CoreJobGlobalTokenExpiredGC is used for the garbage collection of
|
||||
// expired global ACL tokens. We periodically scan for expired tokens and
|
||||
// delete them.
|
||||
CoreJobGlobalTokenExpiredGC = "global-token-expired-gc"
|
||||
|
||||
// CoreJobRootKeyRotateGC is used for periodic key rotation and
|
||||
// garbage collection of unused encryption keys.
|
||||
CoreJobRootKeyRotateOrGC = "root-key-rotate-gc"
|
||||
|
@ -11945,14 +11957,31 @@ type ACLPolicyUpsertRequest struct {
|
|||
|
||||
// ACLToken represents a client token which is used to Authenticate
|
||||
type ACLToken struct {
|
||||
AccessorID string // Public Accessor ID (UUID)
|
||||
SecretID string // Secret ID, private (UUID)
|
||||
Name string // Human friendly name
|
||||
Type string // Client or Management
|
||||
Policies []string // Policies this token ties to
|
||||
Global bool // Global or Region local
|
||||
Hash []byte
|
||||
CreateTime time.Time // Time of creation
|
||||
AccessorID string // Public Accessor ID (UUID)
|
||||
SecretID string // Secret ID, private (UUID)
|
||||
Name string // Human friendly name
|
||||
Type string // Client or Management
|
||||
Policies []string // Policies this token ties to
|
||||
|
||||
// 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 // Global or Region local
|
||||
Hash []byte
|
||||
CreateTime time.Time // Time of creation
|
||||
|
||||
// ExpirationTime represents the point after which a token should be
|
||||
// considered revoked and is eligible for destruction. This time should
|
||||
// always use UTC to account for multi-region global tokens. It is a
|
||||
// pointer, so we can store nil, rather than the zero value of time.Time.
|
||||
ExpirationTime *time.Time
|
||||
|
||||
// ExpirationTTL is a convenience field for helping set ExpirationTime to a
|
||||
// value of CreateTime+ExpirationTTL. This can only be set during token
|
||||
// creation. This is a string version of a time.Duration like "2m".
|
||||
ExpirationTTL time.Duration
|
||||
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
@ -11980,9 +12009,13 @@ func (a *ACLToken) Copy() *ACLToken {
|
|||
|
||||
c.Policies = make([]string, len(a.Policies))
|
||||
copy(c.Policies, a.Policies)
|
||||
|
||||
c.Hash = make([]byte, len(a.Hash))
|
||||
copy(c.Hash, a.Hash)
|
||||
|
||||
c.Roles = make([]*ACLTokenRoleLink, len(a.Roles))
|
||||
copy(c.Roles, a.Roles)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
|
@ -11999,18 +12032,22 @@ var (
|
|||
)
|
||||
|
||||
type ACLTokenListStub struct {
|
||||
AccessorID string
|
||||
Name string
|
||||
Type string
|
||||
Policies []string
|
||||
Global bool
|
||||
Hash []byte
|
||||
CreateTime time.Time
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
AccessorID string
|
||||
Name string
|
||||
Type string
|
||||
Policies []string
|
||||
Roles []*ACLTokenRoleLink
|
||||
Global bool
|
||||
Hash []byte
|
||||
CreateTime time.Time
|
||||
ExpirationTime *time.Time
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
// SetHash is used to compute and set the hash of the ACL token
|
||||
// SetHash is used to compute and set the hash of the ACL token. It only hashes
|
||||
// fields which can be updated, and as such, does not hash fields such as
|
||||
// ExpirationTime.
|
||||
func (a *ACLToken) SetHash() []byte {
|
||||
// Initialize a 256bit Blake2 hash (32 bytes)
|
||||
hash, err := blake2b.New256(nil)
|
||||
|
@ -12030,6 +12067,13 @@ func (a *ACLToken) SetHash() []byte {
|
|||
_, _ = hash.Write([]byte("local"))
|
||||
}
|
||||
|
||||
// Iterate the ACL role links and hash the ID. The ID is immutable and the
|
||||
// canonical way to reference a role. The name can be modified by
|
||||
// operators, but won't impact the ACL token resolution.
|
||||
for _, roleLink := range a.Roles {
|
||||
_, _ = hash.Write([]byte(roleLink.ID))
|
||||
}
|
||||
|
||||
// Finalize the hash
|
||||
hashVal := hash.Sum(nil)
|
||||
|
||||
|
@ -12040,39 +12084,20 @@ func (a *ACLToken) SetHash() []byte {
|
|||
|
||||
func (a *ACLToken) Stub() *ACLTokenListStub {
|
||||
return &ACLTokenListStub{
|
||||
AccessorID: a.AccessorID,
|
||||
Name: a.Name,
|
||||
Type: a.Type,
|
||||
Policies: a.Policies,
|
||||
Global: a.Global,
|
||||
Hash: a.Hash,
|
||||
CreateTime: a.CreateTime,
|
||||
CreateIndex: a.CreateIndex,
|
||||
ModifyIndex: a.ModifyIndex,
|
||||
AccessorID: a.AccessorID,
|
||||
Name: a.Name,
|
||||
Type: a.Type,
|
||||
Policies: a.Policies,
|
||||
Roles: a.Roles,
|
||||
Global: a.Global,
|
||||
Hash: a.Hash,
|
||||
CreateTime: a.CreateTime,
|
||||
ExpirationTime: a.ExpirationTime,
|
||||
CreateIndex: a.CreateIndex,
|
||||
ModifyIndex: a.ModifyIndex,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate is used to check a token for reasonableness
|
||||
func (a *ACLToken) Validate() error {
|
||||
var mErr multierror.Error
|
||||
if len(a.Name) > maxTokenNameLength {
|
||||
mErr.Errors = append(mErr.Errors, fmt.Errorf("token name too long"))
|
||||
}
|
||||
switch a.Type {
|
||||
case ACLClientToken:
|
||||
if len(a.Policies) == 0 {
|
||||
mErr.Errors = append(mErr.Errors, fmt.Errorf("client token missing policies"))
|
||||
}
|
||||
case ACLManagementToken:
|
||||
if len(a.Policies) != 0 {
|
||||
mErr.Errors = append(mErr.Errors, fmt.Errorf("management token cannot be associated with policies"))
|
||||
}
|
||||
default:
|
||||
mErr.Errors = append(mErr.Errors, fmt.Errorf("token type must be client or management"))
|
||||
}
|
||||
return mErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
// PolicySubset checks if a given set of policies is a subset of the token
|
||||
func (a *ACLToken) PolicySubset(policies []string) bool {
|
||||
// Hot-path the management tokens, superset of all policies.
|
||||
|
|
|
@ -6087,53 +6087,6 @@ func TestIsRecoverable(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestACLTokenValidate(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
tk := &ACLToken{}
|
||||
|
||||
// Missing a type
|
||||
err := tk.Validate()
|
||||
assert.NotNil(t, err)
|
||||
if !strings.Contains(err.Error(), "client or management") {
|
||||
t.Fatalf("bad: %v", err)
|
||||
}
|
||||
|
||||
// Missing policies
|
||||
tk.Type = ACLClientToken
|
||||
err = tk.Validate()
|
||||
assert.NotNil(t, err)
|
||||
if !strings.Contains(err.Error(), "missing policies") {
|
||||
t.Fatalf("bad: %v", err)
|
||||
}
|
||||
|
||||
// Invalid policies
|
||||
tk.Type = ACLManagementToken
|
||||
tk.Policies = []string{"foo"}
|
||||
err = tk.Validate()
|
||||
assert.NotNil(t, err)
|
||||
if !strings.Contains(err.Error(), "associated with policies") {
|
||||
t.Fatalf("bad: %v", err)
|
||||
}
|
||||
|
||||
// Name too long policies
|
||||
tk.Name = ""
|
||||
for i := 0; i < 8; i++ {
|
||||
tk.Name += uuid.Generate()
|
||||
}
|
||||
tk.Policies = nil
|
||||
err = tk.Validate()
|
||||
assert.NotNil(t, err)
|
||||
if !strings.Contains(err.Error(), "too long") {
|
||||
t.Fatalf("bad: %v", err)
|
||||
}
|
||||
|
||||
// Make it valid
|
||||
tk.Name = "foo"
|
||||
err = tk.Validate()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestACLTokenPolicySubset(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
|
|
Loading…
Reference in New Issue