Skip to content

Commit

Permalink
Merge pull request kubernetes#3931 from deads2k/deads-pull-more-info-…
Browse files Browse the repository at this point in the history
…from-request

pull more complete information from request
  • Loading branch information
brendandburns committed Feb 4, 2015
2 parents 3f28bad + 889c4cc commit 550b98e
Show file tree
Hide file tree
Showing 21 changed files with 315 additions and 224 deletions.
14 changes: 7 additions & 7 deletions docs/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.

Expand All @@ -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).
Expand All @@ -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)

Expand Down
10 changes: 5 additions & 5 deletions pkg/admission/attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/admission/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
13 changes: 7 additions & 6 deletions pkg/apiserver/api_installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
18 changes: 10 additions & 8 deletions pkg/apiserver/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
}
Expand Down
178 changes: 119 additions & 59 deletions pkg/apiserver/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand All @@ -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}/{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
// 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}/*")
}
namespace = parts[1]
kind = parts[2]
parts = parts[2:]
return
}
requestInfo.Resource = currentParts[2]
requestInfo.Namespace = currentParts[1]
currentParts = currentParts[2:]

// 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
} 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
}
}
}
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]
}

// 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"
}

// 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
}
Loading

0 comments on commit 550b98e

Please sign in to comment.