github.com/mmatczuk/gohan@v0.0.0-20170206152520-30e45d9bdb69/server/middleware/middleware.go (about) 1 // Copyright (C) 2015 NTT Innovation Institute, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 // implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 16 package middleware 17 18 import ( 19 "bytes" 20 "encoding/json" 21 "io/ioutil" 22 "net/http" 23 "regexp" 24 "strings" 25 "time" 26 27 "github.com/cloudwan/gohan/schema" 28 "github.com/go-martini/martini" 29 "github.com/rackspace/gophercloud" 30 ) 31 32 const webuiPATH = "/webui/" 33 34 type responseHijacker struct { 35 martini.ResponseWriter 36 Response *bytes.Buffer 37 } 38 39 func newResponseHijacker(rw martini.ResponseWriter) *responseHijacker { 40 return &responseHijacker{rw, bytes.NewBuffer(nil)} 41 } 42 43 func (rh *responseHijacker) Write(b []byte) (int, error) { 44 rh.Response.Write(b) 45 return rh.ResponseWriter.Write(b) 46 } 47 48 //Logging logs requests and responses 49 func Logging() martini.Handler { 50 return func(res http.ResponseWriter, req *http.Request, c martini.Context) { 51 if strings.HasPrefix(req.URL.Path, webuiPATH) { 52 c.Next() 53 return 54 } 55 start := time.Now() 56 57 addr := req.Header.Get("X-Real-IP") 58 if addr == "" { 59 addr = req.Header.Get("X-Forwarded-For") 60 if addr == "" { 61 addr = req.RemoteAddr 62 } 63 } 64 65 reqData, _ := ioutil.ReadAll(req.Body) 66 buff := ioutil.NopCloser(bytes.NewBuffer(reqData)) 67 req.Body = buff 68 69 log.Info("Started %s %s for client %s data: %s", 70 req.Method, req.URL.String(), addr, string(reqData)) 71 log.Debug("Request headers: %v", filterHeaders(req.Header)) 72 log.Debug("Request body: %s", string(reqData)) 73 74 rw := res.(martini.ResponseWriter) 75 rh := newResponseHijacker(rw) 76 c.MapTo(rh, (*http.ResponseWriter)(nil)) 77 c.MapTo(rh, (*martini.ResponseWriter)(nil)) 78 79 c.Next() 80 81 response, _ := ioutil.ReadAll(rh.Response) 82 log.Debug("Response headers: %v", rh.Header()) 83 log.Debug("Response body: %s", string(response)) 84 log.Info("Completed %v %s in %v", rw.Status(), http.StatusText(rw.Status()), time.Since(start)) 85 } 86 } 87 88 func filterHeaders(headers http.Header) http.Header { 89 filtered := http.Header{} 90 for k, v := range headers { 91 if k == "X-Auth-Token" { 92 filtered[k] = []string{"***"} 93 continue 94 } 95 filtered[k] = v 96 } 97 return filtered 98 } 99 100 //IdentityService for user authentication & authorization 101 type IdentityService interface { 102 GetTenantID(string) (string, error) 103 GetTenantName(string) (string, error) 104 VerifyToken(string) (schema.Authorization, error) 105 GetServiceAuthorization() (schema.Authorization, error) 106 GetClient() *gophercloud.ServiceClient 107 } 108 109 //NobodyResourceService contains a definition of nobody resources (that do not require authorization) 110 type NobodyResourceService interface { 111 VerifyResourcePath(string) bool 112 } 113 114 type DefaultNobodyResourceService struct { 115 resourcePathRegexes []*regexp.Regexp 116 } 117 118 func (nrs *DefaultNobodyResourceService) VerifyResourcePath(resourcePath string) bool { 119 for _, regex := range nrs.resourcePathRegexes { 120 if regex.MatchString(resourcePath) { 121 return true 122 } 123 } 124 return false 125 } 126 127 func NewNobodyResourceService(nobodyResourcePathRegexes []*regexp.Regexp) NobodyResourceService { 128 return &DefaultNobodyResourceService{resourcePathRegexes: nobodyResourcePathRegexes} 129 } 130 131 //NoIdentityService for disabled auth 132 type NoIdentityService struct { 133 } 134 135 //GetTenantID returns always admin 136 func (i *NoIdentityService) GetTenantID(string) (string, error) { 137 return "admin", nil 138 } 139 140 //GetTenantName returns always admin 141 func (i *NoIdentityService) GetTenantName(string) (string, error) { 142 return "admin", nil 143 } 144 145 //VerifyToken returns always authorization for admin 146 func (i *NoIdentityService) VerifyToken(string) (schema.Authorization, error) { 147 return schema.NewAuthorization("admin", "admin", "admin_token", []string{"admin"}, nil), nil 148 } 149 150 //GetServiceAuthorization returns always authorization for admin 151 func (i *NoIdentityService) GetServiceAuthorization() (schema.Authorization, error) { 152 return schema.NewAuthorization("admin", "admin", "admin_token", []string{"admin"}, nil), nil 153 } 154 155 //GetClient returns always nil 156 func (i *NoIdentityService) GetClient() *gophercloud.ServiceClient { 157 return nil 158 } 159 160 //NobodyIdentityService for nobody auth 161 type NobodyIdentityService struct { 162 } 163 164 //GetTenantID returns always nobody 165 func (i *NobodyIdentityService) GetTenantID(string) (string, error) { 166 return "nobody", nil 167 } 168 169 //GetTenantName returns always nobody 170 func (i *NobodyIdentityService) GetTenantName(string) (string, error) { 171 return "nobody", nil 172 } 173 174 //VerifyToken returns always authorization for nobody 175 func (i *NobodyIdentityService) VerifyToken(string) (schema.Authorization, error) { 176 return schema.NewAuthorization("nobody", "nobody", "nobody_token", []string{"Nobody"}, nil), nil 177 } 178 179 //GetServiceAuthorization returns always authorization for nobody 180 func (i *NobodyIdentityService) GetServiceAuthorization() (schema.Authorization, error) { 181 return schema.NewAuthorization("nobody", "nobody", "nobody_token", []string{"Nobody"}, nil), nil 182 } 183 184 //GetClient returns always nil 185 func (i *NobodyIdentityService) GetClient() *gophercloud.ServiceClient { 186 return nil 187 } 188 189 //HTTPJSONError helper for returning JSON errors 190 func HTTPJSONError(res http.ResponseWriter, err string, code int) { 191 errorMessage := "" 192 if code == http.StatusInternalServerError { 193 log.Error(err) 194 } else { 195 errorMessage = err 196 log.Notice(err) 197 } 198 response := map[string]interface{}{"error": errorMessage} 199 responseJSON, _ := json.Marshal(response) 200 http.Error(res, string(responseJSON), code) 201 } 202 203 //Authentication authenticates user using keystone 204 func Authentication() martini.Handler { 205 return func(res http.ResponseWriter, req *http.Request, identityService IdentityService, nobodyResourceService NobodyResourceService, c martini.Context) { 206 if req.Method == "OPTIONS" { 207 c.Next() 208 return 209 } 210 //TODO(nati) make this configurable 211 if strings.HasPrefix(req.URL.Path, webuiPATH) { 212 c.Next() 213 return 214 } 215 216 if req.URL.Path == "/" || req.URL.Path == "/webui" { 217 http.Redirect(res, req, webuiPATH, http.StatusTemporaryRedirect) 218 return 219 } 220 221 if req.URL.Path == "/v2.0/tokens" { 222 c.Next() 223 return 224 } 225 226 authToken := req.Header.Get("X-Auth-Token") 227 228 var targetIdentityService IdentityService 229 230 if authToken == "" { 231 if nobodyResourceService.VerifyResourcePath(req.URL.Path) { 232 targetIdentityService = &NobodyIdentityService{} 233 } else { 234 HTTPJSONError(res, "No X-Auth-Token", http.StatusUnauthorized) 235 return 236 } 237 } else { 238 targetIdentityService = identityService 239 } 240 241 auth, err := targetIdentityService.VerifyToken(authToken) 242 243 if err != nil { 244 HTTPJSONError(res, err.Error(), http.StatusUnauthorized) 245 return 246 } 247 248 c.Map(auth) 249 c.Next() 250 } 251 } 252 253 //Context type 254 type Context map[string]interface{} 255 256 //WithContext injects new empty context object 257 func WithContext() martini.Handler { 258 return func(c martini.Context) { 259 c.Map(Context{}) 260 } 261 } 262 263 //Authorization checks user permissions against policy 264 func Authorization(action string) martini.Handler { 265 return func(res http.ResponseWriter, req *http.Request, auth schema.Authorization, context Context) { 266 context["tenant_id"] = auth.TenantID() 267 context["tenant_name"] = auth.TenantName() 268 context["auth_token"] = auth.AuthToken() 269 context["catalog"] = auth.Catalog() 270 context["auth"] = auth 271 } 272 } 273 274 // JSONURLs strips ".json" suffixes added to URLs 275 func JSONURLs() martini.Handler { 276 return func(res http.ResponseWriter, req *http.Request, c martini.Context) { 277 if !strings.Contains(req.URL.Path, "gohan") && !strings.Contains(req.URL.Path, "webui") { 278 req.URL.Path = strings.TrimSuffix(req.URL.Path, ".json") 279 } 280 c.Next() 281 } 282 }