// 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"` // 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. It is typically the name of the plugin. OperationPrefix string `json:"operationPrefix,omitempty"` // OperationPrefix is a hyphenated lower-case string used to construct // OpenAPI OperationID. It 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", "delete", etc.). OperationVerb string `json:"operationVerb,omitempty"` // OperationPrefix is a hyphenated lower-case string used to construct // OpenAPI OperationID. 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 = "" } tplData.Description = strings.TrimSpace(p.HelpDescription) if tplData.Description == "" { tplData.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 = "" } 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}} `