mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
fae904af02
When daemon fails to load an authz plugin, it should be removed from the plugin list. Else the plugin is retried on every request and response, resulting in undesired behavior (eg. daemon panic) Signed-off-by: Anusha Ragunathan <anusha@docker.com>
189 lines
5.7 KiB
Go
189 lines
5.7 KiB
Go
package authorization
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
"github.com/docker/docker/pkg/ioutils"
|
|
)
|
|
|
|
const maxBodySize = 1048576 // 1MB
|
|
|
|
// NewCtx creates new authZ context, it is used to store authorization information related to a specific docker
|
|
// REST http session
|
|
// A context provides two method:
|
|
// Authenticate Request:
|
|
// Call authZ plugins with current REST request and AuthN response
|
|
// Request contains full HTTP packet sent to the docker daemon
|
|
// https://docs.docker.com/engine/reference/api/docker_remote_api/
|
|
//
|
|
// Authenticate Response:
|
|
// Call authZ plugins with full info about current REST request, REST response and AuthN response
|
|
// The response from this method may contains content that overrides the daemon response
|
|
// This allows authZ plugins to filter privileged content
|
|
//
|
|
// If multiple authZ plugins are specified, the block/allow decision is based on ANDing all plugin results
|
|
// For response manipulation, the response from each plugin is piped between plugins. Plugin execution order
|
|
// is determined according to daemon parameters
|
|
func NewCtx(authZPlugins []Plugin, user, userAuthNMethod, requestMethod, requestURI string) *Ctx {
|
|
return &Ctx{
|
|
plugins: authZPlugins,
|
|
user: user,
|
|
userAuthNMethod: userAuthNMethod,
|
|
requestMethod: requestMethod,
|
|
requestURI: requestURI,
|
|
}
|
|
}
|
|
|
|
// Ctx stores a single request-response interaction context
|
|
type Ctx struct {
|
|
user string
|
|
userAuthNMethod string
|
|
requestMethod string
|
|
requestURI string
|
|
plugins []Plugin
|
|
// authReq stores the cached request object for the current transaction
|
|
authReq *Request
|
|
}
|
|
|
|
// AuthZRequest authorized the request to the docker daemon using authZ plugins
|
|
// Side effect: If the authz plugin is invalid, then update ctx.plugins, so that
|
|
// the caller(middleware) can update its list and stop retrying with invalid plugins.
|
|
func (ctx *Ctx) AuthZRequest(w http.ResponseWriter, r *http.Request) error {
|
|
var body []byte
|
|
if sendBody(ctx.requestURI, r.Header) && r.ContentLength > 0 && r.ContentLength < maxBodySize {
|
|
var err error
|
|
body, r.Body, err = drainBody(r.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var h bytes.Buffer
|
|
if err := r.Header.Write(&h); err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx.authReq = &Request{
|
|
User: ctx.user,
|
|
UserAuthNMethod: ctx.userAuthNMethod,
|
|
RequestMethod: ctx.requestMethod,
|
|
RequestURI: ctx.requestURI,
|
|
RequestBody: body,
|
|
RequestHeaders: headers(r.Header),
|
|
}
|
|
|
|
for i, plugin := range ctx.plugins {
|
|
logrus.Debugf("AuthZ request using plugin %s", plugin.Name())
|
|
|
|
authRes, err := plugin.AuthZRequest(ctx.authReq)
|
|
if err != nil {
|
|
if err == ErrInvalidPlugin {
|
|
ctx.plugins = append(ctx.plugins[:i], ctx.plugins[i+1:]...)
|
|
}
|
|
return fmt.Errorf("plugin %s failed with error: %s", plugin.Name(), err)
|
|
}
|
|
|
|
if !authRes.Allow {
|
|
return newAuthorizationError(plugin.Name(), authRes.Msg)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AuthZResponse authorized and manipulates the response from docker daemon using authZ plugins
|
|
// Side effect: If the authz plugin is invalid, then update ctx.plugins, so that
|
|
// the caller(middleware) can update its list and stop retrying with invalid plugins.
|
|
func (ctx *Ctx) AuthZResponse(rm ResponseModifier, r *http.Request) error {
|
|
ctx.authReq.ResponseStatusCode = rm.StatusCode()
|
|
ctx.authReq.ResponseHeaders = headers(rm.Header())
|
|
|
|
if sendBody(ctx.requestURI, rm.Header()) {
|
|
ctx.authReq.ResponseBody = rm.RawBody()
|
|
}
|
|
|
|
for i, plugin := range ctx.plugins {
|
|
logrus.Debugf("AuthZ response using plugin %s", plugin.Name())
|
|
|
|
authRes, err := plugin.AuthZResponse(ctx.authReq)
|
|
if err != nil {
|
|
if err == ErrInvalidPlugin {
|
|
ctx.plugins = append(ctx.plugins[:i], ctx.plugins[i+1:]...)
|
|
}
|
|
return fmt.Errorf("plugin %s failed with error: %s", plugin.Name(), err)
|
|
}
|
|
|
|
if !authRes.Allow {
|
|
return newAuthorizationError(plugin.Name(), authRes.Msg)
|
|
}
|
|
}
|
|
|
|
rm.FlushAll()
|
|
|
|
return nil
|
|
}
|
|
|
|
// drainBody dump the body (if its length is less than 1MB) without modifying the request state
|
|
func drainBody(body io.ReadCloser) ([]byte, io.ReadCloser, error) {
|
|
bufReader := bufio.NewReaderSize(body, maxBodySize)
|
|
newBody := ioutils.NewReadCloserWrapper(bufReader, func() error { return body.Close() })
|
|
|
|
data, err := bufReader.Peek(maxBodySize)
|
|
// Body size exceeds max body size
|
|
if err == nil {
|
|
logrus.Warnf("Request body is larger than: '%d' skipping body", maxBodySize)
|
|
return nil, newBody, nil
|
|
}
|
|
// Body size is less than maximum size
|
|
if err == io.EOF {
|
|
return data, newBody, nil
|
|
}
|
|
// Unknown error
|
|
return nil, newBody, err
|
|
}
|
|
|
|
// sendBody returns true when request/response body should be sent to AuthZPlugin
|
|
func sendBody(url string, header http.Header) bool {
|
|
// Skip body for auth endpoint
|
|
if strings.HasSuffix(url, "/auth") {
|
|
return false
|
|
}
|
|
|
|
// body is sent only for text or json messages
|
|
return header.Get("Content-Type") == "application/json"
|
|
}
|
|
|
|
// headers returns flatten version of the http headers excluding authorization
|
|
func headers(header http.Header) map[string]string {
|
|
v := make(map[string]string, 0)
|
|
for k, values := range header {
|
|
// Skip authorization headers
|
|
if strings.EqualFold(k, "Authorization") || strings.EqualFold(k, "X-Registry-Config") || strings.EqualFold(k, "X-Registry-Auth") {
|
|
continue
|
|
}
|
|
for _, val := range values {
|
|
v[k] = val
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
// authorizationError represents an authorization deny error
|
|
type authorizationError struct {
|
|
error
|
|
}
|
|
|
|
// HTTPErrorStatusCode returns the authorization error status code (forbidden)
|
|
func (e authorizationError) HTTPErrorStatusCode() int {
|
|
return http.StatusForbidden
|
|
}
|
|
|
|
func newAuthorizationError(plugin, msg string) authorizationError {
|
|
return authorizationError{error: fmt.Errorf("authorization denied by plugin %s: %s", plugin, msg)}
|
|
}
|