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