From 6dc2f6337b4349d0a03b55bd6352c77928bbaf7b Mon Sep 17 00:00:00 2001 From: "Dr. Stefan Schimanski" Date: Mon, 26 Sep 2016 13:07:35 +0200 Subject: [PATCH] Separate apiserver handler filters --- pkg/apiserver/apiserver.go | 26 ---- pkg/apiserver/apiserver_test.go | 5 - pkg/apiserver/errors.go | 19 --- pkg/apiserver/{audit => filters}/audit.go | 5 +- .../{audit => filters}/audit_test.go | 4 +- pkg/apiserver/filters/authorization.go | 91 ++++++++++++ pkg/apiserver/filters/authorization_test.go | 111 +++++++++++++++ pkg/apiserver/filters/doc.go | 19 +++ pkg/apiserver/filters/errors.go | 43 ++++++ .../impersonation.go} | 2 +- .../impersonation_test.go} | 4 +- pkg/apiserver/{handlers.go => requestinfo.go} | 132 ++++-------------- .../{handlers_test.go => requestinfo_test.go} | 117 ++-------------- pkg/apiserver/serviceerror.go | 42 ++++++ pkg/genericapiserver/config.go | 12 +- 15 files changed, 353 insertions(+), 279 deletions(-) rename pkg/apiserver/{audit => filters}/audit.go (95%) rename pkg/apiserver/{audit => filters}/audit_test.go (97%) create mode 100644 pkg/apiserver/filters/authorization.go create mode 100644 pkg/apiserver/filters/authorization_test.go create mode 100644 pkg/apiserver/filters/doc.go create mode 100755 pkg/apiserver/filters/errors.go rename pkg/apiserver/{handler_impersonation.go => filters/impersonation.go} (99%) rename pkg/apiserver/{handler_impersonation_test.go => filters/impersonation_test.go} (99%) rename pkg/apiserver/{handlers.go => requestinfo.go} (71%) rename pkg/apiserver/{handlers_test.go => requestinfo_test.go} (74%) create mode 100644 pkg/apiserver/serviceerror.go diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 2aa82881c43..bc4bf419561 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -27,7 +27,6 @@ import ( "path" rt "runtime" "strconv" - "strings" "time" "k8s.io/kubernetes/pkg/admission" @@ -210,22 +209,6 @@ func logStackOnRecover(s runtime.NegotiatedSerializer, panicReason interface{}, errorNegotiated(apierrors.NewGenericServerResponse(http.StatusInternalServerError, "", api.Resource(""), "", "", 0, false), s, unversioned.GroupVersion{}, w, &http.Request{Header: headers}) } -func InstallServiceErrorHandler(s runtime.NegotiatedSerializer, container *restful.Container) { - container.ServiceErrorHandler(func(serviceErr restful.ServiceError, request *restful.Request, response *restful.Response) { - serviceErrorHandler(s, serviceErr, request, response) - }) -} - -func serviceErrorHandler(s runtime.NegotiatedSerializer, serviceErr restful.ServiceError, request *restful.Request, response *restful.Response) { - errorNegotiated( - apierrors.NewGenericServerResponse(serviceErr.Code, "", api.Resource(""), "", serviceErr.Message, 0, false), - s, - unversioned.GroupVersion{}, - response.ResponseWriter, - request.Request, - ) -} - // Adds a service to return the supported api versions at the legacy /api. func AddApiWebService(s runtime.NegotiatedSerializer, container *restful.Container, apiPrefix string, getAPIVersionsFunc func(req *restful.Request) *unversioned.APIVersions) { // TODO: InstallREST should register each version automatically @@ -501,12 +484,3 @@ func readBody(req *http.Request) ([]byte, error) { defer req.Body.Close() return ioutil.ReadAll(req.Body) } - -// splitPath returns the segments for a URL path. -func splitPath(path string) []string { - path = strings.Trim(path, "/") - if path == "" { - return []string{} - } - return strings.Split(path, "/") -} diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index 6255820b7b5..545336b23f9 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -44,7 +44,6 @@ import ( "k8s.io/kubernetes/pkg/labels" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/util/diff" - "k8s.io/kubernetes/pkg/util/sets" "k8s.io/kubernetes/pkg/watch" "k8s.io/kubernetes/pkg/watch/versioned" "k8s.io/kubernetes/plugin/pkg/admission/admit" @@ -252,10 +251,6 @@ func handleLinker(storage map[string]rest.Storage, selfLinker runtime.SelfLinker return handleInternal(storage, admissionControl, selfLinker) } -func newTestRequestInfoResolver() *RequestInfoResolver { - return &RequestInfoResolver{sets.NewString("api", "apis"), sets.NewString("api")} -} - func handleInternal(storage map[string]rest.Storage, admissionControl admission.Interface, selfLinker runtime.SelfLinker) http.Handler { container := restful.NewContainer() container.Router(restful.CurlyRouter{}) diff --git a/pkg/apiserver/errors.go b/pkg/apiserver/errors.go index e75e4bd826a..a1413a4c31b 100755 --- a/pkg/apiserver/errors.go +++ b/pkg/apiserver/errors.go @@ -76,25 +76,6 @@ func notFound(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "Not Found: %#v", req.RequestURI) } -// badGatewayError renders a simple bad gateway error. -func badGatewayError(w http.ResponseWriter, req *http.Request) { - w.WriteHeader(http.StatusBadGateway) - fmt.Fprintf(w, "Bad Gateway: %#v", req.RequestURI) -} - -// forbidden renders a simple forbidden error -func forbidden(w http.ResponseWriter, req *http.Request) { - w.WriteHeader(http.StatusForbidden) - fmt.Fprintf(w, "Forbidden: %#v", req.RequestURI) -} - -// internalError renders a simple internal error -func internalError(w http.ResponseWriter, req *http.Request, err error) { - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "Internal Server Error: %#v", req.RequestURI) - runtime.HandleError(err) -} - // errAPIPrefixNotFound indicates that a RequestInfo resolution failed because the request isn't under // any known API prefixes type errAPIPrefixNotFound struct { diff --git a/pkg/apiserver/audit/audit.go b/pkg/apiserver/filters/audit.go similarity index 95% rename from pkg/apiserver/audit/audit.go rename to pkg/apiserver/filters/audit.go index 969119ad68e..b4c3c42487a 100644 --- a/pkg/apiserver/audit/audit.go +++ b/pkg/apiserver/filters/audit.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package audit +package filters import ( "bufio" @@ -27,7 +27,6 @@ import ( "github.com/golang/glog" "github.com/pborman/uuid" - "k8s.io/kubernetes/pkg/apiserver" utilnet "k8s.io/kubernetes/pkg/util/net" ) @@ -85,7 +84,7 @@ var _ http.Hijacker = &fancyResponseWriterDelegator{} // 2. the response line containing: // - the unique id from 1 // - response code -func WithAudit(handler http.Handler, attributeGetter apiserver.RequestAttributeGetter, out io.Writer) http.Handler { +func WithAudit(handler http.Handler, attributeGetter RequestAttributeGetter, out io.Writer) http.Handler { if out == nil { return handler } diff --git a/pkg/apiserver/audit/audit_test.go b/pkg/apiserver/filters/audit_test.go similarity index 97% rename from pkg/apiserver/audit/audit_test.go rename to pkg/apiserver/filters/audit_test.go index e7b8809fa8c..1ac8599cf23 100644 --- a/pkg/apiserver/audit/audit_test.go +++ b/pkg/apiserver/filters/audit_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package audit +package filters import ( "bufio" @@ -85,7 +85,7 @@ func (*fakeRequestContextMapper) Update(req *http.Request, context api.Context) func TestAudit(t *testing.T) { var buf bytes.Buffer - attributeGetter := apiserver.NewRequestAttributeGetter(&fakeRequestContextMapper{}, + attributeGetter := NewRequestAttributeGetter(&fakeRequestContextMapper{}, &apiserver.RequestInfoResolver{APIPrefixes: sets.NewString("api", "apis"), GrouplessAPIPrefixes: sets.NewString("api")}) handler := WithAudit(&fakeHTTPHandler{}, attributeGetter, &buf) req, _ := http.NewRequest("GET", "/api/v1/namespaces/default/pods", nil) diff --git a/pkg/apiserver/filters/authorization.go b/pkg/apiserver/filters/authorization.go new file mode 100644 index 00000000000..9395f811a0a --- /dev/null +++ b/pkg/apiserver/filters/authorization.go @@ -0,0 +1,91 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package filters + +import ( + "net/http" + + "github.com/golang/glog" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apiserver" + "k8s.io/kubernetes/pkg/auth/authorizer" +) + +// WithAuthorizationCheck passes all authorized requests on to handler, and returns a forbidden error otherwise. +func WithAuthorization(handler http.Handler, getAttribs RequestAttributeGetter, 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) { + authorized, reason, err := a.Authorize(getAttribs.GetAttribs(req)) + if err != nil { + internalError(w, req, err) + return + } + if !authorized { + glog.V(4).Infof("Forbidden: %#v, Reason: %s", req.RequestURI, reason) + forbidden(w, req) + return + } + handler.ServeHTTP(w, req) + }) +} + +// RequestAttributeGetter is a function that extracts authorizer.Attributes from an http.Request +type RequestAttributeGetter interface { + GetAttribs(req *http.Request) (attribs authorizer.Attributes) +} + +type requestAttributeGetter struct { + requestContextMapper api.RequestContextMapper + requestInfoResolver *apiserver.RequestInfoResolver +} + +// NewAttributeGetter returns an object which implements the RequestAttributeGetter interface. +func NewRequestAttributeGetter(requestContextMapper api.RequestContextMapper, requestInfoResolver *apiserver.RequestInfoResolver) RequestAttributeGetter { + return &requestAttributeGetter{requestContextMapper, requestInfoResolver} +} + +func (r *requestAttributeGetter) GetAttribs(req *http.Request) authorizer.Attributes { + attribs := authorizer.AttributesRecord{} + + ctx, ok := r.requestContextMapper.Get(req) + if ok { + user, ok := api.UserFrom(ctx) + if ok { + attribs.User = user + } + } + + requestInfo, _ := r.requestInfoResolver.GetRequestInfo(req) + + // Start with common attributes that apply to resource and non-resource requests + attribs.ResourceRequest = requestInfo.IsResourceRequest + attribs.Path = requestInfo.Path + attribs.Verb = requestInfo.Verb + + attribs.APIGroup = requestInfo.APIGroup + attribs.APIVersion = requestInfo.APIVersion + attribs.Resource = requestInfo.Resource + attribs.Subresource = requestInfo.Subresource + attribs.Namespace = requestInfo.Namespace + attribs.Name = requestInfo.Name + + return &attribs +} diff --git a/pkg/apiserver/filters/authorization_test.go b/pkg/apiserver/filters/authorization_test.go new file mode 100644 index 00000000000..ad234554e4a --- /dev/null +++ b/pkg/apiserver/filters/authorization_test.go @@ -0,0 +1,111 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package filters + +import ( + "net/http" + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/apiserver" + "k8s.io/kubernetes/pkg/auth/authorizer" + "k8s.io/kubernetes/pkg/util/sets" +) + +func TestGetAttribs(t *testing.T) { + r := &requestAttributeGetter{api.NewRequestContextMapper(), &apiserver.RequestInfoResolver{APIPrefixes: sets.NewString("api", "apis"), GrouplessAPIPrefixes: sets.NewString("api")}} + + 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) + attribs := r.GetAttribs(req) + if !reflect.DeepEqual(attribs, tc.ExpectedAttributes) { + t.Errorf("%s: expected\n\t%#v\ngot\n\t%#v", k, tc.ExpectedAttributes, attribs) + } + } +} diff --git a/pkg/apiserver/filters/doc.go b/pkg/apiserver/filters/doc.go new file mode 100644 index 00000000000..81cb763cfab --- /dev/null +++ b/pkg/apiserver/filters/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package filters contains all the http handler chain filters which +// _are_ api related. +package filters // import "k8s.io/kubernetes/pkg/apiserver/filters" diff --git a/pkg/apiserver/filters/errors.go b/pkg/apiserver/filters/errors.go new file mode 100755 index 00000000000..9c430bdd28f --- /dev/null +++ b/pkg/apiserver/filters/errors.go @@ -0,0 +1,43 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package filters + +import ( + "fmt" + "net/http" + + "k8s.io/kubernetes/pkg/util/runtime" +) + +// badGatewayError renders a simple bad gateway error. +func badGatewayError(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusBadGateway) + fmt.Fprintf(w, "Bad Gateway: %#v", req.RequestURI) +} + +// forbidden renders a simple forbidden error +func forbidden(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusForbidden) + fmt.Fprintf(w, "Forbidden: %#v", req.RequestURI) +} + +// internalError renders a simple internal error +func internalError(w http.ResponseWriter, req *http.Request, err error) { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "Internal Server Error: %#v", req.RequestURI) + runtime.HandleError(err) +} diff --git a/pkg/apiserver/handler_impersonation.go b/pkg/apiserver/filters/impersonation.go similarity index 99% rename from pkg/apiserver/handler_impersonation.go rename to pkg/apiserver/filters/impersonation.go index bda8d11c28d..da393fe9581 100644 --- a/pkg/apiserver/handler_impersonation.go +++ b/pkg/apiserver/filters/impersonation.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package apiserver +package filters import ( "fmt" diff --git a/pkg/apiserver/handler_impersonation_test.go b/pkg/apiserver/filters/impersonation_test.go similarity index 99% rename from pkg/apiserver/handler_impersonation_test.go rename to pkg/apiserver/filters/impersonation_test.go index 313870d79e6..92f7c588074 100644 --- a/pkg/apiserver/handler_impersonation_test.go +++ b/pkg/apiserver/filters/impersonation_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package apiserver +package filters import ( "fmt" @@ -264,7 +264,7 @@ func TestImpersonationFilter(t *testing.T) { }, } - requestContextMapper = api.NewRequestContextMapper() + requestContextMapper := api.NewRequestContextMapper() var ctx api.Context var actualUser user.Info var lock sync.Mutex diff --git a/pkg/apiserver/handlers.go b/pkg/apiserver/requestinfo.go similarity index 71% rename from pkg/apiserver/handlers.go rename to pkg/apiserver/requestinfo.go index b5e65375948..8fb3bfeabf7 100644 --- a/pkg/apiserver/handlers.go +++ b/pkg/apiserver/requestinfo.go @@ -1,5 +1,5 @@ /* -Copyright 2014 The Kubernetes Authors. +Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,115 +21,10 @@ import ( "net/http" "strings" - "github.com/golang/glog" "k8s.io/kubernetes/pkg/api" - "k8s.io/kubernetes/pkg/auth/authorizer" "k8s.io/kubernetes/pkg/util/sets" ) -// specialVerbs contains just strings which are used in REST paths for special actions that don't fall under the normal -// CRUDdy GET/POST/PUT/DELETE actions on REST objects. -// TODO: find a way to keep this up to date automatically. Maybe dynamically populate list as handlers added to -// master's Mux. -var specialVerbs = sets.NewString("proxy", "redirect", "watch") - -// specialVerbsNoSubresources contains root verbs which do not allow subresources -var specialVerbsNoSubresources = sets.NewString("proxy", "redirect") - -// namespaceSubresources contains subresources of namespace -// this list allows the parser to distinguish between a namespace subresource, and a namespaced resource -var namespaceSubresources = sets.NewString("status", "finalize") - -// NamespaceSubResourcesForTest exports namespaceSubresources for testing in pkg/master/master_test.go, so we never drift -var NamespaceSubResourcesForTest = sets.NewString(namespaceSubresources.List()...) - -// IsReadOnlyReq() is true for any (or at least many) request which has no observable -// side effects on state of apiserver (though there may be internal side effects like -// caching and logging). -func IsReadOnlyReq(req http.Request) bool { - if req.Method == "GET" { - // TODO: add OPTIONS and HEAD if we ever support those. - return true - } - return false -} - -// ReadOnly passes all GET requests on to handler, and returns an error on all other requests. -func ReadOnly(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - if IsReadOnlyReq(*req) { - handler.ServeHTTP(w, req) - return - } - w.WriteHeader(http.StatusForbidden) - fmt.Fprintf(w, "This is a read-only endpoint.") - }) -} - -// RequestAttributeGetter is a function that extracts authorizer.Attributes from an http.Request -type RequestAttributeGetter interface { - GetAttribs(req *http.Request) (attribs authorizer.Attributes) -} - -type requestAttributeGetter struct { - requestContextMapper api.RequestContextMapper - requestInfoResolver *RequestInfoResolver -} - -// NewAttributeGetter returns an object which implements the RequestAttributeGetter interface. -func NewRequestAttributeGetter(requestContextMapper api.RequestContextMapper, requestInfoResolver *RequestInfoResolver) RequestAttributeGetter { - return &requestAttributeGetter{requestContextMapper, requestInfoResolver} -} - -func (r *requestAttributeGetter) GetAttribs(req *http.Request) authorizer.Attributes { - attribs := authorizer.AttributesRecord{} - - ctx, ok := r.requestContextMapper.Get(req) - if ok { - user, ok := api.UserFrom(ctx) - if ok { - attribs.User = user - } - } - - requestInfo, _ := r.requestInfoResolver.GetRequestInfo(req) - - // Start with common attributes that apply to resource and non-resource requests - attribs.ResourceRequest = requestInfo.IsResourceRequest - attribs.Path = requestInfo.Path - attribs.Verb = requestInfo.Verb - - attribs.APIGroup = requestInfo.APIGroup - attribs.APIVersion = requestInfo.APIVersion - attribs.Resource = requestInfo.Resource - attribs.Subresource = requestInfo.Subresource - attribs.Namespace = requestInfo.Namespace - attribs.Name = requestInfo.Name - - return &attribs -} - -// WithAuthorizationCheck passes all authorized requests on to handler, and returns a forbidden error otherwise. -func WithAuthorization(handler http.Handler, getAttribs RequestAttributeGetter, 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) { - authorized, reason, err := a.Authorize(getAttribs.GetAttribs(req)) - if err != nil { - internalError(w, req, err) - return - } - if !authorized { - glog.V(4).Infof("Forbidden: %#v, Reason: %s", req.RequestURI, reason) - forbidden(w, req) - return - } - handler.ServeHTTP(w, req) - }) -} - // RequestInfo holds information parsed from the http.Request type RequestInfo struct { // IsResourceRequest indicates whether or not the request is for an API resource or subresource @@ -156,6 +51,22 @@ type RequestInfo struct { Parts []string } +// specialVerbs contains just strings which are used in REST paths for special actions that don't fall under the normal +// CRUDdy GET/POST/PUT/DELETE actions on REST objects. +// TODO: find a way to keep this up to date automatically. Maybe dynamically populate list as handlers added to +// master's Mux. +var specialVerbs = sets.NewString("proxy", "redirect", "watch") + +// specialVerbsNoSubresources contains root verbs which do not allow subresources +var specialVerbsNoSubresources = sets.NewString("proxy", "redirect") + +// namespaceSubresources contains subresources of namespace +// this list allows the parser to distinguish between a namespace subresource, and a namespaced resource +var namespaceSubresources = sets.NewString("status", "finalize") + +// NamespaceSubResourcesForTest exports namespaceSubresources for testing in pkg/master/master_test.go, so we never drift +var NamespaceSubResourcesForTest = sets.NewString(namespaceSubresources.List()...) + type RequestInfoResolver struct { APIPrefixes sets.String GrouplessAPIPrefixes sets.String @@ -295,3 +206,12 @@ func (r *RequestInfoResolver) GetRequestInfo(req *http.Request) (RequestInfo, er return requestInfo, nil } + +// splitPath returns the segments for a URL path. +func splitPath(path string) []string { + path = strings.Trim(path, "/") + if path == "" { + return []string{} + } + return strings.Split(path, "/") +} diff --git a/pkg/apiserver/handlers_test.go b/pkg/apiserver/requestinfo_test.go similarity index 74% rename from pkg/apiserver/handlers_test.go rename to pkg/apiserver/requestinfo_test.go index a9d679b021e..efb8432ccb3 100644 --- a/pkg/apiserver/handlers_test.go +++ b/pkg/apiserver/requestinfo_test.go @@ -18,14 +18,11 @@ package apiserver import ( "net/http" - "net/http/httptest" "reflect" "testing" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/testapi" - "k8s.io/kubernetes/pkg/apis/extensions" - "k8s.io/kubernetes/pkg/auth/authorizer" "k8s.io/kubernetes/pkg/util/sets" ) @@ -43,106 +40,6 @@ func pathWithPrefix(prefix, resource, namespace, name string) string { return testapi.Default.ResourcePathWithPrefix(prefix, resource, namespace, name) } -func TestReadOnly(t *testing.T) { - server := httptest.NewServer(ReadOnly(http.HandlerFunc( - func(w http.ResponseWriter, req *http.Request) { - if req.Method != "GET" { - t.Errorf("Unexpected call: %v", req.Method) - } - }, - ))) - defer server.Close() - for _, verb := range []string{"GET", "POST", "PUT", "DELETE", "CREATE"} { - req, err := http.NewRequest(verb, server.URL, nil) - if err != nil { - t.Fatalf("Couldn't make request: %v", err) - } - http.DefaultClient.Do(req) - } -} - -func TestGetAttribs(t *testing.T) { - r := &requestAttributeGetter{api.NewRequestContextMapper(), &RequestInfoResolver{sets.NewString("api", "apis"), sets.NewString("api")}} - - 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) - attribs := r.GetAttribs(req) - if !reflect.DeepEqual(attribs, tc.ExpectedAttributes) { - t.Errorf("%s: expected\n\t%#v\ngot\n\t%#v", k, tc.ExpectedAttributes, attribs) - } - } -} - func TestGetAPIRequestInfo(t *testing.T) { successCases := []struct { method string @@ -202,12 +99,12 @@ func TestGetAPIRequestInfo(t *testing.T) { {"POST", "/apis/extensions/v1beta3/namespaces/other/pods", "create", "api", "extensions", "v1beta3", "other", "pods", "", "", []string{"pods"}}, } - requestInfoResolver := newTestRequestInfoResolver() + resolver := newTestRequestInfoResolver() for _, successCase := range successCases { req, _ := http.NewRequest(successCase.method, successCase.url, nil) - apiRequestInfo, err := requestInfoResolver.GetRequestInfo(req) + apiRequestInfo, err := resolver.GetRequestInfo(req) if err != nil { t.Errorf("Unexpected error for url: %s %v", successCase.url, err) } @@ -250,7 +147,7 @@ func TestGetAPIRequestInfo(t *testing.T) { if err != nil { t.Errorf("Unexpected error %v", err) } - apiRequestInfo, err := requestInfoResolver.GetRequestInfo(req) + apiRequestInfo, err := resolver.GetRequestInfo(req) if err != nil { t.Errorf("%s: Unexpected error %v", k, err) } @@ -281,12 +178,12 @@ func TestGetNonAPIRequestInfo(t *testing.T) { "empty": {"", false}, } - requestInfoResolver := newTestRequestInfoResolver() + resolver := newTestRequestInfoResolver() for testName, tc := range tests { req, _ := http.NewRequest("GET", tc.url, nil) - apiRequestInfo, err := requestInfoResolver.GetRequestInfo(req) + apiRequestInfo, err := resolver.GetRequestInfo(req) if err != nil { t.Errorf("%s: Unexpected error %v", testName, err) } @@ -295,3 +192,7 @@ func TestGetNonAPIRequestInfo(t *testing.T) { } } } + +func newTestRequestInfoResolver() *RequestInfoResolver { + return &RequestInfoResolver{sets.NewString("api", "apis"), sets.NewString("api")} +} diff --git a/pkg/apiserver/serviceerror.go b/pkg/apiserver/serviceerror.go new file mode 100644 index 00000000000..7a28d52db3c --- /dev/null +++ b/pkg/apiserver/serviceerror.go @@ -0,0 +1,42 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apiserver + +import ( + "github.com/emicklei/go-restful" + + "k8s.io/kubernetes/pkg/api" + apierrors "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/runtime" +) + +func InstallServiceErrorHandler(s runtime.NegotiatedSerializer, container *restful.Container) { + container.ServiceErrorHandler(func(serviceErr restful.ServiceError, request *restful.Request, response *restful.Response) { + serviceErrorHandler(s, serviceErr, request, response) + }) +} + +func serviceErrorHandler(s runtime.NegotiatedSerializer, serviceErr restful.ServiceError, request *restful.Request, response *restful.Response) { + errorNegotiated( + apierrors.NewGenericServerResponse(serviceErr.Code, "", api.Resource(""), "", serviceErr.Message, 0, false), + s, + unversioned.GroupVersion{}, + response.ResponseWriter, + request.Request, + ) +} diff --git a/pkg/genericapiserver/config.go b/pkg/genericapiserver/config.go index 8d367cac584..89aed26e6f2 100644 --- a/pkg/genericapiserver/config.go +++ b/pkg/genericapiserver/config.go @@ -36,7 +36,7 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/apiserver" - "k8s.io/kubernetes/pkg/apiserver/audit" + apiserverfilters "k8s.io/kubernetes/pkg/apiserver/filters" "k8s.io/kubernetes/pkg/auth/authenticator" "k8s.io/kubernetes/pkg/auth/authorizer" authhandlers "k8s.io/kubernetes/pkg/auth/handlers" @@ -363,18 +363,16 @@ func (s *GenericAPIServer) buildHandlerChains(c *Config, handler http.Handler) ( // insecure filters insecure = handler - insecure = api.WithRequestContext(insecure, c.RequestContextMapper) insecure = genericfilters.WithPanicRecovery(insecure, s.NewRequestInfoResolver()) insecure = genericfilters.WithTimeoutForNonLongRunningRequests(insecure, longRunningFunc) // secure filters - attributeGetter := apiserver.NewRequestAttributeGetter(c.RequestContextMapper, s.NewRequestInfoResolver()) + attributeGetter := apiserverfilters.NewRequestAttributeGetter(c.RequestContextMapper, s.NewRequestInfoResolver()) secure = handler - secure = apiserver.WithAuthorization(secure, attributeGetter, c.Authorizer) - secure = apiserver.WithImpersonation(secure, c.RequestContextMapper, c.Authorizer) - secure = audit.WithAudit(secure, attributeGetter, s.auditWriter) // before impersonation to read original user + secure = apiserverfilters.WithAuthorization(secure, attributeGetter, c.Authorizer) + secure = apiserverfilters.WithImpersonation(secure, c.RequestContextMapper, c.Authorizer) + secure = apiserverfilters.WithAudit(secure, attributeGetter, s.auditWriter) // before impersonation to read original user secure = authhandlers.WithAuthentication(secure, c.RequestContextMapper, c.Authenticator, authhandlers.Unauthorized(c.SupportsBasicAuth)) - secure = api.WithRequestContext(secure, c.RequestContextMapper) secure = genericfilters.WithPanicRecovery(secure, s.NewRequestInfoResolver()) secure = genericfilters.WithTimeoutForNonLongRunningRequests(secure, longRunningFunc) secure = genericfilters.WithMaxInFlightLimit(secure, c.MaxRequestsInFlight, longRunningFunc)