open-vault/builtin/logical/pki/path_acme_directory.go
Steven Clark bc57865998
PKI: Initial ACME directory API support (#19803)
* PKI: Initial ACME directory API support along with basic tests for error handler and the directory itself across various paths.
2023-03-29 16:29:19 +00:00

162 lines
4.9 KiB
Go

package pki
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/hashicorp/vault/builtin/logical/pki/acme"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
const (
pathAcmeDirectoryHelpSync = `Read the proper URLs for various ACME operations`
pathAcmeDirectoryHelpDesc = `Provide an ACME directory response that contains URLS for various ACME operations.`
)
func pathAcmeRootDirectory(b *backend) *framework.Path {
return patternAcmeDirectory(b, "acme/directory", false /* requireRole */, false /* requireIssuer */)
}
func pathAcmeRoleDirectory(b *backend) *framework.Path {
return patternAcmeDirectory(b, "roles/"+framework.GenericNameRegex("role")+"/acme/directory",
true /* requireRole */, false /* requireIssuer */)
}
func pathAcmeIssuerDirectory(b *backend) *framework.Path {
return patternAcmeDirectory(b, "issuer/"+framework.GenericNameRegex(issuerRefParam)+"/acme/directory",
false /* requireRole */, true /* requireIssuer */)
}
func pathAcmeIssuerAndRoleDirectory(b *backend) *framework.Path {
return patternAcmeDirectory(b,
"issuer/"+framework.GenericNameRegex(issuerRefParam)+"/roles/"+framework.GenericNameRegex(
"role")+"/acme/directory",
true /* requireRole */, true /* requireIssuer */)
}
func patternAcmeDirectory(b *backend, pattern string, requireRole, requireIssuer bool) *framework.Path {
fields := map[string]*framework.FieldSchema{}
if requireRole {
fields["role"] = &framework.FieldSchema{
Type: framework.TypeString,
Description: `The desired role for the acme request`,
Required: true,
}
}
if requireIssuer {
fields[issuerRefParam] = &framework.FieldSchema{
Type: framework.TypeString,
Description: `Reference to an existing issuer name or issuer id`,
Required: true,
}
}
return &framework.Path{
Pattern: pattern,
Fields: fields,
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.acmeWrapper(b.acmeDirectoryHandler),
ForwardPerformanceSecondary: false,
ForwardPerformanceStandby: true,
},
},
HelpSynopsis: pathAcmeDirectoryHelpSync,
HelpDescription: pathAcmeDirectoryHelpDesc,
}
}
type acmeOperation func(acmeCtx acmeContext, r *logical.Request, _ *framework.FieldData) (*logical.Response, error)
type acmeContext struct {
baseUrl *url.URL
sc *storageContext
}
func (b *backend) acmeWrapper(op acmeOperation) framework.OperationFunc {
return acmeErrorWrapper(func(ctx context.Context, r *logical.Request, data *framework.FieldData) (*logical.Response, error) {
sc := b.makeStorageContext(ctx, r.Storage)
if false {
// TODO sclark: Check if ACME is enable here
return nil, fmt.Errorf("ACME is disabled in configuration: %w", acme.ErrServerInternal)
}
baseUrl, err := getAcmeBaseUrl(sc, r.Path)
if err != nil {
return nil, err
}
acmeCtx := acmeContext{
baseUrl: baseUrl,
sc: sc,
}
return op(acmeCtx, r, data)
})
}
func getAcmeBaseUrl(sc *storageContext, path string) (*url.URL, error) {
cfg, err := sc.getClusterConfig()
if err != nil {
return nil, fmt.Errorf("failed loading cluster config: %w", err)
}
if cfg.Path == "" {
return nil, fmt.Errorf("ACME feature requires local cluster path configuration to be set: %w", acme.ErrServerInternal)
}
baseUrl, err := url.Parse(cfg.Path)
if err != nil {
return nil, fmt.Errorf("ACME feature a proper URL configured in local cluster path: %w", acme.ErrServerInternal)
}
directoryPrefix := ""
lastIndex := strings.LastIndex(path, "/acme/")
if lastIndex != -1 {
directoryPrefix = path[0:lastIndex]
}
return baseUrl.JoinPath(directoryPrefix), nil
}
func acmeErrorWrapper(op framework.OperationFunc) framework.OperationFunc {
return func(ctx context.Context, r *logical.Request, data *framework.FieldData) (*logical.Response, error) {
resp, err := op(ctx, r, data)
if err != nil {
return acme.TranslateError(err)
}
return resp, nil
}
}
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(),
"meta": map[string]interface{}{
"externalAccountRequired": false,
},
})
if err != nil {
return nil, fmt.Errorf("failed encoding response: %w", err)
}
return &logical.Response{
Data: map[string]interface{}{
logical.HTTPContentType: "application/json",
logical.HTTPStatusCode: http.StatusOK,
logical.HTTPRawBody: rawBody,
},
}, nil
}