1package framework
2
3import (
4	"context"
5	"fmt"
6	"sort"
7	"strings"
8
9	"github.com/hashicorp/errwrap"
10	"github.com/hashicorp/vault/sdk/helper/license"
11	"github.com/hashicorp/vault/sdk/logical"
12)
13
14// Helper which returns a generic regex string for creating endpoint patterns
15// that are identified by the given name in the backends
16func GenericNameRegex(name string) string {
17	return fmt.Sprintf("(?P<%s>\\w(([\\w-.]+)?\\w)?)", name)
18}
19
20// GenericNameWithAtRegex returns a generic regex that allows alphanumeric
21// characters along with -, . and @.
22func GenericNameWithAtRegex(name string) string {
23	return fmt.Sprintf("(?P<%s>\\w(([\\w-.@]+)?\\w)?)", name)
24}
25
26// Helper which returns a regex string for optionally accepting the a field
27// from the API URL
28func OptionalParamRegex(name string) string {
29	return fmt.Sprintf("(/(?P<%s>.+))?", name)
30}
31
32// Helper which returns a regex string for capturing an entire endpoint path
33// as the given name.
34func MatchAllRegex(name string) string {
35	return fmt.Sprintf(`(?P<%s>.*)`, name)
36}
37
38// PathAppend is a helper for appending lists of paths into a single
39// list.
40func PathAppend(paths ...[]*Path) []*Path {
41	result := make([]*Path, 0, 10)
42	for _, ps := range paths {
43		result = append(result, ps...)
44	}
45
46	return result
47}
48
49// Path is a single path that the backend responds to.
50type Path struct {
51	// Pattern is the pattern of the URL that matches this path.
52	//
53	// This should be a valid regular expression. Named captures will be
54	// exposed as fields that should map to a schema in Fields. If a named
55	// capture is not a field in the Fields map, then it will be ignored.
56	Pattern string
57
58	// Fields is the mapping of data fields to a schema describing that
59	// field. Named captures in the Pattern also map to fields. If a named
60	// capture name matches a PUT body name, the named capture takes
61	// priority.
62	//
63	// Note that only named capture fields are available in every operation,
64	// whereas all fields are available in the Write operation.
65	Fields map[string]*FieldSchema
66
67	// Operations is the set of operations supported and the associated OperationsHandler.
68	//
69	// If both Create and Update operations are present, documentation and examples from
70	// the Update definition will be used. Similarly if both Read and List are present,
71	// Read will be used for documentation.
72	Operations map[logical.Operation]OperationHandler
73
74	// Callbacks are the set of callbacks that are called for a given
75	// operation. If a callback for a specific operation is not present,
76	// then logical.ErrUnsupportedOperation is automatically generated.
77	//
78	// The help operation is the only operation that the Path will
79	// automatically handle if the Help field is set. If both the Help
80	// field is set and there is a callback registered here, then the
81	// callback will be called.
82	//
83	// Deprecated: Operations should be used instead and will take priority if present.
84	Callbacks map[logical.Operation]OperationFunc
85
86	// ExistenceCheck, if implemented, is used to query whether a given
87	// resource exists or not. This is used for ACL purposes: if an Update
88	// action is specified, and the existence check returns false, the action
89	// is not allowed since the resource must first be created. The reverse is
90	// also true. If not specified, the Update action is forced and the user
91	// must have UpdateCapability on the path.
92	ExistenceCheck ExistenceFunc
93
94	// FeatureRequired, if implemented, will validate if the given feature is
95	// enabled for the set of paths
96	FeatureRequired license.Features
97
98	// Deprecated denotes that this path is considered deprecated. This may
99	// be reflected in help and documentation.
100	Deprecated bool
101
102	// Help is text describing how to use this path. This will be used
103	// to auto-generate the help operation. The Path will automatically
104	// generate a parameter listing and URL structure based on the
105	// regular expression, so the help text should just contain a description
106	// of what happens.
107	//
108	// HelpSynopsis is a one-sentence description of the path. This will
109	// be automatically line-wrapped at 80 characters.
110	//
111	// HelpDescription is a long-form description of the path. This will
112	// be automatically line-wrapped at 80 characters.
113	HelpSynopsis    string
114	HelpDescription string
115
116	// DisplayAttrs provides hints for UI and documentation generators. They
117	// will be included in OpenAPI output if set.
118	DisplayAttrs *DisplayAttributes
119}
120
121// OperationHandler defines and describes a specific operation handler.
122type OperationHandler interface {
123	Handler() OperationFunc
124	Properties() OperationProperties
125}
126
127// OperationProperties describes an operation for documentation, help text,
128// and other clients. A Summary should always be provided, whereas other
129// fields can be populated as needed.
130type OperationProperties struct {
131	// Summary is a brief (usually one line) description of the operation.
132	Summary string
133
134	// Description is extended documentation of the operation and may contain
135	// Markdown-formatted text markup.
136	Description string
137
138	// Examples provides samples of the expected request data. The most
139	// relevant example should be first in the list, as it will be shown in
140	// documentation that supports only a single example.
141	Examples []RequestExample
142
143	// Responses provides a list of response description for a given response
144	// code. The most relevant response should be first in the list, as it will
145	// be shown in documentation that only allows a single example.
146	Responses map[int][]Response
147
148	// Unpublished indicates that this operation should not appear in public
149	// documentation or help text. The operation may still have documentation
150	// attached that can be used internally.
151	Unpublished bool
152
153	// Deprecated indicates that this operation should be avoided.
154	Deprecated bool
155
156	// ForwardPerformanceStandby indicates that this path should not be processed
157	// on a performance standby node, and should be forwarded to the active node instead.
158	ForwardPerformanceStandby bool
159
160	// ForwardPerformanceSecondary indicates that this path should not be processed
161	// on a performance secondary node, and should be forwarded to the active node instead.
162	ForwardPerformanceSecondary bool
163
164	// DisplayAttrs provides hints for UI and documentation generators. They
165	// will be included in OpenAPI output if set.
166	DisplayAttrs *DisplayAttributes
167}
168
169type DisplayAttributes struct {
170	// Name is the name of the field suitable as a label or documentation heading.
171	Name string `json:"name,omitempty"`
172
173	// Value is a sample value to display for this field. This may be used
174	// to indicate a default value, but it is for display only and completely separate
175	// from any Default member handling.
176	Value interface{} `json:"value,omitempty"`
177
178	// Sensitive indicates that the value should be masked by default in the UI.
179	Sensitive bool `json:"sensitive,omitempty"`
180
181	// Navigation indicates that the path should be available as a navigation tab
182	Navigation bool `json:"navigation,omitempty"`
183
184	// ItemType is the type of item this path operates on
185	ItemType string `json:"itemType,omitempty"`
186
187	// Group is the suggested UI group to place this field in.
188	Group string `json:"group,omitempty"`
189
190	// Action is the verb to use for the operation.
191	Action string `json:"action,omitempty"`
192
193	// EditType is the type of form field needed for a property
194	// e.g. "textarea" or "file"
195	EditType string `json:"editType,omitempty"`
196}
197
198// RequestExample is example of request data.
199type RequestExample struct {
200	Description string                 // optional description of the request
201	Data        map[string]interface{} // map version of sample JSON request data
202
203	// Optional example response to the sample request. This approach is considered
204	// provisional for now, and this field may be changed or removed.
205	Response *Response
206}
207
208// Response describes and optional demonstrations an operation response.
209type Response struct {
210	Description string            // summary of the the response and should always be provided
211	MediaType   string            // media type of the response, defaulting to "application/json" if empty
212	Example     *logical.Response // example response data
213}
214
215// PathOperation is a concrete implementation of OperationHandler.
216type PathOperation struct {
217	Callback                    OperationFunc
218	Summary                     string
219	Description                 string
220	Examples                    []RequestExample
221	Responses                   map[int][]Response
222	Unpublished                 bool
223	Deprecated                  bool
224	ForwardPerformanceSecondary bool
225	ForwardPerformanceStandby   bool
226}
227
228func (p *PathOperation) Handler() OperationFunc {
229	return p.Callback
230}
231
232func (p *PathOperation) Properties() OperationProperties {
233	return OperationProperties{
234		Summary:                     strings.TrimSpace(p.Summary),
235		Description:                 strings.TrimSpace(p.Description),
236		Responses:                   p.Responses,
237		Examples:                    p.Examples,
238		Unpublished:                 p.Unpublished,
239		Deprecated:                  p.Deprecated,
240		ForwardPerformanceSecondary: p.ForwardPerformanceSecondary,
241		ForwardPerformanceStandby:   p.ForwardPerformanceStandby,
242	}
243}
244
245func (p *Path) helpCallback(b *Backend) OperationFunc {
246	return func(ctx context.Context, req *logical.Request, data *FieldData) (*logical.Response, error) {
247		var tplData pathTemplateData
248		tplData.Request = req.Path
249		tplData.RoutePattern = p.Pattern
250		tplData.Synopsis = strings.TrimSpace(p.HelpSynopsis)
251		if tplData.Synopsis == "" {
252			tplData.Synopsis = "<no synopsis>"
253		}
254		tplData.Description = strings.TrimSpace(p.HelpDescription)
255		if tplData.Description == "" {
256			tplData.Description = "<no description>"
257		}
258
259		// Alphabetize the fields
260		fieldKeys := make([]string, 0, len(p.Fields))
261		for k, _ := range p.Fields {
262			fieldKeys = append(fieldKeys, k)
263		}
264		sort.Strings(fieldKeys)
265
266		// Build the field help
267		tplData.Fields = make([]pathTemplateFieldData, len(fieldKeys))
268		for i, k := range fieldKeys {
269			schema := p.Fields[k]
270			description := strings.TrimSpace(schema.Description)
271			if description == "" {
272				description = "<no description>"
273			}
274
275			tplData.Fields[i] = pathTemplateFieldData{
276				Key:         k,
277				Type:        schema.Type.String(),
278				Description: description,
279				Deprecated:  schema.Deprecated,
280			}
281		}
282
283		help, err := executeTemplate(pathHelpTemplate, &tplData)
284		if err != nil {
285			return nil, errwrap.Wrapf("error executing template: {{err}}", err)
286		}
287
288		// Build OpenAPI response for this path
289		doc := NewOASDocument()
290		if err := documentPath(p, b.SpecialPaths(), b.BackendType, doc); err != nil {
291			b.Logger().Warn("error generating OpenAPI", "error", err)
292		}
293
294		return logical.HelpResponse(help, nil, doc), nil
295	}
296}
297
298type pathTemplateData struct {
299	Request      string
300	RoutePattern string
301	Synopsis     string
302	Description  string
303	Fields       []pathTemplateFieldData
304}
305
306type pathTemplateFieldData struct {
307	Key         string
308	Type        string
309	Deprecated  bool
310	Description string
311	URL         bool
312}
313
314const pathHelpTemplate = `
315Request:        {{.Request}}
316Matching Route: {{.RoutePattern}}
317
318{{.Synopsis}}
319
320{{ if .Fields -}}
321## PARAMETERS
322{{range .Fields}}
323{{indent 4 .Key}} ({{.Type}})
324{{if .Deprecated}}
325{{printf "(DEPRECATED) %s" .Description | indent 8}}
326{{else}}
327{{indent 8 .Description}}
328{{end}}{{end}}{{end}}
329## DESCRIPTION
330
331{{.Description}}
332`
333