func (s *GenericAPIServer) buildHandlerChains(c *Config, handler http.Handler) (secure http.Handler, insecure http.Handler) { // filters which insecure and secure have in common handler = genericfilters.WithCORS(handler, c.CorsAllowedOriginList, nil, nil, nil, "true") // insecure filters insecure = handler insecure = genericfilters.WithPanicRecovery(insecure, c.RequestContextMapper) insecure = apiserverfilters.WithRequestInfo(insecure, NewRequestInfoResolver(c), c.RequestContextMapper) insecure = api.WithRequestContext(insecure, c.RequestContextMapper) insecure = genericfilters.WithTimeoutForNonLongRunningRequests(insecure, c.LongRunningFunc) // secure filters attributeGetter := apiserverfilters.NewRequestAttributeGetter(c.RequestContextMapper) secure = handler secure = apiserverfilters.WithAuthorization(secure, attributeGetter, c.Authorizer) secure = apiserverfilters.WithImpersonation(secure, c.RequestContextMapper, c.Authorizer) secure = apiserverfilters.WithAudit(secure, attributeGetter, c.AuditWriter) // before impersonation to read original user secure = authhandlers.WithAuthentication(secure, c.RequestContextMapper, c.Authenticator, authhandlers.Unauthorized(c.SupportsBasicAuth)) secure = genericfilters.WithPanicRecovery(secure, c.RequestContextMapper) secure = apiserverfilters.WithRequestInfo(secure, NewRequestInfoResolver(c), c.RequestContextMapper) secure = api.WithRequestContext(secure, c.RequestContextMapper) secure = genericfilters.WithTimeoutForNonLongRunningRequests(secure, c.LongRunningFunc) secure = genericfilters.WithMaxInFlightLimit(secure, c.MaxRequestsInFlight, c.LongRunningFunc) return }
// 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, ) }
func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) (secure, insecure http.Handler) { attributeGetter := apiserverfilters.NewRequestAttributeGetter(c.RequestContextMapper) generic := func(handler http.Handler) http.Handler { handler = genericfilters.WithCORS(handler, c.CorsAllowedOriginList, nil, nil, nil, "true") handler = genericfilters.WithPanicRecovery(handler, c.RequestContextMapper) handler = apiserverfilters.WithRequestInfo(handler, NewRequestInfoResolver(c), c.RequestContextMapper) handler = api.WithRequestContext(handler, c.RequestContextMapper) handler = genericfilters.WithTimeoutForNonLongRunningRequests(handler, c.LongRunningFunc) handler = genericfilters.WithMaxInFlightLimit(handler, c.MaxRequestsInFlight, c.LongRunningFunc) return handler } audit := func(handler http.Handler) http.Handler { return apiserverfilters.WithAudit(handler, attributeGetter, c.AuditWriter) } protect := func(handler http.Handler) http.Handler { handler = apiserverfilters.WithAuthorization(handler, attributeGetter, c.Authorizer) handler = apiserverfilters.WithImpersonation(handler, c.RequestContextMapper, c.Authorizer) handler = audit(handler) // before impersonation to read original user handler = authhandlers.WithAuthentication(handler, c.RequestContextMapper, c.Authenticator, authhandlers.Unauthorized(c.SupportsBasicAuth)) return handler } return generic(protect(apiHandler)), generic(audit(apiHandler)) }
// handlerChain is a method to build the handler chain for this API server. We need a custom handler chain so that we // can have custom handling for `/apis`, since we're hosting discovery differently from anyone else and we're hosting // the endpoints differently, since we're proxying all groups except for apiregistration.k8s.io. func (h *handlerChainConfig) handlerChain(apiHandler http.Handler, c *genericapiserver.Config) (secure, insecure http.Handler) { // add this as a filter so that we never collide with "already registered" failures on `/apis` handler := WithAPIs(apiHandler, h.informers.Apiregistration().InternalVersion().APIServices()) handler = apiserverfilters.WithAuthorization(handler, c.RequestContextMapper, c.Authorizer) // this mux is NOT protected by authorization, but DOES have authentication information // this is so that everyone can hit the proxy and we can properly identify the user. The backing // API server will deal with authorization handler = WithProxyMux(handler, h.proxyMux) handler = apiserverfilters.WithImpersonation(handler, c.RequestContextMapper, c.Authorizer) // audit to stdout to help with debugging as we get this started handler = apiserverfilters.WithAudit(handler, c.RequestContextMapper, os.Stdout) handler = authhandlers.WithAuthentication(handler, c.RequestContextMapper, c.Authenticator, authhandlers.Unauthorized(c.SupportsBasicAuth)) handler = genericfilters.WithCORS(handler, c.CorsAllowedOriginList, nil, nil, nil, "true") handler = genericfilters.WithPanicRecovery(handler, c.RequestContextMapper) handler = genericfilters.WithTimeoutForNonLongRunningRequests(handler, c.RequestContextMapper, c.LongRunningFunc) handler = genericfilters.WithMaxInFlightLimit(handler, c.MaxRequestsInFlight, c.MaxMutatingRequestsInFlight, c.RequestContextMapper, c.LongRunningFunc) handler = apiserverfilters.WithRequestInfo(handler, genericapiserver.NewRequestInfoResolver(c), c.RequestContextMapper) handler = api.WithRequestContext(handler, c.RequestContextMapper) return handler, nil }
func createMaxInflightServer(callsWg, blockWg *sync.WaitGroup, disableCallsWg *bool, disableCallsWgMutex *sync.Mutex, nonMutating, mutating int) *httptest.Server { longRunningRequestCheck := BasicLongRunningRequestCheck(sets.NewString("watch"), sets.NewString("proxy")) requestContextMapper := api.NewRequestContextMapper() requestInfoFactory := &request.RequestInfoFactory{APIPrefixes: sets.NewString("apis", "api"), GrouplessAPIPrefixes: sets.NewString("api")} handler := WithMaxInFlightLimit( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // A short, accounted request that does not wait for block WaitGroup. if strings.Contains(r.URL.Path, "dontwait") { return } disableCallsWgMutex.Lock() waitForCalls := *disableCallsWg disableCallsWgMutex.Unlock() if waitForCalls { callsWg.Done() } blockWg.Wait() }), nonMutating, mutating, requestContextMapper, longRunningRequestCheck, ) handler = apiserverfilters.WithRequestInfo(handler, requestInfoFactory, requestContextMapper) handler = api.WithRequestContext(handler, requestContextMapper) return httptest.NewServer(handler) }
func createMaxInflightServer(callsWg, blockWg *sync.WaitGroup, disableCallsWg *bool, disableCallsWgMutex *sync.Mutex, nonMutating, mutating int) *httptest.Server { // notAccountedPathsRegexp specifies paths requests to which we don't account into // requests in flight. notAccountedPathsRegexp := regexp.MustCompile(".*\\/watch") longRunningRequestCheck := BasicLongRunningRequestCheck(notAccountedPathsRegexp, map[string]string{"watch": "true"}) requestContextMapper := api.NewRequestContextMapper() requestInfoFactory := &request.RequestInfoFactory{} handler := WithMaxInFlightLimit( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // A short, accounted request that does not wait for block WaitGroup. if strings.Contains(r.URL.Path, "dontwait") { return } disableCallsWgMutex.Lock() waitForCalls := *disableCallsWg disableCallsWgMutex.Unlock() if waitForCalls { callsWg.Done() } blockWg.Wait() }), nonMutating, mutating, requestContextMapper, longRunningRequestCheck, ) handler = apiserverfilters.WithRequestInfo(handler, requestInfoFactory, requestContextMapper) handler = api.WithRequestContext(handler, requestContextMapper) return httptest.NewServer(handler) }
func TestGetAttribs(t *testing.T) { mapper := api.NewRequestContextMapper() attributeGetter := NewRequestAttributeGetter(mapper) testcases := map[string]struct { Verb string Path string ExpectedAttributes *authorizer.AttributesRecord }{ "non-resource root": { Verb: "POST", Path: "/", ExpectedAttributes: &authorizer.AttributesRecord{ Verb: "post", Path: "/", }, }, "non-resource api prefix": { Verb: "GET", Path: "/api/", ExpectedAttributes: &authorizer.AttributesRecord{ Verb: "get", Path: "/api/", }, }, "non-resource group api prefix": { Verb: "GET", Path: "/apis/extensions/", ExpectedAttributes: &authorizer.AttributesRecord{ Verb: "get", Path: "/apis/extensions/", }, }, "resource": { Verb: "POST", Path: "/api/v1/nodes/mynode", ExpectedAttributes: &authorizer.AttributesRecord{ Verb: "create", Path: "/api/v1/nodes/mynode", ResourceRequest: true, Resource: "nodes", APIVersion: "v1", Name: "mynode", }, }, "namespaced resource": { Verb: "PUT", Path: "/api/v1/namespaces/myns/pods/mypod", ExpectedAttributes: &authorizer.AttributesRecord{ Verb: "update", Path: "/api/v1/namespaces/myns/pods/mypod", ResourceRequest: true, Namespace: "myns", Resource: "pods", APIVersion: "v1", Name: "mypod", }, }, "API group resource": { Verb: "GET", Path: "/apis/extensions/v1beta1/namespaces/myns/jobs", ExpectedAttributes: &authorizer.AttributesRecord{ Verb: "list", Path: "/apis/extensions/v1beta1/namespaces/myns/jobs", ResourceRequest: true, APIGroup: extensions.GroupName, APIVersion: "v1beta1", Namespace: "myns", Resource: "jobs", }, }, } for k, tc := range testcases { req, _ := http.NewRequest(tc.Verb, tc.Path, nil) req.RemoteAddr = "127.0.0.1" var attribs authorizer.Attributes var err error var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { attribs, err = attributeGetter.GetAttribs(req) }) handler = WithRequestInfo(handler, newTestRequestInfoResolver(), mapper) handler = api.WithRequestContext(handler, mapper) handler.ServeHTTP(httptest.NewRecorder(), req) if err != nil { t.Errorf("%s: unexpected error: %v", k, err) } else if !reflect.DeepEqual(attribs, tc.ExpectedAttributes) { t.Errorf("%s: expected\n\t%#v\ngot\n\t%#v", k, tc.ExpectedAttributes, attribs) } } }
func (c *MasterConfig) buildHandlerChain(assetConfig *AssetConfig) (func(http.Handler, *genericapiserver.Config) (secure, insecure http.Handler), []string, error) { var messages []string if c.Options.OAuthConfig != nil { messages = append(messages, fmt.Sprintf("Started OAuth2 API at %%s%s", OpenShiftOAuthAPIPrefix)) } if assetConfig != nil { publicURL, err := url.Parse(assetConfig.Options.PublicURL) if err != nil { return nil, nil, err } messages = append(messages, fmt.Sprintf("Started Web Console %%s%s", publicURL.Path)) } // TODO(sttts): resync with upstream handler chain and re-use upstream filters as much as possible return func(apiHandler http.Handler, kc *genericapiserver.Config) (secure, insecure http.Handler) { attributeGetter := kapiserverfilters.NewRequestAttributeGetter(c.RequestContextMapper) handler := c.versionSkewFilter(apiHandler, c.getRequestContextMapper()) handler = c.authorizationFilter(handler) handler = c.impersonationFilter(handler) // audit handler must comes before the impersonationFilter to read the original user if c.Options.AuditConfig.Enabled { var writer io.Writer if len(c.Options.AuditConfig.AuditFilePath) > 0 { writer = &lumberjack.Logger{ Filename: c.Options.AuditConfig.AuditFilePath, MaxAge: c.Options.AuditConfig.MaximumFileRetentionDays, MaxBackups: c.Options.AuditConfig.MaximumRetainedFiles, MaxSize: c.Options.AuditConfig.MaximumFileSizeMegabytes, } } else { // backwards compatible writer to regular log writer = cmdutil.NewGLogWriterV(0) } handler = kapiserverfilters.WithAudit(handler, attributeGetter, writer) } handler = authenticationHandlerFilter(handler, c.Authenticator, c.getRequestContextMapper()) handler = namespacingFilter(handler, c.getRequestContextMapper()) handler = cacheControlFilter(handler, "no-store") // protected endpoints should not be cached if c.Options.OAuthConfig != nil { authConfig, err := BuildAuthConfig(c) if err != nil { glog.Fatalf("Failed to setup OAuth2: %v", err) } handler, err = authConfig.WithOAuth(handler) if err != nil { glog.Fatalf("Failed to setup OAuth2: %v", err) } } handler, err := assetConfig.WithAssets(handler) if err != nil { glog.Fatalf("Failed to setup serving of assets: %v", err) } // skip authz/n for the index handler handler = WithPatternsHandler(handler, apiHandler, "/", "") if c.WebConsoleEnabled() { handler = WithAssetServerRedirect(handler, c.Options.AssetConfig.PublicURL) } handler = kgenericfilters.WithCORS(handler, c.Options.CORSAllowedOrigins, nil, nil, nil, "true") handler = kgenericfilters.WithPanicRecovery(handler, c.RequestContextMapper) handler = kgenericfilters.WithTimeoutForNonLongRunningRequests(handler, kc.LongRunningFunc) // TODO: MaxRequestsInFlight should be subdivided by intent, type of behavior, and speed of // execution - updates vs reads, long reads vs short reads, fat reads vs skinny reads. // NOTE: read vs. write is implemented in Kube 1.6+ handler = kgenericfilters.WithMaxInFlightLimit(handler, kc.MaxRequestsInFlight, kc.LongRunningFunc) handler = kapiserverfilters.WithRequestInfo(handler, genericapiserver.NewRequestInfoResolver(kc), kc.RequestContextMapper) handler = kapi.WithRequestContext(handler, kc.RequestContextMapper) return handler, nil }, messages, nil }
func TestImpersonationFilter(t *testing.T) { testCases := []struct { name string user user.Info impersonationUser string impersonationGroups []string impersonationUserExtras map[string][]string expectedUser user.Info expectedCode int }{ { name: "not-impersonating", user: &user.DefaultInfo{ Name: "tester", }, expectedUser: &user.DefaultInfo{ Name: "tester", }, expectedCode: http.StatusOK, }, { name: "impersonating-error", user: &user.DefaultInfo{ Name: "tester", }, impersonationUser: "******", expectedUser: &user.DefaultInfo{ Name: "tester", }, expectedCode: http.StatusForbidden, }, { name: "impersonating-group-without-user", user: &user.DefaultInfo{ Name: "tester", }, impersonationGroups: []string{"some-group"}, expectedUser: &user.DefaultInfo{ Name: "tester", }, expectedCode: http.StatusForbidden, }, { name: "impersonating-extra-without-user", user: &user.DefaultInfo{ Name: "tester", }, impersonationUserExtras: map[string][]string{"scopes": {"scope-a"}}, expectedUser: &user.DefaultInfo{ Name: "tester", }, expectedCode: http.StatusForbidden, }, { name: "disallowed-group", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel"}, }, impersonationUser: "******", impersonationGroups: []string{"some-group"}, expectedUser: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel"}, }, expectedCode: http.StatusForbidden, }, { name: "allowed-group", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel", "group-impersonater"}, }, impersonationUser: "******", impersonationGroups: []string{"some-group"}, expectedUser: &user.DefaultInfo{ Name: "system:admin", Groups: []string{"some-group"}, Extra: map[string][]string{}, }, expectedCode: http.StatusOK, }, { name: "disallowed-userextra-1", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel"}, }, impersonationUser: "******", impersonationGroups: []string{"some-group"}, impersonationUserExtras: map[string][]string{"scopes": {"scope-a"}}, expectedUser: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel"}, }, expectedCode: http.StatusForbidden, }, { name: "disallowed-userextra-2", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel", "extra-setter-project"}, }, impersonationUser: "******", impersonationGroups: []string{"some-group"}, impersonationUserExtras: map[string][]string{"scopes": {"scope-a"}}, expectedUser: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel", "extra-setter-project"}, }, expectedCode: http.StatusForbidden, }, { name: "disallowed-userextra-3", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel", "extra-setter-particular-scopes"}, }, impersonationUser: "******", impersonationGroups: []string{"some-group"}, impersonationUserExtras: map[string][]string{"scopes": {"scope-a", "scope-b"}}, expectedUser: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel", "extra-setter-particular-scopes"}, }, expectedCode: http.StatusForbidden, }, { name: "allowed-userextras", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"wheel", "extra-setter-scopes"}, }, impersonationUser: "******", impersonationUserExtras: map[string][]string{"scopes": {"scope-a", "scope-b"}}, expectedUser: &user.DefaultInfo{ Name: "system:admin", Groups: []string{}, Extra: map[string][]string{"scopes": {"scope-a", "scope-b"}}, }, expectedCode: http.StatusOK, }, { name: "allowed-users-impersonation", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"regular-impersonater"}, }, impersonationUser: "******", expectedUser: &user.DefaultInfo{ Name: "tester", Groups: []string{}, Extra: map[string][]string{}, }, expectedCode: http.StatusOK, }, { name: "disallowed-impersonating", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"sa-impersonater"}, }, impersonationUser: "******", expectedUser: &user.DefaultInfo{ Name: "dev", Groups: []string{"sa-impersonater"}, }, expectedCode: http.StatusForbidden, }, { name: "allowed-sa-impersonating", user: &user.DefaultInfo{ Name: "dev", Groups: []string{"sa-impersonater"}, Extra: map[string][]string{}, }, impersonationUser: "******", expectedUser: &user.DefaultInfo{ Name: "system:serviceaccount:foo:default", Groups: []string{"system:serviceaccounts", "system:serviceaccounts:foo"}, Extra: map[string][]string{}, }, expectedCode: http.StatusOK, }, } requestContextMapper := api.NewRequestContextMapper() var ctx api.Context var actualUser user.Info var lock sync.Mutex doNothingHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { currentCtx, _ := requestContextMapper.Get(req) user, exists := api.UserFrom(currentCtx) if !exists { actualUser = nil return } actualUser = user }) handler := func(delegate http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { defer func() { if r := recover(); r != nil { t.Errorf("Recovered %v", r) } }() lock.Lock() defer lock.Unlock() requestContextMapper.Update(req, ctx) currentCtx, _ := requestContextMapper.Get(req) user, exists := api.UserFrom(currentCtx) if !exists { actualUser = nil return } else { actualUser = user } delegate.ServeHTTP(w, req) }) }(WithImpersonation(doNothingHandler, requestContextMapper, impersonateAuthorizer{})) handler = api.WithRequestContext(handler, requestContextMapper) server := httptest.NewServer(handler) defer server.Close() for _, tc := range testCases { func() { lock.Lock() defer lock.Unlock() ctx = api.WithUser(api.NewContext(), tc.user) }() req, err := http.NewRequest("GET", server.URL, nil) if err != nil { t.Errorf("%s: unexpected error: %v", tc.name, err) continue } req.Header.Add(authenticationapi.ImpersonateUserHeader, tc.impersonationUser) for _, group := range tc.impersonationGroups { req.Header.Add(authenticationapi.ImpersonateGroupHeader, group) } for extraKey, values := range tc.impersonationUserExtras { for _, value := range values { req.Header.Add(authenticationapi.ImpersonateUserExtraHeaderPrefix+extraKey, value) } } resp, err := http.DefaultClient.Do(req) if err != nil { t.Errorf("%s: unexpected error: %v", tc.name, err) continue } if resp.StatusCode != tc.expectedCode { t.Errorf("%s: expected %v, actual %v", tc.name, tc.expectedCode, resp.StatusCode) continue } if !reflect.DeepEqual(actualUser, tc.expectedUser) { t.Errorf("%s: expected %#v, actual %#v", tc.name, tc.expectedUser, actualUser) continue } } }