mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 09:42:25 +00:00
Removing the timeout logic from raw-response functions and adding documentation comments. The following functions are affected: - `ReadRaw` - `ReadRawWithContext` (newly added) - `ReadRawWithData` - `ReadRawWithDataWithContext` The previous logic of using `ctx, _ = c.c.withConfiguredTimeout(ctx)` could cause a potential [context leak](https://pkg.go.dev/context): > Failing to call the CancelFunc leaks the child and its children until the parent is canceled or the timer fires. The go vet tool checks that CancelFuncs are used on all control-flow paths. Cancelling the context would have caused more issues since the context would be cancelled before the request body is closed. Resolves: #18658
286 lines
8.1 KiB
Go
286 lines
8.1 KiB
Go
/*
|
|
* The healthcheck package attempts to allow generic checks of arbitrary
|
|
* engines, while providing a common framework with some performance
|
|
* efficiencies in mind.
|
|
*
|
|
* The core of this package is the Executor context; a caller would
|
|
* provision a set of checks, an API client, and a configuration,
|
|
* which the executor would use to decide which checks to execute
|
|
* and how.
|
|
*
|
|
* Checks are based around a series of remote paths that are fetched by
|
|
* the client; these are broken into two categories: static paths, which
|
|
* can always be fetched; and dynamic paths, which the check fetches based
|
|
* on earlier results.
|
|
*
|
|
* For instance, a basic PKI CA lifetime check will have static fetch against
|
|
* the list of CAs, and a dynamic fetch, using that earlier list, to fetch the
|
|
* PEMs of all CAs.
|
|
*
|
|
* This allows health checks to share data: many PKI checks will need the
|
|
* issuer list and so repeatedly fetching this may result in a performance
|
|
* impact.
|
|
*/
|
|
|
|
package healthcheck
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/vault/api"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
type Executor struct {
|
|
Client *api.Client
|
|
Mount string
|
|
DefaultEnabled bool
|
|
|
|
Config map[string]map[string]interface{}
|
|
|
|
Resources map[string]map[logical.Operation]*PathFetch
|
|
|
|
Checkers []Check
|
|
}
|
|
|
|
func NewExecutor(client *api.Client, mount string) *Executor {
|
|
return &Executor{
|
|
Client: client,
|
|
DefaultEnabled: true,
|
|
Mount: mount,
|
|
Config: make(map[string]map[string]interface{}),
|
|
Resources: make(map[string]map[logical.Operation]*PathFetch),
|
|
}
|
|
}
|
|
|
|
func (e *Executor) AddCheck(c Check) {
|
|
e.Checkers = append(e.Checkers, c)
|
|
}
|
|
|
|
func (e *Executor) BuildConfig(external map[string]interface{}) error {
|
|
merged := e.Config
|
|
|
|
for index, checker := range e.Checkers {
|
|
name := checker.Name()
|
|
if _, present := merged[name]; name == "" || present {
|
|
return fmt.Errorf("bad checker %v: name is empty or already present: %v", index, name)
|
|
}
|
|
|
|
// Fetch the default configuration; if the check returns enabled
|
|
// status, verify it matches our expectations (in the event it should
|
|
// be disabled by default), otherwise, add it in.
|
|
config := checker.DefaultConfig()
|
|
enabled, present := config["enabled"]
|
|
if !present {
|
|
config["enabled"] = e.DefaultEnabled
|
|
} else if enabled.(bool) && !e.DefaultEnabled {
|
|
config["enabled"] = e.DefaultEnabled
|
|
}
|
|
|
|
// Now apply any external config for this check.
|
|
if econfig, present := external[name]; present {
|
|
for param, evalue := range econfig.(map[string]interface{}) {
|
|
if _, ok := config[param]; !ok {
|
|
// Assumption: default configs have all possible
|
|
// configuration options. This external config has
|
|
// an unknown option, so we want to error out.
|
|
return fmt.Errorf("unknown configuration option for %v: %v", name, param)
|
|
}
|
|
|
|
config[param] = evalue
|
|
}
|
|
}
|
|
|
|
// Now apply it and save it.
|
|
if err := checker.LoadConfig(config); err != nil {
|
|
return fmt.Errorf("error saving merged config for %v: %w", name, err)
|
|
}
|
|
merged[name] = config
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *Executor) Execute() (map[string][]*Result, error) {
|
|
ret := make(map[string][]*Result)
|
|
for _, checker := range e.Checkers {
|
|
if !checker.IsEnabled() {
|
|
continue
|
|
}
|
|
|
|
if err := checker.FetchResources(e); err != nil {
|
|
return nil, fmt.Errorf("failed to fetch resources %v: %w", checker.Name(), err)
|
|
}
|
|
|
|
results, err := checker.Evaluate(e)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to evaluate %v: %w", checker.Name(), err)
|
|
}
|
|
|
|
for _, result := range results {
|
|
result.Endpoint = e.templatePath(result.Endpoint)
|
|
result.StatusDisplay = ResultStatusNameMap[result.Status]
|
|
}
|
|
|
|
ret[checker.Name()] = results
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (e *Executor) templatePath(path string) string {
|
|
return strings.ReplaceAll(path, "{{mount}}", e.Mount)
|
|
}
|
|
|
|
func (e *Executor) FetchIfNotFetched(op logical.Operation, rawPath string) (*PathFetch, error) {
|
|
path := e.templatePath(rawPath)
|
|
|
|
byOp, present := e.Resources[path]
|
|
if present && byOp != nil {
|
|
result, present := byOp[op]
|
|
if present && result != nil {
|
|
return result, result.FetchSurfaceError()
|
|
}
|
|
}
|
|
|
|
// Must not exist in cache; create it.
|
|
if byOp == nil {
|
|
e.Resources[path] = make(map[logical.Operation]*PathFetch)
|
|
}
|
|
|
|
ret := &PathFetch{
|
|
Operation: op,
|
|
Path: path,
|
|
ParsedCache: make(map[string]interface{}),
|
|
}
|
|
|
|
data := map[string][]string{}
|
|
if op == logical.ListOperation {
|
|
data["list"] = []string{"true"}
|
|
} else if op != logical.ReadOperation {
|
|
return nil, fmt.Errorf("unknown operation: %v on %v", op, path)
|
|
}
|
|
|
|
// client.ReadRaw* methods require a manual timeout override
|
|
ctx, cancel := context.WithTimeout(context.Background(), e.Client.ClientTimeout())
|
|
defer cancel()
|
|
|
|
response, err := e.Client.Logical().ReadRawWithDataWithContext(ctx, path, data)
|
|
ret.Response = response
|
|
if err != nil {
|
|
ret.FetchError = err
|
|
} else {
|
|
// Not all secrets will parse correctly. Sometimes we really want
|
|
// to fetch a raw endpoint, sometimes we're run with a bad mount
|
|
// or missing permissions.
|
|
secret, secretErr := e.Client.Logical().ParseRawResponseAndCloseBody(response, err)
|
|
if secretErr != nil {
|
|
ret.SecretParseError = secretErr
|
|
} else {
|
|
ret.Secret = secret
|
|
}
|
|
}
|
|
|
|
e.Resources[path][op] = ret
|
|
return ret, ret.FetchSurfaceError()
|
|
}
|
|
|
|
type PathFetch struct {
|
|
Operation logical.Operation
|
|
Path string
|
|
Response *api.Response
|
|
FetchError error
|
|
Secret *api.Secret
|
|
SecretParseError error
|
|
ParsedCache map[string]interface{}
|
|
}
|
|
|
|
func (p *PathFetch) IsOK() bool {
|
|
return p.FetchError == nil && p.Response != nil
|
|
}
|
|
|
|
func (p *PathFetch) IsSecretOK() bool {
|
|
return p.IsOK() && p.SecretParseError == nil && p.Secret != nil
|
|
}
|
|
|
|
func (p *PathFetch) FetchSurfaceError() error {
|
|
if p.IsOK() || p.IsSecretPermissionsError() || p.IsUnsupportedPathError() || p.IsMissingResource() || p.Is404NotFound() {
|
|
return nil
|
|
}
|
|
|
|
if strings.Contains(p.FetchError.Error(), "route entry not found") {
|
|
return fmt.Errorf("Error making API request: was a bad mount given?\n\nOperation: %v\nPath: %v\nOriginal Error:\n%w", p.Operation, p.Path, p.FetchError)
|
|
}
|
|
|
|
return p.FetchError
|
|
}
|
|
|
|
func (p *PathFetch) IsSecretPermissionsError() bool {
|
|
return !p.IsOK() && strings.Contains(p.FetchError.Error(), "permission denied")
|
|
}
|
|
|
|
func (p *PathFetch) IsUnsupportedPathError() bool {
|
|
return !p.IsOK() && strings.Contains(p.FetchError.Error(), "unsupported path")
|
|
}
|
|
|
|
func (p *PathFetch) IsMissingResource() bool {
|
|
return !p.IsOK() && strings.Contains(p.FetchError.Error(), "unable to find")
|
|
}
|
|
|
|
func (p *PathFetch) Is404NotFound() bool {
|
|
return !p.IsOK() && strings.HasSuffix(strings.TrimSpace(p.FetchError.Error()), "Code: 404. Errors:")
|
|
}
|
|
|
|
type Check interface {
|
|
Name() string
|
|
IsEnabled() bool
|
|
|
|
DefaultConfig() map[string]interface{}
|
|
LoadConfig(config map[string]interface{}) error
|
|
|
|
FetchResources(e *Executor) error
|
|
|
|
Evaluate(e *Executor) ([]*Result, error)
|
|
}
|
|
|
|
type ResultStatus int
|
|
|
|
const (
|
|
ResultNotApplicable ResultStatus = iota
|
|
ResultOK
|
|
ResultInformational
|
|
ResultWarning
|
|
ResultCritical
|
|
ResultInvalidVersion
|
|
ResultInsufficientPermissions
|
|
)
|
|
|
|
var ResultStatusNameMap = map[ResultStatus]string{
|
|
ResultNotApplicable: "not_applicable",
|
|
ResultOK: "ok",
|
|
ResultInformational: "informational",
|
|
ResultWarning: "warning",
|
|
ResultCritical: "critical",
|
|
ResultInvalidVersion: "invalid_version",
|
|
ResultInsufficientPermissions: "insufficient_permissions",
|
|
}
|
|
|
|
var NameResultStatusMap = map[string]ResultStatus{
|
|
"not_applicable": ResultNotApplicable,
|
|
"ok": ResultOK,
|
|
"informational": ResultInformational,
|
|
"warning": ResultWarning,
|
|
"critical": ResultCritical,
|
|
"invalid_version": ResultInvalidVersion,
|
|
"insufficient_permissions": ResultInsufficientPermissions,
|
|
}
|
|
|
|
type Result struct {
|
|
Status ResultStatus `json:"status_code"`
|
|
StatusDisplay string `json:"status"`
|
|
Endpoint string `json:"endpoint,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|