d7b311ce55
The client ACL cache was not accounting for tokens which included ACL role links. This change modifies the behaviour to resolve role links to policies. It will also now store ACL roles within the cache for quick lookup. The cache TTL is configurable in the same manner as policies or tokens. Another small fix is included that takes into account the ACL token expiry time. This was not included, which meant tokens with expiry could be used past the expiry time, until they were GC'd.
266 lines
7.5 KiB
Go
266 lines
7.5 KiB
Go
package client
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/nomad/acl"
|
|
"github.com/hashicorp/nomad/ci"
|
|
"github.com/hashicorp/nomad/client/config"
|
|
"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/hashicorp/nomad/testutil"
|
|
"github.com/shoenig/test/must"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func Test_clientACLResolver_init(t *testing.T) {
|
|
resolver := &clientACLResolver{}
|
|
must.NoError(t, resolver.init())
|
|
must.NotNil(t, resolver.aclCache)
|
|
must.NotNil(t, resolver.policyCache)
|
|
must.NotNil(t, resolver.tokenCache)
|
|
must.NotNil(t, resolver.roleCache)
|
|
}
|
|
|
|
func TestClient_ACL_resolveTokenValue(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, _, _, cleanupS1 := testACLServer(t, nil)
|
|
defer cleanupS1()
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
c1, cleanup := TestClient(t, func(c *config.Config) {
|
|
c.RPCHandler = s1
|
|
c.ACLEnabled = true
|
|
})
|
|
defer cleanup()
|
|
|
|
// 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 := s1.State().UpsertACLPolicies(structs.MsgTypeTestSetup, 100, []*structs.ACLPolicy{policy, policy2})
|
|
assert.Nil(t, err)
|
|
err = s1.State().UpsertACLTokens(structs.MsgTypeTestSetup, 110, []*structs.ACLToken{token, token2})
|
|
assert.Nil(t, err)
|
|
|
|
// Test the client resolution
|
|
out0, err := c1.resolveTokenValue("")
|
|
assert.Nil(t, err)
|
|
assert.NotNil(t, out0)
|
|
assert.Equal(t, structs.AnonymousACLToken, out0)
|
|
|
|
// Test the client resolution
|
|
out1, err := c1.resolveTokenValue(token.SecretID)
|
|
assert.Nil(t, err)
|
|
assert.NotNil(t, out1)
|
|
assert.Equal(t, token, out1)
|
|
|
|
out2, err := c1.resolveTokenValue(token2.SecretID)
|
|
assert.Nil(t, err)
|
|
assert.NotNil(t, out2)
|
|
assert.Equal(t, token2, out2)
|
|
|
|
out3, err := c1.resolveTokenValue(token.SecretID)
|
|
assert.Nil(t, err)
|
|
assert.NotNil(t, out3)
|
|
if out1 != out3 {
|
|
t.Fatalf("bad caching")
|
|
}
|
|
}
|
|
|
|
func TestClient_ACL_resolvePolicies(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, _, root, cleanupS1 := testACLServer(t, nil)
|
|
defer cleanupS1()
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
c1, cleanup := TestClient(t, func(c *config.Config) {
|
|
c.RPCHandler = s1
|
|
c.ACLEnabled = true
|
|
})
|
|
defer cleanup()
|
|
|
|
// 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 := s1.State().UpsertACLPolicies(structs.MsgTypeTestSetup, 100, []*structs.ACLPolicy{policy, policy2})
|
|
assert.Nil(t, err)
|
|
err = s1.State().UpsertACLTokens(structs.MsgTypeTestSetup, 110, []*structs.ACLToken{token, token2})
|
|
assert.Nil(t, err)
|
|
|
|
// Test the client resolution
|
|
out, err := c1.resolvePolicies(root.SecretID, []string{policy.Name, policy2.Name})
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 2, len(out))
|
|
|
|
// Test caching
|
|
out2, err := c1.resolvePolicies(root.SecretID, []string{policy.Name, policy2.Name})
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, 2, len(out2))
|
|
|
|
// Check we get the same objects back (ignore ordering)
|
|
if out[0] != out2[0] && out[0] != out2[1] {
|
|
t.Fatalf("bad caching")
|
|
}
|
|
}
|
|
|
|
func TestClient_resolveTokenACLRoles(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testServer, _, rootACLToken, testServerCleanupS1 := testACLServer(t, nil)
|
|
defer testServerCleanupS1()
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
testClient, cleanup := TestClient(t, func(c *config.Config) {
|
|
c.RPCHandler = testServer
|
|
c.ACLEnabled = true
|
|
})
|
|
defer cleanup()
|
|
|
|
// Create an ACL Role and a client token which is linked to this.
|
|
mockACLRole := mock.ACLRole()
|
|
|
|
mockACLToken := mock.ACLToken()
|
|
mockACLToken.Policies = []string{}
|
|
mockACLToken.Roles = []*structs.ACLTokenRoleLink{{ID: mockACLRole.ID}}
|
|
|
|
err := testServer.State().UpsertACLRoles(structs.MsgTypeTestSetup, 10, []*structs.ACLRole{mockACLRole}, true)
|
|
must.NoError(t, err)
|
|
err = testServer.State().UpsertACLTokens(structs.MsgTypeTestSetup, 20, []*structs.ACLToken{mockACLToken})
|
|
must.NoError(t, err)
|
|
|
|
// Resolve the ACL policies linked via the role.
|
|
resolvedRoles1, err := testClient.resolveTokenACLRoles(rootACLToken.SecretID, mockACLToken.Roles)
|
|
must.NoError(t, err)
|
|
must.Len(t, 2, resolvedRoles1)
|
|
|
|
// Test the cache directly and check that the ACL role previously queried
|
|
// is now cached.
|
|
must.Eq(t, 1, testClient.roleCache.Len())
|
|
must.True(t, testClient.roleCache.Contains(mockACLRole.ID))
|
|
|
|
// Resolve the roles again to check we get the same results.
|
|
resolvedRoles2, err := testClient.resolveTokenACLRoles(rootACLToken.SecretID, mockACLToken.Roles)
|
|
must.NoError(t, err)
|
|
must.SliceContainsAll(t, resolvedRoles1, resolvedRoles2)
|
|
}
|
|
|
|
func TestClient_ACL_ResolveToken_Disabled(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, _, cleanupS1 := testServer(t, nil)
|
|
defer cleanupS1()
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
c1, cleanup := TestClient(t, func(c *config.Config) {
|
|
c.RPCHandler = s1
|
|
})
|
|
defer cleanup()
|
|
|
|
// Should always get nil when disabled
|
|
aclObj, err := c1.ResolveToken("blah")
|
|
assert.Nil(t, err)
|
|
assert.Nil(t, aclObj)
|
|
}
|
|
|
|
func TestClient_ACL_ResolveToken(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, _, _, cleanupS1 := testACLServer(t, nil)
|
|
defer cleanupS1()
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
c1, cleanup := TestClient(t, func(c *config.Config) {
|
|
c.RPCHandler = s1
|
|
c.ACLEnabled = true
|
|
})
|
|
defer cleanup()
|
|
|
|
// 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 := s1.State().UpsertACLPolicies(structs.MsgTypeTestSetup, 100, []*structs.ACLPolicy{policy, policy2})
|
|
assert.Nil(t, err)
|
|
err = s1.State().UpsertACLTokens(structs.MsgTypeTestSetup, 110, []*structs.ACLToken{token, token2})
|
|
assert.Nil(t, err)
|
|
|
|
// Test the client resolution
|
|
out, err := c1.ResolveToken(token.SecretID)
|
|
assert.Nil(t, err)
|
|
assert.NotNil(t, out)
|
|
|
|
// Test caching
|
|
out2, err := c1.ResolveToken(token.SecretID)
|
|
assert.Nil(t, err)
|
|
if out != out2 {
|
|
t.Fatalf("should be cached")
|
|
}
|
|
|
|
// Test management token
|
|
out3, err := c1.ResolveToken(token2.SecretID)
|
|
assert.Nil(t, err)
|
|
if acl.ManagementACL != out3 {
|
|
t.Fatalf("should be management")
|
|
}
|
|
|
|
// Test bad token
|
|
out4, err := c1.ResolveToken(uuid.Generate())
|
|
assert.Equal(t, structs.ErrTokenNotFound, err)
|
|
assert.Nil(t, out4)
|
|
}
|
|
|
|
func TestClient_ACL_ResolveSecretToken(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, _, _, cleanupS1 := testACLServer(t, nil)
|
|
defer cleanupS1()
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
c1, cleanup := TestClient(t, func(c *config.Config) {
|
|
c.RPCHandler = s1
|
|
c.ACLEnabled = true
|
|
})
|
|
defer cleanup()
|
|
|
|
token := mock.ACLToken()
|
|
|
|
err := s1.State().UpsertACLTokens(structs.MsgTypeTestSetup, 110, []*structs.ACLToken{token})
|
|
assert.Nil(t, err)
|
|
|
|
respToken, err := c1.ResolveSecretToken(token.SecretID)
|
|
assert.Nil(t, err)
|
|
if assert.NotNil(t, respToken) {
|
|
assert.NotEmpty(t, respToken.AccessorID)
|
|
}
|
|
|
|
// Create and upsert a token which has just expired.
|
|
mockExpiredToken := mock.ACLToken()
|
|
mockExpiredToken.ExpirationTime = pointer.Of(time.Now().Add(-5 * time.Minute))
|
|
|
|
err = s1.State().UpsertACLTokens(structs.MsgTypeTestSetup, 120, []*structs.ACLToken{mockExpiredToken})
|
|
must.NoError(t, err)
|
|
|
|
expiredTokenResp, err := c1.ResolveSecretToken(mockExpiredToken.SecretID)
|
|
must.Nil(t, expiredTokenResp)
|
|
must.StrContains(t, err.Error(), "ACL token expired")
|
|
}
|