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 }