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  }