github.com/demonoid81/moby@v0.0.0-20200517203328-62dd8e17c460/pkg/authorization/authz.go (about)

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