github.com/jbking/gohan@v0.0.0-20151217002006-b41ccf1c2a96/server/api.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 server 17 18 import ( 19 "fmt" 20 "net/http" 21 "strconv" 22 23 "github.com/cloudwan/gohan/db" 24 "github.com/cloudwan/gohan/extension" 25 "github.com/cloudwan/gohan/schema" 26 "github.com/cloudwan/gohan/server/middleware" 27 "github.com/cloudwan/gohan/server/resources" 28 "github.com/cloudwan/gohan/sync" 29 "github.com/drone/routes" 30 "github.com/go-martini/martini" 31 ) 32 33 var ( 34 exceptionObjectDoestNotContainKeyError = "Exception obejct does not contrain '%s'" 35 exceptionPropertyIsNotExpectedTypeError = "Exception property '%s' is not '%s'" 36 ) 37 38 func authorization(w http.ResponseWriter, r *http.Request, action, path string, s *schema.Schema, auth schema.Authorization) (*schema.Policy, *schema.Role) { 39 manager := schema.GetManager() 40 log.Debug("[authorization*] %s %v", action, auth) 41 if auth == nil { 42 return schema.NewEmptyPolicy(), nil 43 } 44 policy, role := manager.PolicyValidate(action, path, auth) 45 if policy == nil { 46 log.Debug("No maching policy: %s %s", action, path) 47 return nil, nil 48 } 49 return policy, role 50 } 51 52 func addParamToQuery(r *http.Request, key, value string) { 53 r.URL.RawQuery += "&" + key + "=" + value 54 } 55 56 func addJSONContentTypeHeader(w http.ResponseWriter) { 57 w.Header().Add("Content-Type", "application/json") 58 } 59 60 func removeResourceWrapper(s *schema.Schema, dataMap map[string]interface{}) map[string]interface{} { 61 if innerData, ok := dataMap[s.Singular]; ok { 62 if innerDataMap, ok := innerData.(map[string]interface{}); ok { 63 return innerDataMap 64 } 65 } 66 67 return dataMap 68 } 69 70 func problemToResponseCode(problem resources.ResourceProblem) int { 71 switch problem { 72 case resources.InternalServerError: 73 return http.StatusInternalServerError 74 case resources.WrongQuery, resources.WrongData: 75 return http.StatusBadRequest 76 case resources.NotFound: 77 return http.StatusNotFound 78 case resources.DeleteFailed, resources.CreateFailed, resources.UpdateFailed: 79 return http.StatusConflict 80 case resources.Unauthorized: 81 return http.StatusUnauthorized 82 } 83 return http.StatusInternalServerError 84 } 85 86 func unwrapExtensionException(exceptionInfo map[string]interface{}) (message map[string]interface{}, code int) { 87 messageRaw, ok := exceptionInfo["message"] 88 if !ok { 89 return map[string]interface{}{"error": fmt.Sprintf(exceptionObjectDoestNotContainKeyError, "message")}, http.StatusInternalServerError 90 } 91 nameRaw, ok := exceptionInfo["name"] 92 if !ok { 93 return map[string]interface{}{"error": fmt.Sprintf(exceptionObjectDoestNotContainKeyError, "name")}, http.StatusInternalServerError 94 } 95 name, ok := nameRaw.(string) 96 if !ok { 97 return map[string]interface{}{"error": fmt.Sprintf(exceptionPropertyIsNotExpectedTypeError, "name", "string")}, http.StatusInternalServerError 98 } 99 code = 400 100 var err error 101 switch name { 102 case "CustomException": 103 codeRaw, ok := exceptionInfo["code"] 104 if !ok { 105 return map[string]interface{}{"error": fmt.Sprintf(exceptionObjectDoestNotContainKeyError, "code")}, http.StatusInternalServerError 106 } 107 code, err = strconv.Atoi(fmt.Sprint(codeRaw)) 108 if err != nil { 109 return map[string]interface{}{"error": fmt.Sprintf(exceptionPropertyIsNotExpectedTypeError, "code", "int")}, http.StatusInternalServerError 110 } 111 case "ResourceException": 112 problemRaw, ok := exceptionInfo["problem"] 113 if !ok { 114 return map[string]interface{}{"error": fmt.Sprintf(exceptionObjectDoestNotContainKeyError, "problem")}, http.StatusInternalServerError 115 } 116 problem, err := strconv.Atoi(fmt.Sprint(problemRaw)) 117 if err != nil { 118 return map[string]interface{}{"error": fmt.Sprintf(exceptionPropertyIsNotExpectedTypeError, "problem", "int")}, http.StatusInternalServerError 119 } 120 code = problemToResponseCode(resources.ResourceProblem(problem)) 121 case "ExtensionException": 122 innerExceptionInfoRaw, ok := exceptionInfo["inner_exception"] 123 if !ok { 124 return map[string]interface{}{"error": fmt.Sprintf(exceptionObjectDoestNotContainKeyError, "inner_exception")}, http.StatusInternalServerError 125 } 126 innerExceptionInfo, ok := innerExceptionInfoRaw.(map[string]interface{}) 127 if !ok { 128 return map[string]interface{}{"error": fmt.Sprintf(exceptionPropertyIsNotExpectedTypeError, "inner_exception", "map[string]interface{}")}, http.StatusInternalServerError 129 } 130 _, code = unwrapExtensionException(innerExceptionInfo) 131 } 132 if 200 <= code && code < 300 { 133 message, ok = messageRaw.(map[string]interface{}) 134 if !ok { 135 return map[string]interface{}{"error": fmt.Sprintf(exceptionPropertyIsNotExpectedTypeError, "message", "map[string]interface{}")}, http.StatusInternalServerError 136 } 137 } else { 138 message = map[string]interface{}{"error": fmt.Sprintf("%v", messageRaw)} 139 } 140 return message, code 141 } 142 143 func handleError(writer http.ResponseWriter, err error) { 144 switch err := err.(type) { 145 default: 146 middleware.HTTPJSONError(writer, err.Error(), http.StatusInternalServerError) 147 case resources.ResourceError: 148 code := problemToResponseCode(err.Problem) 149 middleware.HTTPJSONError(writer, err.Message, code) 150 case extension.Error: 151 message, code := unwrapExtensionException(err.ExceptionInfo) 152 if 200 <= code && code < 300 { 153 writer.WriteHeader(code) 154 routes.ServeJson(writer, message) 155 } else { 156 middleware.HTTPJSONError(writer, message["error"].(string), code) 157 } 158 } 159 } 160 161 func fillInContext(context middleware.Context, r *http.Request, w http.ResponseWriter, s *schema.Schema, sync sync.Sync, identityService middleware.IdentityService) { 162 context["path"] = r.URL.Path 163 context["http_request"] = r 164 context["http_response"] = w 165 context["schema"] = s 166 context["sync"] = sync 167 context["identity_service"] = identityService 168 context["service_auth"], _ = identityService.GetServiceAuthorization() 169 } 170 171 //MapRouteBySchema setup api route by schema 172 func MapRouteBySchema(server *Server, dataStore db.DB, s *schema.Schema) { 173 174 route := server.martini 175 manager := schema.GetManager() 176 177 singleURL := s.GetSingleURL() 178 pluralURL := s.GetPluralURL() 179 singleURLWithParents := s.GetSingleURLWithParents() 180 pluralURLWithParents := s.GetPluralURLWithParents() 181 182 //load extension environments 183 environmentManager := extension.GetManager() 184 if _, ok := environmentManager.GetEnvironment(s.ID); !ok { 185 env := newEnvironment(server.db, server.keystoneIdentity) 186 err := env.LoadExtensionsForPath(manager.Extensions, pluralURL) 187 if err != nil { 188 log.Fatal(fmt.Sprintf("Extensions parsing error: %v", err)) 189 } 190 environmentManager.RegisterEnvironment(s.ID, env) 191 } 192 193 log.Debug("[Plural Path] %s", pluralURL) 194 log.Debug("[Singular Path] %s", singleURL) 195 log.Debug("[Plural Path With Parents] %s", pluralURLWithParents) 196 log.Debug("[Singular Path With Parents] %s", singleURLWithParents) 197 198 //setup list route 199 getPluralFunc := func(w http.ResponseWriter, r *http.Request, p martini.Params, identityService middleware.IdentityService, context middleware.Context) { 200 addJSONContentTypeHeader(w) 201 fillInContext(context, r, w, s, server.sync, identityService) 202 if err := resources.GetMultipleResources(context, dataStore, s, r.URL.Query()); err != nil { 203 handleError(w, err) 204 return 205 } 206 w.Header().Add("X-Total-Count", fmt.Sprint(context["total"])) 207 routes.ServeJson(w, context["response"]) 208 } 209 route.Get(pluralURL, middleware.Authorization(schema.ActionRead), getPluralFunc) 210 route.Get(pluralURLWithParents, middleware.Authorization(schema.ActionRead), func(w http.ResponseWriter, r *http.Request, p martini.Params, identityService middleware.IdentityService, context middleware.Context) { 211 addParamToQuery(r, schema.FormatParentID(s.Parent), p[s.Parent]) 212 getPluralFunc(w, r, p, identityService, context) 213 }) 214 215 //setup show route 216 getSingleFunc := func(w http.ResponseWriter, r *http.Request, p martini.Params, identityService middleware.IdentityService, context middleware.Context) { 217 addJSONContentTypeHeader(w) 218 fillInContext(context, r, w, s, server.sync, identityService) 219 id := p["id"] 220 if err := resources.GetSingleResource(context, dataStore, s, id); err != nil { 221 handleError(w, err) 222 return 223 } 224 routes.ServeJson(w, context["response"]) 225 } 226 route.Get(singleURL, middleware.Authorization(schema.ActionRead), getSingleFunc) 227 route.Get(singleURLWithParents, middleware.Authorization(schema.ActionRead), func(w http.ResponseWriter, r *http.Request, p martini.Params, identityService middleware.IdentityService, context middleware.Context) { 228 addParamToQuery(r, schema.FormatParentID(s.Parent), p[s.Parent]) 229 getSingleFunc(w, r, p, identityService, context) 230 }) 231 232 //setup delete route 233 deleteSingleFunc := func(w http.ResponseWriter, r *http.Request, p martini.Params, identityService middleware.IdentityService, context middleware.Context) { 234 addJSONContentTypeHeader(w) 235 fillInContext(context, r, w, s, server.sync, identityService) 236 id := p["id"] 237 if err := resources.DeleteResource(context, dataStore, s, id); err != nil { 238 handleError(w, err) 239 return 240 } 241 w.WriteHeader(http.StatusNoContent) 242 } 243 route.Delete(singleURL, middleware.Authorization(schema.ActionDelete), deleteSingleFunc) 244 route.Delete(singleURLWithParents, middleware.Authorization(schema.ActionDelete), func(w http.ResponseWriter, r *http.Request, p martini.Params, identityService middleware.IdentityService, context middleware.Context) { 245 addParamToQuery(r, schema.FormatParentID(s.Parent), p[s.Parent]) 246 deleteSingleFunc(w, r, p, identityService, context) 247 }) 248 249 //setup create route 250 postPluralFunc := func(w http.ResponseWriter, r *http.Request, p martini.Params, identityService middleware.IdentityService, context middleware.Context) { 251 addJSONContentTypeHeader(w) 252 fillInContext(context, r, w, s, server.sync, identityService) 253 dataMap, err := middleware.ReadJSON(r) 254 if err != nil { 255 handleError(w, resources.NewResourceError(err, fmt.Sprintf("Failed to parse data: %s", err), resources.WrongData)) 256 return 257 } 258 dataMap = removeResourceWrapper(s, dataMap) 259 if s.Parent != "" { 260 if _, ok := dataMap[s.ParentID()]; !ok { 261 queryParams := r.URL.Query() 262 parentIDParam := queryParams.Get(s.ParentID()) 263 if parentIDParam != "" { 264 dataMap[s.ParentID()] = parentIDParam 265 } 266 } 267 } 268 if err := resources.CreateResource(context, dataStore, identityService, s, dataMap); err != nil { 269 handleError(w, err) 270 return 271 } 272 w.WriteHeader(http.StatusCreated) 273 routes.ServeJson(w, context["response"]) 274 } 275 route.Post(pluralURL, middleware.Authorization(schema.ActionCreate), postPluralFunc) 276 route.Post(pluralURLWithParents, middleware.Authorization(schema.ActionCreate), 277 func(w http.ResponseWriter, r *http.Request, p martini.Params, identityService middleware.IdentityService, context middleware.Context) { 278 addParamToQuery(r, schema.FormatParentID(s.Parent), p[s.Parent]) 279 postPluralFunc(w, r, p, identityService, context) 280 }) 281 282 //setup update route 283 putSingleFunc := func(w http.ResponseWriter, r *http.Request, p martini.Params, identityService middleware.IdentityService, context middleware.Context) { 284 addJSONContentTypeHeader(w) 285 fillInContext(context, r, w, s, server.sync, identityService) 286 id := p["id"] 287 dataMap, err := middleware.ReadJSON(r) 288 if err != nil { 289 handleError(w, resources.NewResourceError(err, fmt.Sprintf("Failed to parse data: %s", err), resources.WrongData)) 290 return 291 } 292 dataMap = removeResourceWrapper(s, dataMap) 293 if err := resources.UpdateResource( 294 context, dataStore, identityService, s, id, dataMap); err != nil { 295 handleError(w, err) 296 return 297 } 298 routes.ServeJson(w, context["response"]) 299 } 300 route.Put(singleURL, middleware.Authorization(schema.ActionUpdate), putSingleFunc) 301 route.Put(singleURLWithParents, middleware.Authorization(schema.ActionUpdate), 302 func(w http.ResponseWriter, r *http.Request, p martini.Params, identityService middleware.IdentityService, context middleware.Context) { 303 addParamToQuery(r, schema.FormatParentID(s.Parent), p[s.Parent]) 304 putSingleFunc(w, r, p, identityService, context) 305 }) 306 307 route.Patch(singleURL, middleware.Authorization(schema.ActionUpdate), putSingleFunc) 308 route.Patch(singleURLWithParents, middleware.Authorization(schema.ActionUpdate), 309 func(w http.ResponseWriter, r *http.Request, p martini.Params, identityService middleware.IdentityService, context middleware.Context) { 310 addParamToQuery(r, schema.FormatParentID(s.Parent), p[s.Parent]) 311 putSingleFunc(w, r, p, identityService, context) 312 }) 313 314 //Custom action support 315 for _, actionExt := range s.Actions { 316 action := actionExt 317 ActionFunc := func(w http.ResponseWriter, r *http.Request, p martini.Params, 318 identityService middleware.IdentityService, auth schema.Authorization, context middleware.Context) { 319 addJSONContentTypeHeader(w) 320 fillInContext(context, r, w, s, server.sync, identityService) 321 id := p["id"] 322 input, err := middleware.ReadJSON(r) 323 324 // TODO use authorization middleware 325 manager := schema.GetManager() 326 path := r.URL.Path 327 policy, role := manager.PolicyValidate(action.ID, path, auth) 328 if policy == nil { 329 middleware.HTTPJSONError(w, fmt.Sprintf("No matching policy: %s %s %s", action, path, s.Actions), http.StatusUnauthorized) 330 return 331 } 332 context["policy"] = policy 333 context["tenant_id"] = auth.TenantID() 334 context["auth_token"] = auth.AuthToken() 335 context["role"] = role 336 context["catalog"] = auth.Catalog() 337 context["auth"] = auth 338 339 if err != nil { 340 handleError(w, resources.NewResourceError(err, fmt.Sprintf("Failed to parse data: %s", err), resources.WrongData)) 341 return 342 } 343 344 if err := resources.ActionResource( 345 context, dataStore, identityService, s, action, id, input); err != nil { 346 handleError(w, err) 347 return 348 } 349 routes.ServeJson(w, context["response"]) 350 } 351 route.AddRoute(action.Method, s.GetActionURL(action.Path), ActionFunc) 352 } 353 } 354 355 //MapRouteBySchemas setup route for all loaded schema 356 func MapRouteBySchemas(server *Server, dataStore db.DB) { 357 route := server.martini 358 log.Debug("[Initializing Routes]") 359 schemaManager := schema.GetManager() 360 route.Get("/_all", func(w http.ResponseWriter, r *http.Request, p martini.Params, auth schema.Authorization) { 361 responses := make(map[string]interface{}) 362 context := map[string]interface{}{ 363 "path": r.URL.Path, 364 "http_request": r, 365 "http_response": w, 366 } 367 for _, s := range schemaManager.Schemas() { 368 policy, role := authorization(w, r, schema.ActionRead, s.GetPluralURL(), s, auth) 369 if policy == nil { 370 continue 371 } 372 context["policy"] = policy 373 context["role"] = role 374 context["auth"] = auth 375 context["sync"] = server.sync 376 if err := resources.GetResources( 377 context, dataStore, 378 s, 379 resources.FilterFromQueryParameter( 380 s, r.URL.Query()), nil); err != nil { 381 handleError(w, err) 382 return 383 } 384 resources.ApplyPolicyForResources(context, s) 385 response := context["response"].(map[string]interface{}) 386 responses[s.GetDbTableName()] = response[s.Plural] 387 } 388 routes.ServeJson(w, responses) 389 }) 390 for _, s := range schemaManager.Schemas() { 391 MapRouteBySchema(server, dataStore, s) 392 } 393 } 394 395 // MapNamespacesRoutes maps routes for all namespaces 396 func MapNamespacesRoutes(route martini.Router) { 397 manager := schema.GetManager() 398 399 for _, namespace := range manager.Namespaces() { 400 if namespace.IsTopLevel() { 401 mapTopLevelNamespaceRoute(route, namespace) 402 } else { 403 mapChildNamespaceRoute(route, namespace) 404 } 405 } 406 } 407 408 // mapTopLevelNamespaceRoute maps route listing available subnamespaces (versions) 409 // for a top-level namespace 410 func mapTopLevelNamespaceRoute(route martini.Router, namespace *schema.Namespace) { 411 log.Debug("[Path] %s/", namespace.GetFullPrefix()) 412 route.Get( 413 namespace.GetFullPrefix()+"/", 414 func(w http.ResponseWriter, r *http.Request, p martini.Params, context martini.Context) { 415 versions := []schema.Version{} 416 for _, childNamespace := range schema.GetManager().Namespaces() { 417 if childNamespace.Parent == namespace.ID { 418 versions = append(versions, schema.Version{ 419 Status: "SUPPORTED", 420 ID: childNamespace.Prefix, 421 Links: []schema.Link{ 422 schema.Link{ 423 Href: childNamespace.GetFullPrefix() + "/", 424 Rel: "self", 425 }, 426 }, 427 }) 428 } 429 } 430 431 if len(versions) != 0 { 432 versions[len(versions)-1].Status = "CURRENT" 433 } 434 435 routes.ServeJson(w, map[string][]schema.Version{"versions": versions}) 436 }) 437 } 438 439 // mapChildNamespaceRoute sets a handler returning a dictionary of resources 440 // supported by a certain API version identified by the given namespace 441 func mapChildNamespaceRoute(route martini.Router, namespace *schema.Namespace) { 442 log.Debug("[Path] %s", namespace.GetFullPrefix()) 443 route.Get( 444 namespace.GetFullPrefix(), 445 func(w http.ResponseWriter, r *http.Request, p martini.Params, context martini.Context) { 446 resources := []schema.NamespaceResource{} 447 for _, s := range schema.GetManager().Schemas() { 448 if s.NamespaceID == namespace.ID { 449 resources = append(resources, schema.NamespaceResource{ 450 Links: []schema.Link{ 451 schema.Link{ 452 Href: s.GetPluralURL(), 453 Rel: "self", 454 }, 455 }, 456 Name: s.Singular, 457 Collection: s.Plural, 458 }) 459 } 460 } 461 462 routes.ServeJson(w, map[string][]schema.NamespaceResource{"resources": resources}) 463 }, 464 ) 465 }