Dynamic parameter for mountpaths in OpenApi Spec generation(#15835)
"generic_mount_paths" query parameter for OpenApiSpec generation
This commit is contained in:
parent
ed9ae70822
commit
3215cdbd32
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
api/sys/internal/specs/openapi: support a new "dynamic" query parameter to generate generic mountpaths
|
||||||
|
```
|
|
@ -528,9 +528,16 @@ func (b *Backend) handleRootHelp(req *logical.Request) (*logical.Response, error
|
||||||
// names in the OAS document.
|
// names in the OAS document.
|
||||||
requestResponsePrefix := req.GetString("requestResponsePrefix")
|
requestResponsePrefix := req.GetString("requestResponsePrefix")
|
||||||
|
|
||||||
|
// Generic mount paths will primarily be used for code generation purposes.
|
||||||
|
// This will result in dynamic mount paths being placed instead of
|
||||||
|
// hardcoded default paths. For example /auth/approle/login would be replaced
|
||||||
|
// with /auth/{mountPath}/login. This will be replaced for all secrets
|
||||||
|
// engines and auth methods that are enabled.
|
||||||
|
genericMountPaths, _ := req.Get("genericMountPaths").(bool)
|
||||||
|
|
||||||
// Build OpenAPI response for the entire backend
|
// Build OpenAPI response for the entire backend
|
||||||
doc := NewOASDocument()
|
doc := NewOASDocument()
|
||||||
if err := documentPaths(b, requestResponsePrefix, doc); err != nil {
|
if err := documentPaths(b, requestResponsePrefix, genericMountPaths, doc); err != nil {
|
||||||
b.Logger().Warn("error generating OpenAPI", "error", err)
|
b.Logger().Warn("error generating OpenAPI", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -213,9 +213,9 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
// documentPaths parses all paths in a framework.Backend into OpenAPI paths.
|
// documentPaths parses all paths in a framework.Backend into OpenAPI paths.
|
||||||
func documentPaths(backend *Backend, requestResponsePrefix string, doc *OASDocument) error {
|
func documentPaths(backend *Backend, requestResponsePrefix string, genericMountPaths bool, doc *OASDocument) error {
|
||||||
for _, p := range backend.Paths {
|
for _, p := range backend.Paths {
|
||||||
if err := documentPath(p, backend.SpecialPaths(), requestResponsePrefix, backend.BackendType, doc); err != nil {
|
if err := documentPath(p, backend.SpecialPaths(), requestResponsePrefix, genericMountPaths, backend.BackendType, doc); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -224,7 +224,7 @@ func documentPaths(backend *Backend, requestResponsePrefix string, doc *OASDocum
|
||||||
}
|
}
|
||||||
|
|
||||||
// documentPath parses a framework.Path into one or more OpenAPI paths.
|
// documentPath parses a framework.Path into one or more OpenAPI paths.
|
||||||
func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix string, backendType logical.BackendType, doc *OASDocument) error {
|
func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix string, genericMountPaths bool, backendType logical.BackendType, doc *OASDocument) error {
|
||||||
var sudoPaths []string
|
var sudoPaths []string
|
||||||
var unauthPaths []string
|
var unauthPaths []string
|
||||||
|
|
||||||
|
@ -263,6 +263,21 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st
|
||||||
// Body fields will be added to individual operations.
|
// Body fields will be added to individual operations.
|
||||||
pathFields, bodyFields := splitFields(p.Fields, path)
|
pathFields, bodyFields := splitFields(p.Fields, path)
|
||||||
|
|
||||||
|
if genericMountPaths && requestResponsePrefix != "system" && requestResponsePrefix != "identity" {
|
||||||
|
// Add mount path as a parameter
|
||||||
|
p := OASParameter{
|
||||||
|
Name: "mountPath",
|
||||||
|
Description: "Path that the backend was mounted at",
|
||||||
|
In: "path",
|
||||||
|
Schema: &OASSchema{
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
pi.Parameters = append(pi.Parameters, p)
|
||||||
|
}
|
||||||
|
|
||||||
for name, field := range pathFields {
|
for name, field := range pathFields {
|
||||||
location := "path"
|
location := "path"
|
||||||
required := true
|
required := true
|
||||||
|
|
|
@ -271,7 +271,7 @@ func TestOpenAPI_SpecialPaths(t *testing.T) {
|
||||||
Root: test.rootPaths,
|
Root: test.rootPaths,
|
||||||
Unauthenticated: test.unauthPaths,
|
Unauthenticated: test.unauthPaths,
|
||||||
}
|
}
|
||||||
err := documentPath(&path, sp, "kv", logical.TypeLogical, doc)
|
err := documentPath(&path, sp, "kv", false, logical.TypeLogical, doc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -519,11 +519,11 @@ func TestOpenAPI_OperationID(t *testing.T) {
|
||||||
|
|
||||||
for _, context := range []string{"", "bar"} {
|
for _, context := range []string{"", "bar"} {
|
||||||
doc := NewOASDocument()
|
doc := NewOASDocument()
|
||||||
err := documentPath(path1, nil, "kv", logical.TypeLogical, doc)
|
err := documentPath(path1, nil, "kv", false, logical.TypeLogical, doc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
err = documentPath(path2, nil, "kv", logical.TypeLogical, doc)
|
err = documentPath(path2, nil, "kv", false, logical.TypeLogical, doc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -583,7 +583,7 @@ func TestOpenAPI_CustomDecoder(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
docOrig := NewOASDocument()
|
docOrig := NewOASDocument()
|
||||||
err := documentPath(p, nil, "kv", logical.TypeLogical, docOrig)
|
err := documentPath(p, nil, "kv", false, logical.TypeLogical, docOrig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -646,7 +646,7 @@ func testPath(t *testing.T, path *Path, sp *logical.Paths, expectedJSON string)
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
doc := NewOASDocument()
|
doc := NewOASDocument()
|
||||||
if err := documentPath(path, sp, "kv", logical.TypeLogical, doc); err != nil {
|
if err := documentPath(path, sp, "kv", false, logical.TypeLogical, doc); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
doc.CreateOperationIDs("")
|
doc.CreateOperationIDs("")
|
||||||
|
|
|
@ -311,7 +311,7 @@ func (p *Path) helpCallback(b *Backend) OperationFunc {
|
||||||
|
|
||||||
// Build OpenAPI response for this path
|
// Build OpenAPI response for this path
|
||||||
doc := NewOASDocument()
|
doc := NewOASDocument()
|
||||||
if err := documentPath(p, b.SpecialPaths(), requestResponsePrefix, b.BackendType, doc); err != nil {
|
if err := documentPath(p, b.SpecialPaths(), requestResponsePrefix, false, b.BackendType, doc); err != nil {
|
||||||
b.Logger().Warn("error generating OpenAPI", "error", err)
|
b.Logger().Warn("error generating OpenAPI", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4030,6 +4030,8 @@ func (b *SystemBackend) pathInternalOpenAPI(ctx context.Context, req *logical.Re
|
||||||
// be received from plugin backends.
|
// be received from plugin backends.
|
||||||
doc := framework.NewOASDocument()
|
doc := framework.NewOASDocument()
|
||||||
|
|
||||||
|
genericMountPaths, _ := d.Get("generic_mount_paths").(bool)
|
||||||
|
|
||||||
procMountGroup := func(group, mountPrefix string) error {
|
procMountGroup := func(group, mountPrefix string) error {
|
||||||
for mount, entry := range resp.Data[group].(map[string]interface{}) {
|
for mount, entry := range resp.Data[group].(map[string]interface{}) {
|
||||||
|
|
||||||
|
@ -4047,7 +4049,7 @@ func (b *SystemBackend) pathInternalOpenAPI(ctx context.Context, req *logical.Re
|
||||||
req := &logical.Request{
|
req := &logical.Request{
|
||||||
Operation: logical.HelpOperation,
|
Operation: logical.HelpOperation,
|
||||||
Storage: req.Storage,
|
Storage: req.Storage,
|
||||||
Data: map[string]interface{}{"requestResponsePrefix": pluginType},
|
Data: map[string]interface{}{"requestResponsePrefix": pluginType, "genericMountPaths": genericMountPaths},
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := backend.HandleRequest(ctx, req)
|
resp, err := backend.HandleRequest(ctx, req)
|
||||||
|
@ -4101,8 +4103,13 @@ func (b *SystemBackend) pathInternalOpenAPI(ctx context.Context, req *logical.Re
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if genericMountPaths && mount != "sys/" && mount != "identity/" {
|
||||||
|
s := fmt.Sprintf("/%s{mountPath}/%s", mountPrefix, path)
|
||||||
|
doc.Paths[s] = obj
|
||||||
|
} else {
|
||||||
doc.Paths["/"+mountPrefix+mount+path] = obj
|
doc.Paths["/"+mountPrefix+mount+path] = obj
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Merge backend schema components
|
// Merge backend schema components
|
||||||
for e, schema := range backendDoc.Components.Schemas {
|
for e, schema := range backendDoc.Components.Schemas {
|
||||||
|
|
|
@ -944,6 +944,12 @@ func (b *SystemBackend) internalPaths() []*framework.Path {
|
||||||
Type: framework.TypeString,
|
Type: framework.TypeString,
|
||||||
Description: "Context string appended to every operationId",
|
Description: "Context string appended to every operationId",
|
||||||
},
|
},
|
||||||
|
"generic_mount_paths": {
|
||||||
|
Type: framework.TypeBool,
|
||||||
|
Description: "Use generic mount paths",
|
||||||
|
Query: true,
|
||||||
|
Default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
logical.ReadOperation: b.pathInternalOpenAPI,
|
logical.ReadOperation: b.pathInternalOpenAPI,
|
||||||
|
|
|
@ -3408,6 +3408,59 @@ func TestSystemBackend_InternalUIMount(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSystemBackend_OASGenericMount(t *testing.T) {
|
||||||
|
_, b, rootToken := testCoreSystemBackend(t)
|
||||||
|
var oapi map[string]interface{}
|
||||||
|
|
||||||
|
// Check that default paths are present with a root token
|
||||||
|
req := logical.TestRequest(t, logical.ReadOperation, "internal/specs/openapi")
|
||||||
|
req.Data["generic_mount_paths"] = true
|
||||||
|
req.ClientToken = rootToken
|
||||||
|
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := resp.Data["http_raw_body"].([]byte)
|
||||||
|
err = jsonutil.DecodeJSON(body, &oapi)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, err := framework.NewOASDocumentFromMap(oapi)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pathSamples := []struct {
|
||||||
|
path string
|
||||||
|
tag string
|
||||||
|
}{
|
||||||
|
{"/auth/{mountPath}/lookup", "auth"},
|
||||||
|
{"/{mountPath}/{path}", "secrets"},
|
||||||
|
{"/identity/group/id", "identity"},
|
||||||
|
{"/{mountPath}/.*", "secrets"},
|
||||||
|
{"/sys/policy", "system"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range pathSamples {
|
||||||
|
if doc.Paths[path.path] == nil {
|
||||||
|
t.Fatalf("didn't find expected path '%s'.", path)
|
||||||
|
}
|
||||||
|
tag := doc.Paths[path.path].Get.Tags[0]
|
||||||
|
if tag != path.tag {
|
||||||
|
t.Fatalf("path: %s; expected tag: %s, actual: %s", path.path, tag, path.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple check of response size (which is much larger than most
|
||||||
|
// Vault responses), mainly to catch mass omission of expected path data.
|
||||||
|
const minLen = 70000
|
||||||
|
if len(body) < minLen {
|
||||||
|
t.Fatalf("response size too small; expected: min %d, actual: %d", minLen, len(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSystemBackend_OpenAPI(t *testing.T) {
|
func TestSystemBackend_OpenAPI(t *testing.T) {
|
||||||
_, b, rootToken := testCoreSystemBackend(t)
|
_, b, rootToken := testCoreSystemBackend(t)
|
||||||
var oapi map[string]interface{}
|
var oapi map[string]interface{}
|
||||||
|
@ -3485,9 +3538,9 @@ func TestSystemBackend_OpenAPI(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple sanity check of response size (which is much larger than most
|
// Simple check of response size (which is much larger than most
|
||||||
// Vault responses), mainly to catch mass omission of expected path data.
|
// Vault responses), mainly to catch mass omission of expected path data.
|
||||||
minLen := 70000
|
const minLen = 70000
|
||||||
if len(body) < minLen {
|
if len(body) < minLen {
|
||||||
t.Fatalf("response size too small; expected: min %d, actual: %d", minLen, len(body))
|
t.Fatalf("response size too small; expected: min %d, actual: %d", minLen, len(body))
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,10 +31,15 @@ This endpoint returns a single OpenAPI document describing all paths visible to
|
||||||
| :----- | :---------------------------- |
|
| :----- | :---------------------------- |
|
||||||
| `GET` | `/sys/internal/specs/openapi` |
|
| `GET` | `/sys/internal/specs/openapi` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
- `generic_mount_paths` `(bool: false)` – Used to specify whether to use generic mount paths. If set, the mount paths will be replaced with a dynamic parameter: `{mountPath}`
|
||||||
|
|
||||||
|
|
||||||
### Sample Request
|
### Sample Request
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ curl http://127.0.0.1:8200/v1/sys/internal/specs/openapi
|
$ curl http://127.0.0.1:8200/v1/sys/internal/specs/openapi?generic_mount_paths=false
|
||||||
```
|
```
|
||||||
|
|
||||||
### Sample Response
|
### Sample Response
|
||||||
|
|
Loading…
Reference in New Issue