mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 18:48:08 +00:00 
			
		
		
		
	Fix multiple OpenAPI generation issues with new AST-based generator (#18554)
* Regexp metacharacter `.` should be escaped when used literally The paths including `/.well-known/` in the Vault API could currently technically be invoked with any random character in place of the dot. * Replace implementation of OpenAPI path translator with regexp AST-based one * Add changelog * Typo fix from PR review - thanks! Co-authored-by: Anton Averchenkov <84287187+averche@users.noreply.github.com> * Add comment based on review feedback * Change style of error handling as suggested in code review * Make a further tweak to the handling of the error case * Add more tests, testing cases which fail with the previous implementation * Resolve issue with a test, and improve comment --------- Co-authored-by: Anton Averchenkov <84287187+averche@users.noreply.github.com>
This commit is contained in:
		
							
								
								
									
										3
									
								
								changelog/18554.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								changelog/18554.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | ```release-note:bug | ||||||
|  | openapi: Fix many incorrect details in generated API spec, by using better techniques to parse path regexps | ||||||
|  | ``` | ||||||
| @@ -1,9 +1,11 @@ | |||||||
| package framework | package framework | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"regexp" | 	"regexp" | ||||||
|  | 	"regexp/syntax" | ||||||
| 	"sort" | 	"sort" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| @@ -194,23 +196,12 @@ var OASStdRespNoContent = &OASResponse{ | |||||||
| 	Description: "empty body", | 	Description: "empty body", | ||||||
| } | } | ||||||
|  |  | ||||||
| // Regex for handling optional and named parameters in paths, and string cleanup. | // Regex for handling fields in paths, and string cleanup. | ||||||
| // Predefined here to avoid substantial recompilation. | // Predefined here to avoid substantial recompilation. | ||||||
|  |  | ||||||
| // Capture optional path elements in ungreedy (?U) fashion |  | ||||||
| // Both "(leases/)?renew" and "(/(?P<name>.+))?" formats are detected |  | ||||||
| var optRe = regexp.MustCompile(`(?U)\([^(]*\)\?|\(/\(\?P<[^(]*\)\)\?`) |  | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	altFieldsGroupRe = regexp.MustCompile(`\(\?P<\w+>\w+(\|\w+)+\)`)              // Match named groups that limit options, e.g. "(?<foo>a|b|c)" |  | ||||||
| 	altFieldsRe      = regexp.MustCompile(`\w+(\|\w+)+`)                          // Match an options set, e.g. "a|b|c" |  | ||||||
| 	altRe            = regexp.MustCompile(`\((.*)\|(.*)\)`)                       // Capture alternation elements, e.g. "(raw/?$|raw/(?P<path>.+))" |  | ||||||
| 	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(`[^\w]+`)  // Match a sequence of non-word characters | ||||||
| 	pathFieldsRe = regexp.MustCompile(`{(\w+)}`) // Capture OpenAPI-style named parameters, e.g. "lookup/{urltoken}", | 	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 | 	wsRe         = regexp.MustCompile(`\s+`)     // Match whitespace, to be compressed during cleaning | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -236,7 +227,22 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Convert optional parameters into distinct patterns to be processed independently. | 	// Convert optional parameters into distinct patterns to be processed independently. | ||||||
| 	paths := expandPattern(p.Pattern) | 	forceUnpublished := false | ||||||
|  | 	paths, err := expandPattern(p.Pattern) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if errors.Is(err, errUnsupportableRegexpOperationForOpenAPI) { | ||||||
|  | 			// Pattern cannot be transformed into sensible OpenAPI paths. In this case, we override the later | ||||||
|  | 			// processing to use the regexp, as is, as the path, and behave as if Unpublished was set on every | ||||||
|  | 			// operation (meaning the operations will not be represented in the OpenAPI document). | ||||||
|  | 			// | ||||||
|  | 			// This allows a human reading the OpenAPI document to notice that, yes, a path handler does exist, | ||||||
|  | 			// even though it was not able to contribute actual OpenAPI operations. | ||||||
|  | 			forceUnpublished = true | ||||||
|  | 			paths = []string{p.Pattern} | ||||||
|  | 		} else { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	for _, path := range paths { | 	for _, path := range paths { | ||||||
| 		// Construct a top level PathItem which will be populated as the path is processed. | 		// Construct a top level PathItem which will be populated as the path is processed. | ||||||
| @@ -305,7 +311,7 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st | |||||||
| 		// with descriptions, properties and examples from the framework.Path data. | 		// with descriptions, properties and examples from the framework.Path data. | ||||||
| 		for opType, opHandler := range operations { | 		for opType, opHandler := range operations { | ||||||
| 			props := opHandler.Properties() | 			props := opHandler.Properties() | ||||||
| 			if props.Unpublished { | 			if props.Unpublished || forceUnpublished { | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| @@ -554,85 +560,198 @@ func specialPathMatch(path string, specialPaths []string) bool { | |||||||
|  |  | ||||||
| // expandPattern expands a regex pattern by generating permutations of any optional parameters | // expandPattern expands a regex pattern by generating permutations of any optional parameters | ||||||
| // and changing named parameters into their {openapi} equivalents. | // and changing named parameters into their {openapi} equivalents. | ||||||
| func expandPattern(pattern string) []string { | func expandPattern(pattern string) ([]string, error) { | ||||||
| 	var paths []string | 	// Happily, the Go regexp library exposes its underlying "parse to AST" functionality, so we can rely on that to do | ||||||
|  | 	// the hard work of interpreting the regexp syntax. | ||||||
| 	// Determine if the pattern starts with an alternation for multiple roots | 	rx, err := syntax.Parse(pattern, syntax.Perl) | ||||||
| 	// example (root1|root2)/(?P<name>regex) -> match['(root1|root2)/(?P<name>regex)','root1|root2','/(?P<name>regex)'] | 	if err != nil { | ||||||
| 	match := altRootsRe.FindStringSubmatch(pattern) | 		// This should be impossible to reach, since regexps have previously been compiled with MustCompile in | ||||||
| 	if len(match) == 3 { | 		// Backend.init. | ||||||
| 		var expandedRoots []string | 		panic(err) | ||||||
| 		for _, root := range strings.Split(match[1], "|") { |  | ||||||
| 			expandedRoots = append(expandedRoots, expandPattern(root+match[2])...) |  | ||||||
| 		} |  | ||||||
| 		return expandedRoots |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// GenericNameRegex adds a regex that complicates our parsing. It is much easier to | 	paths, err := collectPathsFromRegexpAST(rx) | ||||||
| 	// detect and remove it now than to compensate for in the other regexes. | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return paths, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type pathCollector struct { | ||||||
|  | 	strings.Builder | ||||||
|  | 	conditionalSlashAppendedAtLength int | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // collectPathsFromRegexpAST performs a depth-first recursive walk through a regexp AST, collecting an OpenAPI-style | ||||||
|  | // path as it goes. | ||||||
| // | // | ||||||
| 	// example: (?P<foo>\\w(([\\w-.]+)?\\w)?) -> (?P<foo>) | // Each time it encounters alternation (a|b) or an optional part (a?), it forks its processing to produce additional | ||||||
| 	base := GenericNameRegex("") | // results, to account for each possibility. Note: This does mean that an input pattern with lots of these regexp | ||||||
| 	start := strings.Index(base, ">") | // features can produce a lot of different OpenAPI endpoints. At the time of writing, the most complex known example is | ||||||
| 	end := strings.LastIndex(base, ")") | // | ||||||
| 	regexToRemove := "" | //	"issuer/" + framework.GenericNameRegex(issuerRefParam) + "/crl(/pem|/der|/delta(/pem|/der)?)?" | ||||||
| 	if start != -1 && end != -1 && end > start { | // | ||||||
| 		regexToRemove = base[start+1 : end] | // in the PKI secrets engine which expands to 6 separate paths. | ||||||
|  | // | ||||||
|  | // Each named capture group - i.e. (?P<name>something here) - is replaced with an OpenAPI parameter - i.e. {name} - and | ||||||
|  | // the subtree of regexp AST inside the parameter is completely skipped. | ||||||
|  | func collectPathsFromRegexpAST(rx *syntax.Regexp) ([]string, error) { | ||||||
|  | 	pathCollectors, err := collectPathsFromRegexpASTInternal(rx, []*pathCollector{{}}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	paths := make([]string, 0, len(pathCollectors)) | ||||||
|  | 	for _, collector := range pathCollectors { | ||||||
|  | 		if collector.conditionalSlashAppendedAtLength != collector.Len() { | ||||||
|  | 			paths = append(paths, collector.String()) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return paths, nil | ||||||
| } | } | ||||||
| 	pattern = strings.ReplaceAll(pattern, regexToRemove, "") |  | ||||||
|  |  | ||||||
| 	// Simplify named fields that have limited options, e.g. (?P<foo>a|b|c) -> (<P<foo>.+) | var errUnsupportableRegexpOperationForOpenAPI = errors.New("path regexp uses an operation that cannot be translated to an OpenAPI pattern") | ||||||
| 	pattern = altFieldsGroupRe.ReplaceAllStringFunc(pattern, func(s string) string { |  | ||||||
| 		return altFieldsRe.ReplaceAllString(s, ".+") |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	// Initialize paths with the original pattern or the halves of an | func collectPathsFromRegexpASTInternal(rx *syntax.Regexp, appendingTo []*pathCollector) ([]*pathCollector, error) { | ||||||
| 	// alternation, which is also present in some patterns. | 	var err error | ||||||
| 	matches := altRe.FindAllStringSubmatch(pattern, -1) |  | ||||||
| 	if len(matches) > 0 { | 	// Depending on the type of this regexp AST node (its Op, i.e. operation), figure out whether it contributes any | ||||||
| 		paths = []string{matches[0][1], matches[0][2]} | 	// characters to the URL path, and whether we need to recurse through child AST nodes. | ||||||
|  | 	// | ||||||
|  | 	// Each element of the appendingTo slice tracks a separate path, defined by the alternatives chosen when traversing | ||||||
|  | 	// the | and ? conditional regexp features, and new elements are added as each of these features are traversed. | ||||||
|  | 	// | ||||||
|  | 	// To share this slice across multiple recursive calls of this function, it is passed down as a parameter to each | ||||||
|  | 	// recursive call, potentially modified throughout this switch block, and passed back up as a return value at the | ||||||
|  | 	// end of this function - the parent call uses the return value to update its own local variable. | ||||||
|  | 	switch rx.Op { | ||||||
|  |  | ||||||
|  | 	// These AST operations are leaf nodes (no children), that match zero characters, so require no processing at all | ||||||
|  | 	case syntax.OpEmptyMatch: // e.g. (?:) | ||||||
|  | 	case syntax.OpBeginLine: // i.e. ^ when (?m) | ||||||
|  | 	case syntax.OpEndLine: // i.e. $ when (?m) | ||||||
|  | 	case syntax.OpBeginText: // i.e. \A, or ^ when (?-m) | ||||||
|  | 	case syntax.OpEndText: // i.e. \z, or $ when (?-m) | ||||||
|  | 	case syntax.OpWordBoundary: // i.e. \b | ||||||
|  | 	case syntax.OpNoWordBoundary: // i.e. \B | ||||||
|  |  | ||||||
|  | 	// OpConcat simply represents multiple parts of the pattern appearing one after the other, so just recurse through | ||||||
|  | 	// those pieces. | ||||||
|  | 	case syntax.OpConcat: | ||||||
|  | 		for _, child := range rx.Sub { | ||||||
|  | 			appendingTo, err = collectPathsFromRegexpASTInternal(child, appendingTo) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	// OpLiteral is a literal string in the pattern - append it to the paths we are building. | ||||||
|  | 	case syntax.OpLiteral: | ||||||
|  | 		for _, collector := range appendingTo { | ||||||
|  | 			collector.WriteString(string(rx.Rune)) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	// OpAlternate, i.e. a|b, means we clone all of the pathCollector instances we are currently accumulating paths | ||||||
|  | 	// into, and independently recurse through each alternate option. | ||||||
|  | 	case syntax.OpAlternate: // i.e | | ||||||
|  | 		var totalAppendingTo []*pathCollector | ||||||
|  | 		lastIndex := len(rx.Sub) - 1 | ||||||
|  | 		for index, child := range rx.Sub { | ||||||
|  | 			var childAppendingTo []*pathCollector | ||||||
|  | 			if index == lastIndex { | ||||||
|  | 				// Optimization: last time through this loop, we can simply re-use the existing set of pathCollector | ||||||
|  | 				// instances, as we no longer need to preserve them unmodified to make further copies of. | ||||||
|  | 				childAppendingTo = appendingTo | ||||||
| 			} else { | 			} else { | ||||||
| 		paths = []string{pattern} | 				for _, collector := range appendingTo { | ||||||
|  | 					newCollector := new(pathCollector) | ||||||
|  | 					newCollector.WriteString(collector.String()) | ||||||
|  | 					newCollector.conditionalSlashAppendedAtLength = collector.conditionalSlashAppendedAtLength | ||||||
|  | 					childAppendingTo = append(childAppendingTo, newCollector) | ||||||
| 				} | 				} | ||||||
|  | 			} | ||||||
|  | 			childAppendingTo, err = collectPathsFromRegexpASTInternal(child, childAppendingTo) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 			totalAppendingTo = append(totalAppendingTo, childAppendingTo...) | ||||||
|  | 		} | ||||||
|  | 		appendingTo = totalAppendingTo | ||||||
|  |  | ||||||
| 	// Expand all optional regex elements into two paths. This approach is really only useful up to 2 optional | 	// OpQuest, i.e. a?, is much like an alternation between exactly two options, one of which is the empty string. | ||||||
| 	// groups, but we probably don't want to deal with the exponential increase beyond that anyway. | 	case syntax.OpQuest: | ||||||
| 	for i := 0; i < len(paths); i++ { | 		child := rx.Sub[0] | ||||||
| 		p := paths[i] | 		var childAppendingTo []*pathCollector | ||||||
|  | 		for _, collector := range appendingTo { | ||||||
|  | 			newCollector := new(pathCollector) | ||||||
|  | 			newCollector.WriteString(collector.String()) | ||||||
|  | 			newCollector.conditionalSlashAppendedAtLength = collector.conditionalSlashAppendedAtLength | ||||||
|  | 			childAppendingTo = append(childAppendingTo, newCollector) | ||||||
|  | 		} | ||||||
|  | 		childAppendingTo, err = collectPathsFromRegexpASTInternal(child, childAppendingTo) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		appendingTo = append(appendingTo, childAppendingTo...) | ||||||
|  |  | ||||||
| 		// match is a 2-element slice that will have a start and end index | 		// Many Vault path patterns end with `/?` to accept paths that end with or without a slash. Our current | ||||||
| 		// for the left-most match of a regex of form: (lease/)? | 		// convention for generating the OpenAPI is to strip away these slashes. To do that, this very special case | ||||||
| 		match := optRe.FindStringIndex(p) | 		// detects when we just appended a single conditional slash, and records the length of the path at this point, | ||||||
|  | 		// so we can later discard this path variant, if nothing else is appended to it later. | ||||||
| 		if match != nil { | 		if child.Op == syntax.OpLiteral && string(child.Rune) == "/" { | ||||||
| 			// create a path that includes the optional element but without | 			for _, collector := range childAppendingTo { | ||||||
| 			// parenthesis or the '?' character. | 				collector.conditionalSlashAppendedAtLength = collector.Len() | ||||||
| 			paths[i] = p[:match[0]] + p[match[0]+1:match[1]-2] + p[match[1]:] |  | ||||||
|  |  | ||||||
| 			// create a path that excludes the optional element. |  | ||||||
| 			paths = append(paths, p[:match[0]]+p[match[1]:]) |  | ||||||
| 			i-- |  | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 	// Replace named parameters (?P<foo>) with {foo} | 	// OpCapture, i.e. ( ) or (?P<name> ), a capturing group | ||||||
| 	var replacedPaths []string | 	case syntax.OpCapture: | ||||||
|  | 		if rx.Name == "" { | ||||||
| 	for _, path := range paths { | 			// In Vault, an unnamed capturing group is not actually used for capturing. | ||||||
| 		result := reqdRe.FindAllStringSubmatch(path, -1) | 			// We treat it exactly the same as OpConcat. | ||||||
| 		if result != nil { | 			for _, child := range rx.Sub { | ||||||
| 			for _, p := range result { | 				appendingTo, err = collectPathsFromRegexpASTInternal(child, appendingTo) | ||||||
| 				par := p[1] | 				if err != nil { | ||||||
| 				path = strings.Replace(path, p[0], fmt.Sprintf("{%s}", par), 1) | 					return nil, err | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		// Final cleanup | 		} else { | ||||||
| 		path = cleanSuffixRe.ReplaceAllString(path, "") | 			// A named capturing group is replaced with the OpenAPI parameter syntax, and the regexp inside the group | ||||||
| 		path = cleanCharsRe.ReplaceAllString(path, "") | 			// is NOT added to the OpenAPI path. | ||||||
| 		replacedPaths = append(replacedPaths, path) | 			for _, builder := range appendingTo { | ||||||
|  | 				builder.WriteRune('{') | ||||||
|  | 				builder.WriteString(rx.Name) | ||||||
|  | 				builder.WriteRune('}') | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 	return replacedPaths | 	// Any other kind of operation is a problem, and will trigger an error, resulting in the pattern being left out of | ||||||
|  | 	// the OpenAPI entirely - that's better than generating a path which is incorrect. | ||||||
|  | 	// | ||||||
|  | 	// The Op types we expect to hit the default condition are: | ||||||
|  | 	// | ||||||
|  | 	//     OpCharClass    - i.e. [something] | ||||||
|  | 	//     OpAnyCharNotNL - i.e. . | ||||||
|  | 	//     OpAnyChar      - i.e. (?s:.) | ||||||
|  | 	//     OpStar         - i.e. * | ||||||
|  | 	//     OpPlus         - i.e. + | ||||||
|  | 	//     OpRepeat       - i.e. {N}, {N,M}, etc. | ||||||
|  | 	// | ||||||
|  | 	// In any of these conditions, there is no sensible translation of the path to OpenAPI syntax. (Note, this only | ||||||
|  | 	// applies to these appearing outside of a named capture group, otherwise they are handled in the previous case.) | ||||||
|  | 	// | ||||||
|  | 	// At the time of writing, the only pattern in the builtin Vault plugins that hits this codepath is the ".*" | ||||||
|  | 	// pattern in the KVv2 secrets engine, which is not a valid path, but rather, is a catch-all used to implement | ||||||
|  | 	// custom error handling behaviour to guide users who attempt to treat a KVv2 as a KVv1. It is already marked as | ||||||
|  | 	// Unpublished, so is withheld from the OpenAPI anyway. | ||||||
|  | 	// | ||||||
|  | 	// For completeness, one other Op type exists, OpNoMatch, which is never generated by syntax.Parse - only by | ||||||
|  | 	// subsequent Simplify in preparation to Compile, which is not used here. | ||||||
|  | 	default: | ||||||
|  | 		return nil, errUnsupportableRegexpOperationForOpenAPI | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return appendingTo, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // schemaType is a subset of the JSON Schema elements used as a target | // schemaType is a subset of the JSON Schema elements used as a target | ||||||
|   | |||||||
| @@ -18,75 +18,6 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestOpenAPI_Regex(t *testing.T) { | func TestOpenAPI_Regex(t *testing.T) { | ||||||
| 	t.Run("Required", func(t *testing.T) { |  | ||||||
| 		tests := []struct { |  | ||||||
| 			input    string |  | ||||||
| 			captures []string |  | ||||||
| 		}{ |  | ||||||
| 			{`/foo/bar/(?P<val>.*)`, []string{"val"}}, |  | ||||||
| 			{`/foo/bar/` + GenericNameRegex("val"), []string{"val"}}, |  | ||||||
| 			{`/foo/bar/` + GenericNameRegex("first") + "/b/" + GenericNameRegex("second"), []string{"first", "second"}}, |  | ||||||
| 			{`/foo/bar`, []string{}}, |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		for _, test := range tests { |  | ||||||
| 			result := reqdRe.FindAllStringSubmatch(test.input, -1) |  | ||||||
| 			if len(result) != len(test.captures) { |  | ||||||
| 				t.Fatalf("Capture error (%s): expected %d matches, actual: %d", test.input, len(test.captures), len(result)) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			for i := 0; i < len(result); i++ { |  | ||||||
| 				if result[i][1] != test.captures[i] { |  | ||||||
| 					t.Fatalf("Capture error (%s): expected %s, actual: %s", test.input, test.captures[i], result[i][1]) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	}) |  | ||||||
| 	t.Run("Optional", func(t *testing.T) { |  | ||||||
| 		input := "foo/(maybe/)?bar" |  | ||||||
| 		expStart := len("foo/") |  | ||||||
| 		expEnd := len(input) - len("bar") |  | ||||||
|  |  | ||||||
| 		match := optRe.FindStringIndex(input) |  | ||||||
| 		if diff := deep.Equal(match, []int{expStart, expEnd}); diff != nil { |  | ||||||
| 			t.Fatal(diff) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		input = "/foo/maybe/bar" |  | ||||||
| 		match = optRe.FindStringIndex(input) |  | ||||||
| 		if match != nil { |  | ||||||
| 			t.Fatalf("Expected nil match (%s), got %+v", input, match) |  | ||||||
| 		} |  | ||||||
| 	}) |  | ||||||
| 	t.Run("Alternation", func(t *testing.T) { |  | ||||||
| 		input := `(raw/?$|raw/(?P<path>.+))` |  | ||||||
|  |  | ||||||
| 		matches := altRe.FindAllStringSubmatch(input, -1) |  | ||||||
| 		exp1 := "raw/?$" |  | ||||||
| 		exp2 := "raw/(?P<path>.+)" |  | ||||||
| 		if matches[0][1] != exp1 || matches[0][2] != exp2 { |  | ||||||
| 			t.Fatalf("Capture error. Expected %s and %s, got %v", exp1, exp2, matches[0][1:]) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		input = `/foo/bar/` + GenericNameRegex("val") |  | ||||||
|  |  | ||||||
| 		matches = altRe.FindAllStringSubmatch(input, -1) |  | ||||||
| 		if matches != nil { |  | ||||||
| 			t.Fatalf("Expected nil match (%s), got %+v", input, matches) |  | ||||||
| 		} |  | ||||||
| 	}) |  | ||||||
| 	t.Run("Alternation Fields", func(t *testing.T) { |  | ||||||
| 		input := `/foo/bar/(?P<type>auth|database|secret)/(?P<blah>a|b)` |  | ||||||
|  |  | ||||||
| 		act := altFieldsGroupRe.ReplaceAllStringFunc(input, func(s string) string { |  | ||||||
| 			return altFieldsRe.ReplaceAllString(s, ".+") |  | ||||||
| 		}) |  | ||||||
|  |  | ||||||
| 		exp := "/foo/bar/(?P<type>.+)/(?P<blah>.+)" |  | ||||||
| 		if act != exp { |  | ||||||
| 			t.Fatalf("Replace error. Expected %s, got %v", exp, act) |  | ||||||
| 		} |  | ||||||
| 	}) |  | ||||||
| 	t.Run("Path fields", func(t *testing.T) { | 	t.Run("Path fields", func(t *testing.T) { | ||||||
| 		input := `/foo/bar/{inner}/baz/{outer}` | 		input := `/foo/bar/{inner}/baz/{outer}` | ||||||
|  |  | ||||||
| @@ -111,21 +42,6 @@ func TestOpenAPI_Regex(t *testing.T) { | |||||||
| 			regex  *regexp.Regexp | 			regex  *regexp.Regexp | ||||||
| 			output string | 			output string | ||||||
| 		}{ | 		}{ | ||||||
| 			{ |  | ||||||
| 				input:  `ab?cde^fg(hi?j$k`, |  | ||||||
| 				regex:  cleanCharsRe, |  | ||||||
| 				output: "abcdefghijk", |  | ||||||
| 			}, |  | ||||||
| 			{ |  | ||||||
| 				input:  `abcde/?`, |  | ||||||
| 				regex:  cleanSuffixRe, |  | ||||||
| 				output: "abcde", |  | ||||||
| 			}, |  | ||||||
| 			{ |  | ||||||
| 				input:  `abcde/?$`, |  | ||||||
| 				regex:  cleanSuffixRe, |  | ||||||
| 				output: "abcde", |  | ||||||
| 			}, |  | ||||||
| 			{ | 			{ | ||||||
| 				input:  `abcde`, | 				input:  `abcde`, | ||||||
| 				regex:  wsRe, | 				regex:  wsRe, | ||||||
| @@ -152,62 +68,99 @@ func TestOpenAPI_ExpandPattern(t *testing.T) { | |||||||
| 		inPattern   string | 		inPattern   string | ||||||
| 		outPathlets []string | 		outPathlets []string | ||||||
| 	}{ | 	}{ | ||||||
|  | 		// A simple string without regexp metacharacters passes through as is | ||||||
| 		{"rekey/backup", []string{"rekey/backup"}}, | 		{"rekey/backup", []string{"rekey/backup"}}, | ||||||
|  | 		// A trailing regexp anchor metacharacter is removed | ||||||
| 		{"rekey/backup$", []string{"rekey/backup"}}, | 		{"rekey/backup$", []string{"rekey/backup"}}, | ||||||
|  | 		// As is a leading one | ||||||
|  | 		{"^rekey/backup", []string{"rekey/backup"}}, | ||||||
|  | 		// Named capture groups become OpenAPI parameters | ||||||
| 		{"auth/(?P<path>.+?)/tune$", []string{"auth/{path}/tune"}}, | 		{"auth/(?P<path>.+?)/tune$", []string{"auth/{path}/tune"}}, | ||||||
| 		{"auth/(?P<path>.+?)/tune/(?P<more>.*?)$", []string{"auth/{path}/tune/{more}"}}, | 		{"auth/(?P<path>.+?)/tune/(?P<more>.*?)$", []string{"auth/{path}/tune/{more}"}}, | ||||||
|  | 		// Even if the capture group contains very complex regexp structure inside it | ||||||
|  | 		{"something/(?P<something>(a|b(c|d))|e+|f{1,3}[ghi-k]?.*)", []string{"something/{something}"}}, | ||||||
|  | 		// A question-mark results in a result without and with the optional path part | ||||||
| 		{"tools/hash(/(?P<urlalgorithm>.+))?", []string{ | 		{"tools/hash(/(?P<urlalgorithm>.+))?", []string{ | ||||||
| 			"tools/hash", | 			"tools/hash", | ||||||
| 			"tools/hash/{urlalgorithm}", | 			"tools/hash/{urlalgorithm}", | ||||||
| 		}}, | 		}}, | ||||||
|  | 		// Multiple question-marks evaluate each possible combination | ||||||
| 		{"(leases/)?renew(/(?P<url_lease_id>.+))?", []string{ | 		{"(leases/)?renew(/(?P<url_lease_id>.+))?", []string{ | ||||||
| 			"leases/renew", | 			"leases/renew", | ||||||
| 			"leases/renew/{url_lease_id}", | 			"leases/renew/{url_lease_id}", | ||||||
| 			"renew", | 			"renew", | ||||||
| 			"renew/{url_lease_id}", | 			"renew/{url_lease_id}", | ||||||
| 		}}, | 		}}, | ||||||
|  | 		// GenericNameRegex is one particular way of writing a named capture group, so behaves the same | ||||||
| 		{`config/ui/headers/` + GenericNameRegex("header"), []string{"config/ui/headers/{header}"}}, | 		{`config/ui/headers/` + GenericNameRegex("header"), []string{"config/ui/headers/{header}"}}, | ||||||
|  | 		// The question-mark behaviour is still works when the question-mark is directly applied to a named capture group | ||||||
| 		{`leases/lookup/(?P<prefix>.+?)?`, []string{ | 		{`leases/lookup/(?P<prefix>.+?)?`, []string{ | ||||||
| 			"leases/lookup/", | 			"leases/lookup/", | ||||||
| 			"leases/lookup/{prefix}", | 			"leases/lookup/{prefix}", | ||||||
| 		}}, | 		}}, | ||||||
|  | 		// Optional trailing slashes at the end of the path get stripped - even if appearing deep inside an alternation | ||||||
| 		{`(raw/?$|raw/(?P<path>.+))`, []string{ | 		{`(raw/?$|raw/(?P<path>.+))`, []string{ | ||||||
| 			"raw", | 			"raw", | ||||||
| 			"raw/{path}", | 			"raw/{path}", | ||||||
| 		}}, | 		}}, | ||||||
|  | 		// OptionalParamRegex is also another way of writing a named capture group, that is optional | ||||||
| 		{"lookup" + OptionalParamRegex("urltoken"), []string{ | 		{"lookup" + OptionalParamRegex("urltoken"), []string{ | ||||||
| 			"lookup", | 			"lookup", | ||||||
| 			"lookup/{urltoken}", | 			"lookup/{urltoken}", | ||||||
| 		}}, | 		}}, | ||||||
|  | 		// Optional trailign slashes at the end of the path get stripped in simpler cases too | ||||||
| 		{"roles/?$", []string{ | 		{"roles/?$", []string{ | ||||||
| 			"roles", | 			"roles", | ||||||
| 		}}, | 		}}, | ||||||
| 		{"roles/?", []string{ | 		{"roles/?", []string{ | ||||||
| 			"roles", | 			"roles", | ||||||
| 		}}, | 		}}, | ||||||
|  | 		// Non-optional trailing slashes remain... although don't do this, it breaks HelpOperation! | ||||||
|  | 		// (Existing real examples of this pattern being fixed via https://github.com/hashicorp/vault/pull/18571) | ||||||
| 		{"accessors/$", []string{ | 		{"accessors/$", []string{ | ||||||
| 			"accessors/", | 			"accessors/", | ||||||
| 		}}, | 		}}, | ||||||
|  | 		// GenericNameRegex and OptionalParamRegex still work when concatenated | ||||||
| 		{"verify/" + GenericNameRegex("name") + OptionalParamRegex("urlalgorithm"), []string{ | 		{"verify/" + GenericNameRegex("name") + OptionalParamRegex("urlalgorithm"), []string{ | ||||||
| 			"verify/{name}", | 			"verify/{name}", | ||||||
| 			"verify/{name}/{urlalgorithm}", | 			"verify/{name}/{urlalgorithm}", | ||||||
| 		}}, | 		}}, | ||||||
|  | 		// Named capture groups that specify enum-like parameters work as expected | ||||||
| 		{"^plugins/catalog/(?P<type>auth|database|secret)/(?P<name>.+)$", []string{ | 		{"^plugins/catalog/(?P<type>auth|database|secret)/(?P<name>.+)$", []string{ | ||||||
| 			"plugins/catalog/{type}/{name}", | 			"plugins/catalog/{type}/{name}", | ||||||
| 		}}, | 		}}, | ||||||
| 		{"^plugins/catalog/(?P<type>auth|database|secret)/?$", []string{ | 		{"^plugins/catalog/(?P<type>auth|database|secret)/?$", []string{ | ||||||
| 			"plugins/catalog/{type}", | 			"plugins/catalog/{type}", | ||||||
| 		}}, | 		}}, | ||||||
|  | 		// Alternations between various literal path segments work | ||||||
| 		{"(pathOne|pathTwo)/", []string{"pathOne/", "pathTwo/"}}, | 		{"(pathOne|pathTwo)/", []string{"pathOne/", "pathTwo/"}}, | ||||||
| 		{"(pathOne|pathTwo)/" + GenericNameRegex("name"), []string{"pathOne/{name}", "pathTwo/{name}"}}, | 		{"(pathOne|pathTwo)/" + GenericNameRegex("name"), []string{"pathOne/{name}", "pathTwo/{name}"}}, | ||||||
| 		{ | 		{ | ||||||
| 			"(pathOne|path-2|Path_3)/" + GenericNameRegex("name"), | 			"(pathOne|path-2|Path_3)/" + GenericNameRegex("name"), | ||||||
| 			[]string{"Path_3/{name}", "path-2/{name}", "pathOne/{name}"}, | 			[]string{"Path_3/{name}", "path-2/{name}", "pathOne/{name}"}, | ||||||
| 		}, | 		}, | ||||||
|  | 		// They still work when combined with GenericNameWithAtRegex | ||||||
|  | 		{"(creds|sts)/" + GenericNameWithAtRegex("name"), []string{ | ||||||
|  | 			"creds/{name}", | ||||||
|  | 			"sts/{name}", | ||||||
|  | 		}}, | ||||||
|  | 		// And when they're somewhere other than the start of the pattern | ||||||
|  | 		{"keys/generate/(internal|exported|kms)", []string{ | ||||||
|  | 			"keys/generate/exported", | ||||||
|  | 			"keys/generate/internal", | ||||||
|  | 			"keys/generate/kms", | ||||||
|  | 		}}, | ||||||
|  | 		// If a plugin author makes their list operation support both singular and plural forms, the OpenAPI notices | ||||||
|  | 		{"rolesets?/?", []string{"roleset", "rolesets"}}, | ||||||
|  | 		// Complex nested alternation and question-marks are correctly interpreted | ||||||
|  | 		{"crl(/pem|/delta(/pem)?)?", []string{"crl", "crl/delta", "crl/delta/pem", "crl/pem"}}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for i, test := range tests { | 	for i, test := range tests { | ||||||
| 		out := expandPattern(test.inPattern) | 		out, err := expandPattern(test.inPattern) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatal(err) | ||||||
|  | 		} | ||||||
| 		sort.Strings(out) | 		sort.Strings(out) | ||||||
| 		if !reflect.DeepEqual(out, test.outPathlets) { | 		if !reflect.DeepEqual(out, test.outPathlets) { | ||||||
| 			t.Fatalf("Test %d: Expected %v got %v", i, test.outPathlets, out) | 			t.Fatalf("Test %d: Expected %v got %v", i, test.outPathlets, out) | ||||||
| @@ -215,6 +168,30 @@ func TestOpenAPI_ExpandPattern(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestOpenAPI_ExpandPattern_ReturnsError(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		inPattern string | ||||||
|  | 		outError  error | ||||||
|  | 	}{ | ||||||
|  | 		// None of these regexp constructs are allowed outside of named capture groups | ||||||
|  | 		{"[a-z]", errUnsupportableRegexpOperationForOpenAPI}, | ||||||
|  | 		{".", errUnsupportableRegexpOperationForOpenAPI}, | ||||||
|  | 		{"a+", errUnsupportableRegexpOperationForOpenAPI}, | ||||||
|  | 		{"a*", errUnsupportableRegexpOperationForOpenAPI}, | ||||||
|  | 		// So this pattern, which is a combination of two of the above isn't either - this pattern occurs in the KV | ||||||
|  | 		// secrets engine for its catch-all error handler, which provides a helpful hint to people treating a KV v2 as | ||||||
|  | 		// a KV v1. | ||||||
|  | 		{".*", errUnsupportableRegexpOperationForOpenAPI}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for i, test := range tests { | ||||||
|  | 		_, err := expandPattern(test.inPattern) | ||||||
|  | 		if err != test.outError { | ||||||
|  | 			t.Fatalf("Test %d: Expected %q got %q", i, test.outError, err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func TestOpenAPI_SplitFields(t *testing.T) { | func TestOpenAPI_SplitFields(t *testing.T) { | ||||||
| 	fields := map[string]*FieldSchema{ | 	fields := map[string]*FieldSchema{ | ||||||
| 		"a": {Description: "path"}, | 		"a": {Description: "path"}, | ||||||
|   | |||||||
| @@ -216,7 +216,7 @@ func oidcPaths(i *IdentityStore) []*framework.Path { | |||||||
| 			HelpDescription: "List all named OIDC keys", | 			HelpDescription: "List all named OIDC keys", | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Pattern: "oidc/.well-known/openid-configuration/?$", | 			Pattern: "oidc/\\.well-known/openid-configuration/?$", | ||||||
| 			Callbacks: map[logical.Operation]framework.OperationFunc{ | 			Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||||
| 				logical.ReadOperation: i.pathOIDCDiscovery, | 				logical.ReadOperation: i.pathOIDCDiscovery, | ||||||
| 			}, | 			}, | ||||||
| @@ -224,7 +224,7 @@ func oidcPaths(i *IdentityStore) []*framework.Path { | |||||||
| 			HelpDescription: "Query this path to retrieve the configured OIDC Issuer and Keys endpoints, response types, subject types, and signing algorithms used by the OIDC backend.", | 			HelpDescription: "Query this path to retrieve the configured OIDC Issuer and Keys endpoints, response types, subject types, and signing algorithms used by the OIDC backend.", | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Pattern: "oidc/.well-known/keys/?$", | 			Pattern: "oidc/\\.well-known/keys/?$", | ||||||
| 			Callbacks: map[logical.Operation]framework.OperationFunc{ | 			Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||||
| 				logical.ReadOperation: i.pathOIDCReadPublicKeys, | 				logical.ReadOperation: i.pathOIDCReadPublicKeys, | ||||||
| 			}, | 			}, | ||||||
|   | |||||||
| @@ -389,7 +389,7 @@ func oidcProviderPaths(i *IdentityStore) []*framework.Path { | |||||||
| 			HelpDescription: "List all configured OIDC providers in the identity backend.", | 			HelpDescription: "List all configured OIDC providers in the identity backend.", | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Pattern: "oidc/provider/" + framework.GenericNameRegex("name") + "/.well-known/openid-configuration", | 			Pattern: "oidc/provider/" + framework.GenericNameRegex("name") + "/\\.well-known/openid-configuration", | ||||||
| 			Fields: map[string]*framework.FieldSchema{ | 			Fields: map[string]*framework.FieldSchema{ | ||||||
| 				"name": { | 				"name": { | ||||||
| 					Type:        framework.TypeString, | 					Type:        framework.TypeString, | ||||||
| @@ -405,7 +405,7 @@ func oidcProviderPaths(i *IdentityStore) []*framework.Path { | |||||||
| 			HelpDescription: "Query this path to retrieve the configured OIDC Issuer and Keys endpoints, response types, subject types, and signing algorithms used by the OIDC backend.", | 			HelpDescription: "Query this path to retrieve the configured OIDC Issuer and Keys endpoints, response types, subject types, and signing algorithms used by the OIDC backend.", | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Pattern: "oidc/provider/" + framework.GenericNameRegex("name") + "/.well-known/keys", | 			Pattern: "oidc/provider/" + framework.GenericNameRegex("name") + "/\\.well-known/keys", | ||||||
| 			Fields: map[string]*framework.FieldSchema{ | 			Fields: map[string]*framework.FieldSchema{ | ||||||
| 				"name": { | 				"name": { | ||||||
| 					Type:        framework.TypeString, | 					Type:        framework.TypeString, | ||||||
|   | |||||||
| @@ -3618,64 +3618,11 @@ 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/token/lookup", "auth"}, |  | ||||||
| 		{"/cubbyhole/{path}", "secrets"}, |  | ||||||
| 		{"/identity/group/id", "identity"}, |  | ||||||
| 		{"/{secret_mount_path}/.*", "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{} |  | ||||||
|  |  | ||||||
| 	// Ensure no paths are reported if there is no token | 	// Ensure no paths are reported if there is no token | ||||||
|  | 	{ | ||||||
| 		req := logical.TestRequest(t, logical.ReadOperation, "internal/specs/openapi") | 		req := logical.TestRequest(t, logical.ReadOperation, "internal/specs/openapi") | ||||||
| 		resp, err := b.HandleRequest(namespace.RootContext(nil), req) | 		resp, err := b.HandleRequest(namespace.RootContext(nil), req) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @@ -3683,6 +3630,7 @@ func TestSystemBackend_OpenAPI(t *testing.T) { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		body := resp.Data["http_raw_body"].([]byte) | 		body := resp.Data["http_raw_body"].([]byte) | ||||||
|  | 		var oapi map[string]interface{} | ||||||
| 		err = jsonutil.DecodeJSON(body, &oapi) | 		err = jsonutil.DecodeJSON(body, &oapi) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			t.Fatalf("err: %v", err) | 			t.Fatalf("err: %v", err) | ||||||
| @@ -3707,16 +3655,22 @@ func TestSystemBackend_OpenAPI(t *testing.T) { | |||||||
| 		if diff := deep.Equal(oapi, exp); diff != nil { | 		if diff := deep.Equal(oapi, exp); diff != nil { | ||||||
| 			t.Fatal(diff) | 			t.Fatal(diff) | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Check that default paths are present with a root token | 	// Check that default paths are present with a root token (with and without generic_mount_paths) | ||||||
| 	req = logical.TestRequest(t, logical.ReadOperation, "internal/specs/openapi") | 	for _, genericMountPaths := range []bool{false, true} { | ||||||
|  | 		req := logical.TestRequest(t, logical.ReadOperation, "internal/specs/openapi") | ||||||
|  | 		if genericMountPaths { | ||||||
|  | 			req.Data["generic_mount_paths"] = true | ||||||
|  | 		} | ||||||
| 		req.ClientToken = rootToken | 		req.ClientToken = rootToken | ||||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | 		resp, err := b.HandleRequest(namespace.RootContext(nil), req) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			t.Fatalf("err: %v", err) | 			t.Fatalf("err: %v", err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 	body = resp.Data["http_raw_body"].([]byte) | 		body := resp.Data["http_raw_body"].([]byte) | ||||||
|  | 		var oapi map[string]interface{} | ||||||
| 		err = jsonutil.DecodeJSON(body, &oapi) | 		err = jsonutil.DecodeJSON(body, &oapi) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			t.Fatalf("err: %v", err) | 			t.Fatalf("err: %v", err) | ||||||
| @@ -3727,26 +3681,41 @@ func TestSystemBackend_OpenAPI(t *testing.T) { | |||||||
| 			t.Fatal(err) | 			t.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		expectedSecretPrefix := "/secret/" | ||||||
|  | 		if genericMountPaths { | ||||||
|  | 			expectedSecretPrefix = "/{secret_mount_path}/" | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		pathSamples := []struct { | 		pathSamples := []struct { | ||||||
| 			path        string | 			path        string | ||||||
| 			tag         string | 			tag         string | ||||||
|  | 			unpublished bool | ||||||
| 		}{ | 		}{ | ||||||
| 		{"/auth/token/lookup", "auth"}, | 			{path: "/auth/token/lookup", tag: "auth"}, | ||||||
| 		{"/cubbyhole/{path}", "secrets"}, | 			{path: "/cubbyhole/{path}", tag: "secrets"}, | ||||||
| 		{"/identity/group/id", "identity"}, | 			{path: "/identity/group/id", tag: "identity"}, | ||||||
| 		{"/secret/.*", "secrets"}, | 			{path: expectedSecretPrefix + "^.*$", unpublished: true}, | ||||||
| 		{"/sys/policy", "system"}, | 			{path: "/sys/policy", tag: "system"}, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		for _, path := range pathSamples { | 		for _, path := range pathSamples { | ||||||
| 			if doc.Paths[path.path] == nil { | 			if doc.Paths[path.path] == nil { | ||||||
| 			t.Fatalf("didn't find expected path %q.", path) | 				t.Fatalf("didn't find expected path %q.", path.path) | ||||||
| 			} | 			} | ||||||
| 		tag := doc.Paths[path.path].Get.Tags[0] | 			getOperation := doc.Paths[path.path].Get | ||||||
|  | 			if getOperation == nil && !path.unpublished { | ||||||
|  | 				t.Fatalf("path: %s; expected a get operation, but it was absent", path.path) | ||||||
|  | 			} | ||||||
|  | 			if getOperation != nil && path.unpublished { | ||||||
|  | 				t.Fatalf("path: %s; expected absent get operation, but it was present", path.path) | ||||||
|  | 			} | ||||||
|  | 			if !path.unpublished { | ||||||
|  | 				tag := getOperation.Tags[0] | ||||||
| 				if tag != path.tag { | 				if tag != path.tag { | ||||||
| 					t.Fatalf("path: %s; expected tag: %s, actual: %s", path.path, 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 | 		// 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. | ||||||
| @@ -3754,16 +3723,18 @@ func TestSystemBackend_OpenAPI(t *testing.T) { | |||||||
| 		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)) | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Test path-help response | 	// Test path-help response | ||||||
| 	req = logical.TestRequest(t, logical.HelpOperation, "rotate") | 	{ | ||||||
|  | 		req := logical.TestRequest(t, logical.HelpOperation, "rotate") | ||||||
| 		req.ClientToken = rootToken | 		req.ClientToken = rootToken | ||||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | 		resp, err := b.HandleRequest(namespace.RootContext(nil), req) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			t.Fatalf("err: %v", err) | 			t.Fatalf("err: %v", err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 	doc = resp.Data["openapi"].(*framework.OASDocument) | 		doc := resp.Data["openapi"].(*framework.OASDocument) | ||||||
| 		if len(doc.Paths) != 1 { | 		if len(doc.Paths) != 1 { | ||||||
| 			t.Fatalf("expected 1 path, actual: %d", len(doc.Paths)) | 			t.Fatalf("expected 1 path, actual: %d", len(doc.Paths)) | ||||||
| 		} | 		} | ||||||
| @@ -3772,6 +3743,7 @@ func TestSystemBackend_OpenAPI(t *testing.T) { | |||||||
| 			t.Fatalf("expected to find path '/rotate'") | 			t.Fatalf("expected to find path '/rotate'") | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func TestSystemBackend_PathWildcardPreflight(t *testing.T) { | func TestSystemBackend_PathWildcardPreflight(t *testing.T) { | ||||||
| 	core, b, _ := testCoreSystemBackend(t) | 	core, b, _ := testCoreSystemBackend(t) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Max Bowsher
					Max Bowsher