diff --git a/logical/framework/openapi.go b/logical/framework/openapi.go index 237311f29..e6d467304 100644 --- a/logical/framework/openapi.go +++ b/logical/framework/openapi.go @@ -119,6 +119,7 @@ func NewOASOperation() *OASOperation { type OASOperation struct { Summary string `json:"summary,omitempty"` Description string `json:"description,omitempty"` + OperationID string `json:"operationId,omitempty"` Tags []string `json:"tags,omitempty"` Parameters []OASParameter `json:"parameters,omitempty"` RequestBody *OASRequestBody `json:"requestBody,omitempty"` @@ -185,6 +186,7 @@ var cleanSuffixRe = regexp.MustCompile(`/\?\$?$`) // Path suf var wsRe = regexp.MustCompile(`\s+`) // Match whitespace, to be compressed during cleaning var altFieldsGroupRe = regexp.MustCompile(`\(\?P<\w+>\w+(\|\w+)+\)`) // Match named groups that limit options, e.g. "(?a|b|c)" var altFieldsRe = regexp.MustCompile(`\w+(\|\w+)+`) // Match an options set, e.g. "a|b|c" +var nonWordRe = regexp.MustCompile(`[^\w]+`) // Match a sequence of non-word characters // documentPaths parses all paths in a framework.Backend into OpenAPI paths. func documentPaths(backend *Backend, doc *OASDocument) error { @@ -611,3 +613,52 @@ func cleanResponse(resp *logical.Response) (*cleanedResponse, error) { return &r, nil } + +// CreateOperationIDs generates unique operationIds for all paths/methods. +// The transform will convert path/method into camelcase. e.g.: +// +// /sys/tools/random/{urlbytes} -> postSysToolsRandomUrlbytes +// +// In the unlikely case of a duplicate ids, a numeric suffix is added: +// postSysToolsRandomUrlbytes_2 +// +// An optional user-provided suffix ("context") may also be appended. +func (d *OASDocument) CreateOperationIDs(context string) { + opIDCount := make(map[string]int) + + for path, pi := range d.Paths { + for _, method := range []string{"get", "post", "delete"} { + var oasOperation *OASOperation + switch method { + case "get": + oasOperation = pi.Get + case "post": + oasOperation = pi.Post + case "delete": + oasOperation = pi.Delete + } + + if oasOperation == nil { + continue + } + + // Space-split on non-words, title case everything, recombine + opID := nonWordRe.ReplaceAllString(strings.ToLower(path), " ") + opID = strings.Title(opID) + opID = method + strings.Replace(opID, " ", "", -1) + + // deduplicate operationIds. This is a safeguard, since generated IDs should + // already be unique given our current path naming conventions. + opIDCount[opID]++ + if opIDCount[opID] > 1 { + opID = fmt.Sprintf("%s_%d", opID, opIDCount[opID]) + } + + if context != "" { + opID += "_" + context + } + + oasOperation.OperationID = opID + } + } +} diff --git a/vault/logical_system.go b/vault/logical_system.go index 7113df45f..05438f401 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -3064,6 +3064,8 @@ func (b *SystemBackend) pathInternalOpenAPI(ctx context.Context, req *logical.Re return nil, err } + context := d.Get("context").(string) + // Set up target document and convert to map[string]interface{} which is what will // be received from plugin backends. doc := framework.NewOASDocument() @@ -3144,6 +3146,8 @@ func (b *SystemBackend) pathInternalOpenAPI(ctx context.Context, req *logical.Re return nil, err } + doc.CreateOperationIDs(context) + buf, err := json.Marshal(doc) if err != nil { return nil, err diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index 366f83538..bcd0a6ef2 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -754,8 +754,15 @@ func (b *SystemBackend) internalPaths() []*framework.Path { return []*framework.Path{ { Pattern: "internal/specs/openapi", + Fields: map[string]*framework.FieldSchema{ + "context": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Context string appended to every operationId", + }, + }, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.pathInternalOpenAPI, + logical.ReadOperation: b.pathInternalOpenAPI, + logical.UpdateOperation: b.pathInternalOpenAPI, }, }, {