Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mount path into the default generated openapi.json spec (UI) #17926

Merged
merged 12 commits into from
Dec 8, 2022
6 changes: 3 additions & 3 deletions api/plugin_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/.+$`),
Expand Down
9 changes: 1 addition & 8 deletions sdk/framework/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}

Expand Down
33 changes: 22 additions & 11 deletions sdk/framework/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,16 +208,16 @@ var (
altRootsRe = regexp.MustCompile(`^\(([\w\-_]+(?:\|[\w\-_]+)+)\)(/.*)$`) // Pattern starting with alts, e.g. "(root1|root2)/(?P<name>regex)"
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
dhuckins marked this conversation as resolved.
Show resolved Hide resolved
pathFieldsRe = regexp.MustCompile(`{(\w+)}`) // Capture OpenAPI-style named parameters, e.g. "lookup/{urltoken}",
reqdRe = regexp.MustCompile(`\(?\?P<(\w+)>[^)]*\)?`) // Capture required parameters, e.g. "(?P<name>regex)"
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
}
}
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
12 changes: 7 additions & 5 deletions sdk/framework/openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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("")
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion sdk/framework/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
9 changes: 9 additions & 0 deletions sdk/framework/testdata/legacy.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
9 changes: 9 additions & 0 deletions sdk/framework/testdata/operations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
9 changes: 9 additions & 0 deletions sdk/framework/testdata/operations_list.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
11 changes: 11 additions & 0 deletions sdk/framework/testdata/responses.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
40 changes: 22 additions & 18 deletions ui/app/services/path-help.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the crux of the change that fixes the UI issues. Before, we only cared about the first param for the path. Now that the backend mount path is also a path param, we instead iterate over all the path params, throw out _mount_path (because we already capture it as backend) and add the rest to the model

const label = capitalize(name.split('_').join(' '));

paramProp[name] = {
'x-vault-displayAttrs': {
name: label,
group: 'default',
},
type: schema.type,
description: description,
isId: true,
};
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,
};
});
}

let props = {};
Expand All @@ -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);
});
},
Expand Down
20 changes: 10 additions & 10 deletions vault/logical_system.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}) {

Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -5028,9 +5030,7 @@ func sanitizePath(path string) string {
path += "/"
}

if strings.HasPrefix(path, "/") {
path = path[1:]
}
path = strings.TrimPrefix(path, "/")

return path
}
Expand Down
Loading