5f64520dac
* remove intercepting helpText * add subtext directly to StringList input component * update tests and add coverage for new openapi-attrs util * update test * add warning validation to input * lol is this right i dont know go * literally no idea what im doing * add Description to display attrs struct * update struct comment * add descriptions to remaining go fields * add missing comma * remaining commas..." * add description to display attrs * update tests * update tests * add changelog; * Update ui/app/utils/openapi-to-attrs.js * update tests following backend changes * clearly name variable * format files * no longer need to test for modified tooltip since coming from backend now
420 lines
15 KiB
Go
420 lines
15 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.
|
|
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}}
|
|
`
|