// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package acl import ( "fmt" "testing" "time" "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/e2e/e2eutil" "github.com/hashicorp/nomad/helper/uuid" "github.com/stretchr/testify/require" ) var ( minTokenExpiryDur = 1 * time.Second maxTokenExpiryDur = 24 * time.Hour ) // testACLTokenExpiration tests tokens that have expiration values set. // Expirations are timing based which makes this test sensitive to timing // problems when running the E2E suite. // // When running the test, the Nomad server ACL config must have the // token_min_expiration_ttl value set to minTokenExpiryDur. The // token_min_expiration_ttl value must be set to maxTokenExpiryDur if this // value differs from the default. This is so we can test expired tokens, // without blocking for an extended period of time as well as other time // related aspects. func testACLTokenExpiration(t *testing.T) { nomadClient := e2eutil.NomadClient(t) // Create and defer the Cleanup process. This is used to remove all // resources created by this test and covers situations where the test // fails or during normal running. cleanUpProcess := NewCleanup() defer cleanUpProcess.Run(t, nomadClient) // Create an ACL policy which will be assigned to the created ACL tokens. customNamespacePolicy := api.ACLPolicy{ Name: "e2e-acl-" + uuid.Short(), Description: "E2E ACL Role Testing", Rules: `namespace "default" {policy = "read"}`, } _, err := nomadClient.ACLPolicies().Upsert(&customNamespacePolicy, nil) require.NoError(t, err) cleanUpProcess.Add(customNamespacePolicy.Name, ACLPolicyTestResourceType) // Create our default query options which can be used when testing a token // against the API. The caller should update the auth token as needed. defaultNSQueryMeta := api.QueryOptions{Namespace: "default"} // Attempt to create a token with a lower than acceptable TTL and ensure we // get an error. tokenTTLLow := api.ACLToken{ Name: "e2e-acl-" + uuid.Short(), Type: "client", Policies: []string{customNamespacePolicy.Name}, ExpirationTTL: minTokenExpiryDur / 2, } aclTokenCreateResp, _, err := nomadClient.ACLTokens().Create(&tokenTTLLow, nil) require.ErrorContains(t, err, fmt.Sprintf( "expiration time cannot be less than %s in the future", minTokenExpiryDur)) require.Nil(t, aclTokenCreateResp) // Attempt to create a token with a higher than acceptable TTL and ensure // we get an error. tokenTTLHigh := api.ACLToken{ Name: "e2e-acl-" + uuid.Short(), Type: "client", Policies: []string{customNamespacePolicy.Name}, ExpirationTTL: 8766 * time.Hour, } aclTokenCreateResp, _, err = nomadClient.ACLTokens().Create(&tokenTTLHigh, nil) require.ErrorContains(t, err, fmt.Sprintf( "expiration time cannot be more than %s in the future", maxTokenExpiryDur)) require.Nil(t, aclTokenCreateResp) // Create an ACL token that has a fairly standard TTL that will allow us to // make successful calls without it expiring. tokenNormalExpiry := api.ACLToken{ Name: "e2e-acl-" + uuid.Short(), Type: "client", Policies: []string{customNamespacePolicy.Name}, ExpirationTTL: 10 * time.Minute, } tokenNormalExpiryCreateResp, _, err := nomadClient.ACLTokens().Create(&tokenNormalExpiry, nil) require.NoError(t, err) require.NotNil(t, tokenNormalExpiryCreateResp) require.Equal(t, *tokenNormalExpiryCreateResp.ExpirationTime, tokenNormalExpiryCreateResp.CreateTime.Add(tokenNormalExpiryCreateResp.ExpirationTTL)) cleanUpProcess.Add(tokenNormalExpiryCreateResp.AccessorID, ACLTokenTestResourceType) // Add the token to our query options and ensure we can now list jobs with // the default namespace. defaultNSQueryMeta.AuthToken = tokenNormalExpiryCreateResp.SecretID jobListResp, _, err := nomadClient.Jobs().List(&defaultNSQueryMeta) require.NoError(t, err) // Create an ACL token with the lowest expiry TTL possible, so it will // expire almost immediately. tokenQuickExpiry := api.ACLToken{ Name: "e2e-acl-" + uuid.Short(), Type: "client", Policies: []string{customNamespacePolicy.Name}, ExpirationTTL: minTokenExpiryDur, } tokenQuickExpiryCreateResp, _, err := nomadClient.ACLTokens().Create(&tokenQuickExpiry, nil) require.NoError(t, err) require.NotNil(t, tokenQuickExpiryCreateResp) require.Equal(t, *tokenQuickExpiryCreateResp.ExpirationTime, tokenQuickExpiryCreateResp.CreateTime.Add(tokenQuickExpiryCreateResp.ExpirationTTL)) cleanUpProcess.Add(tokenQuickExpiryCreateResp.AccessorID, ACLTokenTestResourceType) // Block the test (sorry) until the token has expired. time.Sleep(tokenQuickExpiry.ExpirationTTL) // Update our query options and attempt to list the job using our expired // token. defaultNSQueryMeta.AuthToken = tokenQuickExpiryCreateResp.SecretID jobListResp, _, err = nomadClient.Jobs().List(&defaultNSQueryMeta) require.ErrorContains(t, err, "ACL token expired") require.Nil(t, jobListResp) cleanUpProcess.Remove(tokenQuickExpiryCreateResp.AccessorID, ACLTokenTestResourceType) // List the tokens to ensure the output correctly shows the token // expiration. Other tests may have left tokens in state, so do not perform // a length check. tokenListResp, _, err := nomadClient.ACLTokens().List(nil) require.NoError(t, err) var quickExpiryFound, normalExpiryFound bool for _, token := range tokenListResp { switch token.AccessorID { case tokenQuickExpiryCreateResp.AccessorID: quickExpiryFound = true require.NotNil(t, token.ExpirationTime) case tokenNormalExpiryCreateResp.AccessorID: normalExpiryFound = true require.NotNil(t, token.ExpirationTime) default: continue } } require.True(t, quickExpiryFound) require.True(t, normalExpiryFound) // Ensure we can manually delete unexpired tokens and that they are // immediately removed from state. _, err = nomadClient.ACLTokens().Delete(tokenNormalExpiryCreateResp.AccessorID, nil) require.NoError(t, err) tokenNormalExpiryReadResp, _, err := nomadClient.ACLTokens().Info(tokenNormalExpiryCreateResp.AccessorID, nil) require.ErrorContains(t, err, "ACL token not found") require.Nil(t, tokenNormalExpiryReadResp) cleanUpProcess.Remove(tokenNormalExpiryCreateResp.AccessorID, ACLTokenTestResourceType) } // testACLTokenRolePolicyAssignment tests that tokens allow and have the // expected permissions when created or updated with a combination of role and // policy assignments. func testACLTokenRolePolicyAssignment(t *testing.T) { nomadClient := e2eutil.NomadClient(t) // Create and defer the Cleanup process. This is used to remove all // resources created by this test and covers situations where the test // fails or during normal running. cleanUpProcess := NewCleanup() defer cleanUpProcess.Run(t, nomadClient) // Create two ACL policies which will be used throughout this test. One // grants read access to the default namespace, the other grants read // access to node objects. defaultNamespacePolicy := api.ACLPolicy{ Name: "e2e-acl-" + uuid.Short(), Description: "E2E ACL Role Testing", Rules: `namespace "default" {policy = "read"}`, } _, err := nomadClient.ACLPolicies().Upsert(&defaultNamespacePolicy, nil) require.NoError(t, err) cleanUpProcess.Add(defaultNamespacePolicy.Name, ACLPolicyTestResourceType) nodePolicy := api.ACLPolicy{ Name: "e2e-acl-" + uuid.Short(), Description: "E2E ACL Role Testing", Rules: `node { policy = "read" }`, } _, err = nomadClient.ACLPolicies().Upsert(&nodePolicy, nil) require.NoError(t, err) cleanUpProcess.Add(nodePolicy.Name, ACLPolicyTestResourceType) // Create an ACL role that has the node read policy assigned. aclRole := api.ACLRole{ Name: "e2e-acl-" + uuid.Short(), Description: "E2E ACL Role Testing", Policies: []*api.ACLRolePolicyLink{{Name: nodePolicy.Name}}, } aclRoleCreateResp, _, err := nomadClient.ACLRoles().Create(&aclRole, nil) require.NoError(t, err) require.NotNil(t, aclRoleCreateResp) require.NotEmpty(t, aclRoleCreateResp.ID) cleanUpProcess.Add(aclRoleCreateResp.ID, ACLRoleTestResourceType) // Create an ACL token which only has the ACL policy which allows reading // the default namespace assigned. token := api.ACLToken{ Name: "e2e-acl-" + uuid.Short(), Type: "client", Policies: []string{defaultNamespacePolicy.Name}, } aclTokenCreateResp, _, err := nomadClient.ACLTokens().Create(&token, nil) require.NoError(t, err) require.NotNil(t, aclTokenCreateResp) require.NotEmpty(t, aclTokenCreateResp.SecretID) cleanUpProcess.Add(aclTokenCreateResp.AccessorID, ACLTokenTestResourceType) // Test that the token can read the default namespace, but that it cannot // read node objects. defaultNSQueryMeta := api.QueryOptions{Namespace: "default", AuthToken: aclTokenCreateResp.SecretID} jobListResp, _, err := nomadClient.Jobs().List(&defaultNSQueryMeta) require.NoError(t, err) nodeStubList, _, err := nomadClient.Nodes().List(&defaultNSQueryMeta) require.ErrorContains(t, err, "Permission denied") require.Nil(t, nodeStubList) // Update the token to also include the ACL role which will allow reading // node objects. newToken := aclTokenCreateResp newToken.Roles = []*api.ACLTokenRoleLink{{ID: aclRoleCreateResp.ID}} aclTokenUpdateResp, _, err := nomadClient.ACLTokens().Update(newToken, nil) require.NoError(t, err) require.Equal(t, aclTokenUpdateResp.SecretID, aclTokenCreateResp.SecretID) // Test that the token can now read the default namespace and node objects. jobListResp, _, err = nomadClient.Jobs().List(&defaultNSQueryMeta) require.NoError(t, err) nodeStubList, _, err = nomadClient.Nodes().List(&defaultNSQueryMeta) require.NoError(t, err) require.Greater(t, len(nodeStubList), 0) // Remove the policy assignment from the token. newToken.Policies = []string{} aclTokenUpdateResp, _, err = nomadClient.ACLTokens().Update(newToken, nil) require.NoError(t, err) require.Equal(t, aclTokenUpdateResp.SecretID, aclTokenCreateResp.SecretID) // Test that the token can now only read node objects and not the default // namespace. jobListResp, _, err = nomadClient.Jobs().List(&defaultNSQueryMeta) require.ErrorContains(t, err, "Permission denied") require.Nil(t, jobListResp) nodeStubList, _, err = nomadClient.Nodes().List(&defaultNSQueryMeta) require.NoError(t, err) require.Greater(t, len(nodeStubList), 0) // Try and remove the role assignment which should result in a validation // error as it needs to include either a policy or role linking. newToken.Roles = nil aclTokenUpdateResp, _, err = nomadClient.ACLTokens().Update(newToken, nil) require.ErrorContains(t, err, "client token missing policies or roles") require.Nil(t, aclTokenUpdateResp) // Create a new token that has both the role and policy linking in place. token = api.ACLToken{ Name: "e2e-acl-" + uuid.Short(), Type: "client", Policies: []string{defaultNamespacePolicy.Name}, Roles: []*api.ACLTokenRoleLink{{ID: aclRoleCreateResp.ID}}, } aclTokenCreateResp, _, err = nomadClient.ACLTokens().Create(&token, nil) require.NoError(t, err) require.NotNil(t, aclTokenCreateResp) require.NotEmpty(t, aclTokenCreateResp.SecretID) cleanUpProcess.Add(aclTokenCreateResp.AccessorID, ACLTokenTestResourceType) // Test that the token is working as expected. defaultNSQueryMeta.AuthToken = aclTokenCreateResp.SecretID jobListResp, _, err = nomadClient.Jobs().List(&defaultNSQueryMeta) require.NoError(t, err) nodeStubList, _, err = nomadClient.Nodes().List(&defaultNSQueryMeta) require.NoError(t, err) require.Greater(t, len(nodeStubList), 0) // Now delete both the policy and the role from underneath the token. This // differs to the graceful approaches above where the token was modified to // remove the assignment. _, err = nomadClient.ACLPolicies().Delete(defaultNamespacePolicy.Name, nil) require.NoError(t, err) cleanUpProcess.Remove(defaultNamespacePolicy.Name, ACLPolicyTestResourceType) _, err = nomadClient.ACLRoles().Delete(aclRoleCreateResp.ID, nil) require.NoError(t, err) cleanUpProcess.Remove(aclRoleCreateResp.ID, ACLRoleTestResourceType) // The token now should not have any power here; quite different to // Gandalf's power over the spell on King Theoden. jobListResp, _, err = nomadClient.Jobs().List(&defaultNSQueryMeta) require.ErrorContains(t, err, "Permission denied") require.Nil(t, jobListResp) nodeStubList, _, err = nomadClient.Nodes().List(&defaultNSQueryMeta) require.ErrorContains(t, err, "Permission denied") require.Nil(t, nodeStubList) }