diff --git a/api/plugin_helpers.go b/api/plugin_helpers.go index 5bb566300536..b96cb9becf78 100644 --- a/api/plugin_helpers.go +++ b/api/plugin_helpers.go @@ -40,7 +40,8 @@ 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/?$`), + "/auth/token/accessors": regexp.MustCompile(`^/auth/token/accessors/?$`), + // TODO /auth/token/revoke-orphan requires sudo but isn't represented as such in the OpenAPI spec "/pki/root": regexp.MustCompile(`^/pki/root$`), "/pki/root/sign-self-issued": regexp.MustCompile(`^/pki/root/sign-self-issued$`), "/sys/audit": regexp.MustCompile(`^/sys/audit$`), @@ -52,28 +53,33 @@ var sudoPaths = map[string]*regexp.Regexp{ "/sys/config/cors": regexp.MustCompile(`^/sys/config/cors$`), "/sys/config/ui/headers": regexp.MustCompile(`^/sys/config/ui/headers/?$`), "/sys/config/ui/headers/{header}": regexp.MustCompile(`^/sys/config/ui/headers/.+$`), - "/sys/leases": regexp.MustCompile(`^/sys/leases$`), - "/sys/leases/lookup/": regexp.MustCompile(`^/sys/leases/lookup/?$`), - "/sys/leases/lookup/{prefix}": regexp.MustCompile(`^/sys/leases/lookup/.+$`), - "/sys/leases/revoke-force/{prefix}": regexp.MustCompile(`^/sys/leases/revoke-force/.+$`), - "/sys/leases/revoke-prefix/{prefix}": regexp.MustCompile(`^/sys/leases/revoke-prefix/.+$`), - "/sys/plugins/catalog/{name}": regexp.MustCompile(`^/sys/plugins/catalog/[^/]+$`), - "/sys/plugins/catalog/{type}": regexp.MustCompile(`^/sys/plugins/catalog/[\w-]+$`), - "/sys/plugins/catalog/{type}/{name}": regexp.MustCompile(`^/sys/plugins/catalog/[\w-]+/[^/]+$`), - "/sys/raw": regexp.MustCompile(`^/sys/raw$`), - "/sys/raw/{path}": regexp.MustCompile(`^/sys/raw/.+$`), - "/sys/remount": regexp.MustCompile(`^/sys/remount$`), - "/sys/revoke-force/{prefix}": regexp.MustCompile(`^/sys/revoke-force/.+$`), - "/sys/revoke-prefix/{prefix}": regexp.MustCompile(`^/sys/revoke-prefix/.+$`), - "/sys/rotate": regexp.MustCompile(`^/sys/rotate$`), "/sys/internal/inspect/router/{tag}": regexp.MustCompile(`^/sys/internal/inspect/router/.+$`), + "/sys/leases": regexp.MustCompile(`^/sys/leases$`), + // This entry is a bit wrong... sys/leases/lookup does NOT require sudo. But sys/leases/lookup/ with a trailing + // slash DOES require sudo. But the part of the Vault CLI that uses this logic doesn't pass operation-appropriate + // trailing slashes, it always strips them off, so we end up giving the wrong answer for one of these. + "/sys/leases/lookup": regexp.MustCompile(`^/sys/leases/lookup/?$`), + "/sys/leases/lookup/{prefix}": regexp.MustCompile(`^/sys/leases/lookup/.+$`), + "/sys/leases/revoke-force/{prefix}": regexp.MustCompile(`^/sys/leases/revoke-force/.+$`), + "/sys/leases/revoke-prefix/{prefix}": regexp.MustCompile(`^/sys/leases/revoke-prefix/.+$`), + "/sys/plugins/catalog/{name}": regexp.MustCompile(`^/sys/plugins/catalog/[^/]+$`), + "/sys/plugins/catalog/{type}": regexp.MustCompile(`^/sys/plugins/catalog/[\w-]+$`), + "/sys/plugins/catalog/{type}/{name}": regexp.MustCompile(`^/sys/plugins/catalog/[\w-]+/[^/]+$`), + "/sys/raw": regexp.MustCompile(`^/sys/raw$`), + "/sys/raw/{path}": regexp.MustCompile(`^/sys/raw/.+$`), + "/sys/remount": regexp.MustCompile(`^/sys/remount$`), + "/sys/revoke-force/{prefix}": regexp.MustCompile(`^/sys/revoke-force/.+$`), + "/sys/revoke-prefix/{prefix}": regexp.MustCompile(`^/sys/revoke-prefix/.+$`), + "/sys/rotate": regexp.MustCompile(`^/sys/rotate$`), + // TODO /sys/seal requires sudo but isn't represented as such in the OpenAPI spec + // TODO /sys/step-down requires sudo but isn't represented as such in the OpenAPI spec // enterprise-only paths "/sys/replication/dr/primary/secondary-token": regexp.MustCompile(`^/sys/replication/dr/primary/secondary-token$`), "/sys/replication/performance/primary/secondary-token": regexp.MustCompile(`^/sys/replication/performance/primary/secondary-token$`), "/sys/replication/primary/secondary-token": regexp.MustCompile(`^/sys/replication/primary/secondary-token$`), "/sys/replication/reindex": regexp.MustCompile(`^/sys/replication/reindex$`), - "/sys/storage/raft/snapshot-auto/config/": regexp.MustCompile(`^/sys/storage/raft/snapshot-auto/config/?$`), + "/sys/storage/raft/snapshot-auto/config": regexp.MustCompile(`^/sys/storage/raft/snapshot-auto/config/?$`), "/sys/storage/raft/snapshot-auto/config/{name}": regexp.MustCompile(`^/sys/storage/raft/snapshot-auto/config/[^/]+$`), } @@ -252,6 +258,8 @@ func SudoPaths() map[string]*regexp.Regexp { // Determine whether the given path requires the sudo capability. // Note that this uses hardcoded static path information, so will return incorrect results for paths in namespaces, // or for secret engines mounted at non-default paths. +// Expects to receive a path with an initial slash, but no trailing slashes, as the Vault CLI (the only known and +// expected user of this function) sanitizes its paths that way. func IsSudoPath(path string) bool { // Return early if the path is any of the non-templated sudo paths. if _, ok := sudoPaths[path]; ok { diff --git a/builtin/credential/github/backend.go b/builtin/credential/github/backend.go index f8bbcc403c61..f3669bbfd4db 100644 --- a/builtin/credential/github/backend.go +++ b/builtin/credential/github/backend.go @@ -8,7 +8,7 @@ import ( "net/url" "github.com/google/go-github/github" - cleanhttp "github.com/hashicorp/go-cleanhttp" + "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" "golang.org/x/oauth2" @@ -43,6 +43,21 @@ func Backend() *backend { OperationPrefix: operationPrefixGithub, OperationSuffix: "team-mapping", } + teamMapPaths[0].Operations = map[logical.Operation]framework.OperationHandler{ + logical.ListOperation: &framework.PathOperation{ + Callback: teamMapPaths[0].Callbacks[logical.ListOperation], + Summary: teamMapPaths[0].HelpSynopsis, + }, + logical.ReadOperation: &framework.PathOperation{ + Callback: teamMapPaths[0].Callbacks[logical.ReadOperation], + Summary: teamMapPaths[0].HelpSynopsis, + DisplayAttrs: &framework.DisplayAttributes{ + OperationVerb: "list", + OperationSuffix: "teams2", // The ReadOperation is redundant with the ListOperation + }, + }, + } + teamMapPaths[0].Callbacks = nil b.UserMap = &framework.PolicyMap{ PathMap: framework.PathMap{ @@ -61,6 +76,21 @@ func Backend() *backend { OperationPrefix: operationPrefixGithub, OperationSuffix: "user-mapping", } + userMapPaths[0].Operations = map[logical.Operation]framework.OperationHandler{ + logical.ListOperation: &framework.PathOperation{ + Callback: userMapPaths[0].Callbacks[logical.ListOperation], + Summary: userMapPaths[0].HelpSynopsis, + }, + logical.ReadOperation: &framework.PathOperation{ + Callback: userMapPaths[0].Callbacks[logical.ReadOperation], + Summary: userMapPaths[0].HelpSynopsis, + DisplayAttrs: &framework.DisplayAttributes{ + OperationVerb: "list", + OperationSuffix: "users2", // The ReadOperation is redundant with the ListOperation + }, + }, + } + userMapPaths[0].Callbacks = nil allPaths := append(teamMapPaths, userMapPaths...) b.Backend = &framework.Backend{ diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go index 308d661d6698..4a5132b553c6 100644 --- a/builtin/logical/pki/backend_test.go +++ b/builtin/logical/pki/backend_test.go @@ -6767,8 +6767,8 @@ func TestProperAuthing(t *testing.T) { "cert/unified-delta-crl": shouldBeUnauthedReadList, "cert/unified-delta-crl/raw": shouldBeUnauthedReadList, "cert/unified-delta-crl/raw/pem": shouldBeUnauthedReadList, - "certs": shouldBeAuthed, - "certs/revoked": shouldBeAuthed, + "certs/": shouldBeAuthed, + "certs/revoked/": shouldBeAuthed, "certs/revocation-queue": shouldBeAuthed, "certs/unified-revoked": shouldBeAuthed, "config/acme": shouldBeAuthed, @@ -6817,7 +6817,7 @@ func TestProperAuthing(t *testing.T) { "issuer/default/sign-verbatim": shouldBeAuthed, "issuer/default/sign-verbatim/test": shouldBeAuthed, "issuer/default/sign/test": shouldBeAuthed, - "issuers": shouldBeUnauthedReadList, + "issuers/": shouldBeUnauthedReadList, "issuers/generate/intermediate/exported": shouldBeAuthed, "issuers/generate/intermediate/internal": shouldBeAuthed, "issuers/generate/intermediate/existing": shouldBeAuthed, @@ -6829,7 +6829,7 @@ func TestProperAuthing(t *testing.T) { "issuers/import/cert": shouldBeAuthed, "issuers/import/bundle": shouldBeAuthed, "key/default": shouldBeAuthed, - "keys": shouldBeAuthed, + "keys/": shouldBeAuthed, "keys/generate/internal": shouldBeAuthed, "keys/generate/exported": shouldBeAuthed, "keys/generate/kms": shouldBeAuthed, @@ -6839,7 +6839,7 @@ func TestProperAuthing(t *testing.T) { "revoke": shouldBeAuthed, "revoke-with-key": shouldBeAuthed, "roles/test": shouldBeAuthed, - "roles": shouldBeAuthed, + "roles/": shouldBeAuthed, "root": shouldBeAuthed, "root/generate/exported": shouldBeAuthed, "root/generate/internal": shouldBeAuthed, @@ -6864,7 +6864,7 @@ func TestProperAuthing(t *testing.T) { "unified-crl/delta/pem": shouldBeUnauthedReadList, "unified-ocsp": shouldBeUnauthedWriteOnly, "unified-ocsp/dGVzdAo=": shouldBeUnauthedReadList, - "eab": shouldBeAuthed, + "eab/": shouldBeAuthed, "eab/" + eabKid: shouldBeAuthed, } @@ -6953,7 +6953,8 @@ func TestProperAuthing(t *testing.T) { handler, present := paths[raw_path] if !present { - t.Fatalf("OpenAPI reports PKI mount contains %v->%v but was not tested to be authed or authed.", openapi_path, raw_path) + t.Fatalf("OpenAPI reports PKI mount contains %v -> %v but was not tested to be authed or not authed.", + openapi_path, raw_path) } openapi_data := raw_data.(map[string]interface{}) diff --git a/builtin/logical/ssh/backend_test.go b/builtin/logical/ssh/backend_test.go index 13f9f73624af..a33c6224585c 100644 --- a/builtin/logical/ssh/backend_test.go +++ b/builtin/logical/ssh/backend_test.go @@ -2765,7 +2765,7 @@ func TestProperAuthing(t *testing.T) { "public_key": shouldBeUnauthedReadList, "roles/test-ca": shouldBeAuthed, "roles/test-otp": shouldBeAuthed, - "roles": shouldBeAuthed, + "roles/": shouldBeAuthed, "sign/test-ca": shouldBeAuthed, "tidy/dynamic-keys": shouldBeAuthed, "verify": shouldBeUnauthedWriteOnly, @@ -2809,7 +2809,8 @@ func TestProperAuthing(t *testing.T) { handler, present := paths[raw_path] if !present { - t.Fatalf("OpenAPI reports SSH mount contains %v->%v but was not tested to be authed or authed.", openapi_path, raw_path) + t.Fatalf("OpenAPI reports SSH mount contains %v -> %v but was not tested to be authed or not authed.", + openapi_path, raw_path) } openapi_data := raw_data.(map[string]interface{}) diff --git a/changelog/21723.txt b/changelog/21723.txt new file mode 100644 index 000000000000..cefe5e1c5ad3 --- /dev/null +++ b/changelog/21723.txt @@ -0,0 +1,3 @@ +```release-note:improvement +openapi: List operations are now given first-class representation in the OpenAPI document, rather than sometimes being overlaid with a read operation at the same path +``` diff --git a/sdk/framework/openapi.go b/sdk/framework/openapi.go index 6ef8a8414df6..d92b14c68ea9 100644 --- a/sdk/framework/openapi.go +++ b/sdk/framework/openapi.go @@ -107,13 +107,12 @@ type OASLicense struct { } type OASPathItem struct { - Description string `json:"description,omitempty"` - Parameters []OASParameter `json:"parameters,omitempty"` - Sudo bool `json:"x-vault-sudo,omitempty" mapstructure:"x-vault-sudo"` - Unauthenticated bool `json:"x-vault-unauthenticated,omitempty" mapstructure:"x-vault-unauthenticated"` - CreateSupported bool `json:"x-vault-createSupported,omitempty" mapstructure:"x-vault-createSupported"` - DisplayNavigation bool `json:"x-vault-displayNavigation,omitempty" mapstructure:"x-vault-displayNavigation"` - DisplayAttrs *DisplayAttributes `json:"x-vault-displayAttrs,omitempty" mapstructure:"x-vault-displayAttrs"` + Description string `json:"description,omitempty"` + Parameters []OASParameter `json:"parameters,omitempty"` + Sudo bool `json:"x-vault-sudo,omitempty" mapstructure:"x-vault-sudo"` + Unauthenticated bool `json:"x-vault-unauthenticated,omitempty" mapstructure:"x-vault-unauthenticated"` + CreateSupported bool `json:"x-vault-createSupported,omitempty" mapstructure:"x-vault-createSupported"` + DisplayAttrs *DisplayAttributes `json:"x-vault-displayAttrs,omitempty" mapstructure:"x-vault-displayAttrs"` Get *OASOperation `json:"get,omitempty"` Post *OASOperation `json:"post,omitempty"` @@ -309,6 +308,7 @@ func documentPath(p *Path, backend *Backend, requestResponsePrefix string, doc * // Process each supported operation by building up an Operation object // with descriptions, properties and examples from the framework.Path data. + var listOperation *OASOperation for opType, opHandler := range operations { props := opHandler.Properties() if props.Unpublished || forceUnpublished { @@ -324,11 +324,6 @@ func documentPath(p *Path, backend *Backend, requestResponsePrefix string, doc * } } - // If both List and Read are defined, only process Read. - if opType == logical.ListOperation && operations[logical.ReadOperation] != nil { - continue - } - op := NewOASOperation() operationID := constructOperationID( @@ -408,9 +403,10 @@ func documentPath(p *Path, backend *Backend, requestResponsePrefix string, doc * } } - // LIST is represented as GET with a `list` query parameter. + // LIST is represented as GET with a `list` query parameter. Code later on in this function will assign + // list operations to a path with an extra trailing slash, ensuring they do not collide with read + // operations. if opType == logical.ListOperation { - // Only accepts List (due to the above skipping of ListOperations that also have ReadOperations) op.Parameters = append(op.Parameters, OASParameter{ Name: "list", Description: "Must be set to `true`", @@ -418,14 +414,6 @@ func documentPath(p *Path, backend *Backend, requestResponsePrefix string, doc * In: "query", Schema: &OASSchema{Type: "string", Enum: []interface{}{"true"}}, }) - } else if opType == logical.ReadOperation && operations[logical.ListOperation] != nil { - // Accepts both Read and List - op.Parameters = append(op.Parameters, OASParameter{ - Name: "list", - Description: "Return a list if `true`", - In: "query", - Schema: &OASSchema{Type: "string"}, - }) } // Add tags based on backend type @@ -521,18 +509,79 @@ func documentPath(p *Path, backend *Backend, requestResponsePrefix string, doc * switch opType { case logical.CreateOperation, logical.UpdateOperation: pi.Post = op - case logical.ReadOperation, logical.ListOperation: + case logical.ReadOperation: pi.Get = op case logical.DeleteOperation: pi.Delete = op + case logical.ListOperation: + listOperation = op } } - openAPIPath := "/" + path - if doc.Paths[openAPIPath] != nil { - backend.Logger().Warn("OpenAPI spec generation: multiple framework.Path instances generated the same path; last processed wins", "path", openAPIPath) + // The conventions enforced by the Vault HTTP routing code make it impossible to match a path with a trailing + // slash to anything other than a ListOperation. Catch mistakes in path definition, to enforce that if both of + // the two following blocks of code (non-list, and list) write an OpenAPI path to the output document, then the + // first one will definitely not have a trailing slash. + originalPathHasTrailingSlash := strings.HasSuffix(path, "/") + if originalPathHasTrailingSlash && (pi.Get != nil || pi.Post != nil || pi.Delete != nil) { + backend.Logger().Warn( + "OpenAPI spec generation: discarding impossible-to-invoke non-list operations from path with "+ + "required trailing slash; this is a bug in the backend code", "path", path) + pi.Get = nil + pi.Post = nil + pi.Delete = nil + } + + // Write the regular, non-list, OpenAPI path to the OpenAPI document, UNLESS we generated a ListOperation, and + // NO OTHER operation types. In that fairly common case (there are lots of list-only endpoints), we avoid + // writing a redundant OpenAPI path for (e.g.) "auth/token/accessors" with no operations, only to then write + // one for "auth/token/accessors/" immediately below. + // + // On the other hand, we do still write the OpenAPI path here if we generated ZERO operation types - this serves + // to provide documentation to a human that an endpoint exists, even if it has no invokable OpenAPI operations. + // Examples of this include kv-v2's ".*" endpoint (regex cannot be translated to OpenAPI parameters), and the + // auth/oci/login endpoint (implements ResolveRoleOperation only, only callable from inside Vault). + if listOperation == nil || pi.Get != nil || pi.Post != nil || pi.Delete != nil { + openAPIPath := "/" + path + if doc.Paths[openAPIPath] != nil { + backend.Logger().Warn( + "OpenAPI spec generation: multiple framework.Path instances generated the same path; "+ + "last processed wins", "path", openAPIPath) + } + doc.Paths[openAPIPath] = &pi + } + + // If there is a ListOperation, write it to a separate OpenAPI path in the document. + if listOperation != nil { + // Append a slash here to disambiguate from the path written immediately above. + // However, if the path already contains a trailing slash, we want to avoid doubling it, and it is + // guaranteed (through the interaction of logic in the last two blocks) that the block immediately above + // will NOT have written a path to the OpenAPI document. + if !originalPathHasTrailingSlash { + path += "/" + } + + listPathItem := OASPathItem{ + Description: pi.Description, + Parameters: pi.Parameters, + DisplayAttrs: pi.DisplayAttrs, + + // Since the path may now have an extra slash on the end, we need to recalculate the special path + // matches, as the sudo or unauthenticated status may be changed as a result! + Sudo: specialPathMatch(path, sudoPaths), + Unauthenticated: specialPathMatch(path, unauthPaths), + + Get: listOperation, + } + + openAPIPath := "/" + path + if doc.Paths[openAPIPath] != nil { + backend.Logger().Warn( + "OpenAPI spec generation: multiple framework.Path instances generated the same path; "+ + "last processed wins", "path", openAPIPath) + } + doc.Paths[openAPIPath] = &listPathItem } - doc.Paths[openAPIPath] = &pi } return nil diff --git a/sdk/framework/testdata/operations.json b/sdk/framework/testdata/operations.json index 7fca0e265014..91bd64d27059 100644 --- a/sdk/framework/testdata/operations.json +++ b/sdk/framework/testdata/operations.json @@ -47,17 +47,7 @@ "200": { "description": "OK" } - }, - "parameters": [ - { - "name": "list", - "description": "Return a list if `true`", - "in": "query", - "schema": { - "type": "string" - } - } - ] + } }, "post": { "operationId": "kv-write-foo-id", @@ -82,6 +72,59 @@ } } } + }, + "/foo/{id}/": { + "description": "Synopsis", + "x-vault-sudo": true, + "x-vault-displayAttrs": { + "navigation": true + }, + "parameters": [ + { + "name": "format", + "description": "a query param", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "description": "id path parameter", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "get": { + "operationId": "kv-list-foo-id", + "tags": [ + "secrets" + ], + "summary": "List Summary", + "description": "List Description", + "responses": { + "200": { + "description": "OK" + } + }, + "parameters": [ + { + "name": "list", + "description": "Must be set to `true`", + "required": true, + "in": "query", + "schema": { + "type": "string", + "enum": [ + "true" + ] + } + } + ] + } } }, "components": { diff --git a/sdk/framework/testdata/operations_list.json b/sdk/framework/testdata/operations_list.json index a08208b24fa4..d7bc50187c78 100644 --- a/sdk/framework/testdata/operations_list.json +++ b/sdk/framework/testdata/operations_list.json @@ -10,7 +10,7 @@ } }, "paths": { - "/foo/{id}": { + "/foo/{id}/": { "description": "Synopsis", "x-vault-sudo": true, "x-vault-displayAttrs": { diff --git a/vault/external_tests/api/sudo_paths_test.go b/vault/external_tests/api/sudo_paths_test.go index 0ca470f1d87c..f2590992a86f 100644 --- a/vault/external_tests/api/sudo_paths_test.go +++ b/vault/external_tests/api/sudo_paths_test.go @@ -5,19 +5,13 @@ package api import ( "fmt" + "strings" "testing" log "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/audit" - auditFile "github.com/hashicorp/vault/builtin/audit/file" - credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" - "github.com/hashicorp/vault/builtin/logical/database" - "github.com/hashicorp/vault/builtin/logical/pki" - "github.com/hashicorp/vault/builtin/logical/transit" "github.com/hashicorp/vault/helper/builtinplugins" "github.com/hashicorp/vault/sdk/helper/jsonutil" - "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/vault" ) @@ -33,24 +27,13 @@ func TestSudoPaths(t *testing.T) { EnableRaw: true, EnableIntrospection: true, Logger: log.NewNullLogger(), - CredentialBackends: map[string]logical.Factory{ - "userpass": credUserpass.Factory, - }, - AuditBackends: map[string]audit.Factory{ - "file": auditFile.Factory, - }, - LogicalBackends: map[string]logical.Factory{ - "database": database.Factory, - "generic-leased": vault.LeasedPassthroughBackendFactory, - "pki": pki.Factory, - "transit": transit.Factory, - }, - BuiltinRegistry: builtinplugins.Registry, + BuiltinRegistry: builtinplugins.Registry, } client, _, closer := testVaultServerCoreConfig(t, coreConfig) defer closer() - for credBackendName := range coreConfig.CredentialBackends { + // At present there are no auth methods with sudo paths, except for the automatically mounted token backend + for _, credBackendName := range []string{} { err := client.Sys().EnableAuthWithOptions(credBackendName, &api.EnableAuthOptions{ Type: credBackendName, }) @@ -59,7 +42,8 @@ func TestSudoPaths(t *testing.T) { } } - for logicalBackendName := range coreConfig.LogicalBackends { + // Each secrets engine that contains sudo paths (other than automatically mounted ones) must be mounted here + for _, logicalBackendName := range []string{"pki"} { err := client.Sys().Mount(logicalBackendName, &api.MountInput{ Type: logicalBackendName, }) @@ -77,11 +61,24 @@ func TestSudoPaths(t *testing.T) { // check for missing paths for path := range sudoPathsFromSpec { - if _, ok := sudoPathsInCode[path]; !ok { + pathTrimmed := strings.TrimRight(path, "/") + if _, ok := sudoPathsInCode[pathTrimmed]; !ok { t.Fatalf( "A path in the OpenAPI spec is missing from the static list of "+ "sudo paths in the api module (%s). Please reconcile the two "+ - "accordingly.", path) + "accordingly.", pathTrimmed) + } + } + + // check for extra paths + for path := range sudoPathsInCode { + if _, ok := sudoPathsFromSpec[path]; !ok { + if _, ok := sudoPathsFromSpec[path+"/"]; !ok { + t.Fatalf( + "A path in the static list of sudo paths in the api module "+ + "is not marked as a sudo path in the OpenAPI spec (%s). Please reconcile the two "+ + "accordingly.", path) + } } } } diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index ce525d5b9134..f90b50c0cac2 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -3678,6 +3678,9 @@ func (b *SystemBackend) policyPaths() []*framework.Path { }, }}, }, + DisplayAttrs: &framework.DisplayAttributes{ + OperationSuffix: "acl-policies2", // this endpoint duplicates sys/policies/acl + }, }, logical.ListOperation: &framework.PathOperation{ Callback: b.handlePoliciesList(PolicyTypeACL), @@ -3695,6 +3698,9 @@ func (b *SystemBackend) policyPaths() []*framework.Path { }, }}, }, + DisplayAttrs: &framework.DisplayAttributes{ + OperationSuffix: "acl-policies3", // this endpoint duplicates sys/policies/acl + }, }, }, diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index 5ee5b3fdf221..cea1c8afbc6c 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -4103,7 +4103,7 @@ func TestSystemBackend_OpenAPI(t *testing.T) { }{ {path: "/auth/token/lookup", tag: "auth"}, {path: "/cubbyhole/{path}", tag: "secrets"}, - {path: "/identity/group/id", tag: "identity"}, + {path: "/identity/group/id/", tag: "identity"}, {path: expectedSecretPrefix + "^.*$", unpublished: true}, {path: "/sys/policy", tag: "system"}, }