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 }