dotcloud/docker

View on GitHub
pkg/authorization/authz.go

Summary

Maintainability
A
2 hrs
Test Coverage
package authorization // import "github.com/docker/docker/pkg/authorization"

import (
    "bufio"
    "bytes"
    "context"
    "fmt"
    "io"
    "mime"
    "net/http"
    "net/url"
    "regexp"
    "strings"

    "github.com/containerd/log"
    "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/reference/api/engine/
//
// 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
}

func isChunked(r *http.Request) bool {
    // RFC 7230 specifies that content length is to be ignored if Transfer-Encoding is chunked
    if strings.EqualFold(r.Header.Get("Transfer-Encoding"), "chunked") {
        return true
    }
    for _, v := range r.TransferEncoding {
        if strings.EqualFold(v, "chunked") {
            return true
        }
    }
    return false
}

// AuthZRequest authorized the request to the docker daemon using authZ plugins
func (ctx *Ctx) AuthZRequest(w http.ResponseWriter, r *http.Request) error {
    var body []byte
    if sendBody(ctx.requestURI, r.Header) && (r.ContentLength > 0 || isChunked(r)) && 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),
    }

    if r.TLS != nil {
        for _, c := range r.TLS.PeerCertificates {
            pc := PeerCertificate(*c)
            ctx.authReq.RequestPeerCertificates = append(ctx.authReq.RequestPeerCertificates, &pc)
        }
    }

    for _, plugin := range ctx.plugins {
        log.G(context.TODO()).Debugf("AuthZ request using plugin %s", plugin.Name())

        authRes, err := plugin.AuthZRequest(ctx.authReq)
        if err != nil {
            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
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 _, plugin := range ctx.plugins {
        log.G(context.TODO()).Debugf("AuthZ response using plugin %s", plugin.Name())

        authRes, err := plugin.AuthZResponse(ctx.authReq)
        if err != nil {
            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 {
        log.G(context.TODO()).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
}

func isAuthEndpoint(urlPath string) (bool, error) {
    // eg www.test.com/v1.24/auth/optional?optional1=something&optional2=something (version optional)
    matched, err := regexp.MatchString(`^[^\/]*\/(v\d[\d\.]*\/)?auth.*`, urlPath)
    if err != nil {
        return false, err
    }
    return matched, nil
}

// sendBody returns true when request/response body should be sent to AuthZPlugin
func sendBody(inURL string, header http.Header) bool {
    u, err := url.Parse(inURL)
    // Assume no if the URL cannot be parsed - an empty request will still be forwarded to the plugin and should be rejected
    if err != nil {
        return false
    }

    // Skip body for auth endpoint
    isAuth, err := isAuthEndpoint(u.Path)
    if isAuth || err != nil {
        return false
    }

    // body is sent only for text or json messages
    contentType, _, err := mime.ParseMediaType(header.Get("Content-Type"))
    if err != nil {
        return false
    }

    return contentType == "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)
    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
}

func (authorizationError) Forbidden() {}

func newAuthorizationError(plugin, msg string) authorizationError {
    return authorizationError{error: fmt.Errorf("authorization denied by plugin %s: %s", plugin, msg)}
}