342b61984a
Move version out of SDK. For now it's a copy rather than move: the part not addressed by this change is sdk/helper/useragent.String, which we'll want to remove in favour of PluginString. That will have to wait until we've removed uses of useragent.String from all builtins.
376 lines
13 KiB
Go
376 lines
13 KiB
Go
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.
|
|
Pattern string
|
|
|
|
// Fields is the mapping of data fields to a schema describing that
|
|
// field. Named captures in the Pattern also map to fields. If a named
|
|
// capture name matches a PUT body name, the named capture takes
|
|
// priority.
|
|
//
|
|
// Note that only named capture fields are available in every operation,
|
|
// whereas all fields are available in the Write operation.
|
|
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"`
|
|
|
|
// 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"`
|
|
|
|
// 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
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
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, false, 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}}
|
|
`
|