Rework the ACME test suite to use full Vault cluster to validate behavior (#19874)
- Instead of using tests that just test the plugin storage/interface layer, use a full Vault instance to validate that we can send/receive the proper headers and responses back to a client. - Found an issue with HEAD new-nounce api calls returning 500 errors. - Add the /acme/ suffix to the baseUrl in the acme context so we don't have to keep adding it a bit everywhere.
This commit is contained in:
parent
b1c557bd73
commit
8ddead7a17
|
@ -106,7 +106,7 @@ func getAcmeBaseUrl(sc *storageContext, path string) (*url.URL, error) {
|
|||
directoryPrefix = path[0:lastIndex]
|
||||
}
|
||||
|
||||
return baseUrl.JoinPath(directoryPrefix), nil
|
||||
return baseUrl.JoinPath(directoryPrefix, "/acme/"), nil
|
||||
}
|
||||
|
||||
func acmeErrorWrapper(op framework.OperationFunc) framework.OperationFunc {
|
||||
|
@ -122,11 +122,11 @@ func acmeErrorWrapper(op framework.OperationFunc) framework.OperationFunc {
|
|||
|
||||
func (b *backend) acmeDirectoryHandler(acmeCtx acmeContext, r *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
rawBody, err := json.Marshal(map[string]interface{}{
|
||||
"newNonce": acmeCtx.baseUrl.JoinPath("/acme/new-nonce").String(),
|
||||
"newAccount": acmeCtx.baseUrl.JoinPath("/acme/new-account").String(),
|
||||
"newOrder": acmeCtx.baseUrl.JoinPath("/acme/new-order").String(),
|
||||
"revokeCert": acmeCtx.baseUrl.JoinPath("/acme/revoke-cert").String(),
|
||||
"keyChange": acmeCtx.baseUrl.JoinPath("/acme/key-change").String(),
|
||||
"newNonce": acmeCtx.baseUrl.JoinPath("new-nonce").String(),
|
||||
"newAccount": acmeCtx.baseUrl.JoinPath("new-account").String(),
|
||||
"newOrder": acmeCtx.baseUrl.JoinPath("new-order").String(),
|
||||
"revokeCert": acmeCtx.baseUrl.JoinPath("revoke-cert").String(),
|
||||
"keyChange": acmeCtx.baseUrl.JoinPath("key-change").String(),
|
||||
"meta": map[string]interface{}{
|
||||
"externalAccountRequired": false,
|
||||
},
|
||||
|
|
|
@ -169,7 +169,7 @@ func (b *backend) acmeNewAccountSearchHandler(acmeCtx acmeContext, r *logical.Re
|
|||
return nil, fmt.Errorf("error loading account: %w", err)
|
||||
}
|
||||
|
||||
location := acmeCtx.baseUrl.String() + "/acme/account/" + userCtx.Kid
|
||||
location := acmeCtx.baseUrl.String() + "account/" + userCtx.Kid
|
||||
return formatAccountResponse(location, account["status"].(string), account["contact"].([]string)), nil
|
||||
}
|
||||
|
||||
|
@ -201,6 +201,6 @@ func (b *backend) acmeNewAccountCreateHandler(acmeCtx acmeContext, r *logical.Re
|
|||
return nil, fmt.Errorf("failed to create account: %w", err)
|
||||
}
|
||||
|
||||
location := acmeCtx.baseUrl.String() + "/acme/account/" + userCtx.Kid
|
||||
location := acmeCtx.baseUrl.String() + "account/" + userCtx.Kid
|
||||
return formatAccountResponse(location, account["status"].(string), account["contact"].([]string)), nil
|
||||
}
|
||||
|
|
|
@ -71,11 +71,14 @@ func (b *backend) acmeNonceHandler(ctx acmeContext, r *logical.Request, _ *frame
|
|||
},
|
||||
Data: map[string]interface{}{
|
||||
logical.HTTPStatusCode: httpStatus,
|
||||
// Get around Vault limitation of requiring a body set if the status is not http.StatusNoContent
|
||||
// for our HEAD request responses.
|
||||
logical.HTTPContentType: "",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func genAcmeLinkHeader(ctx acmeContext) []string {
|
||||
path := fmt.Sprintf("<%s>;rel=\"index\"", ctx.baseUrl.JoinPath("/acme/directory").String())
|
||||
path := fmt.Sprintf("<%s>;rel=\"index\"", ctx.baseUrl.JoinPath("directory").String())
|
||||
return []string{path}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
vaulthttp "github.com/hashicorp/vault/http"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -15,28 +21,29 @@ import (
|
|||
// are available and produce the correct responses.
|
||||
func TestAcmeDirectory(t *testing.T) {
|
||||
t.Parallel()
|
||||
b, s, pathConfig := setupAcmeBackend(t)
|
||||
cluster, client, pathConfig := setupAcmeBackend(t)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
prefixUrl string
|
||||
directoryUrl string
|
||||
}{
|
||||
{"root", "", "acme/directory"},
|
||||
{"role", "/roles/test-role", "roles/test-role/acme/directory"},
|
||||
{"issuer", "/issuer/default", "issuer/default/acme/directory"},
|
||||
{"issuer_role", "/issuer/default/roles/test-role", "issuer/default/roles/test-role/acme/directory"},
|
||||
{"issuer_role_acme", "/issuer/acme/roles/acme", "issuer/acme/roles/acme/acme/directory"},
|
||||
{"root", "", "pki/acme/directory"},
|
||||
{"role", "/roles/test-role", "pki/roles/test-role/acme/directory"},
|
||||
{"issuer", "/issuer/default", "pki/issuer/default/acme/directory"},
|
||||
{"issuer_role", "/issuer/default/roles/test-role", "pki/issuer/default/roles/test-role/acme/directory"},
|
||||
{"issuer_role_acme", "/issuer/acme/roles/acme", "pki/issuer/acme/roles/acme/acme/directory"},
|
||||
}
|
||||
testCtx := context.Background()
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dirResp, err := CBRead(b, s, tc.directoryUrl)
|
||||
dirResp, err := client.Logical().ReadRawWithContext(testCtx, tc.directoryUrl)
|
||||
require.NoError(t, err, "failed reading ACME directory configuration")
|
||||
|
||||
require.Contains(t, dirResp.Data, "http_content_type", "missing Content-Type header")
|
||||
require.Contains(t, dirResp.Data["http_content_type"], "application/json",
|
||||
"missing appropriate content type in header")
|
||||
require.Equal(t, 200, dirResp.StatusCode)
|
||||
require.Equal(t, "application/json", dirResp.Header.Get("Content-Type"))
|
||||
|
||||
requiredUrls := map[string]string{
|
||||
"newNonce": pathConfig + tc.prefixUrl + "/acme/new-nonce",
|
||||
|
@ -46,7 +53,10 @@ func TestAcmeDirectory(t *testing.T) {
|
|||
"keyChange": pathConfig + tc.prefixUrl + "/acme/key-change",
|
||||
}
|
||||
|
||||
rawBodyBytes := dirResp.Data["http_raw_body"].([]byte)
|
||||
rawBodyBytes, err := io.ReadAll(dirResp.Body)
|
||||
require.NoError(t, err, "failed reading from directory response body")
|
||||
_ = dirResp.Body.Close()
|
||||
|
||||
respType := map[string]interface{}{}
|
||||
err = json.Unmarshal(rawBodyBytes, &respType)
|
||||
require.NoError(t, err, "failed unmarshalling ACME directory response body")
|
||||
|
@ -59,60 +69,60 @@ func TestAcmeDirectory(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestAcmeNonce a basic test that will validate we get back a nonce with the proper status codes
|
||||
// based on the
|
||||
func TestAcmeNonce(t *testing.T) {
|
||||
t.Parallel()
|
||||
b, s, pathConfig := setupAcmeBackend(t)
|
||||
cluster, client, pathConfig := setupAcmeBackend(t)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
prefixUrl string
|
||||
directoryUrl string
|
||||
}{
|
||||
{"root", "", "acme/new-nonce"},
|
||||
{"role", "/roles/test-role", "roles/test-role/acme/new-nonce"},
|
||||
{"issuer", "/issuer/default", "issuer/default/acme/new-nonce"},
|
||||
{"issuer_role", "/issuer/default/roles/test-role", "issuer/default/roles/test-role/acme/new-nonce"},
|
||||
{"root", "", "pki/acme/new-nonce"},
|
||||
{"role", "/roles/test-role", "pki/roles/test-role/acme/new-nonce"},
|
||||
{"issuer", "/issuer/default", "pki/issuer/default/acme/new-nonce"},
|
||||
{"issuer_role", "/issuer/default/roles/test-role", "pki/issuer/default/roles/test-role/acme/new-nonce"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
for _, httpOp := range []string{"get", "header"} {
|
||||
t.Run(fmt.Sprintf("%s-%s", tc.name, httpOp), func(t *testing.T) {
|
||||
var resp *logical.Response
|
||||
var err error
|
||||
var req *api.Request
|
||||
switch httpOp {
|
||||
case "get":
|
||||
resp, err = CBRead(b, s, tc.directoryUrl)
|
||||
req = client.NewRequest(http.MethodGet, "/v1/"+tc.directoryUrl)
|
||||
case "header":
|
||||
resp, err = CBHeader(b, s, tc.directoryUrl)
|
||||
req = client.NewRequest(http.MethodHead, "/v1/"+tc.directoryUrl)
|
||||
}
|
||||
require.NoError(t, err, "failed %s op for new-nouce", httpOp)
|
||||
res, err := client.RawRequestWithContext(ctx, req)
|
||||
require.NoError(t, err, "failed sending raw request")
|
||||
_ = res.Body.Close()
|
||||
|
||||
// Proper Status Code
|
||||
switch httpOp {
|
||||
case "get":
|
||||
require.Equal(t, http.StatusNoContent, resp.Data["http_status_code"])
|
||||
require.Equal(t, http.StatusNoContent, res.StatusCode)
|
||||
case "header":
|
||||
require.Equal(t, http.StatusOK, resp.Data["http_status_code"])
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
}
|
||||
|
||||
// Make sure we don't have a Content-Type header.
|
||||
require.Equal(t, "", res.Header.Get("Content-Type"))
|
||||
|
||||
// Make sure we return the Cache-Control header
|
||||
require.Contains(t, resp.Headers, "Cache-Control", "missing Cache-Control header")
|
||||
require.Contains(t, resp.Headers["Cache-Control"], "no-store",
|
||||
require.Contains(t, res.Header.Get("Cache-Control"), "no-store",
|
||||
"missing Cache-Control header with no-store header value")
|
||||
require.Len(t, resp.Headers["Cache-Control"], 1,
|
||||
"Cache-Control header should have only a single header")
|
||||
|
||||
// Test for our nonce header value
|
||||
require.Contains(t, resp.Headers, "Replay-Nonce", "missing Replay-Nonce header")
|
||||
require.NotEmpty(t, resp.Headers["Replay-Nonce"], "missing Replay-Nonce header with an actual value")
|
||||
require.Len(t, resp.Headers["Replay-Nonce"], 1,
|
||||
"Replay-Nonce header should have only a single header")
|
||||
require.NotEmpty(t, res.Header.Get("Replay-Nonce"), "missing Replay-Nonce header with an actual value")
|
||||
|
||||
// Test Link header value
|
||||
require.Contains(t, resp.Headers, "Link", "missing Link header")
|
||||
expectedLinkHeader := fmt.Sprintf("<%s>;rel=\"index\"", pathConfig+tc.prefixUrl+"/acme/directory")
|
||||
require.Contains(t, resp.Headers["Link"], expectedLinkHeader,
|
||||
require.Contains(t, res.Header.Get("Link"), expectedLinkHeader,
|
||||
"different value for link header than expected")
|
||||
require.Len(t, resp.Headers["Link"], 1, "Link header should have only a single header")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -121,31 +131,33 @@ func TestAcmeNonce(t *testing.T) {
|
|||
// TestAcmeClusterPathNotConfigured basic testing of the ACME error handler.
|
||||
func TestAcmeClusterPathNotConfigured(t *testing.T) {
|
||||
t.Parallel()
|
||||
b, s := CreateBackendWithStorage(t)
|
||||
cluster, client := setupTestPkiCluster(t)
|
||||
defer cluster.Cleanup()
|
||||
|
||||
// Do not fill in the path option within the local cluster configuration
|
||||
cases := []struct {
|
||||
name string
|
||||
directoryUrl string
|
||||
}{
|
||||
{"root", "acme/directory"},
|
||||
{"role", "roles/test-role/acme/directory"},
|
||||
{"issuer", "issuer/default/acme/directory"},
|
||||
{"issuer_role", "issuer/default/roles/test-role/acme/directory"},
|
||||
{"root", "pki/acme/directory"},
|
||||
{"role", "pki/roles/test-role/acme/directory"},
|
||||
{"issuer", "pki/issuer/default/acme/directory"},
|
||||
{"issuer_role", "pki/issuer/default/roles/test-role/acme/directory"},
|
||||
}
|
||||
testCtx := context.Background()
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dirResp, err := CBRead(b, s, tc.directoryUrl)
|
||||
require.NoError(t, err, "failed reading ACME directory configuration")
|
||||
dirResp, err := client.Logical().ReadRawWithContext(testCtx, tc.directoryUrl)
|
||||
require.Error(t, err, "expected failure reading ACME directory configuration got none")
|
||||
|
||||
require.Contains(t, dirResp.Data, "http_content_type", "missing Content-Type header")
|
||||
require.Contains(t, dirResp.Data["http_content_type"], "application/problem+json",
|
||||
"missing appropriate content type in header")
|
||||
require.Equal(t, "application/problem+json", dirResp.Header.Get("Content-Type"))
|
||||
require.Equal(t, http.StatusInternalServerError, dirResp.StatusCode)
|
||||
|
||||
require.Equal(t, http.StatusInternalServerError, dirResp.Data["http_status_code"])
|
||||
rawBodyBytes, err := io.ReadAll(dirResp.Body)
|
||||
require.NoError(t, err, "failed reading from directory response body")
|
||||
_ = dirResp.Body.Close()
|
||||
|
||||
require.Contains(t, dirResp.Data, "http_raw_body", "missing http_raw_body from data")
|
||||
rawBodyBytes := dirResp.Data["http_raw_body"].([]byte)
|
||||
respType := map[string]interface{}{}
|
||||
err = json.Unmarshal(rawBodyBytes, &respType)
|
||||
require.NoError(t, err, "failed unmarshalling ACME directory response body")
|
||||
|
@ -156,16 +168,38 @@ func TestAcmeClusterPathNotConfigured(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func setupAcmeBackend(t *testing.T) (*backend, logical.Storage, string) {
|
||||
b, s := CreateBackendWithStorage(t)
|
||||
func setupAcmeBackend(t *testing.T) (*vault.TestCluster, *api.Client, string) {
|
||||
cluster, client := setupTestPkiCluster(t)
|
||||
|
||||
// Setting templated AIAs should succeed.
|
||||
pathConfig := "https://localhost:8200/v1/pki"
|
||||
|
||||
_, err := CBWrite(b, s, "config/cluster", map[string]interface{}{
|
||||
_, err := client.Logical().WriteWithContext(context.Background(), "pki/config/cluster", map[string]interface{}{
|
||||
"path": pathConfig,
|
||||
"aia_path": "http://localhost:8200/cdn/pki",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return b, s, pathConfig
|
||||
|
||||
// Allow certain headers to pass through for ACME support
|
||||
_, err = client.Logical().WriteWithContext(context.Background(), "sys/mounts/pki/tune", map[string]interface{}{
|
||||
"allowed_response_headers": []string{"Last-Modified", "Replay-Nonce", "Link"},
|
||||
})
|
||||
require.NoError(t, err, "failed tuning mount response headers")
|
||||
|
||||
return cluster, client, pathConfig
|
||||
}
|
||||
|
||||
func setupTestPkiCluster(t *testing.T) (*vault.TestCluster, *api.Client) {
|
||||
coreConfig := &vault.CoreConfig{
|
||||
LogicalBackends: map[string]logical.Factory{
|
||||
"pki": Factory,
|
||||
},
|
||||
}
|
||||
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
})
|
||||
cluster.Start()
|
||||
client := cluster.Cores[0].Client
|
||||
mountPKIEndpoint(t, client, "pki")
|
||||
return cluster, client
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue