Files
nomad/client/acl_test.go
James Rasell 668dc5f7a7 client: fix role permission issue with duplicate policies. (#18419)
This change deduplicates the ACL policy list generated from ACL
roles referenced within an ACL token on the client.

Previously the list could contain duplicates, which would cause
erronous permission denied errors when calling client related RPC/
HTTP API endpoints. This is because the client calls the ACL get
policies endpoint which subsequently ensures the caller has
permission to view the ACL policies. This check is performed by
comparing the requested list args with the policies referenced by
the caller ACL token. When a duplicate is present, this check
fails, as the check must ensure the slices match exactly.
2023-09-11 12:52:08 +01:00

374 lines
11 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
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"
"github.com/shoenig/test/must"
)
func Test_clientACLResolver_init(t *testing.T) {
resolver := new(clientACLResolver)
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})
must.NoError(t, err)
err = s1.State().UpsertACLTokens(structs.MsgTypeTestSetup, 110, []*structs.ACLToken{token, token2})
must.NoError(t, err)
// Test the client resolution
out0, err := c1.resolveTokenValue("")
test.Nil(t, err)
must.NotNil(t, out0)
test.Eq(t, structs.AnonymousACLToken, out0.ACLToken)
out1, err := c1.resolveTokenValue(token.SecretID)
test.Nil(t, err)
must.NotNil(t, out1)
test.Eq(t, token, out1.ACLToken)
out2, err := c1.resolveTokenValue(token2.SecretID)
test.Nil(t, err)
must.NotNil(t, out2)
test.Eq(t, token2, out2.ACLToken)
out3, err := c1.resolveTokenValue(token.SecretID)
test.Nil(t, err)
must.Eq(t, out1, out3, must.Sprintf("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})
must.NoError(t, err)
err = s1.State().UpsertACLTokens(structs.MsgTypeTestSetup, 110, []*structs.ACLToken{token, token2})
must.NoError(t, err)
// Test the client resolution
out, err := c1.resolvePolicies(root.SecretID, []string{policy.Name, policy2.Name})
must.NoError(t, err)
test.Len(t, 2, out)
// Test caching
out2, err := c1.resolvePolicies(root.SecretID, []string{policy.Name, policy2.Name})
must.NoError(t, err)
test.Len(t, 2, 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.
mockACLRole1 := mock.ACLRole()
mockACLToken := mock.ACLToken()
mockACLToken.Policies = []string{}
mockACLToken.Roles = []*structs.ACLTokenRoleLink{{ID: mockACLRole1.ID}}
err := testServer.State().UpsertACLRoles(structs.MsgTypeTestSetup, 10, []*structs.ACLRole{mockACLRole1}, 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(mockACLRole1.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)
// Create another ACL role which will have the same ACL policy links as the
// previous
mockACLRole2 := mock.ACLRole()
must.NoError(t,
testServer.State().UpsertACLRoles(
structs.MsgTypeTestSetup, 30, []*structs.ACLRole{mockACLRole2}, true))
// Update the ACL token so that it links to two ACL roles, which include
// duplicate ACL policies.
mockACLToken.Roles = append(mockACLToken.Roles, &structs.ACLTokenRoleLink{ID: mockACLRole2.ID})
must.NoError(t,
testServer.State().UpsertACLTokens(
structs.MsgTypeTestSetup, 40, []*structs.ACLToken{mockACLToken}))
// Ensure when resolving the ACL token, we are returned a deduplicated list
// of ACL policy names.
resolvedRoles3, err := testClient.resolveTokenACLRoles(rootACLToken.SecretID, mockACLToken.Roles)
must.NoError(t, err)
must.SliceContainsAll(t, []string{"mocked-test-policy-1", "mocked-test-policy-2"}, resolvedRoles3)
}
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")
must.NoError(t, err)
must.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})
must.NoError(t, err)
err = s1.State().UpsertACLTokens(structs.MsgTypeTestSetup, 110, []*structs.ACLToken{token, token2})
must.NoError(t, err)
// Test the client resolution
out, err := c1.ResolveToken(token.SecretID)
must.NoError(t, err)
test.NotNil(t, out)
// Test caching
out2, err := c1.ResolveToken(token.SecretID)
must.NoError(t, err)
must.Eq(t, out, out2, must.Sprintf("should be cached"))
// Test management token
out3, err := c1.ResolveToken(token2.SecretID)
must.NoError(t, err)
must.Eq(t, acl.ManagementACL, out3)
// Test bad token
out4, err := c1.ResolveToken(uuid.Generate())
test.EqError(t, err, structs.ErrPermissionDenied.Error())
test.Nil(t, out4)
}
func TestClient_ACL_ResolveToken_Expired(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 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.ResolveToken(mockExpiredToken.SecretID)
must.Nil(t, expiredTokenResp)
must.ErrorContains(t, err, "ACL token expired")
}
// TestClient_ACL_ResolveToken_Claims asserts that ResolveToken
// properly resolves valid workload identity claims.
func TestClient_ACL_ResolveToken_Claims(t *testing.T) {
ci.Parallel(t)
s1, _, rootToken, 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 minimal job
job := mock.MinJob()
// Add a job policy
polArgs := structs.ACLPolicyUpsertRequest{
Policies: []*structs.ACLPolicy{
{
Name: "nw",
Description: "test job can write to nodes",
Rules: `node { policy = "write" }`,
JobACL: &structs.JobACL{
Namespace: job.Namespace,
JobID: job.ID,
},
},
},
WriteRequest: structs.WriteRequest{
Region: job.Region,
AuthToken: rootToken.SecretID,
Namespace: job.Namespace,
},
}
polReply := structs.GenericResponse{}
must.NoError(t, s1.RPC("ACL.UpsertPolicies", &polArgs, &polReply))
must.NonZero(t, polReply.WriteMeta.Index)
allocs := testutil.WaitForRunningWithToken(t, s1.RPC, job, rootToken.SecretID)
must.Len(t, 1, allocs)
alloc, err := s1.State().AllocByID(nil, allocs[0].ID)
must.NoError(t, err)
must.MapContainsKey(t, alloc.SignedIdentities, "t")
wid := alloc.SignedIdentities["t"]
aclObj, err := c1.ResolveToken(wid)
must.NoError(t, err)
must.True(t, aclObj.AllowNodeWrite(), must.Sprintf("expected workload id to allow node write"))
}
// TestClient_ACL_ResolveToken_InvalidClaims asserts that ResolveToken properly
// rejects invalid workload identity claims.
func TestClient_ACL_ResolveToken_InvalidClaims(t *testing.T) {
ci.Parallel(t)
s1, _, rootToken, 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 minimal job
job := mock.MinJob()
allocs := testutil.WaitForRunningWithToken(t, s1.RPC, job, rootToken.SecretID)
must.Len(t, 1, allocs)
// Get wid while it's still running
alloc, err := s1.State().AllocByID(nil, allocs[0].ID)
must.NoError(t, err)
must.MapContainsKey(t, alloc.SignedIdentities, "t")
wid := alloc.SignedIdentities["t"]
// Stop job
deregArgs := structs.JobDeregisterRequest{
JobID: job.ID,
WriteRequest: structs.WriteRequest{
Region: job.Region,
Namespace: job.Namespace,
AuthToken: rootToken.SecretID,
},
}
deregReply := structs.JobDeregisterResponse{}
must.NoError(t, s1.RPC("Job.Deregister", &deregArgs, &deregReply))
cond := map[string]int{
structs.AllocClientStatusComplete: 1,
}
allocs = testutil.WaitForJobAllocStatusWithToken(t, s1.RPC, job, cond, rootToken.SecretID)
must.Len(t, 1, allocs)
// ResolveToken should error now that alloc is dead
aclObj, err := c1.ResolveToken(wid)
must.ErrorContains(t, err, "allocation is terminal")
must.Nil(t, aclObj)
}