github.com/zhouyu0/docker-note@v0.0.0-20190722021225-b8d3825084db/pkg/authorization/authz.go (about)

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