-
Notifications
You must be signed in to change notification settings - Fork 4.3k
/
Copy pathpath.go
434 lines (373 loc) · 16.2 KB
/
path.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
// 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 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
}
}
redactVersion, _, _, _ := logical.CtxRedactionSettingsValue(ctx)
if redactVersion {
vaultVersion = ""
}
doc := NewOASDocument(vaultVersion)
if err := documentPath(p, b, requestResponsePrefix, 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}}
`