diff --git a/docs/authorization.md b/docs/authorization.md index 488d97849b7..6d5a077f9f0 100644 --- a/docs/authorization.md +++ b/docs/authorization.md @@ -10,7 +10,7 @@ readonly port is not currently subject to authorization, but is planned to be removed soon.) The authorization check for any request compares attributes of the context of -the request, (such as user, resource kind, and namespace) with access +the request, (such as user, resource, and namespace) with access policies. An API call must be allowed by some policy in order to proceed. The following implementations are available, and are selected by flag: @@ -28,10 +28,10 @@ The following implementations are available, and are selected by flag: A request has 4 attributes that can be considered for authorization: - user (the user-string which a user was authenticated as). - whether the request is readonly (GETs are readonly) - - what kind of object is being accessed + - what resource is being accessed - applies only to the API endpoints, such as `/api/v1beta1/pods`. For miscelaneous endpoints, like `/version`, the - kind is the empty string. + resource is the empty string. - the namespace of the object being access, or the empty string if the endpoint does not support namespaced objects. @@ -49,7 +49,7 @@ Each line is a "policy object". A policy object is a map with the following pro - `user`, type string; the user-string from `--token_auth_file` - `readonly`, type boolean, when true, means that the policy only applies to GET operations. - - `kind`, type string; a kind of object, from an URL, such as `pods`. + - `resource`, type string; a resource from an URL, such as `pods`. - `namespace`, type string; a namespace string. An unset property is the same as a property set to the zero value for its type (e.g. empty string, 0, false). @@ -76,9 +76,9 @@ To permit an action Policy with an unset namespace applies regardless of namespa ### Examples 1. Alice can do anything: `{"user":"alice"}` - 2. Kubelet can read any pods: `{"user":"kubelet", "kind": "pods", "readonly": true}` - 3. Kubelet can read and write events: `{"user":"kubelet", "kind": "events"}` - 4. Bob can just read pods in namespace "projectCaribou": `{"user":"bob", "kind": "pods", "readonly": true, "ns": "projectCaribou"}` + 2. Kubelet can read any pods: `{"user":"kubelet", "resource": "pods", "readonly": true}` + 3. Kubelet can read and write events: `{"user":"kubelet", "resource": "events"}` + 4. Bob can just read pods in namespace "projectCaribou": `{"user":"bob", "resource": "pods", "readonly": true, "ns": "projectCaribou"}` [Complete file example](../pkg/auth/authorizer/abac/example_policy_file.jsonl) diff --git a/pkg/admission/attributes.go b/pkg/admission/attributes.go index f762116c725..b4eac199e0d 100644 --- a/pkg/admission/attributes.go +++ b/pkg/admission/attributes.go @@ -22,15 +22,15 @@ import ( type attributesRecord struct { namespace string - kind string + resource string operation string object runtime.Object } -func NewAttributesRecord(object runtime.Object, namespace, kind, operation string) Attributes { +func NewAttributesRecord(object runtime.Object, namespace, resource, operation string) Attributes { return &attributesRecord{ namespace: namespace, - kind: kind, + resource: resource, operation: operation, object: object, } @@ -40,8 +40,8 @@ func (record *attributesRecord) GetNamespace() string { return record.namespace } -func (record *attributesRecord) GetKind() string { - return record.kind +func (record *attributesRecord) GetResource() string { + return record.resource } func (record *attributesRecord) GetOperation() string { diff --git a/pkg/admission/interfaces.go b/pkg/admission/interfaces.go index 9f72c0b35c2..af5d8b877a8 100644 --- a/pkg/admission/interfaces.go +++ b/pkg/admission/interfaces.go @@ -24,7 +24,7 @@ import ( // that is used to make an admission decision. type Attributes interface { GetNamespace() string - GetKind() string + GetResource() string GetOperation() string GetObject() runtime.Object } diff --git a/pkg/apiserver/api_installer.go b/pkg/apiserver/api_installer.go index 4c9bb1ac234..ce21e230855 100644 --- a/pkg/apiserver/api_installer.go +++ b/pkg/apiserver/api_installer.go @@ -49,13 +49,14 @@ func (a *APIInstaller) Install() (ws *restful.WebService, errors []error) { // Initialize the custom handlers. watchHandler := (&WatchHandler{ - storage: a.restHandler.storage, - codec: a.restHandler.codec, - canonicalPrefix: a.restHandler.canonicalPrefix, - selfLinker: a.restHandler.selfLinker, + storage: a.restHandler.storage, + codec: a.restHandler.codec, + canonicalPrefix: a.restHandler.canonicalPrefix, + selfLinker: a.restHandler.selfLinker, + apiRequestInfoResolver: a.restHandler.apiRequestInfoResolver, }) - redirectHandler := (&RedirectHandler{a.restHandler.storage, a.restHandler.codec}) - proxyHandler := (&ProxyHandler{a.prefix + "/proxy/", a.restHandler.storage, a.restHandler.codec}) + redirectHandler := (&RedirectHandler{a.restHandler.storage, a.restHandler.codec, a.restHandler.apiRequestInfoResolver}) + proxyHandler := (&ProxyHandler{a.prefix + "/proxy/", a.restHandler.storage, a.restHandler.codec, a.restHandler.apiRequestInfoResolver}) for path, storage := range a.restHandler.storage { if err := a.registerResourceHandlers(path, storage, ws, watchHandler, redirectHandler, proxyHandler); err != nil { diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 7d1f572a8c5..62002dbcdb9 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -28,6 +28,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" @@ -57,7 +58,7 @@ type defaultAPIServer struct { // Note: This method is used only in tests. func Handle(storage map[string]RESTStorage, codec runtime.Codec, root string, version string, selfLinker runtime.SelfLinker, admissionControl admission.Interface, mapper meta.RESTMapper) http.Handler { prefix := root + "/" + version - group := NewAPIGroupVersion(storage, codec, prefix, selfLinker, admissionControl, mapper) + group := NewAPIGroupVersion(storage, codec, root, prefix, selfLinker, admissionControl, mapper) container := restful.NewContainer() container.Router(restful.CurlyRouter{}) mux := container.ServeMux @@ -85,15 +86,16 @@ type APIGroupVersion struct { // This is a helper method for registering multiple sets of REST handlers under different // prefixes onto a server. // TODO: add multitype codec serialization -func NewAPIGroupVersion(storage map[string]RESTStorage, codec runtime.Codec, canonicalPrefix string, selfLinker runtime.SelfLinker, admissionControl admission.Interface, mapper meta.RESTMapper) *APIGroupVersion { +func NewAPIGroupVersion(storage map[string]RESTStorage, codec runtime.Codec, apiRoot, canonicalPrefix string, selfLinker runtime.SelfLinker, admissionControl admission.Interface, mapper meta.RESTMapper) *APIGroupVersion { return &APIGroupVersion{ handler: RESTHandler{ - storage: storage, - codec: codec, - canonicalPrefix: canonicalPrefix, - selfLinker: selfLinker, - ops: NewOperations(), - admissionControl: admissionControl, + storage: storage, + codec: codec, + canonicalPrefix: canonicalPrefix, + selfLinker: selfLinker, + ops: NewOperations(), + admissionControl: admissionControl, + apiRequestInfoResolver: &APIRequestInfoResolver{util.NewStringSet(apiRoot), latest.RESTMapper}, }, mapper: mapper, } diff --git a/pkg/apiserver/handlers.go b/pkg/apiserver/handlers.go index 46eda63af0c..01ceab40ad3 100644 --- a/pkg/apiserver/handlers.go +++ b/pkg/apiserver/handlers.go @@ -25,6 +25,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer" authhandlers "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/handlers" "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog" @@ -153,12 +154,13 @@ type RequestAttributeGetter interface { } type requestAttributeGetter struct { - userContexts authhandlers.RequestContext + userContexts authhandlers.RequestContext + apiRequestInfoResolver *APIRequestInfoResolver } // NewAttributeGetter returns an object which implements the RequestAttributeGetter interface. -func NewRequestAttributeGetter(userContexts authhandlers.RequestContext) RequestAttributeGetter { - return &requestAttributeGetter{userContexts} +func NewRequestAttributeGetter(userContexts authhandlers.RequestContext, restMapper meta.RESTMapper, apiRoots ...string) RequestAttributeGetter { + return &requestAttributeGetter{userContexts, &APIRequestInfoResolver{util.NewStringSet(apiRoots...), restMapper}} } func (r *requestAttributeGetter) GetAttribs(req *http.Request) authorizer.Attributes { @@ -171,16 +173,16 @@ func (r *requestAttributeGetter) GetAttribs(req *http.Request) authorizer.Attrib attribs.ReadOnly = IsReadOnlyReq(*req) - namespace, kind, _, _ := KindAndNamespace(req) + apiRequestInfo, _ := r.apiRequestInfoResolver.GetAPIRequestInfo(req) // If a path follows the conventions of the REST object store, then - // we can extract the object Kind. Otherwise, not. - attribs.Kind = kind + // we can extract the resource. Otherwise, not. + attribs.Resource = apiRequestInfo.Resource // If the request specifies a namespace, then the namespace is filled in. // Assumes there is no empty string namespace. Unspecified results // in empty (does not understand defaulting rules.) - attribs.Namespace = namespace + attribs.Namespace = apiRequestInfo.Namespace return &attribs } @@ -197,78 +199,136 @@ func WithAuthorizationCheck(handler http.Handler, getAttribs RequestAttributeGet }) } -// KindAndNamespace returns the kind, namespace, and path parts for the request relative to /{kind}/{name} +// APIRequestInfo holds information parsed from the http.Request +type APIRequestInfo struct { + // Verb is the kube verb associated with the request, not the http verb. This includes things like list and watch. + Verb string + APIVersion string + Namespace string + // Resource is the name of the resource being requested. This is not the kind. For example: pods + Resource string + // Kind is the type of object being manipulated. For example: Pod + Kind string + // Name is empty for some verbs, but if the request directly indicates a name (not in body content) then this field is filled in. + Name string + // Parts are the path parts for the request relative to /{resource}/{name} + Parts []string +} + +type APIRequestInfoResolver struct { + apiPrefixes util.StringSet + restMapper meta.RESTMapper +} + +// GetAPIRequestInfo returns the information from the http request. If error is not nil, APIRequestInfo holds the information as best it is known before the failure // Valid Inputs: // Storage paths -// /ns/{namespace}/{kind} -// /ns/{namespace}/{kind}/{resourceName} -// /{kind} -// /{kind}/{resourceName} -// /{kind}/{resourceName}?namespace={namespace} -// /{kind}?namespace={namespace} +// /ns/{namespace}/{resource} +// /ns/{namespace}/{resource}/{resourceName} +// /{resource} +// /{resource}/{resourceName} +// /{resource}/{resourceName}?namespace={namespace} +// /{resource}?namespace={namespace} // // Special verbs: -// /proxy/{kind}/{resourceName} -// /proxy/ns/{namespace}/{kind}/{resourceName} -// /redirect/ns/{namespace}/{kind}/{resourceName} -// /redirect/{kind}/{resourceName} -// /watch/{kind} -// /watch/ns/{namespace}/{kind} +// /proxy/{resource}/{resourceName} +// /proxy/ns/{namespace}/{resource}/{resourceName} +// /redirect/ns/{namespace}/{resource}/{resourceName} +// /redirect/{resource}/{resourceName} +// /watch/{resource} +// /watch/ns/{namespace}/{resource} // // Fully qualified paths for above: // /api/{version}/* // /api/{version}/* -func KindAndNamespace(req *http.Request) (namespace, kind string, parts []string, err error) { - parts = splitPath(req.URL.Path) - if len(parts) < 1 { - err = fmt.Errorf("Unable to determine kind and namespace from an empty URL path") - return +func (r *APIRequestInfoResolver) GetAPIRequestInfo(req *http.Request) (APIRequestInfo, error) { + requestInfo := APIRequestInfo{} + + currentParts := splitPath(req.URL.Path) + if len(currentParts) < 1 { + return requestInfo, fmt.Errorf("Unable to determine kind and namespace from an empty URL path") } - // handle input of form /api/{version}/* by adjusting special paths - if parts[0] == "api" { - if len(parts) > 2 { - parts = parts[2:] - } else { - err = fmt.Errorf("Unable to determine kind and namespace from url, %v", req.URL) - return + for _, currPrefix := range r.apiPrefixes.List() { + // handle input of form /api/{version}/* by adjusting special paths + if currentParts[0] == currPrefix { + if len(currentParts) > 1 { + requestInfo.APIVersion = currentParts[1] + } + + if len(currentParts) > 2 { + currentParts = currentParts[2:] + } else { + return requestInfo, fmt.Errorf("Unable to determine kind and namespace from url, %v", req.URL) + } } } // handle input of form /{specialVerb}/* - if _, ok := specialVerbs[parts[0]]; ok { - if len(parts) > 1 { - parts = parts[1:] + if _, ok := specialVerbs[currentParts[0]]; ok { + requestInfo.Verb = currentParts[0] + + if len(currentParts) > 1 { + currentParts = currentParts[1:] } else { - err = fmt.Errorf("Unable to determine kind and namespace from url, %v", req.URL) - return + return requestInfo, fmt.Errorf("Unable to determine kind and namespace from url, %v", req.URL) + } + } else { + switch req.Method { + case "POST": + requestInfo.Verb = "create" + case "GET": + requestInfo.Verb = "get" + case "PUT": + requestInfo.Verb = "update" + case "DELETE": + requestInfo.Verb = "delete" + } + + } + + // URL forms: /ns/{namespace}/{resource}/*, where parts are adjusted to be relative to kind + if currentParts[0] == "ns" { + if len(currentParts) < 3 { + return requestInfo, fmt.Errorf("ResourceTypeAndNamespace expects a path of form /ns/{namespace}/*") + } + requestInfo.Resource = currentParts[2] + requestInfo.Namespace = currentParts[1] + currentParts = currentParts[2:] + + } else { + // URL forms: /{resource}/* + // URL forms: POST /{resource} is a legacy API convention to create in "default" namespace + // URL forms: /{resource}/{resourceName} use the "default" namespace if omitted from query param + // URL forms: /{resource} assume cross-namespace operation if omitted from query param + requestInfo.Resource = currentParts[0] + requestInfo.Namespace = req.URL.Query().Get("namespace") + if len(requestInfo.Namespace) == 0 { + if len(currentParts) > 1 || req.Method == "POST" { + requestInfo.Namespace = api.NamespaceDefault + } else { + requestInfo.Namespace = api.NamespaceAll + } } } - // URL forms: /ns/{namespace}/{kind}/*, where parts are adjusted to be relative to kind - if parts[0] == "ns" { - if len(parts) < 3 { - err = fmt.Errorf("ResourceTypeAndNamespace expects a path of form /ns/{namespace}/*") - return - } - namespace = parts[1] - kind = parts[2] - parts = parts[2:] - return + // parsing successful, so we now know the proper value for .Parts + requestInfo.Parts = currentParts + + // if there's another part remaining after the kind, then that's the resource name + if len(requestInfo.Parts) >= 2 { + requestInfo.Name = requestInfo.Parts[1] } - // URL forms: /{kind}/* - // URL forms: POST /{kind} is a legacy API convention to create in "default" namespace - // URL forms: /{kind}/{resourceName} use the "default" namespace if omitted from query param - // URL forms: /{kind} assume cross-namespace operation if omitted from query param - kind = parts[0] - namespace = req.URL.Query().Get("namespace") - if len(namespace) == 0 { - if len(parts) > 1 || req.Method == "POST" { - namespace = api.NamespaceDefault - } else { - namespace = api.NamespaceAll - } + // if there's no name on the request and we thought it was a get before, then the actual verb is a list + if len(requestInfo.Name) == 0 && requestInfo.Verb == "get" { + requestInfo.Verb = "list" } - return + + // if we have a resource, we have a good shot at being able to determine kind + if len(requestInfo.Resource) > 0 { + _, requestInfo.Kind, _ = r.restMapper.VersionAndKindForResource(requestInfo.Resource) + } + + return requestInfo, nil } diff --git a/pkg/apiserver/handlers_test.go b/pkg/apiserver/handlers_test.go index e4ad0fec932..3cc5b05d685 100644 --- a/pkg/apiserver/handlers_test.go +++ b/pkg/apiserver/handlers_test.go @@ -23,6 +23,8 @@ import ( "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) type fakeRL bool @@ -63,59 +65,78 @@ func TestReadOnly(t *testing.T) { } } -func TestKindAndNamespace(t *testing.T) { +func TestGetAPIRequestInfo(t *testing.T) { successCases := []struct { - method string - url string - expectedNamespace string - expectedKind string - expectedParts []string + method string + url string + expectedVerb string + expectedAPIVersion string + expectedNamespace string + expectedResource string + expectedKind string + expectedName string + expectedParts []string }{ // resource paths - {"GET", "/ns/other/pods", "other", "pods", []string{"pods"}}, - {"GET", "/ns/other/pods/foo", "other", "pods", []string{"pods", "foo"}}, - {"GET", "/pods", api.NamespaceAll, "pods", []string{"pods"}}, - {"POST", "/pods", api.NamespaceDefault, "pods", []string{"pods"}}, - {"GET", "/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}}, - {"GET", "/pods/foo?namespace=other", "other", "pods", []string{"pods", "foo"}}, - {"GET", "/pods?namespace=other", "other", "pods", []string{"pods"}}, + {"GET", "/ns/other/pods", "list", "", "other", "pods", "Pod", "", []string{"pods"}}, + {"GET", "/ns/other/pods/foo", "get", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/pods", "list", "", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}}, + {"POST", "/pods", "create", "", api.NamespaceDefault, "pods", "Pod", "", []string{"pods"}}, + {"GET", "/pods/foo", "get", "", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/pods/foo?namespace=other", "get", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/pods?namespace=other", "list", "", "other", "pods", "Pod", "", []string{"pods"}}, // special verbs - {"GET", "/proxy/ns/other/pods/foo", "other", "pods", []string{"pods", "foo"}}, - {"GET", "/proxy/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}}, - {"GET", "/redirect/ns/other/pods/foo", "other", "pods", []string{"pods", "foo"}}, - {"GET", "/redirect/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}}, - {"GET", "/watch/pods", api.NamespaceAll, "pods", []string{"pods"}}, - {"GET", "/watch/ns/other/pods", "other", "pods", []string{"pods"}}, + {"GET", "/proxy/ns/other/pods/foo", "proxy", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/proxy/pods/foo", "proxy", "", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/redirect/ns/other/pods/foo", "redirect", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/redirect/pods/foo", "redirect", "", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/watch/pods", "watch", "", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}}, + {"GET", "/watch/ns/other/pods", "watch", "", "other", "pods", "Pod", "", []string{"pods"}}, // fully-qualified paths - {"GET", "/api/v1beta1/ns/other/pods", "other", "pods", []string{"pods"}}, - {"GET", "/api/v1beta1/ns/other/pods/foo", "other", "pods", []string{"pods", "foo"}}, - {"GET", "/api/v1beta1/pods", api.NamespaceAll, "pods", []string{"pods"}}, - {"POST", "/api/v1beta1/pods", api.NamespaceDefault, "pods", []string{"pods"}}, - {"GET", "/api/v1beta1/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}}, - {"GET", "/api/v1beta1/pods/foo?namespace=other", "other", "pods", []string{"pods", "foo"}}, - {"GET", "/api/v1beta1/pods?namespace=other", "other", "pods", []string{"pods"}}, - {"GET", "/api/v1beta1/proxy/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}}, - {"GET", "/api/v1beta1/redirect/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}}, - {"GET", "/api/v1beta1/watch/pods", api.NamespaceAll, "pods", []string{"pods"}}, - {"GET", "/api/v1beta1/watch/ns/other/pods", "other", "pods", []string{"pods"}}, + {"GET", "/api/v1beta1/ns/other/pods", "list", "v1beta1", "other", "pods", "Pod", "", []string{"pods"}}, + {"GET", "/api/v1beta1/ns/other/pods/foo", "get", "v1beta1", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/api/v1beta1/pods", "list", "v1beta1", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}}, + {"POST", "/api/v1beta1/pods", "create", "v1beta1", api.NamespaceDefault, "pods", "Pod", "", []string{"pods"}}, + {"GET", "/api/v1beta1/pods/foo", "get", "v1beta1", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/api/v1beta1/pods/foo?namespace=other", "get", "v1beta1", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/api/v1beta1/pods?namespace=other", "list", "v1beta1", "other", "pods", "Pod", "", []string{"pods"}}, + {"GET", "/api/v1beta1/proxy/pods/foo", "proxy", "v1beta1", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/api/v1beta1/redirect/pods/foo", "redirect", "v1beta1", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/api/v1beta1/watch/pods", "watch", "v1beta1", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}}, + {"GET", "/api/v1beta1/watch/ns/other/pods", "watch", "v1beta1", "other", "pods", "Pod", "", []string{"pods"}}, } + apiRequestInfoResolver := &APIRequestInfoResolver{util.NewStringSet("api"), latest.RESTMapper} + for _, successCase := range successCases { req, _ := http.NewRequest(successCase.method, successCase.url, nil) - namespace, kind, parts, err := KindAndNamespace(req) + + apiRequestInfo, err := apiRequestInfoResolver.GetAPIRequestInfo(req) if err != nil { - t.Errorf("Unexpected error for url: %s", successCase.url) + t.Errorf("Unexpected error for url: %s %v", successCase.url, err) } - if successCase.expectedNamespace != namespace { - t.Errorf("Unexpected namespace for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedNamespace, namespace) + if successCase.expectedVerb != apiRequestInfo.Verb { + t.Errorf("Unexpected verb for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedVerb, apiRequestInfo.Verb) } - if successCase.expectedKind != kind { - t.Errorf("Unexpected resourceType for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedKind, kind) + if successCase.expectedAPIVersion != apiRequestInfo.APIVersion { + t.Errorf("Unexpected apiVersion for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedAPIVersion, apiRequestInfo.APIVersion) } - if !reflect.DeepEqual(successCase.expectedParts, parts) { - t.Errorf("Unexpected parts for url: %s, expected: %v, actual: %v", successCase.url, successCase.expectedParts, parts) + if successCase.expectedNamespace != apiRequestInfo.Namespace { + t.Errorf("Unexpected namespace for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedNamespace, apiRequestInfo.Namespace) + } + if successCase.expectedKind != apiRequestInfo.Kind { + t.Errorf("Unexpected kind for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedKind, apiRequestInfo.Kind) + } + if successCase.expectedResource != apiRequestInfo.Resource { + t.Errorf("Unexpected resource for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedResource, apiRequestInfo.Resource) + } + if successCase.expectedName != apiRequestInfo.Name { + t.Errorf("Unexpected name for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedName, apiRequestInfo.Name) + } + if !reflect.DeepEqual(successCase.expectedParts, apiRequestInfo.Parts) { + t.Errorf("Unexpected parts for url: %s, expected: %v, actual: %v", successCase.url, successCase.expectedParts, apiRequestInfo.Parts) } } @@ -131,7 +152,7 @@ func TestKindAndNamespace(t *testing.T) { if err != nil { t.Errorf("Unexpected error %v", err) } - _, _, _, err = KindAndNamespace(req) + _, err = apiRequestInfoResolver.GetAPIRequestInfo(req) if err == nil { t.Errorf("Expected error for key: %s", k) } diff --git a/pkg/apiserver/proxy.go b/pkg/apiserver/proxy.go index b0e8151f032..6a3609ea8bf 100644 --- a/pkg/apiserver/proxy.go +++ b/pkg/apiserver/proxy.go @@ -75,17 +75,20 @@ var tagsToAttrs = map[string]util.StringSet{ // ProxyHandler provides a http.Handler which will proxy traffic to locations // specified by items implementing Redirector. type ProxyHandler struct { - prefix string - storage map[string]RESTStorage - codec runtime.Codec + prefix string + storage map[string]RESTStorage + codec runtime.Codec + apiRequestInfoResolver *APIRequestInfoResolver } func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - namespace, kind, parts, err := KindAndNamespace(req) + requestInfo, err := r.apiRequestInfoResolver.GetAPIRequestInfo(req) if err != nil { notFound(w, req) return } + namespace, resource, parts := requestInfo.Namespace, requestInfo.Resource, requestInfo.Parts + ctx := api.WithNamespace(api.NewContext(), namespace) if len(parts) < 2 { notFound(w, req) @@ -103,17 +106,17 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { rest = rest + "/" } } - storage, ok := r.storage[kind] + storage, ok := r.storage[resource] if !ok { - httplog.LogOf(req, w).Addf("'%v' has no storage object", kind) + httplog.LogOf(req, w).Addf("'%v' has no storage object", resource) notFound(w, req) return } redirector, ok := storage.(Redirector) if !ok { - httplog.LogOf(req, w).Addf("'%v' is not a redirector", kind) - errorJSON(errors.NewMethodNotSupported(kind, "proxy"), r.codec, w) + httplog.LogOf(req, w).Addf("'%v' is not a redirector", resource) + errorJSON(errors.NewMethodNotSupported(resource, "proxy"), r.codec, w) return } @@ -156,7 +159,7 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { proxy.Transport = &proxyTransport{ proxyScheme: req.URL.Scheme, proxyHost: req.URL.Host, - proxyPathPrepend: path.Join(r.prefix, "ns", namespace, kind, id), + proxyPathPrepend: path.Join(r.prefix, "ns", namespace, resource, id), } proxy.FlushInterval = 200 * time.Millisecond proxy.ServeHTTP(w, newReq) diff --git a/pkg/apiserver/redirect.go b/pkg/apiserver/redirect.go index bd6f5b5c47d..1c817d5bebe 100644 --- a/pkg/apiserver/redirect.go +++ b/pkg/apiserver/redirect.go @@ -26,35 +26,37 @@ import ( ) type RedirectHandler struct { - storage map[string]RESTStorage - codec runtime.Codec + storage map[string]RESTStorage + codec runtime.Codec + apiRequestInfoResolver *APIRequestInfoResolver } func (r *RedirectHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - namespace, kind, parts, err := KindAndNamespace(req) + requestInfo, err := r.apiRequestInfoResolver.GetAPIRequestInfo(req) if err != nil { notFound(w, req) return } - ctx := api.WithNamespace(api.NewContext(), namespace) + resource, parts := requestInfo.Resource, requestInfo.Parts + ctx := api.WithNamespace(api.NewContext(), requestInfo.Namespace) - // redirection requires /kind/resourceName path parts + // redirection requires /resource/resourceName path parts if len(parts) != 2 || req.Method != "GET" { notFound(w, req) return } id := parts[1] - storage, ok := r.storage[kind] + storage, ok := r.storage[resource] if !ok { - httplog.LogOf(req, w).Addf("'%v' has no storage object", kind) + httplog.LogOf(req, w).Addf("'%v' has no storage object", resource) notFound(w, req) return } redirector, ok := storage.(Redirector) if !ok { - httplog.LogOf(req, w).Addf("'%v' is not a redirector", kind) - errorJSON(errors.NewMethodNotSupported(kind, "redirect"), r.codec, w) + httplog.LogOf(req, w).Addf("'%v' is not a redirector", resource) + errorJSON(errors.NewMethodNotSupported(resource, "redirect"), r.codec, w) return } diff --git a/pkg/apiserver/resthandler.go b/pkg/apiserver/resthandler.go index cb34aa1e233..22678f4c900 100644 --- a/pkg/apiserver/resthandler.go +++ b/pkg/apiserver/resthandler.go @@ -32,28 +32,29 @@ import ( // RESTHandler implements HTTP verbs on a set of RESTful resources identified by name. type RESTHandler struct { - storage map[string]RESTStorage - codec runtime.Codec - canonicalPrefix string - selfLinker runtime.SelfLinker - ops *Operations - admissionControl admission.Interface + storage map[string]RESTStorage + codec runtime.Codec + canonicalPrefix string + selfLinker runtime.SelfLinker + ops *Operations + admissionControl admission.Interface + apiRequestInfoResolver *APIRequestInfoResolver } // ServeHTTP handles requests to all RESTStorage objects. func (h *RESTHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - namespace, kind, parts, err := KindAndNamespace(req) + requestInfo, err := h.apiRequestInfoResolver.GetAPIRequestInfo(req) if err != nil { notFound(w, req) return } - storage, ok := h.storage[kind] + storage, ok := h.storage[requestInfo.Resource] if !ok { notFound(w, req) return } - h.handleRESTStorage(parts, req, w, storage, namespace, kind) + h.handleRESTStorage(requestInfo.Parts, req, w, storage, requestInfo.Namespace, requestInfo.Resource) } // Sets the SelfLink field of the object. diff --git a/pkg/apiserver/watch.go b/pkg/apiserver/watch.go index 867259a7366..4b2b4e17ef9 100644 --- a/pkg/apiserver/watch.go +++ b/pkg/apiserver/watch.go @@ -36,10 +36,11 @@ import ( ) type WatchHandler struct { - storage map[string]RESTStorage - codec runtime.Codec - canonicalPrefix string - selfLinker runtime.SelfLinker + storage map[string]RESTStorage + codec runtime.Codec + canonicalPrefix string + selfLinker runtime.SelfLinker + apiRequestInfoResolver *APIRequestInfoResolver } // setSelfLinkAddName sets the self link, appending the object's name to the canonical path & type. @@ -87,21 +88,21 @@ func (h *WatchHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - namespace, kind, _, err := KindAndNamespace(req) + requestInfo, err := h.apiRequestInfoResolver.GetAPIRequestInfo(req) if err != nil { notFound(w, req) return } - ctx := api.WithNamespace(api.NewContext(), namespace) + ctx := api.WithNamespace(api.NewContext(), requestInfo.Namespace) - storage := h.storage[kind] + storage := h.storage[requestInfo.Resource] if storage == nil { notFound(w, req) return } watcher, ok := storage.(ResourceWatcher) if !ok { - errorJSON(errors.NewMethodNotSupported(kind, "watch"), h.codec, w) + errorJSON(errors.NewMethodNotSupported(requestInfo.Resource, "watch"), h.codec, w) return } diff --git a/pkg/auth/authorizer/abac/abac.go b/pkg/auth/authorizer/abac/abac.go index cba7a796e78..6963df99080 100644 --- a/pkg/auth/authorizer/abac/abac.go +++ b/pkg/auth/authorizer/abac/abac.go @@ -52,7 +52,7 @@ type policy struct { // TODO: make this a proper REST object with its own registry. Readonly bool `json:"readonly,omitempty"` - Kind string `json:"kind,omitempty"` + Resource string `json:"resource,omitempty"` Namespace string `json:"namespace,omitempty"` // TODO: "expires" string in RFC3339 format. @@ -100,7 +100,7 @@ func NewFromFile(path string) (policyList, error) { func (p policy) matches(a authorizer.Attributes) bool { if p.subjectMatches(a) { if p.Readonly == false || (p.Readonly == a.IsReadOnly()) { - if p.Kind == "" || (p.Kind == a.GetKind()) { + if p.Resource == "" || (p.Resource == a.GetResource()) { if p.Namespace == "" || (p.Namespace == a.GetNamespace()) { return true } diff --git a/pkg/auth/authorizer/abac/abac_test.go b/pkg/auth/authorizer/abac/abac_test.go index 06855586a87..40cfff0ebfb 100644 --- a/pkg/auth/authorizer/abac/abac_test.go +++ b/pkg/auth/authorizer/abac/abac_test.go @@ -76,49 +76,49 @@ func NotTestAuthorize(t *testing.T) { testCases := []struct { User user.DefaultInfo RO bool - Kind string + Resource string NS string ExpectAllow bool }{ // Scheduler can read pods - {User: uScheduler, RO: true, Kind: "pods", NS: "ns1", ExpectAllow: true}, - {User: uScheduler, RO: true, Kind: "pods", NS: "", ExpectAllow: true}, + {User: uScheduler, RO: true, Resource: "pods", NS: "ns1", ExpectAllow: true}, + {User: uScheduler, RO: true, Resource: "pods", NS: "", ExpectAllow: true}, // Scheduler cannot write pods - {User: uScheduler, RO: false, Kind: "pods", NS: "ns1", ExpectAllow: false}, - {User: uScheduler, RO: false, Kind: "pods", NS: "", ExpectAllow: false}, + {User: uScheduler, RO: false, Resource: "pods", NS: "ns1", ExpectAllow: false}, + {User: uScheduler, RO: false, Resource: "pods", NS: "", ExpectAllow: false}, // Scheduler can write bindings - {User: uScheduler, RO: true, Kind: "bindings", NS: "ns1", ExpectAllow: true}, - {User: uScheduler, RO: true, Kind: "bindings", NS: "", ExpectAllow: true}, + {User: uScheduler, RO: true, Resource: "bindings", NS: "ns1", ExpectAllow: true}, + {User: uScheduler, RO: true, Resource: "bindings", NS: "", ExpectAllow: true}, // Alice can read and write anything in the right namespace. - {User: uAlice, RO: true, Kind: "pods", NS: "projectCaribou", ExpectAllow: true}, - {User: uAlice, RO: true, Kind: "widgets", NS: "projectCaribou", ExpectAllow: true}, - {User: uAlice, RO: true, Kind: "", NS: "projectCaribou", ExpectAllow: true}, - {User: uAlice, RO: false, Kind: "pods", NS: "projectCaribou", ExpectAllow: true}, - {User: uAlice, RO: false, Kind: "widgets", NS: "projectCaribou", ExpectAllow: true}, - {User: uAlice, RO: false, Kind: "", NS: "projectCaribou", ExpectAllow: true}, + {User: uAlice, RO: true, Resource: "pods", NS: "projectCaribou", ExpectAllow: true}, + {User: uAlice, RO: true, Resource: "widgets", NS: "projectCaribou", ExpectAllow: true}, + {User: uAlice, RO: true, Resource: "", NS: "projectCaribou", ExpectAllow: true}, + {User: uAlice, RO: false, Resource: "pods", NS: "projectCaribou", ExpectAllow: true}, + {User: uAlice, RO: false, Resource: "widgets", NS: "projectCaribou", ExpectAllow: true}, + {User: uAlice, RO: false, Resource: "", NS: "projectCaribou", ExpectAllow: true}, // .. but not the wrong namespace. - {User: uAlice, RO: true, Kind: "pods", NS: "ns1", ExpectAllow: false}, - {User: uAlice, RO: true, Kind: "widgets", NS: "ns1", ExpectAllow: false}, - {User: uAlice, RO: true, Kind: "", NS: "ns1", ExpectAllow: false}, + {User: uAlice, RO: true, Resource: "pods", NS: "ns1", ExpectAllow: false}, + {User: uAlice, RO: true, Resource: "widgets", NS: "ns1", ExpectAllow: false}, + {User: uAlice, RO: true, Resource: "", NS: "ns1", ExpectAllow: false}, // Chuck can read events, since anyone can. - {User: uChuck, RO: true, Kind: "events", NS: "ns1", ExpectAllow: true}, - {User: uChuck, RO: true, Kind: "events", NS: "", ExpectAllow: true}, + {User: uChuck, RO: true, Resource: "events", NS: "ns1", ExpectAllow: true}, + {User: uChuck, RO: true, Resource: "events", NS: "", ExpectAllow: true}, // Chuck can't do other things. - {User: uChuck, RO: false, Kind: "events", NS: "ns1", ExpectAllow: false}, - {User: uChuck, RO: true, Kind: "pods", NS: "ns1", ExpectAllow: false}, - {User: uChuck, RO: true, Kind: "floop", NS: "ns1", ExpectAllow: false}, + {User: uChuck, RO: false, Resource: "events", NS: "ns1", ExpectAllow: false}, + {User: uChuck, RO: true, Resource: "pods", NS: "ns1", ExpectAllow: false}, + {User: uChuck, RO: true, Resource: "floop", NS: "ns1", ExpectAllow: false}, // Chunk can't access things with no kind or namespace // TODO: find a way to give someone access to miscelaneous endpoints, such as // /healthz, /version, etc. - {User: uChuck, RO: true, Kind: "", NS: "", ExpectAllow: false}, + {User: uChuck, RO: true, Resource: "", NS: "", ExpectAllow: false}, } for _, tc := range testCases { attr := authorizer.AttributesRecord{ User: &tc.User, ReadOnly: tc.RO, - Kind: tc.Kind, + Resource: tc.Resource, Namespace: tc.NS, } t.Logf("tc: %v -> attr %v", tc, attr) diff --git a/pkg/auth/authorizer/abac/example_policy_file.jsonl b/pkg/auth/authorizer/abac/example_policy_file.jsonl index ba566f16ea3..a9bdb9eea02 100644 --- a/pkg/auth/authorizer/abac/example_policy_file.jsonl +++ b/pkg/auth/authorizer/abac/example_policy_file.jsonl @@ -1,9 +1,9 @@ {"user":"admin"} -{"user":"scheduler", "readonly": true, "kind": "pods"} -{"user":"scheduler", "kind": "bindings"} -{"user":"kubelet", "readonly": true, "kind": "pods"} -{"user":"kubelet", "readonly": true, "kind": "services"} -{"user":"kubelet", "readonly": true, "kind": "endpoints"} -{"user":"kubelet", "kind": "events"} +{"user":"scheduler", "readonly": true, "resource": "pods"} +{"user":"scheduler", "resource": "bindings"} +{"user":"kubelet", "readonly": true, "resource": "pods"} +{"user":"kubelet", "readonly": true, "resource": "services"} +{"user":"kubelet", "readonly": true, "resource": "endpoints"} +{"user":"kubelet", "resource": "events"} {"user":"alice", "ns": "projectCaribou"} {"user":"bob", "readonly": true, "ns": "projectCaribou"} diff --git a/pkg/auth/authorizer/interfaces.go b/pkg/auth/authorizer/interfaces.go index 30d897d0f41..2567a48af66 100644 --- a/pkg/auth/authorizer/interfaces.go +++ b/pkg/auth/authorizer/interfaces.go @@ -40,7 +40,7 @@ type Attributes interface { GetNamespace() string // The kind of object, if a request is for a REST object. - GetKind() string + GetResource() string } // Authorizer makes an authorization decision based on information gained by making @@ -55,7 +55,7 @@ type AttributesRecord struct { User user.Info ReadOnly bool Namespace string - Kind string + Resource string } func (a AttributesRecord) GetUserName() string { @@ -74,6 +74,6 @@ func (a AttributesRecord) GetNamespace() string { return a.Namespace } -func (a AttributesRecord) GetKind() string { - return a.Kind +func (a AttributesRecord) GetResource() string { + return a.Resource } diff --git a/pkg/master/master.go b/pkg/master/master.go index fabc8dee29c..7d0a90d9270 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -454,7 +454,7 @@ func (m *Master) init(c *Config) { m.InsecureHandler = handler - attributeGetter := apiserver.NewRequestAttributeGetter(userContexts) + attributeGetter := apiserver.NewRequestAttributeGetter(userContexts, latest.RESTMapper, "api") handler = apiserver.WithAuthorizationCheck(handler, attributeGetter, m.authorizer) // Install Authenticator @@ -531,25 +531,25 @@ func (m *Master) getServersToValidate(c *Config) map[string]apiserver.Server { } // api_v1beta1 returns the resources and codec for API version v1beta1. -func (m *Master) api_v1beta1() (map[string]apiserver.RESTStorage, runtime.Codec, string, runtime.SelfLinker, admission.Interface, meta.RESTMapper) { +func (m *Master) api_v1beta1() (map[string]apiserver.RESTStorage, runtime.Codec, string, string, runtime.SelfLinker, admission.Interface, meta.RESTMapper) { storage := make(map[string]apiserver.RESTStorage) for k, v := range m.storage { storage[k] = v } - return storage, v1beta1.Codec, "/api/v1beta1", latest.SelfLinker, m.admissionControl, latest.RESTMapper + return storage, v1beta1.Codec, "api", "/api/v1beta1", latest.SelfLinker, m.admissionControl, latest.RESTMapper } // api_v1beta2 returns the resources and codec for API version v1beta2. -func (m *Master) api_v1beta2() (map[string]apiserver.RESTStorage, runtime.Codec, string, runtime.SelfLinker, admission.Interface, meta.RESTMapper) { +func (m *Master) api_v1beta2() (map[string]apiserver.RESTStorage, runtime.Codec, string, string, runtime.SelfLinker, admission.Interface, meta.RESTMapper) { storage := make(map[string]apiserver.RESTStorage) for k, v := range m.storage { storage[k] = v } - return storage, v1beta2.Codec, "/api/v1beta2", latest.SelfLinker, m.admissionControl, latest.RESTMapper + return storage, v1beta2.Codec, "api", "/api/v1beta2", latest.SelfLinker, m.admissionControl, latest.RESTMapper } // api_v1beta3 returns the resources and codec for API version v1beta3. -func (m *Master) api_v1beta3() (map[string]apiserver.RESTStorage, runtime.Codec, string, runtime.SelfLinker, admission.Interface, meta.RESTMapper) { +func (m *Master) api_v1beta3() (map[string]apiserver.RESTStorage, runtime.Codec, string, string, runtime.SelfLinker, admission.Interface, meta.RESTMapper) { storage := make(map[string]apiserver.RESTStorage) for k, v := range m.storage { if k == "minions" { @@ -557,5 +557,5 @@ func (m *Master) api_v1beta3() (map[string]apiserver.RESTStorage, runtime.Codec, } storage[strings.ToLower(k)] = v } - return storage, v1beta3.Codec, "/api/v1beta3", latest.SelfLinker, m.admissionControl, latest.RESTMapper + return storage, v1beta3.Codec, "api", "/api/v1beta3", latest.SelfLinker, m.admissionControl, latest.RESTMapper } diff --git a/plugin/pkg/admission/deny/admission.go b/plugin/pkg/admission/deny/admission.go index e780cab27b4..203cc4f12b7 100644 --- a/plugin/pkg/admission/deny/admission.go +++ b/plugin/pkg/admission/deny/admission.go @@ -36,7 +36,7 @@ func init() { type alwaysDeny struct{} func (alwaysDeny) Admit(a admission.Attributes) (err error) { - return apierrors.NewForbidden(a.GetKind(), "", errors.New("Admission control is denying all modifications")) + return apierrors.NewForbidden(a.GetResource(), "", errors.New("Admission control is denying all modifications")) } func NewAlwaysDeny() admission.Interface { diff --git a/plugin/pkg/admission/limitranger/admission.go b/plugin/pkg/admission/limitranger/admission.go index f13c2294d1d..bf7f4f83a4c 100644 --- a/plugin/pkg/admission/limitranger/admission.go +++ b/plugin/pkg/admission/limitranger/admission.go @@ -58,7 +58,7 @@ func (l *limitRanger) Admit(a admission.Attributes) (err error) { // ensure it meets each prescribed min/max for i := range items.Items { limitRange := &items.Items[i] - err = l.limitFunc(limitRange, a.GetKind(), a.GetObject()) + err = l.limitFunc(limitRange, a.GetResource(), a.GetObject()) if err != nil { return err } @@ -86,8 +86,8 @@ func Max(a int64, b int64) int64 { } // PodLimitFunc enforces that a pod spec does not exceed any limits specified on the supplied limit range -func PodLimitFunc(limitRange *api.LimitRange, kind string, obj runtime.Object) error { - if kind != "pods" { +func PodLimitFunc(limitRange *api.LimitRange, resourceName string, obj runtime.Object) error { + if resourceName != "pods" { return nil } @@ -161,11 +161,11 @@ func PodLimitFunc(limitRange *api.LimitRange, kind string, obj runtime.Object) e switch minOrMax { case "Min": if observed < enforced { - return apierrors.NewForbidden(kind, pod.Name, err) + return apierrors.NewForbidden(resourceName, pod.Name, err) } case "Max": if observed > enforced { - return apierrors.NewForbidden(kind, pod.Name, err) + return apierrors.NewForbidden(resourceName, pod.Name, err) } } } diff --git a/plugin/pkg/admission/resourcedefaults/admission.go b/plugin/pkg/admission/resourcedefaults/admission.go index 11ea55a59ad..733dcf2a940 100644 --- a/plugin/pkg/admission/resourcedefaults/admission.go +++ b/plugin/pkg/admission/resourcedefaults/admission.go @@ -47,7 +47,7 @@ func (resourceDefaults) Admit(a admission.Attributes) (err error) { } // we only care about pods - if a.GetKind() != "pods" { + if a.GetResource() != "pods" { return nil } diff --git a/plugin/pkg/admission/resourcequota/admission.go b/plugin/pkg/admission/resourcequota/admission.go index 3093f95f5fe..bad1edbb750 100644 --- a/plugin/pkg/admission/resourcequota/admission.go +++ b/plugin/pkg/admission/resourcequota/admission.go @@ -44,7 +44,7 @@ func NewResourceQuota(client client.Interface) admission.Interface { return "a{client: client} } -var kindToResourceName = map[string]api.ResourceName{ +var resourceToResourceName = map[string]api.ResourceName{ "pods": api.ResourcePods, "services": api.ResourceServices, "replicationControllers": api.ResourceReplicationControllers, @@ -57,7 +57,7 @@ func (q *quota) Admit(a admission.Attributes) (err error) { } obj := a.GetObject() - kind := a.GetKind() + resource := a.GetResource() name := "Unknown" if obj != nil { name, _ = meta.NewAccessor().Name(obj) @@ -65,7 +65,7 @@ func (q *quota) Admit(a admission.Attributes) (err error) { list, err := q.client.ResourceQuotas(a.GetNamespace()).List(labels.Everything()) if err != nil { - return apierrors.NewForbidden(a.GetKind(), name, fmt.Errorf("Unable to %s %s at this time because there was an error enforcing quota", a.GetOperation(), kind)) + return apierrors.NewForbidden(a.GetResource(), name, fmt.Errorf("Unable to %s %s at this time because there was an error enforcing quota", a.GetOperation(), resource)) } if len(list.Items) == 0 { @@ -90,7 +90,7 @@ func (q *quota) Admit(a admission.Attributes) (err error) { usage.Status = quota.Status err = q.client.ResourceQuotaUsages(usage.Namespace).Create(&usage) if err != nil { - return apierrors.NewForbidden(a.GetKind(), name, fmt.Errorf("Unable to %s %s at this time because there was an error enforcing quota", a.GetOperation(), a.GetKind())) + return apierrors.NewForbidden(a.GetResource(), name, fmt.Errorf("Unable to %s %s at this time because there was an error enforcing quota", a.GetOperation(), a.GetResource())) } } } @@ -102,7 +102,7 @@ func (q *quota) Admit(a admission.Attributes) (err error) { // Return an error if the operation should not pass admission control func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, client client.Interface) (bool, error) { obj := a.GetObject() - kind := a.GetKind() + resourceName := a.GetResource() name := "Unknown" if obj != nil { name, _ = meta.NewAccessor().Name(obj) @@ -114,15 +114,15 @@ func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, cli } // handle max counts for each kind of resource (pods, services, replicationControllers, etc.) if a.GetOperation() == "CREATE" { - resourceName := kindToResourceName[a.GetKind()] + resourceName := resourceToResourceName[a.GetResource()] hard, hardFound := status.Hard[resourceName] if hardFound { used, usedFound := status.Used[resourceName] if !usedFound { - return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed.")) + return false, apierrors.NewForbidden(a.GetResource(), name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed.")) } if used.Value() >= hard.Value() { - return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Limited to %s %s", hard.String(), kind)) + return false, apierrors.NewForbidden(a.GetResource(), name, fmt.Errorf("Limited to %s %s", hard.String(), a.GetResource())) } else { status.Used[resourceName] = *resource.NewQuantity(used.Value()+int64(1), resource.DecimalSI) dirty = true @@ -130,7 +130,7 @@ func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, cli } } // handle memory/cpu constraints, and any diff of usage based on memory/cpu on updates - if a.GetKind() == "pods" && (set[api.ResourceMemory] || set[api.ResourceCPU]) { + if a.GetResource() == "pods" && (set[api.ResourceMemory] || set[api.ResourceCPU]) { pod := obj.(*api.Pod) deltaCPU := resourcequota.PodCPU(pod) deltaMemory := resourcequota.PodMemory(pod) @@ -138,7 +138,7 @@ func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, cli if a.GetOperation() == "UPDATE" { oldPod, err := client.Pods(a.GetNamespace()).Get(pod.Name) if err != nil { - return false, apierrors.NewForbidden(kind, name, err) + return false, apierrors.NewForbidden(resourceName, name, err) } oldCPU := resourcequota.PodCPU(oldPod) oldMemory := resourcequota.PodMemory(oldPod) @@ -150,10 +150,10 @@ func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, cli if hardMemFound { used, usedFound := status.Used[api.ResourceMemory] if !usedFound { - return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed.")) + return false, apierrors.NewForbidden(resourceName, name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed.")) } if used.Value()+deltaMemory.Value() > hardMem.Value() { - return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Limited to %s memory", hardMem.String())) + return false, apierrors.NewForbidden(resourceName, name, fmt.Errorf("Limited to %s memory", hardMem.String())) } else { status.Used[api.ResourceMemory] = *resource.NewQuantity(used.Value()+deltaMemory.Value(), resource.DecimalSI) dirty = true @@ -163,10 +163,10 @@ func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, cli if hardCPUFound { used, usedFound := status.Used[api.ResourceCPU] if !usedFound { - return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed.")) + return false, apierrors.NewForbidden(resourceName, name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed.")) } if used.MilliValue()+deltaCPU.MilliValue() > hardCPU.MilliValue() { - return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Limited to %s CPU", hardCPU.String())) + return false, apierrors.NewForbidden(resourceName, name, fmt.Errorf("Limited to %s CPU", hardCPU.String())) } else { status.Used[api.ResourceCPU] = *resource.NewMilliQuantity(used.MilliValue()+deltaCPU.MilliValue(), resource.DecimalSI) dirty = true diff --git a/test/integration/auth_test.go b/test/integration/auth_test.go index 1043de8e534..d4fde99808b 100644 --- a/test/integration/auth_test.go +++ b/test/integration/auth_test.go @@ -691,7 +691,7 @@ func TestKindAuthorization(t *testing.T) { t.Fatalf("unexpected error: %v", err) } - a := newAuthorizerWithContents(t, `{"kind": "services"} + a := newAuthorizerWithContents(t, `{"resource": "services"} `) var m *master.Master