// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 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].Equal(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() }) } }