diff --git a/api/plugin_helpers.go b/api/plugin_helpers.go index 6602a044bd..0077ec769b 100644 --- a/api/plugin_helpers.go +++ b/api/plugin_helpers.go @@ -37,9 +37,9 @@ const ( // path matches that path or not (useful specifically for the paths that // contain templated fields.) var sudoPaths = map[string]*regexp.Regexp{ - "/auth/token/accessors/": regexp.MustCompile(`^/auth/token/accessors/$`), - "/pki/root": regexp.MustCompile(`^/pki/root$`), - "/pki/root/sign-self-issued": regexp.MustCompile(`^/pki/root/sign-self-issued$`), + "/auth/{token_mount_path}/accessors/": regexp.MustCompile(`^/auth/.+/accessors/$`), + "/{pki_mount_path}/root": regexp.MustCompile(`^/.+/root$`), + "/{pki_mount_path}/root/sign-self-issued": regexp.MustCompile(`^/.+/root/sign-self-issued$`), "/sys/audit": regexp.MustCompile(`^/sys/audit$`), "/sys/audit/{path}": regexp.MustCompile(`^/sys/audit/.+$`), "/sys/auth/{path}": regexp.MustCompile(`^/sys/auth/.+$`), diff --git a/sdk/framework/backend.go b/sdk/framework/backend.go index 33c965e26a..4895097217 100644 --- a/sdk/framework/backend.go +++ b/sdk/framework/backend.go @@ -539,13 +539,6 @@ 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 vaultVersion := "unknown" if b.System() != nil { @@ -557,7 +550,7 @@ func (b *Backend) handleRootHelp(req *logical.Request) (*logical.Response, error } doc := NewOASDocument(vaultVersion) - if err := documentPaths(b, requestResponsePrefix, genericMountPaths, doc); err != nil { + if err := documentPaths(b, requestResponsePrefix, doc); err != nil { b.Logger().Warn("error generating OpenAPI", "error", err) } diff --git a/sdk/framework/openapi.go b/sdk/framework/openapi.go index 8ea8206315..b09d43a344 100644 --- a/sdk/framework/openapi.go +++ b/sdk/framework/openapi.go @@ -208,16 +208,16 @@ var ( altRootsRe = regexp.MustCompile(`^\(([\w\-_]+(?:\|[\w\-_]+)+)\)(/.*)$`) // Pattern starting with alts, e.g. "(root1|root2)/(?Pregex)" cleanCharsRe = regexp.MustCompile("[()^$?]") // Set of regex characters that will be stripped during cleaning cleanSuffixRe = regexp.MustCompile(`/\?\$?$`) // Path suffix patterns that will be stripped during cleaning - nonWordRe = regexp.MustCompile(`[^\w]+`) // Match a sequence of non-word characters + nonWordRe = regexp.MustCompile(`[^a-zA-Z0-9]+`) // Match a sequence of non-word characters pathFieldsRe = regexp.MustCompile(`{(\w+)}`) // Capture OpenAPI-style named parameters, e.g. "lookup/{urltoken}", reqdRe = regexp.MustCompile(`\(?\?P<(\w+)>[^)]*\)?`) // Capture required parameters, e.g. "(?Pregex)" wsRe = regexp.MustCompile(`\s+`) // Match whitespace, to be compressed during cleaning ) // documentPaths parses all paths in a framework.Backend into OpenAPI paths. -func documentPaths(backend *Backend, requestResponsePrefix string, genericMountPaths bool, doc *OASDocument) error { +func documentPaths(backend *Backend, requestResponsePrefix string, doc *OASDocument) error { for _, p := range backend.Paths { - if err := documentPath(p, backend.SpecialPaths(), requestResponsePrefix, genericMountPaths, backend.BackendType, doc); err != nil { + if err := documentPath(p, backend.SpecialPaths(), requestResponsePrefix, backend.BackendType, doc); err != nil { return err } } @@ -226,7 +226,7 @@ func documentPaths(backend *Backend, requestResponsePrefix string, genericMountP } // documentPath parses a framework.Path into one or more OpenAPI paths. -func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix string, genericMountPaths bool, backendType logical.BackendType, doc *OASDocument) error { +func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix string, backendType logical.BackendType, doc *OASDocument) error { var sudoPaths []string var unauthPaths []string @@ -265,16 +265,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 + defaultMountPath := requestResponsePrefix + if requestResponsePrefix == "kv" { + defaultMountPath = "secret" + } + + if defaultMountPath != "system" && defaultMountPath != "identity" { p := OASParameter{ - Name: "mountPath", - Description: "Path that the backend was mounted at", + Name: fmt.Sprintf("%s_mount_path", defaultMountPath), + Description: "Path where the backend was mounted; the endpoint path will be offset by the mount path", In: "path", Schema: &OASSchema{ - Type: "string", + Type: "string", + Default: defaultMountPath, }, - Required: true, + Required: false, } pi.Parameters = append(pi.Parameters, p) @@ -780,6 +785,9 @@ func cleanResponse(resp *logical.Response) *cleanedResponse { // // An optional user-provided suffix ("context") may also be appended. func (d *OASDocument) CreateOperationIDs(context string) { + // title caser + title := cases.Title(language.English) + opIDCount := make(map[string]int) var paths []string @@ -806,9 +814,12 @@ func (d *OASDocument) CreateOperationIDs(context string) { continue } + // Discard "_mount_path" from any {thing_mount_path} parameters + path = strings.Replace(path, "_mount_path", "", 1) + // Space-split on non-words, title case everything, recombine opID := nonWordRe.ReplaceAllString(strings.ToLower(path), " ") - opID = strings.Title(opID) + opID = title.String(opID) opID = method + strings.ReplaceAll(opID, " ", "") // deduplicate operationIds. This is a safeguard, since generated IDs should diff --git a/sdk/framework/openapi_test.go b/sdk/framework/openapi_test.go index 47b4452712..00895ca3fb 100644 --- a/sdk/framework/openapi_test.go +++ b/sdk/framework/openapi_test.go @@ -270,7 +270,7 @@ func TestOpenAPI_SpecialPaths(t *testing.T) { Root: test.rootPaths, Unauthenticated: test.unauthPaths, } - err := documentPath(&path, sp, "kv", false, logical.TypeLogical, doc) + err := documentPath(&path, sp, "kv", logical.TypeLogical, doc) if err != nil { t.Fatal(err) } @@ -528,11 +528,11 @@ func TestOpenAPI_OperationID(t *testing.T) { for _, context := range []string{"", "bar"} { doc := NewOASDocument("version") - err := documentPath(path1, nil, "kv", false, logical.TypeLogical, doc) + err := documentPath(path1, nil, "kv", logical.TypeLogical, doc) if err != nil { t.Fatal(err) } - err = documentPath(path2, nil, "kv", false, logical.TypeLogical, doc) + err = documentPath(path2, nil, "kv", logical.TypeLogical, doc) if err != nil { t.Fatal(err) } @@ -592,7 +592,7 @@ func TestOpenAPI_CustomDecoder(t *testing.T) { } docOrig := NewOASDocument("version") - err := documentPath(p, nil, "kv", false, logical.TypeLogical, docOrig) + err := documentPath(p, nil, "kv", logical.TypeLogical, docOrig) if err != nil { t.Fatal(err) } @@ -655,7 +655,7 @@ func testPath(t *testing.T, path *Path, sp *logical.Paths, expectedJSON string) t.Helper() doc := NewOASDocument("dummyversion") - if err := documentPath(path, sp, "kv", false, logical.TypeLogical, doc); err != nil { + if err := documentPath(path, sp, "kv", logical.TypeLogical, doc); err != nil { t.Fatal(err) } doc.CreateOperationIDs("") @@ -665,6 +665,8 @@ func testPath(t *testing.T, path *Path, sp *logical.Paths, expectedJSON string) t.Fatal(err) } + t.Log(string(docJSON)) + // Compare json by first decoding, then comparing with a deep equality check. var expected, actual interface{} if err := jsonutil.DecodeJSON(docJSON, &actual); err != nil { diff --git a/sdk/framework/path.go b/sdk/framework/path.go index c3cf385cdb..80f4d5dc6c 100644 --- a/sdk/framework/path.go +++ b/sdk/framework/path.go @@ -330,7 +330,7 @@ func (p *Path) helpCallback(b *Backend) OperationFunc { } } doc := NewOASDocument(vaultVersion) - if err := documentPath(p, b.SpecialPaths(), requestResponsePrefix, false, b.BackendType, doc); err != nil { + if err := documentPath(p, b.SpecialPaths(), requestResponsePrefix, b.BackendType, doc); err != nil { b.Logger().Warn("error generating OpenAPI", "error", err) } diff --git a/sdk/framework/testdata/legacy.json b/sdk/framework/testdata/legacy.json index f526f1e2aa..3b4b1c2afd 100644 --- a/sdk/framework/testdata/legacy.json +++ b/sdk/framework/testdata/legacy.json @@ -21,6 +21,15 @@ "type": "string" }, "required": true + }, + { + "name": "secret_mount_path", + "description": "Path where the backend was mounted; the endpoint path will be offset by the mount path", + "in": "path", + "schema": { + "type": "string", + "default": "secret" + } } ], "get": { diff --git a/sdk/framework/testdata/operations.json b/sdk/framework/testdata/operations.json index e1db674401..0cd198c069 100644 --- a/sdk/framework/testdata/operations.json +++ b/sdk/framework/testdata/operations.json @@ -34,6 +34,15 @@ "type": "string" }, "required": true + }, + { + "name": "secret_mount_path", + "description": "Path where the backend was mounted; the endpoint path will be offset by the mount path", + "in": "path", + "schema": { + "type": "string", + "default": "secret" + } } ], "get": { diff --git a/sdk/framework/testdata/operations_list.json b/sdk/framework/testdata/operations_list.json index e89622a3c4..f9616c8e12 100644 --- a/sdk/framework/testdata/operations_list.json +++ b/sdk/framework/testdata/operations_list.json @@ -33,6 +33,15 @@ "type": "string" }, "required": true + }, + { + "name": "secret_mount_path", + "description": "Path where the backend was mounted; the endpoint path will be offset by the mount path", + "in": "path", + "schema": { + "type": "string", + "default": "secret" + } } ], "get": { diff --git a/sdk/framework/testdata/responses.json b/sdk/framework/testdata/responses.json index 4e442cfb49..b9cb5d152b 100644 --- a/sdk/framework/testdata/responses.json +++ b/sdk/framework/testdata/responses.json @@ -12,6 +12,17 @@ "paths": { "/foo": { "description": "Synopsis", + "parameters": [ + { + "name": "secret_mount_path", + "description": "Path where the backend was mounted; the endpoint path will be offset by the mount path", + "in": "path", + "schema": { + "type": "string", + "default": "secret" + } + } + ], "x-vault-unauthenticated": true, "delete": { "operationId": "deleteFoo", diff --git a/ui/app/services/path-help.js b/ui/app/services/path-help.js index b295b25a67..e3e96e6c3d 100644 --- a/ui/app/services/path-help.js +++ b/ui/app/services/path-help.js @@ -7,7 +7,6 @@ import Model from '@ember-data/model'; import Service from '@ember/service'; import { encodePath } from 'vault/utils/path-encoding-helpers'; import { getOwner } from '@ember/application'; -import { assign } from '@ember/polyfills'; import { expandOpenApiProps, combineAttributes } from 'vault/utils/openapi-to-attrs'; import fieldToAttrs from 'vault/utils/field-to-attrs'; import { resolve, reject } from 'rsvp'; @@ -179,31 +178,36 @@ export default Service.extend({ // Returns relevant information from OpenAPI // as determined by the expandOpenApiProps util getProps(helpUrl, backend) { - // add name of thing you want debug(`Fetching schema properties for ${backend} from ${helpUrl}`); return this.ajax(helpUrl, backend).then((help) => { - // paths is an array but it will have a single entry - // for the scope we're in - const path = Object.keys(help.openapi.paths)[0]; // do this or look at name + // help.openapi.paths is an array with one item + const path = Object.keys(help.openapi.paths)[0]; const pathInfo = help.openapi.paths[path]; const params = pathInfo.parameters; const paramProp = {}; // include url params if (params) { - const { name, schema, description } = params[0]; - const label = capitalize(name.split('_').join(' ')); + params.forEach((param) => { + const { name, schema, description } = param; + if (name === '_mount_path') { + // this param refers to the engine mount path, + // which is already accounted for as backend + return; + } + const label = capitalize(name.split('_').join(' ')); - paramProp[name] = { - 'x-vault-displayAttrs': { - name: label, - group: 'default', - }, - type: schema.type, - description: description, - isId: true, - }; + paramProp[name] = { + 'x-vault-displayAttrs': { + name: label, + group: 'default', + }, + type: schema.type, + description: description, + isId: true, + }; + }); } let props = {}; @@ -220,7 +224,7 @@ export default Service.extend({ } // put url params (e.g. {name}, {role}) // at the front of the props list - const newProps = assign({}, paramProp, props); + const newProps = { ...paramProp, ...props }; return expandOpenApiProps(newProps); }); }, diff --git a/vault/logical_system.go b/vault/logical_system.go index 961c30a9e9..5c467b1daa 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -4440,8 +4440,6 @@ func (b *SystemBackend) pathInternalOpenAPI(ctx context.Context, req *logical.Re // be received from plugin backends. doc := framework.NewOASDocument(version.Version) - genericMountPaths, _ := d.Get("generic_mount_paths").(bool) - procMountGroup := func(group, mountPrefix string) error { for mount, entry := range resp.Data[group].(map[string]interface{}) { @@ -4459,7 +4457,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, "genericMountPaths": genericMountPaths}, + Data: map[string]interface{}{"requestResponsePrefix": pluginType}, } resp, err := backend.HandleRequest(ctx, req) @@ -4513,12 +4511,16 @@ 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 + var docPath string + if mount == "kv/" { + docPath = fmt.Sprintf("/%s{secret_mount_path}/%s", mountPrefix, path) + } else if mount != "sys/" && mount != "identity/" { + docPath = fmt.Sprintf("/%s{%s_mount_path}/%s", mountPrefix, strings.TrimRight(mount, "/"), path) } else { - doc.Paths["/"+mountPrefix+mount+path] = obj + docPath = fmt.Sprintf("/%s%s%s", mountPrefix, mount, path) } + + doc.Paths[docPath] = obj } // Merge backend schema components @@ -5028,9 +5030,7 @@ func sanitizePath(path string) string { path += "/" } - if strings.HasPrefix(path, "/") { - path = path[1:] - } + path = strings.TrimPrefix(path, "/") return path } diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index e0e4dd50f7..e3bd92389b 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -3602,66 +3602,13 @@ 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{} // Ensure no paths are reported if there is no token req := logical.TestRequest(t, logical.ReadOperation, "internal/specs/openapi") - resp, err := b.HandleRequest(namespace.RootContext(nil), req) + resp, err := b.HandleRequest(namespace.RootContext(context.Background()), req) if err != nil { t.Fatalf("err: %v", err) } @@ -3715,10 +3662,10 @@ func TestSystemBackend_OpenAPI(t *testing.T) { path string tag string }{ - {"/auth/token/lookup", "auth"}, - {"/cubbyhole/{path}", "secrets"}, + {"/auth/{token_mount_path}/lookup", "auth"}, + {"/{cubbyhole_mount_path}/{path}", "secrets"}, {"/identity/group/id", "identity"}, - {"/secret/.*", "secrets"}, // TODO update after kv repo update + {"/{secret_mount_path}/.*", "secrets"}, // TODO update after kv repo update {"/sys/policy", "system"}, }