open-vault/sdk/framework/path.go
hc-github-team-secure-vault-core 1d9e560f0f
backport of commit 2f677665b37fcced51737cdef7abbebb4c719529 (#21527)
Co-authored-by: Max Bowsher <maxbowsher@gmail.com>
2023-07-06 20:01:33 +00:00

431 lines
16 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package framework
import (
"context"
"fmt"
"sort"
"strings"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/sdk/helper/license"
"github.com/hashicorp/vault/sdk/logical"
)
// Helper which returns a generic regex string for creating endpoint patterns
// that are identified by the given name in the backends
func GenericNameRegex(name string) string {
return fmt.Sprintf("(?P<%s>\\w(([\\w-.]+)?\\w)?)", name)
}
// GenericNameWithAtRegex returns a generic regex that allows alphanumeric
// characters along with -, . and @.
func GenericNameWithAtRegex(name string) string {
return fmt.Sprintf("(?P<%s>\\w(([\\w-.@]+)?\\w)?)", name)
}
// Helper which returns a regex string for optionally accepting the a field
// from the API URL
func OptionalParamRegex(name string) string {
return fmt.Sprintf("(/(?P<%s>.+))?", name)
}
// Helper which returns a regex string for capturing an entire endpoint path
// as the given name.
func MatchAllRegex(name string) string {
return fmt.Sprintf(`(?P<%s>.*)`, name)
}
// PathAppend is a helper for appending lists of paths into a single
// list.
func PathAppend(paths ...[]*Path) []*Path {
result := make([]*Path, 0, 10)
for _, ps := range paths {
result = append(result, ps...)
}
return result
}
// Path is a single path that the backend responds to.
type Path struct {
// Pattern is the pattern of the URL that matches this path.
//
// This should be a valid regular expression. Named captures will be
// exposed as fields that should map to a schema in Fields. If a named
// capture is not a field in the Fields map, then it will be ignored.
//
// The pattern will automatically have a ^ prepended and a $ appended before
// use, if these are not already present, so these may be omitted for clarity.
//
// If a ListOperation is being defined, the pattern must end with /? to match
// a trailing slash optionally, as ListOperations are always processed with a
// trailing slash added to the path if not already present. The match must not
// require the presence of a trailing slash, as HelpOperations, even for a
// path which only implements ListOperation, are processed without a trailing
// slash - so failure to make the trailing slash optional will break the
// `vault path-help` command for the path.
Pattern string
// Fields is the mapping of data fields to a schema describing that
// field.
//
// Field values are obtained from:
//
// - Named captures in the Pattern.
//
// - Parameters in the HTTP request body, for HTTP methods where a
// request body is expected, i.e. PUT/POST/PATCH. The request body is
// typically formatted as JSON, though
// "application/x-www-form-urlencoded" format can also be accepted.
//
// - Parameters in the HTTP URL query-string, for HTTP methods where
// there is no request body, i.e. GET/LIST/DELETE. The query-string
// is *not* parsed at all for PUT/POST/PATCH requests.
//
// Should the same field be specified both as a named capture and as
// a parameter, the named capture takes precedence, and a warning is
// returned.
Fields map[string]*FieldSchema
// Operations is the set of operations supported and the associated OperationsHandler.
//
// If both Create and Update operations are present, documentation and examples from
// the Update definition will be used. Similarly if both Read and List are present,
// Read will be used for documentation.
Operations map[logical.Operation]OperationHandler
// Callbacks are the set of callbacks that are called for a given
// operation. If a callback for a specific operation is not present,
// then logical.ErrUnsupportedOperation is automatically generated.
//
// The help operation is the only operation that the Path will
// automatically handle if the Help field is set. If both the Help
// field is set and there is a callback registered here, then the
// callback will be called.
//
// Deprecated: Operations should be used instead and will take priority if present.
Callbacks map[logical.Operation]OperationFunc
// ExistenceCheck, if implemented, is used to query whether a given
// resource exists or not. This is used for ACL purposes: if an Update
// action is specified, and the existence check returns false, the action
// is not allowed since the resource must first be created. The reverse is
// also true. If not specified, the Update action is forced and the user
// must have UpdateCapability on the path.
ExistenceCheck ExistenceFunc
// FeatureRequired, if implemented, will validate if the given feature is
// enabled for the set of paths
FeatureRequired license.Features
// Deprecated denotes that this path is considered deprecated. This may
// be reflected in help and documentation.
Deprecated bool
// Help is text describing how to use this path. This will be used
// to auto-generate the help operation. The Path will automatically
// generate a parameter listing and URL structure based on the
// regular expression, so the help text should just contain a description
// of what happens.
//
// HelpSynopsis is a one-sentence description of the path. This will
// be automatically line-wrapped at 80 characters.
//
// HelpDescription is a long-form description of the path. This will
// be automatically line-wrapped at 80 characters.
HelpSynopsis string
HelpDescription string
// DisplayAttrs provides hints for UI and documentation generators. They
// will be included in OpenAPI output if set.
DisplayAttrs *DisplayAttributes
// TakesArbitraryInput is used for endpoints that take arbitrary input, instead
// of or as well as their Fields. This is taken into account when printing
// warnings about ignored fields. If this is set, we will not warn when data is
// provided that is not part of the Fields declaration.
TakesArbitraryInput bool
}
// OperationHandler defines and describes a specific operation handler.
type OperationHandler interface {
Handler() OperationFunc
Properties() OperationProperties
}
// OperationProperties describes an operation for documentation, help text,
// and other clients. A Summary should always be provided, whereas other
// fields can be populated as needed.
type OperationProperties struct {
// Summary is a brief (usually one line) description of the operation.
Summary string
// Description is extended documentation of the operation and may contain
// Markdown-formatted text markup.
Description string
// Examples provides samples of the expected request data. The most
// relevant example should be first in the list, as it will be shown in
// documentation that supports only a single example.
Examples []RequestExample
// Responses provides a list of response description for a given response
// code. The most relevant response should be first in the list, as it will
// be shown in documentation that only allows a single example.
Responses map[int][]Response
// Unpublished indicates that this operation should not appear in public
// documentation or help text. The operation may still have documentation
// attached that can be used internally.
Unpublished bool
// Deprecated indicates that this operation should be avoided.
Deprecated bool
// The ForwardPerformance* parameters tell the router to unconditionally forward requests
// to this path if the processing node is a performance secondary/standby. This is generally
// *not* needed as there is already handling in place to automatically forward requests
// that try to write to storage. But there are a few cases where explicit forwarding is needed,
// for example:
//
// * The handler makes requests to other systems (e.g. an external API, database, ...) that
// change external state somehow, and subsequently writes to storage. In this case the
// default forwarding logic could result in multiple mutative calls to the external system.
//
// * The operation spans multiple requests (e.g. an OIDC callback), in-memory caching used,
// and the same node (and therefore cache) should process both steps.
//
// If explicit forwarding is needed, it is usually true that forwarding from both performance
// standbys and performance secondaries should be enabled.
//
// ForwardPerformanceStandby indicates that this path should not be processed
// on a performance standby node, and should be forwarded to the active node instead.
ForwardPerformanceStandby bool
// ForwardPerformanceSecondary indicates that this path should not be processed
// on a performance secondary node, and should be forwarded to the active node instead.
ForwardPerformanceSecondary bool
// DisplayAttrs provides hints for UI and documentation generators. They
// will be included in OpenAPI output if set.
DisplayAttrs *DisplayAttributes
}
type DisplayAttributes struct {
// Name is the name of the field suitable as a label or documentation heading.
Name string `json:"name,omitempty"`
// Description of the field that renders as tooltip help text beside the label (name) in the UI.
// This may be used to replace descriptions that reference comma separation but correspond
// to UI inputs where only arrays are valid. For example params with Type: framework.TypeCommaStringSlice
Description string `json:"description,omitempty"`
// Value is a sample value to display for this field. This may be used
// to indicate a default value, but it is for display only and completely separate
// from any Default member handling.
Value interface{} `json:"value,omitempty"`
// Sensitive indicates that the value should be masked by default in the UI.
Sensitive bool `json:"sensitive,omitempty"`
// Navigation indicates that the path should be available as a navigation tab
Navigation bool `json:"navigation,omitempty"`
// ItemType is the type of item this path operates on
ItemType string `json:"itemType,omitempty"`
// Group is the suggested UI group to place this field in.
Group string `json:"group,omitempty"`
// Action is the verb to use for the operation.
Action string `json:"action,omitempty"`
// OperationPrefix is a hyphenated lower-case string used to construct
// OpenAPI OperationID (prefix + verb + suffix). OperationPrefix is
// typically a human-readable name of the plugin or a prefix shared by
// multiple related endpoints.
OperationPrefix string `json:"operationPrefix,omitempty"`
// OperationVerb is a hyphenated lower-case string used to construct
// OpenAPI OperationID (prefix + verb + suffix). OperationVerb is typically
// an action to be performed (e.g. "generate", "sign", "login", etc.). If
// not specified, the verb defaults to `logical.Operation.String()`
// (e.g. "read", "list", "delete", "write" for Create/Update)
OperationVerb string `json:"operationVerb,omitempty"`
// OperationSuffix is a hyphenated lower-case string used to construct
// OpenAPI OperationID (prefix + verb + suffix). It is typically the name
// of the resource on which the action is performed (e.g. "role",
// "credentials", etc.). A pipe (|) separator can be used to list different
// suffixes for various permutations of the `Path.Pattern` regular
// expression. If not specified, the suffix defaults to the `Path.Pattern`
// split by dashes.
OperationSuffix string `json:"operationSuffix,omitempty"`
// EditType is the optional type of form field needed for a property
// This is only necessary for a "textarea" or "file"
EditType string `json:"editType,omitempty"`
}
// RequestExample is example of request data.
type RequestExample struct {
Description string // optional description of the request
Data map[string]interface{} // map version of sample JSON request data
// Optional example response to the sample request. This approach is considered
// provisional for now, and this field may be changed or removed.
Response *Response
}
// Response describes and optional demonstrations an operation response.
type Response struct {
Description string // summary of the the response and should always be provided
MediaType string // media type of the response, defaulting to "application/json" if empty
Fields map[string]*FieldSchema // the fields present in this response, used to generate openapi response
Example *logical.Response // example response data
}
// PathOperation is a concrete implementation of OperationHandler.
type PathOperation struct {
Callback OperationFunc
Summary string
Description string
Examples []RequestExample
Responses map[int][]Response
Unpublished bool
Deprecated bool
ForwardPerformanceSecondary bool
ForwardPerformanceStandby bool
DisplayAttrs *DisplayAttributes
}
func (p *PathOperation) Handler() OperationFunc {
return p.Callback
}
func (p *PathOperation) Properties() OperationProperties {
return OperationProperties{
Summary: strings.TrimSpace(p.Summary),
Description: strings.TrimSpace(p.Description),
Responses: p.Responses,
Examples: p.Examples,
Unpublished: p.Unpublished,
Deprecated: p.Deprecated,
ForwardPerformanceSecondary: p.ForwardPerformanceSecondary,
ForwardPerformanceStandby: p.ForwardPerformanceStandby,
DisplayAttrs: p.DisplayAttrs,
}
}
func (p *Path) helpCallback(b *Backend) OperationFunc {
return func(ctx context.Context, req *logical.Request, data *FieldData) (*logical.Response, error) {
var tplData pathTemplateData
tplData.Request = req.Path
tplData.RoutePattern = p.Pattern
tplData.Synopsis = strings.TrimSpace(p.HelpSynopsis)
if tplData.Synopsis == "" {
tplData.Synopsis = "<no synopsis>"
}
tplData.Description = strings.TrimSpace(p.HelpDescription)
if tplData.Description == "" {
tplData.Description = "<no description>"
}
// Alphabetize the fields
fieldKeys := make([]string, 0, len(p.Fields))
for k := range p.Fields {
fieldKeys = append(fieldKeys, k)
}
sort.Strings(fieldKeys)
// Build the field help
tplData.Fields = make([]pathTemplateFieldData, len(fieldKeys))
for i, k := range fieldKeys {
schema := p.Fields[k]
description := strings.TrimSpace(schema.Description)
if description == "" {
description = "<no description>"
}
tplData.Fields[i] = pathTemplateFieldData{
Key: k,
Type: schema.Type.String(),
Description: description,
Deprecated: schema.Deprecated,
}
}
help, err := executeTemplate(pathHelpTemplate, &tplData)
if err != nil {
return nil, errwrap.Wrapf("error executing template: {{err}}", err)
}
// The plugin type (e.g. "kv", "cubbyhole") is only assigned at the time
// the plugin is enabled (mounted). If specified in the request, the type
// will be used as part of the request/response names in the OAS document
var requestResponsePrefix string
if v, ok := req.Data["requestResponsePrefix"]; ok {
requestResponsePrefix = v.(string)
}
// Build OpenAPI response for this path
vaultVersion := "unknown"
if b.System() != nil {
// b.System() should always be non-nil, except tests might create a
// Backend without one.
env, err := b.System().PluginEnv(context.Background())
if err != nil {
return nil, err
}
if env != nil {
vaultVersion = env.VaultVersion
}
}
doc := NewOASDocument(vaultVersion)
if err := documentPath(p, b.SpecialPaths(), requestResponsePrefix, b.BackendType, doc); err != nil {
b.Logger().Warn("error generating OpenAPI", "error", err)
}
return logical.HelpResponse(help, nil, doc), nil
}
}
type pathTemplateData struct {
Request string
RoutePattern string
Synopsis string
Description string
Fields []pathTemplateFieldData
}
type pathTemplateFieldData struct {
Key string
Type string
Deprecated bool
Description string
URL bool
}
const pathHelpTemplate = `
Request: {{.Request}}
Matching Route: {{.RoutePattern}}
{{.Synopsis}}
{{ if .Fields -}}
## PARAMETERS
{{range .Fields}}
{{indent 4 .Key}} ({{.Type}})
{{if .Deprecated}}
{{printf "(DEPRECATED) %s" .Description | indent 8}}
{{else}}
{{indent 8 .Description}}
{{end}}{{end}}{{end}}
## DESCRIPTION
{{.Description}}
`