github.com/kim0/docker@v0.6.2-0.20161130212042-4addda3f07e7/pkg/authorization/authz.go (about)

     1  package authorization
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"strings"
    10  
    11  	"github.com/Sirupsen/logrus"
    12  	"github.com/docker/docker/pkg/ioutils"
    13  )
    14  
    15  const maxBodySize = 1048576 // 1MB
    16  
    17  // NewCtx creates new authZ context, it is used to store authorization information related to a specific docker
    18  // REST http session
    19  // A context provides two method:
    20  // Authenticate Request:
    21  // Call authZ plugins with current REST request and AuthN response
    22  // Request contains full HTTP packet sent to the docker daemon
    23  // https://docs.docker.com/engine/reference/api/docker_remote_api/
    24  //
    25  // Authenticate Response:
    26  // Call authZ plugins with full info about current REST request, REST response and AuthN response
    27  // The response from this method may contains content that overrides the daemon response
    28  // This allows authZ plugins to filter privileged content
    29  //
    30  // If multiple authZ plugins are specified, the block/allow decision is based on ANDing all plugin results
    31  // For response manipulation, the response from each plugin is piped between plugins. Plugin execution order
    32  // is determined according to daemon parameters
    33  func NewCtx(authZPlugins []Plugin, user, userAuthNMethod, requestMethod, requestURI string) *Ctx {
    34  	return &Ctx{
    35  		plugins:         authZPlugins,
    36  		user:            user,
    37  		userAuthNMethod: userAuthNMethod,
    38  		requestMethod:   requestMethod,
    39  		requestURI:      requestURI,
    40  	}
    41  }
    42  
    43  // Ctx stores a single request-response interaction context
    44  type Ctx struct {
    45  	user            string
    46  	userAuthNMethod string
    47  	requestMethod   string
    48  	requestURI      string
    49  	plugins         []Plugin
    50  	// authReq stores the cached request object for the current transaction
    51  	authReq *Request
    52  }
    53  
    54  // AuthZRequest authorized the request to the docker daemon using authZ plugins
    55  // Side effect: If the authz plugin is invalid, then update ctx.plugins, so that
    56  // the caller(middleware) can update its list and stop retrying with invalid plugins.
    57  func (ctx *Ctx) AuthZRequest(w http.ResponseWriter, r *http.Request) error {
    58  	var body []byte
    59  	if sendBody(ctx.requestURI, r.Header) && r.ContentLength > 0 && r.ContentLength < maxBodySize {
    60  		var err error
    61  		body, r.Body, err = drainBody(r.Body)
    62  		if err != nil {
    63  			return err
    64  		}
    65  	}
    66  
    67  	var h bytes.Buffer
    68  	if err := r.Header.Write(&h); err != nil {
    69  		return err
    70  	}
    71  
    72  	ctx.authReq = &Request{
    73  		User:            ctx.user,
    74  		UserAuthNMethod: ctx.userAuthNMethod,
    75  		RequestMethod:   ctx.requestMethod,
    76  		RequestURI:      ctx.requestURI,
    77  		RequestBody:     body,
    78  		RequestHeaders:  headers(r.Header),
    79  	}
    80  
    81  	for i, plugin := range ctx.plugins {
    82  		logrus.Debugf("AuthZ request using plugin %s", plugin.Name())
    83  
    84  		authRes, err := plugin.AuthZRequest(ctx.authReq)
    85  		if err != nil {
    86  			if err == ErrInvalidPlugin {
    87  				ctx.plugins = append(ctx.plugins[:i], ctx.plugins[i+1:]...)
    88  			}
    89  			return fmt.Errorf("plugin %s failed with error: %s", plugin.Name(), err)
    90  		}
    91  
    92  		if !authRes.Allow {
    93  			return newAuthorizationError(plugin.Name(), authRes.Msg)
    94  		}
    95  	}
    96  
    97  	return nil
    98  }
    99  
   100  // AuthZResponse authorized and manipulates the response from docker daemon using authZ plugins
   101  // Side effect: If the authz plugin is invalid, then update ctx.plugins, so that
   102  // the caller(middleware) can update its list and stop retrying with invalid plugins.
   103  func (ctx *Ctx) AuthZResponse(rm ResponseModifier, r *http.Request) error {
   104  	ctx.authReq.ResponseStatusCode = rm.StatusCode()
   105  	ctx.authReq.ResponseHeaders = headers(rm.Header())
   106  
   107  	if sendBody(ctx.requestURI, rm.Header()) {
   108  		ctx.authReq.ResponseBody = rm.RawBody()
   109  	}
   110  
   111  	for i, plugin := range ctx.plugins {
   112  		logrus.Debugf("AuthZ response using plugin %s", plugin.Name())
   113  
   114  		authRes, err := plugin.AuthZResponse(ctx.authReq)
   115  		if err != nil {
   116  			if err == ErrInvalidPlugin {
   117  				ctx.plugins = append(ctx.plugins[:i], ctx.plugins[i+1:]...)
   118  			}
   119  			return fmt.Errorf("plugin %s failed with error: %s", plugin.Name(), err)
   120  		}
   121  
   122  		if !authRes.Allow {
   123  			return newAuthorizationError(plugin.Name(), authRes.Msg)
   124  		}
   125  	}
   126  
   127  	rm.FlushAll()
   128  
   129  	return nil
   130  }
   131  
   132  // drainBody dump the body (if its length is less than 1MB) without modifying the request state
   133  func drainBody(body io.ReadCloser) ([]byte, io.ReadCloser, error) {
   134  	bufReader := bufio.NewReaderSize(body, maxBodySize)
   135  	newBody := ioutils.NewReadCloserWrapper(bufReader, func() error { return body.Close() })
   136  
   137  	data, err := bufReader.Peek(maxBodySize)
   138  	// Body size exceeds max body size
   139  	if err == nil {
   140  		logrus.Warnf("Request body is larger than: '%d' skipping body", maxBodySize)
   141  		return nil, newBody, nil
   142  	}
   143  	// Body size is less than maximum size
   144  	if err == io.EOF {
   145  		return data, newBody, nil
   146  	}
   147  	// Unknown error
   148  	return nil, newBody, err
   149  }
   150  
   151  // sendBody returns true when request/response body should be sent to AuthZPlugin
   152  func sendBody(url string, header http.Header) bool {
   153  	// Skip body for auth endpoint
   154  	if strings.HasSuffix(url, "/auth") {
   155  		return false
   156  	}
   157  
   158  	// body is sent only for text or json messages
   159  	return header.Get("Content-Type") == "application/json"
   160  }
   161  
   162  // headers returns flatten version of the http headers excluding authorization
   163  func headers(header http.Header) map[string]string {
   164  	v := make(map[string]string, 0)
   165  	for k, values := range header {
   166  		// Skip authorization headers
   167  		if strings.EqualFold(k, "Authorization") || strings.EqualFold(k, "X-Registry-Config") || strings.EqualFold(k, "X-Registry-Auth") {
   168  			continue
   169  		}
   170  		for _, val := range values {
   171  			v[k] = val
   172  		}
   173  	}
   174  	return v
   175  }
   176  
   177  // authorizationError represents an authorization deny error
   178  type authorizationError struct {
   179  	error
   180  }
   181  
   182  // HTTPErrorStatusCode returns the authorization error status code (forbidden)
   183  func (e authorizationError) HTTPErrorStatusCode() int {
   184  	return http.StatusForbidden
   185  }
   186  
   187  func newAuthorizationError(plugin, msg string) authorizationError {
   188  	return authorizationError{error: fmt.Errorf("authorization denied by plugin %s: %s", plugin, msg)}
   189  }