diff --git a/changelog/15835.txt b/changelog/15835.txt new file mode 100644 index 000000000..d689c2a38 --- /dev/null +++ b/changelog/15835.txt @@ -0,0 +1,3 @@ +```release-note:bug +api/sys/internal/specs/openapi: support a new "dynamic" query parameter to generate generic mountpaths +``` diff --git a/sdk/framework/backend.go b/sdk/framework/backend.go index 28071e5ac..0efb798b9 100644 --- a/sdk/framework/backend.go +++ b/sdk/framework/backend.go @@ -528,9 +528,16 @@ func (b *Backend) handleRootHelp(req *logical.Request) (*logical.Response, error // names in the OAS document. 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 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) } diff --git a/sdk/framework/openapi.go b/sdk/framework/openapi.go index 2c3377f50..3c8ce2481 100644 --- a/sdk/framework/openapi.go +++ b/sdk/framework/openapi.go @@ -213,9 +213,9 @@ var ( ) // 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 { - 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 } } @@ -224,7 +224,7 @@ func documentPaths(backend *Backend, requestResponsePrefix string, doc *OASDocum } // 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 unauthPaths []string @@ -263,6 +263,21 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st // Body fields will be added to individual operations. 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 { location := "path" required := true diff --git a/sdk/framework/openapi_test.go b/sdk/framework/openapi_test.go index 592406d9f..8d3ecfea0 100644 --- a/sdk/framework/openapi_test.go +++ b/sdk/framework/openapi_test.go @@ -271,7 +271,7 @@ func TestOpenAPI_SpecialPaths(t *testing.T) { Root: test.rootPaths, Unauthenticated: test.unauthPaths, } - err := documentPath(&path, sp, "kv", logical.TypeLogical, doc) + err := documentPath(&path, sp, "kv", false, logical.TypeLogical, doc) if err != nil { t.Fatal(err) } @@ -519,11 +519,11 @@ func TestOpenAPI_OperationID(t *testing.T) { for _, context := range []string{"", "bar"} { doc := NewOASDocument() - err := documentPath(path1, nil, "kv", logical.TypeLogical, doc) + err := documentPath(path1, nil, "kv", false, logical.TypeLogical, doc) if err != nil { t.Fatal(err) } - err = documentPath(path2, nil, "kv", logical.TypeLogical, doc) + err = documentPath(path2, nil, "kv", false, logical.TypeLogical, doc) if err != nil { t.Fatal(err) } @@ -583,7 +583,7 @@ func TestOpenAPI_CustomDecoder(t *testing.T) { } docOrig := NewOASDocument() - err := documentPath(p, nil, "kv", logical.TypeLogical, docOrig) + err := documentPath(p, nil, "kv", false, logical.TypeLogical, docOrig) if err != nil { t.Fatal(err) } @@ -646,7 +646,7 @@ func testPath(t *testing.T, path *Path, sp *logical.Paths, expectedJSON string) t.Helper() 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) } doc.CreateOperationIDs("") diff --git a/sdk/framework/path.go b/sdk/framework/path.go index b316d2cc1..07ce84c97 100644 --- a/sdk/framework/path.go +++ b/sdk/framework/path.go @@ -311,7 +311,7 @@ func (p *Path) helpCallback(b *Backend) OperationFunc { // Build OpenAPI response for this path 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) } diff --git a/vault/logical_system.go b/vault/logical_system.go index 5f85ec514..f0b0d3ef8 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -4030,6 +4030,8 @@ func (b *SystemBackend) pathInternalOpenAPI(ctx context.Context, req *logical.Re // be received from plugin backends. doc := framework.NewOASDocument() + genericMountPaths, _ := d.Get("generic_mount_paths").(bool) + procMountGroup := func(group, mountPrefix string) error { 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{ Operation: logical.HelpOperation, Storage: req.Storage, - Data: map[string]interface{}{"requestResponsePrefix": pluginType}, + Data: map[string]interface{}{"requestResponsePrefix": pluginType, "genericMountPaths": genericMountPaths}, } resp, err := backend.HandleRequest(ctx, req) @@ -4101,7 +4103,12 @@ func (b *SystemBackend) pathInternalOpenAPI(ctx context.Context, req *logical.Re } } - doc.Paths["/"+mountPrefix+mount+path] = obj + 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 + } } // Merge backend schema components diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index 6b1516217..f004d3dfa 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -944,6 +944,12 @@ func (b *SystemBackend) internalPaths() []*framework.Path { Type: framework.TypeString, 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{ logical.ReadOperation: b.pathInternalOpenAPI, diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index b23df0d36..55e06d9b0 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -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) { _, b, rootToken := testCoreSystemBackend(t) 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. - minLen := 70000 + const minLen = 70000 if len(body) < minLen { t.Fatalf("response size too small; expected: min %d, actual: %d", minLen, len(body)) } diff --git a/website/content/api-docs/system/internal-specs-openapi.mdx b/website/content/api-docs/system/internal-specs-openapi.mdx index 887bc2875..68545600d 100644 --- a/website/content/api-docs/system/internal-specs-openapi.mdx +++ b/website/content/api-docs/system/internal-specs-openapi.mdx @@ -31,10 +31,15 @@ This endpoint returns a single OpenAPI document describing all paths visible to | :----- | :---------------------------- | | `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 ```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