diff --git a/internal/request/http.go b/internal/request/http.go index 3390ff44..a4d8b2d3 100644 --- a/internal/request/http.go +++ b/internal/request/http.go @@ -4,6 +4,7 @@ package request import ( + "context" "encoding/base64" "fmt" h "net/http" @@ -17,6 +18,15 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +var websocketBearerTokenRegexp = regexp.MustCompile(`base64url\.bearer\.authorization\.k8s\.io\.([^,]*)`) //nolint:gochecknoglobals + +type userAndGroupsContextKey struct{} + +type userAndGroupsContextValue struct { + username string + groups []string +} + type http struct { *h.Request @@ -32,6 +42,25 @@ func NewHTTP(request *h.Request, authTypes []AuthType, usernameClaimField string return &http{Request: request, authTypes: authTypes, usernameClaimField: usernameClaimField, client: client, ignoredImpersonationGroups: ignoredImpersonationGroups, impersonationGroupsRegexp: impersonationGroupsRegexp, skipImpersonationReview: skipImpersonationReview} } +func ResolveUserAndGroups(request *h.Request, authTypes []AuthType, usernameClaimField string, writer client.Writer, ignoredImpersonationGroups []string, impersonationGroupsRegexp *regexp.Regexp, skipImpersonationReview bool) (*h.Request, string, []string, error) { + if cachedUsername, cachedGroups, ok := cachedUserAndGroups(request.Context()); ok { + return request, cachedUsername, cachedGroups, nil + } + + proxyRequest := NewHTTP(request, authTypes, usernameClaimField, writer, ignoredImpersonationGroups, impersonationGroupsRegexp, skipImpersonationReview) + username, groups, err := proxyRequest.GetUserAndGroups() + if err != nil { + return request, "", nil, err + } + + ctx := context.WithValue(request.Context(), userAndGroupsContextKey{}, userAndGroupsContextValue{ + username: username, + groups: groups, + }) + + return request.WithContext(ctx), username, groups, nil +} + func (h http) GetHTTPRequest() *h.Request { return h.Request } @@ -158,19 +187,17 @@ func (h http) processBearerToken() (username string, groups []string, err error) // Get the JWT from headers // If there is no Authorizaion Bearer, then try finding the Bearer in Websocket Protocols header. This is for browser support. func (h http) bearerToken() (string, error) { - tradBearer := strings.ReplaceAll(h.Header.Get("Authorization"), "Bearer ", "") + tradBearer := strings.TrimPrefix(h.Header.Get("Authorization"), "Bearer ") wsHeader := h.Header.Get("Sec-Websocket-Protocol") switch { case tradBearer != "": return tradBearer, nil case wsHeader != "": - re := regexp.MustCompile(`base64url\.bearer\.authorization\.k8s\.io\.([^,]*)`) - - match := re.FindStringSubmatch(wsHeader)[1] - if match != "" { + match := websocketBearerTokenRegexp.FindStringSubmatch(wsHeader) + if len(match) > 1 && match[1] != "" { // our token is base64 encoded without padding - b64decode, err := base64.RawStdEncoding.DecodeString(match) + b64decode, err := base64.RawStdEncoding.DecodeString(match[1]) if err != nil { return "", NewErrUnauthorized("failed to decode websocket auth bearer: " + err.Error()) } @@ -184,6 +211,15 @@ func (h http) bearerToken() (string, error) { } } +func cachedUserAndGroups(ctx context.Context) (string, []string, bool) { + value, ok := ctx.Value(userAndGroupsContextKey{}).(userAndGroupsContextValue) + if !ok { + return "", nil, false + } + + return value.username, value.groups, true +} + type authenticationFn func() (username string, groups []string, err error) func (h http) authenticationFns() []authenticationFn { diff --git a/internal/webserver/middleware/user_in_group.go b/internal/webserver/middleware/user_in_group.go index bcda35e6..7add3929 100644 --- a/internal/webserver/middleware/user_in_group.go +++ b/internal/webserver/middleware/user_in_group.go @@ -21,7 +21,13 @@ func CheckUserInIgnoredGroupMiddleware(client client.Writer, log logr.Logger, cl return func(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if ignoredUserGroups.Len() > 0 { - user, groups, err := req.NewHTTP(request, authTypes, claim, client, ignoredImpersonationGroups, impersonationGroupsRegexp, skipImpersonationReview).GetUserAndGroups() + var ( + err error + user string + groups []string + ) + + request, user, groups, err = req.ResolveUserAndGroups(request, authTypes, claim, client, ignoredImpersonationGroups, impersonationGroupsRegexp, skipImpersonationReview) if err != nil { log.Error(err, "Cannot retrieve username and group from request") } @@ -44,7 +50,7 @@ func CheckUserInIgnoredGroupMiddleware(client client.Writer, log logr.Logger, cl func CheckUserInCapsuleGroupMiddleware(client client.Writer, log logr.Logger, claim string, authTypes []req.AuthType, ignoredImpersonationGroups []string, impersonationGroupsRegexp *regexp.Regexp, skipImpersonationReview bool, impersonate func(http.ResponseWriter, *http.Request)) mux.MiddlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - user, groups, err := req.NewHTTP(request, authTypes, claim, client, ignoredImpersonationGroups, impersonationGroupsRegexp, skipImpersonationReview).GetUserAndGroups() + request, user, groups, err := req.ResolveUserAndGroups(request, authTypes, claim, client, ignoredImpersonationGroups, impersonationGroupsRegexp, skipImpersonationReview) if err != nil { log.Error(err, "Cannot retrieve username and group from request") } diff --git a/internal/webserver/webserver.go b/internal/webserver/webserver.go index 45dd3e34..9163a558 100644 --- a/internal/webserver/webserver.go +++ b/internal/webserver/webserver.go @@ -328,16 +328,19 @@ func (n *kubeFilter) authorizationMiddleware(next http.Handler) http.Handler { w := httptest.NewRecorder() next.ServeHTTP(w, request) - body, err := io.ReadAll(w.Result().Body) + result := w.Result() + defer func() { + _ = result.Body.Close() + }() + + body, err := io.ReadAll(result.Body) if err != nil { n.log.Error(err, "cannot read response body") return } - proxyRequest := req.NewHTTP(request, n.authTypes, n.usernameClaimField, n.writer, n.ignoredImpersonationGroups, n.impersonationGroupsRegexp, n.skipImpersonationReview) - - username, groups, err := proxyRequest.GetUserAndGroups() + request, username, groups, err := req.ResolveUserAndGroups(request, n.authTypes, n.usernameClaimField, n.writer, n.ignoredImpersonationGroups, n.impersonationGroupsRegexp, n.skipImpersonationReview) if err != nil { server.HandleError(writer, err, "cannot retrieve user and group from the request") } @@ -369,7 +372,7 @@ func (n *kubeFilter) authorizationMiddleware(next http.Handler) http.Handler { } } - for k, v := range w.Result().Header { + for k, v := range result.Header { if k == "Content-Length" { continue } @@ -379,7 +382,7 @@ func (n *kubeFilter) authorizationMiddleware(next http.Handler) http.Handler { } } - writer.WriteHeader(w.Result().StatusCode) + writer.WriteHeader(result.StatusCode) write, err := writer.Write(body) if err != nil { @@ -390,32 +393,31 @@ func (n *kubeFilter) authorizationMiddleware(next http.Handler) http.Handler { func (n *kubeFilter) handleRequest(request *http.Request, selector labels.Selector) { req.SanitizeImpersonationHeaders(request) + selectorValue := selector.String() q := request.URL.Query() if e := q.Get("labelSelector"); len(e) > 0 { n.log.V(4).Info("handling current labelSelector", "selector", e) - v := strings.Join([]string{e, selector.String()}, ",") + v := strings.Join([]string{e, selectorValue}, ",") q.Set("labelSelector", v) n.log.V(4).Info("labelSelector updated", "selector", v) } else { - q.Set("labelSelector", selector.String()) - n.log.V(4).Info("labelSelector added", "selector", selector.String()) + q.Set("labelSelector", selectorValue) + n.log.V(4).Info("labelSelector added", "selector", selectorValue) } n.log.V(4).Info("updating RawQuery", "query", q.Encode()) request.URL.RawQuery = q.Encode() - if len(n.BearerToken()) > 0 { - n.log.V(10).Info("Updating the token", "token", n.BearerToken()) - request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", n.BearerToken())) + if token := n.BearerToken(); len(token) > 0 { + n.log.V(10).Info("Updating the token", "token", token) + request.Header.Set("Authorization", "Bearer "+token) } } func (n *kubeFilter) impersonateHandler(writer http.ResponseWriter, request *http.Request) { - hr := req.NewHTTP(request, n.authTypes, n.usernameClaimField, n.writer, n.ignoredImpersonationGroups, n.impersonationGroupsRegexp, n.skipImpersonationReview) - - username, groups, err := hr.GetUserAndGroups() + request, username, groups, err := req.ResolveUserAndGroups(request, n.authTypes, n.usernameClaimField, n.writer, n.ignoredImpersonationGroups, n.impersonationGroupsRegexp, n.skipImpersonationReview) if err != nil { msg := "cannot retrieve user and group" @@ -429,8 +431,8 @@ func (n *kubeFilter) impersonateHandler(writer http.ResponseWriter, request *htt n.log.V(4).Info("impersonating for the current request", "username", username, "groups", groups, "uri", request.URL.Path) - if len(n.BearerToken()) > 0 { - request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", n.BearerToken())) + if token := n.BearerToken(); len(token) > 0 { + request.Header.Set("Authorization", "Bearer "+token) } // Dropping malicious header connection // https://github.com/projectcapsule/capsule-proxy/issues/188 @@ -532,9 +534,7 @@ func (n *kubeFilter) registerModules(ctx context.Context, root *mux.Router) { middleware.CheckUserInCapsuleGroupMiddleware(n.writer, n.log, n.usernameClaimField, n.authTypes, n.ignoredImpersonationGroups, n.impersonationGroupsRegexp, n.skipImpersonationReview, n.impersonateHandler), ) sr.HandleFunc("", func(writer http.ResponseWriter, request *http.Request) { - proxyRequest := req.NewHTTP(request, n.authTypes, n.usernameClaimField, n.writer, n.ignoredImpersonationGroups, n.impersonationGroupsRegexp, n.skipImpersonationReview) - - username, groups, err := proxyRequest.GetUserAndGroups() + request, username, groups, err := req.ResolveUserAndGroups(request, n.authTypes, n.usernameClaimField, n.writer, n.ignoredImpersonationGroups, n.impersonationGroupsRegexp, n.skipImpersonationReview) if err != nil { server.HandleError(writer, err, "cannot retrieve user and group from the request") } @@ -546,7 +546,7 @@ func (n *kubeFilter) registerModules(ctx context.Context, root *mux.Router) { var selector labels.Selector - selector, err = mod.Handle(proxyTenants, proxyRequest) + selector, err = mod.Handle(proxyTenants, req.NewHTTP(request, n.authTypes, n.usernameClaimField, n.writer, n.ignoredImpersonationGroups, n.impersonationGroupsRegexp, n.skipImpersonationReview)) switch { case err != nil: