337 lines
12 KiB
Go
337 lines
12 KiB
Go
/*
|
|
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 (
|
|
"fmt"
|
|
"net/http"
|
|
"runtime/debug"
|
|
"strings"
|
|
|
|
"github.com/golang/glog"
|
|
"k8s.io/kubernetes/pkg/api"
|
|
"k8s.io/kubernetes/pkg/api/errors"
|
|
"k8s.io/kubernetes/pkg/auth/authorizer"
|
|
"k8s.io/kubernetes/pkg/httplog"
|
|
"k8s.io/kubernetes/pkg/util/runtime"
|
|
"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.")
|
|
})
|
|
}
|
|
|
|
// RecoverPanics wraps an http Handler to recover and log panics.
|
|
func RecoverPanics(handler http.Handler, resolver *RequestInfoResolver) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
defer runtime.HandleCrash(func(err interface{}) {
|
|
http.Error(w, "This request caused apisever to panic. Look in log for details.", http.StatusInternalServerError)
|
|
glog.Errorf("APIServer panic'd on %v %v: %v\n%s\n", req.Method, req.RequestURI, err, debug.Stack())
|
|
})
|
|
|
|
logger := httplog.NewLogged(req, &w)
|
|
requestInfo, err := resolver.GetRequestInfo(req)
|
|
if err != nil || requestInfo.Verb != "proxy" {
|
|
logger.StacktraceWhen(
|
|
httplog.StatusIsNot(
|
|
http.StatusOK,
|
|
http.StatusCreated,
|
|
http.StatusAccepted,
|
|
http.StatusBadRequest,
|
|
http.StatusMovedPermanently,
|
|
http.StatusTemporaryRedirect,
|
|
http.StatusConflict,
|
|
http.StatusNotFound,
|
|
http.StatusUnauthorized,
|
|
http.StatusForbidden,
|
|
http.StatusNotModified,
|
|
errors.StatusUnprocessableEntity,
|
|
http.StatusSwitchingProtocols,
|
|
),
|
|
)
|
|
}
|
|
defer logger.Log()
|
|
// Dispatch to the internal handler
|
|
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 *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
|
|
IsResourceRequest bool
|
|
// Path is the URL path of the request
|
|
Path string
|
|
// Verb is the kube verb associated with the request for API requests, not the http verb. This includes things like list and watch.
|
|
// for non-resource requests, this is the lowercase http verb
|
|
Verb string
|
|
|
|
APIPrefix string
|
|
APIGroup string
|
|
APIVersion string
|
|
Namespace string
|
|
// Resource is the name of the resource being requested. This is not the kind. For example: pods
|
|
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
|
|
// 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, always starting with /{resource}/{name}
|
|
Parts []string
|
|
}
|
|
|
|
type RequestInfoResolver struct {
|
|
APIPrefixes sets.String
|
|
GrouplessAPIPrefixes sets.String
|
|
}
|
|
|
|
// TODO write an integration test against the swagger doc to test the RequestInfo and match up behavior to responses
|
|
// GetRequestInfo returns the information from the http request. If error is not nil, RequestInfo holds the information as best it is known before the failure
|
|
// It handles both resource and non-resource requests and fills in all the pertinent information for each.
|
|
// Valid Inputs:
|
|
// Resource paths
|
|
// /apis/{api-group}/{version}/namespaces
|
|
// /api/{version}/namespaces
|
|
// /api/{version}/namespaces/{namespace}
|
|
// /api/{version}/namespaces/{namespace}/{resource}
|
|
// /api/{version}/namespaces/{namespace}/{resource}/{resourceName}
|
|
// /api/{version}/{resource}
|
|
// /api/{version}/{resource}/{resourceName}
|
|
//
|
|
// Special verbs without subresources:
|
|
// /api/{version}/proxy/{resource}/{resourceName}
|
|
// /api/{version}/proxy/namespaces/{namespace}/{resource}/{resourceName}
|
|
// /api/{version}/redirect/namespaces/{namespace}/{resource}/{resourceName}
|
|
// /api/{version}/redirect/{resource}/{resourceName}
|
|
//
|
|
// Special verbs with subresources:
|
|
// /api/{version}/watch/{resource}
|
|
// /api/{version}/watch/namespaces/{namespace}/{resource}
|
|
//
|
|
// NonResource paths
|
|
// /apis/{api-group}/{version}
|
|
// /apis/{api-group}
|
|
// /apis
|
|
// /api/{version}
|
|
// /api
|
|
// /healthz
|
|
// /
|
|
func (r *RequestInfoResolver) GetRequestInfo(req *http.Request) (RequestInfo, error) {
|
|
// start with a non-resource request until proven otherwise
|
|
requestInfo := RequestInfo{
|
|
IsResourceRequest: false,
|
|
Path: req.URL.Path,
|
|
Verb: strings.ToLower(req.Method),
|
|
}
|
|
|
|
currentParts := splitPath(req.URL.Path)
|
|
if len(currentParts) < 3 {
|
|
// return a non-resource request
|
|
return requestInfo, nil
|
|
}
|
|
|
|
if !r.APIPrefixes.Has(currentParts[0]) {
|
|
// return a non-resource request
|
|
return requestInfo, nil
|
|
}
|
|
requestInfo.APIPrefix = currentParts[0]
|
|
currentParts = currentParts[1:]
|
|
|
|
if !r.GrouplessAPIPrefixes.Has(requestInfo.APIPrefix) {
|
|
// one part (APIPrefix) has already been consumed, so this is actually "do we have four parts?"
|
|
if len(currentParts) < 3 {
|
|
// return a non-resource request
|
|
return requestInfo, nil
|
|
}
|
|
|
|
requestInfo.APIGroup = currentParts[0]
|
|
currentParts = currentParts[1:]
|
|
}
|
|
|
|
requestInfo.IsResourceRequest = true
|
|
requestInfo.APIVersion = currentParts[0]
|
|
currentParts = currentParts[1:]
|
|
|
|
// handle input of form /{specialVerb}/*
|
|
if specialVerbs.Has(currentParts[0]) {
|
|
if len(currentParts) < 2 {
|
|
return requestInfo, fmt.Errorf("unable to determine kind and namespace from url, %v", req.URL)
|
|
}
|
|
|
|
requestInfo.Verb = currentParts[0]
|
|
currentParts = currentParts[1:]
|
|
|
|
} else {
|
|
switch req.Method {
|
|
case "POST":
|
|
requestInfo.Verb = "create"
|
|
case "GET", "HEAD":
|
|
requestInfo.Verb = "get"
|
|
case "PUT":
|
|
requestInfo.Verb = "update"
|
|
case "PATCH":
|
|
requestInfo.Verb = "patch"
|
|
case "DELETE":
|
|
requestInfo.Verb = "delete"
|
|
default:
|
|
requestInfo.Verb = ""
|
|
}
|
|
}
|
|
|
|
// URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to be relative to kind
|
|
if currentParts[0] == "namespaces" {
|
|
if len(currentParts) > 1 {
|
|
requestInfo.Namespace = currentParts[1]
|
|
|
|
// if there is another step after the namespace name and it is not a known namespace subresource
|
|
// move currentParts to include it as a resource in its own right
|
|
if len(currentParts) > 2 && !namespaceSubresources.Has(currentParts[2]) {
|
|
currentParts = currentParts[2:]
|
|
}
|
|
}
|
|
} else {
|
|
requestInfo.Namespace = api.NamespaceNone
|
|
}
|
|
|
|
// parsing successful, so we now know the proper value for .Parts
|
|
requestInfo.Parts = currentParts
|
|
|
|
// parts look like: resource/resourceName/subresource/other/stuff/we/don't/interpret
|
|
switch {
|
|
case len(requestInfo.Parts) >= 3 && !specialVerbsNoSubresources.Has(requestInfo.Verb):
|
|
requestInfo.Subresource = requestInfo.Parts[2]
|
|
fallthrough
|
|
case len(requestInfo.Parts) >= 2:
|
|
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 len(requestInfo.Name) == 0 && requestInfo.Verb == "get" {
|
|
requestInfo.Verb = "list"
|
|
}
|
|
// if there's no name on the request and we thought it was a delete before, then the actual verb is deletecollection
|
|
if len(requestInfo.Name) == 0 && requestInfo.Verb == "delete" {
|
|
requestInfo.Verb = "deletecollection"
|
|
}
|
|
|
|
return requestInfo, nil
|
|
}
|