// namespacingFilter adds a filter that adds the namespace of the request to the context. Not all requests will have namespaces, // but any that do will have the appropriate value added. func namespacingFilter(handler http.Handler, contextMapper kapi.RequestContextMapper) http.Handler { infoResolver := &apiserver.RequestInfoResolver{APIPrefixes: sets.NewString("api", "osapi", "oapi", "apis"), GrouplessAPIPrefixes: sets.NewString("api", "osapi", "oapi")} return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { ctx, ok := contextMapper.Get(req) if !ok { http.Error(w, "Unable to find request context", http.StatusInternalServerError) return } if _, exists := kapi.NamespaceFrom(ctx); !exists { if requestInfo, err := infoResolver.GetRequestInfo(req); err == nil { // only set the namespace if the apiRequestInfo was resolved // keep in mind that GetAPIRequestInfo will fail on non-api requests, so don't fail the entire http request on that // kind of failure. // TODO reconsider special casing this. Having the special case hereallow us to fully share the kube // APIRequestInfoResolver without any modification or customization. namespace := requestInfo.Namespace if (requestInfo.Resource == "projects") && (len(requestInfo.Name) > 0) { namespace = requestInfo.Name } ctx = kapi.WithNamespace(ctx, namespace) contextMapper.Update(req, ctx) } } handler.ServeHTTP(w, req) }) }
// WithAuthorizationCheck passes all authorized requests on to handler, and returns a forbidden error otherwise. func WithAuthorization(handler http.Handler, requestContextMapper api.RequestContextMapper, a authorizer.Authorizer) http.Handler { if a == nil { glog.Warningf("Authorization is disabled") return handler } return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { ctx, ok := requestContextMapper.Get(req) if !ok { internalError(w, req, errors.New("no context found for request")) return } attrs, err := GetAuthorizerAttributes(ctx) if err != nil { internalError(w, req, err) return } authorized, reason, err := a.Authorize(attrs) if authorized { handler.ServeHTTP(w, req) return } if err != nil { internalError(w, req, err) return } glog.V(4).Infof("Forbidden: %#v, Reason: %s", req.RequestURI, reason) forbidden(w, req) }) }
// WithAuthentication creates an http handler that tries to authenticate the given request as a user, and then // stores any such user found onto the provided context for the request. If authentication fails or returns an error // the failed handler is used. On success, handler is invoked to serve the request. func WithAuthentication(handler http.Handler, mapper api.RequestContextMapper, auth authenticator.Request, failed http.Handler) http.Handler { if auth == nil { glog.Warningf("Authentication is disabled") return handler } return api.WithRequestContext( http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { user, ok, err := auth.AuthenticateRequest(req) if err != nil || !ok { if err != nil { glog.Errorf("Unable to authenticate the request due to an error: %v", err) } failed.ServeHTTP(w, req) return } if ctx, ok := mapper.Get(req); ok { mapper.Update(req, api.WithUser(ctx, user)) } authenticatedUserCounter.WithLabelValues(compressUsername(user.GetName())).Inc() handler.ServeHTTP(w, req) }), mapper, ) }
// NewRequestAuthenticator creates an http handler that tries to authenticate the given request as a user, and then // stores any such user found onto the provided context for the request. If authentication fails or returns an error // the failed handler is used. On success, "Authorization" header is removed from the request and handler // is invoked to serve the request. func NewRequestAuthenticator(mapper api.RequestContextMapper, auth authenticator.Request, failed http.Handler, handler http.Handler) (http.Handler, error) { return api.NewRequestContextFilter( mapper, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { user, ok, err := auth.AuthenticateRequest(req) if err != nil || !ok { if err != nil { glog.Errorf("Unable to authenticate the request due to an error: %v", err) } failed.ServeHTTP(w, req) return } // authorization header is not required anymore in case of a successful authentication. req.Header.Del("Authorization") if ctx, ok := mapper.Get(req); ok { mapper.Update(req, api.WithUser(ctx, user)) } authenticatedUserCounter.WithLabelValues(compressUsername(user.GetName())).Inc() handler.ServeHTTP(w, req) }), ) }
// WithMaxInFlightLimit limits the number of in-flight requests to buffer size of the passed in channel. func WithMaxInFlightLimit( handler http.Handler, nonMutatingLimit int, mutatingLimit int, requestContextMapper api.RequestContextMapper, longRunningRequestCheck LongRunningRequestCheck, ) http.Handler { if nonMutatingLimit == 0 && mutatingLimit == 0 { return handler } var nonMutatingChan chan bool var mutatingChan chan bool if nonMutatingLimit != 0 { nonMutatingChan = make(chan bool, nonMutatingLimit) } if mutatingLimit != 0 { mutatingChan = make(chan bool, mutatingLimit) } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // TODO: migrate to use requestInfo instead of having custom request parser. if longRunningRequestCheck(r) { // Skip tracking long running events. handler.ServeHTTP(w, r) return } ctx, ok := requestContextMapper.Get(r) if !ok { handleError(w, r, fmt.Errorf("no context found for request, handler chain must be wrong")) return } requestInfo, ok := request.RequestInfoFrom(ctx) if !ok { handleError(w, r, fmt.Errorf("no RequestInfo found in context, handler chain must be wrong")) return } var c chan bool if !nonMutatingRequestVerbs.Has(requestInfo.Verb) { c = mutatingChan } else { c = nonMutatingChan } if c == nil { handler.ServeHTTP(w, r) } else { select { case c <- true: defer func() { <-c }() handler.ServeHTTP(w, r) default: tooManyRequests(r, w) } } }) }
// WithAuthorizationCheck passes all authorized requests on to handler, and returns a forbidden error otherwise. func WithAuthorizationCheck(handler http.Handler, getAttribs RequestAttributeGetter, a authorizer.Authorizer, mapper api.RequestContextMapper) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { tenant, err := a.Authorize(getAttribs.GetAttribs(req)) if err == nil { if ctx, ok := mapper.Get(req); ok { mapper.Update(req, api.WithTenant(ctx, tenant)) } handler.ServeHTTP(w, req) return } forbidden(w, req) }) }
// WithAudit decorates a http.Handler with audit logging information for all the // requests coming to the server. If out is nil, no decoration takes place. // Each audit log contains two entries: // 1. the request line containing: // - unique id allowing to match the response line (see 2) // - source ip of the request // - HTTP method being invoked // - original user invoking the operation // - impersonated user for the operation // - namespace of the request or <none> // - uri is the full URI as requested // 2. the response line containing: // - the unique id from 1 // - response code func WithAudit(handler http.Handler, requestContextMapper api.RequestContextMapper, out io.Writer) http.Handler { if out == nil { return handler } return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { ctx, ok := requestContextMapper.Get(req) if !ok { internalError(w, req, errors.New("no context found for request")) return } attribs, err := GetAuthorizerAttributes(ctx) if err != nil { internalError(w, req, err) return } username := "******" groups := "<none>" if attribs.GetUser() != nil { username = attribs.GetUser().GetName() if userGroups := attribs.GetUser().GetGroups(); len(userGroups) > 0 { groups = auditStringSlice(userGroups) } } asuser := req.Header.Get(authenticationapi.ImpersonateUserHeader) if len(asuser) == 0 { asuser = "******" } asgroups := "<lookup>" requestedGroups := req.Header[authenticationapi.ImpersonateGroupHeader] if len(requestedGroups) > 0 { asgroups = auditStringSlice(requestedGroups) } namespace := attribs.GetNamespace() if len(namespace) == 0 { namespace = "<none>" } id := uuid.NewRandom().String() line := fmt.Sprintf("%s AUDIT: id=%q ip=%q method=%q user=%q groups=%q as=%q asgroups=%q namespace=%q uri=%q\n", time.Now().Format(time.RFC3339Nano), id, utilnet.GetClientIP(req), req.Method, username, groups, asuser, asgroups, namespace, req.URL) if _, err := fmt.Fprint(out, line); err != nil { glog.Errorf("Unable to write audit log: %s, the error is: %v", line, err) } respWriter := decorateResponseWriter(w, out, id) handler.ServeHTTP(respWriter, req) }) }
func WithActingAs(handler http.Handler, requestContextMapper api.RequestContextMapper, a authorizer.Authorizer) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { requestedSubject := req.Header.Get("Impersonate-User") if len(requestedSubject) == 0 { handler.ServeHTTP(w, req) return } ctx, exists := requestContextMapper.Get(req) if !exists { forbidden(w, req) return } requestor, exists := api.UserFrom(ctx) if !exists { forbidden(w, req) return } actingAsAttributes := &authorizer.AttributesRecord{ User: requestor, Verb: "impersonate", APIGroup: api.GroupName, Resource: "users", // ResourceName: requestedSubject, ResourceRequest: true, } err := a.Authorize(actingAsAttributes) if err != nil { forbidden(w, req) return } switch { case strings.HasPrefix(requestedSubject, serviceaccount.ServiceAccountUsernamePrefix): namespace, name, err := serviceaccount.SplitUsername(requestedSubject) if err != nil { forbidden(w, req) return } requestContextMapper.Update(req, api.WithUser(ctx, serviceaccount.UserInfo(namespace, name, ""))) default: newUser := &user.DefaultInfo{ Name: requestedSubject, } requestContextMapper.Update(req, api.WithUser(ctx, newUser)) } newCtx, _ := requestContextMapper.Get(req) oldUser, _ := api.UserFrom(ctx) newUser, _ := api.UserFrom(newCtx) httplog.LogOf(req, w).Addf("%v is acting as %v", oldUser, newUser) handler.ServeHTTP(w, req) }) }
// WithPanicRecovery wraps an http Handler to recover and log panics. func WithPanicRecovery(handler http.Handler, requestContextMapper api.RequestContextMapper) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { defer runtime.HandleCrash(func(err interface{}) { http.Error(w, "This request caused apisever to panic. Look in log for details.", http.StatusInternalServerError) glog.Errorf("APIServer panic'd on %v %v: %v\n%s\n", req.Method, req.RequestURI, err, debug.Stack()) }) logger := httplog.NewLogged(req, &w) var requestInfo *request.RequestInfo ctx, ok := requestContextMapper.Get(req) if !ok { glog.Errorf("no context found for request, handler chain must be wrong") } else { requestInfo, ok = request.RequestInfoFrom(ctx) if !ok { glog.Errorf("no RequestInfo found in context, handler chain must be wrong") } } if !ok || requestInfo.Verb != "proxy" { logger.StacktraceWhen( httplog.StatusIsNot( http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusBadRequest, http.StatusMovedPermanently, http.StatusTemporaryRedirect, http.StatusConflict, http.StatusNotFound, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotModified, apierrors.StatusUnprocessableEntity, http.StatusSwitchingProtocols, ), ) } defer logger.Log() // Dispatch to the internal handler handler.ServeHTTP(w, req) }) }
// WithRequestInfo attaches a RequestInfo to the context. func WithRequestInfo(handler http.Handler, resolver *request.RequestInfoResolver, requestContextMapper api.RequestContextMapper) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { ctx, ok := requestContextMapper.Get(req) if !ok { internalError(w, req, errors.New("no context found for request")) return } info, err := resolver.GetRequestInfo(req) if err != nil { internalError(w, req, fmt.Errorf("failed to create RequestInfo: %v", err)) return } requestContextMapper.Update(req, request.WithRequestInfo(ctx, info)) handler.ServeHTTP(w, req) }) }
// WithAudit decorates a http.Handler with audit logging information for all the // requests coming to the server. Each audit log contains two entries: // 1. the request line containing: // - unique id allowing to match the response line (see 2) // - source ip of the request // - HTTP method being invoked // - original user invoking the operation // - impersonated user for the operation // - namespace of the request or <none> // - uri is the full URI as requested // 2. the response line containing: // - the unique id from 1 // - response code func WithAudit(handler http.Handler, requestContextMapper api.RequestContextMapper, out io.Writer) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { ctx, _ := requestContextMapper.Get(req) user, _ := api.UserFrom(ctx) asuser := req.Header.Get("Impersonate-User") if len(asuser) == 0 { asuser = "******" } namespace := api.NamespaceValue(ctx) if len(namespace) == 0 { namespace = "<none>" } id := uuid.NewRandom().String() fmt.Fprintf(out, "%s AUDIT: id=%q ip=%q method=%q user=%q as=%q namespace=%q uri=%q\n", time.Now().Format(time.RFC3339Nano), id, utilnet.GetClientIP(req), req.Method, user.GetName(), asuser, namespace, req.URL) respWriter := decorateResponseWriter(w, out, id) handler.ServeHTTP(respWriter, req) }) }
// NewRequestAuthenticator creates an http handler that tries to authenticate the given request as a user, and then // stores any such user found onto the provided context for the request. If authentication fails or returns an error // the failed handler is used. On success, handler is invoked to serve the request. func NewRequestAuthenticator(mapper api.RequestContextMapper, auth authenticator.Request, failed http.Handler, handler http.Handler) (http.Handler, error) { return api.NewRequestContextFilter( mapper, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { user, ok, err := auth.AuthenticateRequest(req) if err != nil || !ok { if err != nil { glog.Errorf("Unable to authenticate the request due to an error: %v", err) } failed.ServeHTTP(w, req) return } if ctx, ok := mapper.Get(req); ok { mapper.Update(req, api.WithUser(ctx, user)) } handler.ServeHTTP(w, req) }), ) }
// authenticationHandlerFilter creates a filter object that will enforce authentication directly func authenticationHandlerFilter(handler http.Handler, authenticator authenticator.Request, contextMapper kapi.RequestContextMapper) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { user, ok, err := authenticator.AuthenticateRequest(req) if err != nil || !ok { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } ctx, ok := contextMapper.Get(req) if !ok { http.Error(w, "Unable to find request context", http.StatusInternalServerError) return } if err := contextMapper.Update(req, kapi.WithUser(ctx, user)); err != nil { glog.V(4).Infof("Error setting authenticated context: %v", err) http.Error(w, "Unable to set authenticated request context", http.StatusInternalServerError) return } handler.ServeHTTP(w, req) }) }
// WithTimeoutForNonLongRunningRequests times out non-long-running requests after the time given by globalTimeout. func WithTimeoutForNonLongRunningRequests(handler http.Handler, requestContextMapper api.RequestContextMapper, longRunning LongRunningRequestCheck) http.Handler { if longRunning == nil { return handler } timeoutFunc := func(req *http.Request) (<-chan time.Time, string) { // TODO unify this with apiserver.MaxInFlightLimit ctx, ok := requestContextMapper.Get(req) if !ok { return time.After(globalTimeout), "" } requestInfo, ok := apiserverrequest.RequestInfoFrom(ctx) if !ok { return time.After(globalTimeout), "" } if longRunning(req, requestInfo) { return nil, "" } return time.After(globalTimeout), "" } return WithTimeout(handler, timeoutFunc) }
// WithImpersonation is a filter that will inspect and check requests that attempt to change the user.Info for their requests func WithImpersonation(handler http.Handler, requestContextMapper api.RequestContextMapper, a authorizer.Authorizer) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { impersonationRequests, err := buildImpersonationRequests(req.Header) if err != nil { glog.V(4).Infof("%v", err) internalError(w, req, err) return } if len(impersonationRequests) == 0 { handler.ServeHTTP(w, req) return } ctx, exists := requestContextMapper.Get(req) if !exists { internalError(w, req, errors.New("no context found for request")) return } requestor, exists := api.UserFrom(ctx) if !exists { internalError(w, req, errors.New("no user found for request")) return } // if groups are not specified, then we need to look them up differently depending on the type of user // if they are specified, then they are the authority groupsSpecified := len(req.Header[authenticationapi.ImpersonateGroupHeader]) > 0 // make sure we're allowed to impersonate each thing we're requesting. While we're iterating through, start building username // and group information username := "" groups := []string{} userExtra := map[string][]string{} for _, impersonationRequest := range impersonationRequests { actingAsAttributes := &authorizer.AttributesRecord{ User: requestor, Verb: "impersonate", APIGroup: impersonationRequest.GetObjectKind().GroupVersionKind().Group, Namespace: impersonationRequest.Namespace, Name: impersonationRequest.Name, ResourceRequest: true, } switch impersonationRequest.GetObjectKind().GroupVersionKind().GroupKind() { case api.Kind("ServiceAccount"): actingAsAttributes.Resource = "serviceaccounts" username = serviceaccount.MakeUsername(impersonationRequest.Namespace, impersonationRequest.Name) if !groupsSpecified { // if groups aren't specified for a service account, we know the groups because its a fixed mapping. Add them groups = serviceaccount.MakeGroupNames(impersonationRequest.Namespace, impersonationRequest.Name) } case api.Kind("User"): actingAsAttributes.Resource = "users" username = impersonationRequest.Name case api.Kind("Group"): actingAsAttributes.Resource = "groups" groups = append(groups, impersonationRequest.Name) case authenticationapi.Kind("UserExtra"): extraKey := impersonationRequest.FieldPath extraValue := impersonationRequest.Name actingAsAttributes.Resource = "userextras" actingAsAttributes.Subresource = extraKey userExtra[extraKey] = append(userExtra[extraKey], extraValue) default: glog.V(4).Infof("unknown impersonation request type: %v", impersonationRequest) forbidden(actingAsAttributes, w, req, fmt.Sprintf("unknown impersonation request type: %v", impersonationRequest)) return } allowed, reason, err := a.Authorize(actingAsAttributes) if err != nil || !allowed { glog.V(4).Infof("Forbidden: %#v, Reason: %s, Error: %v", req.RequestURI, reason, err) forbidden(actingAsAttributes, w, req, reason) return } } newUser := &user.DefaultInfo{ Name: username, Groups: groups, Extra: userExtra, } requestContextMapper.Update(req, api.WithUser(ctx, newUser)) oldUser, _ := api.UserFrom(ctx) httplog.LogOf(req, w).Addf("%v is acting as %v", oldUser, newUser) // clear all the impersonation headers from the request req.Header.Del(authenticationapi.ImpersonateUserHeader) req.Header.Del(authenticationapi.ImpersonateGroupHeader) for headerName := range req.Header { if strings.HasPrefix(headerName, authenticationapi.ImpersonateUserExtraHeaderPrefix) { req.Header.Del(headerName) } } handler.ServeHTTP(w, req) }) }