Merge pull request #6683 from deads2k/deads-handle-subresources

add support for authorizing subresources
This commit is contained in:
Clayton Coleman 2015-04-13 13:53:54 -04:00
commit 0c8a358de0
2 changed files with 64 additions and 61 deletions

View File

@ -19,7 +19,6 @@ package apiserver
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"path"
"regexp" "regexp"
"runtime/debug" "runtime/debug"
"strings" "strings"
@ -240,6 +239,10 @@ type APIRequestInfo struct {
Namespace string Namespace string
// Resource is the name of the resource being requested. This is not the kind. For example: pods // Resource is the name of the resource being requested. This is not the kind. For example: pods
Resource string Resource string
// Subresource is the name of the subresource being requested. This is a different resource, scoped to the parent resource, but it may have a different kind.
// For instance, /pods has the resource "pods" and the kind "Pod", while /pods/foo/status has the resource "pods", the sub resource "status", and the kind "Pod"
// (because status operates on pods). The binding resource for a pod though may be /pods/foo/binding, which has resource "pods", subresource "binding", and kind "Binding".
Subresource string
// Kind is the type of object being manipulated. For example: Pod // Kind is the type of object being manipulated. For example: Pod
Kind string 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 is empty for some verbs, but if the request directly indicates a name (not in body content) then this field is filled in.
@ -251,22 +254,12 @@ type APIRequestInfo struct {
Raw []string Raw []string
} }
// URLPath returns the URL path for this request, including /{resource}/{name} if present but nothing
// following that.
func (info APIRequestInfo) URLPath() string {
p := info.Parts
if n := len(p); n > 2 {
// Only take resource and name
p = p[:2]
}
return path.Join("/", path.Join(info.Raw...), path.Join(p...))
}
type APIRequestInfoResolver struct { type APIRequestInfoResolver struct {
APIPrefixes util.StringSet APIPrefixes util.StringSet
RestMapper meta.RESTMapper RestMapper meta.RESTMapper
} }
// TODO write an integration test against the swagger doc to test the APIRequestInfo and match up behavior to responses
// 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 // 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: // Valid Inputs:
// Storage paths // Storage paths
@ -340,22 +333,20 @@ func (r *APIRequestInfoResolver) GetAPIRequestInfo(req *http.Request) (APIReques
// URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to be relative to kind // URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to be relative to kind
if currentParts[0] == "namespaces" { if currentParts[0] == "namespaces" {
if len(currentParts) < 3 {
requestInfo.Resource = "namespaces"
if len(currentParts) > 1 { if len(currentParts) > 1 {
requestInfo.Namespace = currentParts[1] requestInfo.Namespace = currentParts[1]
}
} else { // if there is another step after the namespace name and it is not a known namespace subresource
requestInfo.Resource = currentParts[2] // move currentParts to include it as a resource in its own right
requestInfo.Namespace = currentParts[1] if len(currentParts) > 2 {
currentParts = currentParts[2:] currentParts = currentParts[2:]
} }
}
} else { } else {
// URL forms: /{resource}/* // URL forms: /{resource}/*
// URL forms: POST /{resource} is a legacy API convention to create in "default" namespace // 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}/{resourceName} use the "default" namespace if omitted from query param
// URL forms: /{resource} assume cross-namespace operation 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") requestInfo.Namespace = req.URL.Query().Get("namespace")
if len(requestInfo.Namespace) == 0 { if len(requestInfo.Namespace) == 0 {
if len(currentParts) > 1 || req.Method == "POST" { if len(currentParts) > 1 || req.Method == "POST" {
@ -371,9 +362,16 @@ func (r *APIRequestInfoResolver) GetAPIRequestInfo(req *http.Request) (APIReques
// Raw should have everything not in Parts // Raw should have everything not in Parts
requestInfo.Raw = requestInfo.Raw[:len(requestInfo.Raw)-len(currentParts)] requestInfo.Raw = requestInfo.Raw[:len(requestInfo.Raw)-len(currentParts)]
// if there's another part remaining after the kind, then that's the resource name // parts look like: resource/resourceName/subresource/other/stuff/we/don't/interpret
if len(requestInfo.Parts) >= 2 { switch {
case len(requestInfo.Parts) >= 3:
requestInfo.Subresource = requestInfo.Parts[2]
fallthrough
case len(requestInfo.Parts) == 2:
requestInfo.Name = requestInfo.Parts[1] requestInfo.Name = requestInfo.Parts[1]
fallthrough
case len(requestInfo.Parts) == 1:
requestInfo.Resource = requestInfo.Parts[0]
} }
// if there's no name on the request and we thought it was a get before, then the actual verb is a list // if there's no name on the request and we thought it was a get before, then the actual verb is a list

View File

@ -141,43 +141,48 @@ func TestGetAPIRequestInfo(t *testing.T) {
expectedAPIVersion string expectedAPIVersion string
expectedNamespace string expectedNamespace string
expectedResource string expectedResource string
expectedSubresource string
expectedKind string expectedKind string
expectedName string expectedName string
expectedParts []string expectedParts []string
}{ }{
// resource paths // resource paths
{"GET", "/namespaces", "list", "", "", "namespaces", "Namespace", "", []string{"namespaces"}}, {"GET", "/namespaces", "list", "", "", "namespaces", "", "Namespace", "", []string{"namespaces"}},
{"GET", "/namespaces/other", "get", "", "other", "namespaces", "Namespace", "other", []string{"namespaces", "other"}}, {"GET", "/namespaces/other", "get", "", "other", "namespaces", "", "Namespace", "other", []string{"namespaces", "other"}},
{"GET", "/namespaces/other/pods", "list", "", "other", "pods", "Pod", "", []string{"pods"}}, {"GET", "/namespaces/other/pods", "list", "", "other", "pods", "", "Pod", "", []string{"pods"}},
{"GET", "/namespaces/other/pods/foo", "get", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, {"GET", "/namespaces/other/pods/foo", "get", "", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/pods", "list", "", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}}, {"GET", "/pods", "list", "", api.NamespaceAll, "pods", "", "Pod", "", []string{"pods"}},
{"POST", "/pods", "create", "", api.NamespaceDefault, "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", "get", "", api.NamespaceDefault, "pods", "", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/pods/foo?namespace=other", "get", "", "other", "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"}}, {"GET", "/pods?namespace=other", "list", "", "other", "pods", "", "Pod", "", []string{"pods"}},
// special verbs // special verbs
{"GET", "/proxy/namespaces/other/pods/foo", "proxy", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, {"GET", "/proxy/namespaces/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", "/proxy/pods/foo", "proxy", "", api.NamespaceDefault, "pods", "", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/redirect/namespaces/other/pods/foo", "redirect", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, {"GET", "/redirect/namespaces/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", "/redirect/pods/foo", "redirect", "", api.NamespaceDefault, "pods", "", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/watch/pods", "watch", "", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}}, {"GET", "/watch/pods", "watch", "", api.NamespaceAll, "pods", "", "Pod", "", []string{"pods"}},
{"GET", "/watch/namespaces/other/pods", "watch", "", "other", "pods", "Pod", "", []string{"pods"}}, {"GET", "/watch/namespaces/other/pods", "watch", "", "other", "pods", "", "Pod", "", []string{"pods"}},
// fully-qualified paths // fully-qualified paths
{"GET", "/api/v1beta1/namespaces/other/pods", "list", "v1beta1", "other", "pods", "Pod", "", []string{"pods"}}, {"GET", "/api/v1beta1/namespaces/other/pods", "list", "v1beta1", "other", "pods", "", "Pod", "", []string{"pods"}},
{"GET", "/api/v1beta1/namespaces/other/pods/foo", "get", "v1beta1", "other", "pods", "Pod", "foo", []string{"pods", "foo"}}, {"GET", "/api/v1beta1/namespaces/other/pods/foo", "get", "v1beta1", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/api/v1beta1/pods", "list", "v1beta1", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}}, {"GET", "/api/v1beta1/pods", "list", "v1beta1", api.NamespaceAll, "pods", "", "Pod", "", []string{"pods"}},
{"POST", "/api/v1beta1/pods", "create", "v1beta1", api.NamespaceDefault, "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", "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/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/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/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/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/pods", "watch", "v1beta1", api.NamespaceAll, "pods", "", "Pod", "", []string{"pods"}},
{"GET", "/api/v1beta1/watch/namespaces/other/pods", "watch", "v1beta1", "other", "pods", "Pod", "", []string{"pods"}}, {"GET", "/api/v1beta1/watch/namespaces/other/pods", "watch", "v1beta1", "other", "pods", "", "Pod", "", []string{"pods"}},
// subresource identification
{"GET", "/namespaces/other/pods/foo/status", "get", "", "other", "pods", "status", "Pod", "foo", []string{"pods", "foo", "status"}},
{"PUT", "/namespaces/other/finalize", "update", "", "other", "finalize", "", "", "", []string{"finalize"}},
} }
apiRequestInfoResolver := &APIRequestInfoResolver{util.NewStringSet("api"), latest.RESTMapper} apiRequestInfoResolver := &APIRequestInfoResolver{util.NewStringSet("api"), latest.RESTMapper}
@ -204,15 +209,15 @@ func TestGetAPIRequestInfo(t *testing.T) {
if successCase.expectedResource != apiRequestInfo.Resource { if successCase.expectedResource != apiRequestInfo.Resource {
t.Errorf("Unexpected resource for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedResource, apiRequestInfo.Resource) t.Errorf("Unexpected resource for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedResource, apiRequestInfo.Resource)
} }
if successCase.expectedSubresource != apiRequestInfo.Subresource {
t.Errorf("Unexpected resource for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedSubresource, apiRequestInfo.Subresource)
}
if successCase.expectedName != apiRequestInfo.Name { if successCase.expectedName != apiRequestInfo.Name {
t.Errorf("Unexpected name for url: %s, expected: %s, actual: %s", successCase.url, 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) { if !reflect.DeepEqual(successCase.expectedParts, apiRequestInfo.Parts) {
t.Errorf("Unexpected parts for url: %s, expected: %v, actual: %v", successCase.url, successCase.expectedParts, apiRequestInfo.Parts) t.Errorf("Unexpected parts for url: %s, expected: %v, actual: %v", successCase.url, successCase.expectedParts, apiRequestInfo.Parts)
} }
if e, a := strings.Split(successCase.url, "?")[0], apiRequestInfo.URLPath(); e != a {
t.Errorf("Expected %v, got %v", e, a)
}
} }
errorCases := map[string]string{ errorCases := map[string]string{