From d41f8cf1d0e52fa7c3af59a1214053904909e7a0 Mon Sep 17 00:00:00 2001
From: Vinny Mannello <94396874+VinnyHC@users.noreply.github.com>
Date: Wed, 22 Dec 2021 15:36:47 -0800
Subject: [PATCH] [Vault-4628] OpenAPI endpoint not expanding root alternations
 (#13487)

---
 changelog/13487.txt           |  3 +++
 sdk/framework/openapi.go      | 30 +++++++++++++++++++++---------
 sdk/framework/openapi_test.go |  4 ++++
 3 files changed, 28 insertions(+), 9 deletions(-)
 create mode 100644 changelog/13487.txt

diff --git a/changelog/13487.txt b/changelog/13487.txt
new file mode 100644
index 000000000..1df81d25a
--- /dev/null
+++ b/changelog/13487.txt
@@ -0,0 +1,3 @@
+```release-note:bug
+sdk/framework: Generate proper OpenAPI specs for path patterns that use an alternation as the root.
+```
\ No newline at end of file
diff --git a/sdk/framework/openapi.go b/sdk/framework/openapi.go
index 02667cda4..4c9d073f0 100644
--- a/sdk/framework/openapi.go
+++ b/sdk/framework/openapi.go
@@ -191,15 +191,16 @@ var OASStdRespNoContent = &OASResponse{
 var optRe = regexp.MustCompile(`(?U)\([^(]*\)\?|\(/\(\?P<[^(]*\)\)\?`)
 
 var (
-	reqdRe           = regexp.MustCompile(`\(?\?P<(\w+)>[^)]*\)?`)   // Capture required parameters, e.g. "(?P<name>regex)"
-	altRe            = regexp.MustCompile(`\((.*)\|(.*)\)`)          // Capture alternation elements, e.g. "(raw/?$|raw/(?P<path>.+))"
-	pathFieldsRe     = regexp.MustCompile(`{(\w+)}`)                 // Capture OpenAPI-style named parameters, e.g. "lookup/{urltoken}",
-	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
-	wsRe             = regexp.MustCompile(`\s+`)                     // Match whitespace, to be compressed during cleaning
-	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"
-	nonWordRe        = regexp.MustCompile(`[^\w]+`)                  // Match a sequence of non-word characters
+	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
+	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.
@@ -464,6 +465,17 @@ func specialPathMatch(path string, specialPaths []string) bool {
 func expandPattern(pattern string) []string {
 	var paths []string
 
+	// Determine if the pattern starts with an alternation for multiple roots
+	// example (root1|root2)/(?P<name>regex) -> match['(root1|root2)/(?P<name>regex)','root1|root2','/(?P<name>regex)']
+	match := altRootsRe.FindStringSubmatch(pattern)
+	if len(match) == 3 {
+		var expandedRoots []string
+		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
 	// detect and remove it now than to compensate for in the other regexes.
 	//
diff --git a/sdk/framework/openapi_test.go b/sdk/framework/openapi_test.go
index 3efbecc61..9c7226820 100644
--- a/sdk/framework/openapi_test.go
+++ b/sdk/framework/openapi_test.go
@@ -199,6 +199,10 @@ func TestOpenAPI_ExpandPattern(t *testing.T) {
 		{"^plugins/catalog/(?P<type>auth|database|secret)/?$", []string{
 			"plugins/catalog/{type}",
 		}},
+		{"(pathOne|pathTwo)/", []string{"pathOne/", "pathTwo/"}},
+		{"(pathOne|pathTwo)/" + GenericNameRegex("name"), []string{"pathOne/{name}", "pathTwo/{name}"}},
+		{"(pathOne|path-2|Path_3)/" + GenericNameRegex("name"),
+			[]string{"Path_3/{name}", "path-2/{name}", "pathOne/{name}"}},
 	}
 
 	for i, test := range tests {
-- 
GitLab