open-nomad/nomad/state/state_store_acl_test.go
James Rasell 581a5bb6ad
rpc: add ACL Role RPC endpoint for CRUD actions.
New ACL Role RPC endpoints have been created to allow the creation,
update, read, and deletion of ACL roles. All endpoints require a
management token; in the future readers will also be allowed to
view roles associated to their ACL token.

The create endpoint in particular is responsible for deduplicating
ACL policy links and ensuring named policies are found within
state. This is done within the RPC handler so we perform a single
loop through the links for slight efficiency.
2022-08-11 08:43:50 +01:00

491 lines
16 KiB
Go

package state
import (
"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)
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))
// 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))
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}))
// 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")
}
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)
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)
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))
}
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))
// 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))
// 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))
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))
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))
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)
}