Files
nomad/e2e/acl/acl_token_test.go
Tim Gross c144af2823 E2E: fix expected error message from token expiration test (#19119)
In Nomad 1.5 we started masking the specific error returned from the
authentication method and returned the "permission denied" error instead. Update
the E2E test that covers token expiration to ensure we're asserting the correct
error here.

Fixes: https://github.com/hashicorp/nomad/issues/16803
2023-11-17 14:00:47 -05:00

330 lines
12 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
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/shoenig/test/must"
)
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)
must.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)
must.ErrorContains(t, err, fmt.Sprintf(
"expiration time cannot be less than %s in the future", minTokenExpiryDur))
must.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)
must.ErrorContains(t, err, fmt.Sprintf(
"expiration time cannot be more than %s in the future", maxTokenExpiryDur))
must.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)
must.NoError(t, err)
must.NotNil(t, tokenNormalExpiryCreateResp)
must.Eq(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)
must.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)
must.NoError(t, err)
must.NotNil(t, tokenQuickExpiryCreateResp)
must.Eq(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)
must.ErrorContains(t, err, "Permission denied")
must.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)
must.NoError(t, err)
var quickExpiryFound, normalExpiryFound bool
for _, token := range tokenListResp {
switch token.AccessorID {
case tokenQuickExpiryCreateResp.AccessorID:
quickExpiryFound = true
must.NotNil(t, token.ExpirationTime)
case tokenNormalExpiryCreateResp.AccessorID:
normalExpiryFound = true
must.NotNil(t, token.ExpirationTime)
default:
continue
}
}
must.True(t, quickExpiryFound)
must.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)
must.NoError(t, err)
tokenNormalExpiryReadResp, _, err := nomadClient.ACLTokens().Info(tokenNormalExpiryCreateResp.AccessorID, nil)
must.ErrorContains(t, err, "ACL token not found")
must.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)
must.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)
must.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)
must.NoError(t, err)
must.NotNil(t, aclRoleCreateResp)
must.NotEq(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)
must.NoError(t, err)
must.NotNil(t, aclTokenCreateResp)
must.NotEq(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)
must.NoError(t, err)
nodeStubList, _, err := nomadClient.Nodes().List(&defaultNSQueryMeta)
must.ErrorContains(t, err, "Permission denied")
must.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)
must.NoError(t, err)
must.Eq(t, aclTokenUpdateResp.SecretID, aclTokenCreateResp.SecretID)
// Test that the token can now read the default namespace and node objects.
jobListResp, _, err = nomadClient.Jobs().List(&defaultNSQueryMeta)
must.NoError(t, err)
nodeStubList, _, err = nomadClient.Nodes().List(&defaultNSQueryMeta)
must.NoError(t, err)
must.Greater(t, 0, len(nodeStubList))
// Remove the policy assignment from the token.
newToken.Policies = []string{}
aclTokenUpdateResp, _, err = nomadClient.ACLTokens().Update(newToken, nil)
must.NoError(t, err)
must.Eq(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)
must.ErrorContains(t, err, "Permission denied")
must.Nil(t, jobListResp)
nodeStubList, _, err = nomadClient.Nodes().List(&defaultNSQueryMeta)
must.NoError(t, err)
must.Greater(t, 0, len(nodeStubList))
// 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)
must.ErrorContains(t, err, "client token missing policies or roles")
must.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)
must.NoError(t, err)
must.NotNil(t, aclTokenCreateResp)
must.NotEq(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)
must.NoError(t, err)
nodeStubList, _, err = nomadClient.Nodes().List(&defaultNSQueryMeta)
must.NoError(t, err)
must.Greater(t, 0, len(nodeStubList))
// 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)
must.NoError(t, err)
cleanUpProcess.Remove(defaultNamespacePolicy.Name, ACLPolicyTestResourceType)
_, err = nomadClient.ACLRoles().Delete(aclRoleCreateResp.ID, nil)
must.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)
must.ErrorContains(t, err, "Permission denied")
must.Nil(t, jobListResp)
nodeStubList, _, err = nomadClient.Nodes().List(&defaultNSQueryMeta)
must.ErrorContains(t, err, "Permission denied")
must.Nil(t, nodeStubList)
}