diff --git a/changelog/27660.txt b/changelog/27660.txt new file mode 100644 index 000000000000..e754dbbfa360 --- /dev/null +++ b/changelog/27660.txt @@ -0,0 +1,3 @@ +```release-note:bug +core (enterprise): Fix HTTP redirects in namespaces to use the correct path and (in the case of event subscriptions) the correct URI scheme. +``` \ No newline at end of file diff --git a/http/handler.go b/http/handler.go index 3ed3d800415a..46b2199682c9 100644 --- a/http/handler.go +++ b/http/handler.go @@ -116,7 +116,7 @@ var ( "/v1/sys/wrapping/wrap", } websocketRawPaths = []string{ - "/v1/sys/events/subscribe", + "sys/events/subscribe", } oidcProtectedPathRegex = regexp.MustCompile(`^identity/oidc/provider/\w(([\w-.]+)?\w)?/userinfo$`) ) @@ -128,9 +128,7 @@ func init() { "!sys/storage/raft/snapshot-auto/config", }) websocketPaths.AddPaths(websocketRawPaths) - for _, path := range websocketRawPaths { - alwaysRedirectPaths.AddPaths([]string{strings.TrimPrefix(path, "/v1/")}) - } + alwaysRedirectPaths.AddPaths(websocketRawPaths) } type HandlerAnchor struct{} @@ -434,7 +432,7 @@ func wrapGenericHandler(core *vault.Core, h http.Handler, props *vault.HandlerPr } else if standby && !perfStandby { // Standby nodes, not performance standbys, don't start plugins // so registration can not happen, instead redirect to active - respondStandby(core, w, r.URL) + respondStandby(core, w, r) cancelFunc() return } else { @@ -909,7 +907,7 @@ func handleRequestForwarding(core *vault.Core, handler http.Handler) http.Handle respondError(w, http.StatusBadRequest, err) return } - path := ns.TrimmedPath(r.URL.Path[len("/v1/"):]) + path := trimPath(ns, r.URL.Path) if !perfStandbyAlwaysForwardPaths.HasPath(path) && !alwaysRedirectPaths.HasPath(path) { handler.ServeHTTP(w, r) return @@ -946,14 +944,14 @@ func handleRequestForwarding(core *vault.Core, handler http.Handler) http.Handle func forwardRequest(core *vault.Core, w http.ResponseWriter, r *http.Request) { if r.Header.Get(vault.IntNoForwardingHeaderName) != "" { - respondStandby(core, w, r.URL) + respondStandby(core, w, r) return } if r.Header.Get(NoRequestForwardingHeaderName) != "" { // Forwarding explicitly disabled, fall back to previous behavior core.Logger().Debug("handleRequestForwarding: forwarding disabled by client request") - respondStandby(core, w, r.URL) + respondStandby(core, w, r) return } @@ -962,10 +960,25 @@ func forwardRequest(core *vault.Core, w http.ResponseWriter, r *http.Request) { respondError(w, http.StatusBadRequest, err) return } - path := ns.TrimmedPath(r.URL.Path[len("/v1/"):]) - if alwaysRedirectPaths.HasPath(path) { + path := trimPath(ns, r.URL.Path) + redirect := alwaysRedirectPaths.HasPath(path) + // websocket paths are special, because they can contain a namespace + // in front of them. This isn't an issue on perf standbys where the + // namespace manager will know all the namespaces, so we will have + // already extracted it from the path. But regular standbys don't have + // knowledge of the namespaces, so we need + // to add an extra check + if !redirect && !core.PerfStandby() { + for _, websocketPath := range websocketRawPaths { + if strings.Contains(path, websocketPath) { + redirect = true + break + } + } + } + if redirect { core.Logger().Trace("cannot forward request (path included in always redirect paths), falling back to redirection to standby") - respondStandby(core, w, r.URL) + respondStandby(core, w, r) return } @@ -981,7 +994,7 @@ func forwardRequest(core *vault.Core, w http.ResponseWriter, r *http.Request) { } // Fall back to redirection - respondStandby(core, w, r.URL) + respondStandby(core, w, r) return } @@ -1045,7 +1058,7 @@ func request(core *vault.Core, w http.ResponseWriter, rawReq *http.Request, r *l return resp, false, false } if errwrap.Contains(err, consts.ErrStandby.Error()) { - respondStandby(core, w, rawReq.URL) + respondStandby(core, w, rawReq) return resp, false, false } if err != nil && errwrap.Contains(err, logical.ErrPerfStandbyPleaseForward.Error()) { @@ -1094,7 +1107,8 @@ func request(core *vault.Core, w http.ResponseWriter, rawReq *http.Request, r *l } // respondStandby is used to trigger a redirect in the case that this Vault is currently a hot standby -func respondStandby(core *vault.Core, w http.ResponseWriter, reqURL *url.URL) { +func respondStandby(core *vault.Core, w http.ResponseWriter, r *http.Request) { + reqURL := r.URL // Request the leader address _, redirectAddr, _, err := core.Leader() if err != nil { @@ -1131,8 +1145,13 @@ func respondStandby(core *vault.Core, w http.ResponseWriter, reqURL *url.URL) { RawQuery: reqURL.RawQuery, } + ctx := r.Context() + ns, err := namespace.FromContext(ctx) + if err != nil { + respondError(w, http.StatusBadRequest, err) + } // WebSockets schemas are ws or wss - if websocketPaths.HasPath(reqURL.Path) { + if websocketPaths.HasPath(trimPath(ns, reqURL.Path)) { if finalURL.Scheme == "http" { finalURL.Scheme = "ws" } else { @@ -1140,6 +1159,11 @@ func respondStandby(core *vault.Core, w http.ResponseWriter, reqURL *url.URL) { } } + originalPath, ok := logical.ContextOriginalRequestPathValue(ctx) + if ok { + finalURL.Path = originalPath + } + // Ensure there is a scheme, default to https if finalURL.Scheme == "" { finalURL.Scheme = "https" @@ -1391,3 +1415,8 @@ func respondOIDCPermissionDenied(w http.ResponseWriter) { enc := json.NewEncoder(w) enc.Encode(oidcResponse) } + +// trimPath removes the /v1/ prefix and the namespace from the path +func trimPath(ns *namespace.Namespace, path string) string { + return ns.TrimmedPath(path[len("/v1/"):]) +} diff --git a/http/help.go b/http/help.go index e4d03b261c9f..24ff14a8e96f 100644 --- a/http/help.go +++ b/http/help.go @@ -40,7 +40,7 @@ func handleHelp(core *vault.Core, w http.ResponseWriter, r *http.Request) { respondError(w, http.StatusNotFound, errors.New("Missing /v1/ prefix in path. Use vault path-help command to retrieve API help for paths")) return } - path := ns.TrimmedPath(r.URL.Path[len("/v1/"):]) + path := trimPath(ns, r.URL.Path) req := &logical.Request{ Operation: logical.HelpOperation, diff --git a/http/logical.go b/http/logical.go index 089a24b9c97a..20076ae29c70 100644 --- a/http/logical.go +++ b/http/logical.go @@ -50,8 +50,7 @@ func buildLogicalRequestNoAuth(perfStandby bool, ra *vault.RouterAccess, w http. if err != nil { return nil, nil, http.StatusBadRequest, nil } - path := ns.TrimmedPath(r.URL.Path[len("/v1/"):]) - + path := trimPath(ns, r.URL.Path) var data map[string]interface{} var origBody io.ReadCloser var passHTTPReq bool @@ -361,11 +360,13 @@ func handleLogicalInternal(core *vault.Core, injectDataIntoTopLevel bool, noForw respondError(w, http.StatusInternalServerError, err) return } + trimmedPath := trimPath(ns, r.URL.Path) + nsPath := ns.Path if ns.ID == namespace.RootNamespaceID { nsPath = "" } - if strings.HasPrefix(r.URL.Path, fmt.Sprintf("/v1/%ssys/events/subscribe/", nsPath)) { + if websocketPaths.HasPath(trimmedPath) { handler := entHandleEventsSubscribe(core, req) if handler != nil { handler.ServeHTTP(w, r) diff --git a/http/sys_rekey.go b/http/sys_rekey.go index 0968076246c9..a43da4f1dfe4 100644 --- a/http/sys_rekey.go +++ b/http/sys_rekey.go @@ -20,7 +20,7 @@ func handleSysRekeyInit(core *vault.Core, recovery bool) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { standby, _ := core.Standby() if standby { - respondStandby(core, w, r.URL) + respondStandby(core, w, r) return } @@ -155,7 +155,7 @@ func handleSysRekeyUpdate(core *vault.Core, recovery bool) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { standby, _ := core.Standby() if standby { - respondStandby(core, w, r.URL) + respondStandby(core, w, r) return } @@ -228,7 +228,7 @@ func handleSysRekeyVerify(core *vault.Core, recovery bool) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { standby, _ := core.Standby() if standby { - respondStandby(core, w, r.URL) + respondStandby(core, w, r) return }