URL Registration

Routes Registration

The application can register its list of HTTP APIs in the CALLBACK_ROUTES callback. When CALLBACK_ROUTES is invoked, a mapper object will be passed along with it. URLs can be connected to this mapper through this callback. Here we use Routes module version 2.2.0 to manage the URL routing.

Every URL to be accessed should start with /<provider-code>/<app-code>/

where <provider-code> and <app-code> corresponds to the application’s provider code and app code registered with the Aalam developer portal respectively. Any URL which violates the above format can never be accessed.

To know more about the structure of the URL to be registered, one can go through the documentation of python-routes modules.

Following are the type of URLs along with their formats.

  • Static URL with no dynamic arguments. Only the requests matching this exact format will be routed to this handler.

    /aalam/base/users
    
  • A URL with one dynamic argument. The dynamic argument should be enclosed in {}. For this URL the name of the argument will be passed as a parameter to the handler.

    /aalam/base/user/{user_email_id}
    

    In the above, {user_email_id} is a dynamic argument and the handler method will be passed with an argument with the value of {user_email_id}. The above URL format will be a match for

    /aalam/base/user/user1@test.test
    /aalam/base/user/self
    
  • A URL with multiple dynamic arguments. The arguments to the handler will be passed in the order of their appearence.

    /aalam/base/user/{email_id}/status/{status}/mark
    

    In the above example, email_id and status are passed to the action method in the order of the appearence. The action method can be defined like

    def mark_user_status(self, request, email_id, status):
        # Actual logic here
        pass
    

    The dynamic arguments defined like above can be just one element in the URL path. If a URL wants to have sub portion of itself as a dynamic argument, it can defined like

    /aalam/base/statics/{path_info:.*}
    

    The above URL format will be a match for

    /aalam/base/statics/1
    /aalam/base/statics/1/2
    /aalam/base/statics/1/2/3/4/5
    
  • The URLs are overlapping. In such case, only the first registration will be chosen. Let’s say we have the following URL formats.

    /aalam/base/static/{f}/{s}/{t}
    /aalam/base/static/{path_info:.*}
    

Above URLs both will be a match for /aalam/base/static/1/2/3. If we register /aalam/base/static/{f}/{s}/{t} before the {path_info.*}, any three path element will be routed to the former.

Mapper Arguments

When a URL is registered, following kwargs will be used by the framework.

  • handler

    Object of a class that inherits BaseHandler which defines the actions and serializers for the URL. This argument is mandatory.

  • action

    Name of the method in the handler which actually processes the URL functionality. This argument is mandatory. If an invalid method name is passed, a response of ‘503 - Not Implemented’ will be given as a response to all the requests for this URL.

    See Action handler

  • permissions

    Permissions object, describing the list of rules and permissions to use the URL. This is an optional parameter, if this parameter is present, it means anyone can use this URL.

  • conditions

    A Dictionary object. It should have a key value ‘method’ which should be a list with the list of HTTP methods that are applicable on this URL. Ex.

    conditions={"method": ["GET"]}
    
  • hook_data

    The hook input data sent by ‘B’ Hooks. The hook input will be relevant only if the URL is interested in the hook inputs.

    This is a dictionary object with keys being the hooker application in the format provider_code/app_code, and the value being the data returned by the hooker.

  • static_file

    Though applications can pre-inform the static URL and the way to access the static data through it’s PKG-INFO, there might be some situation which would want to send a static file on a non-static URL. For example, if an application has a static-url prefix as /aalam/base/s/ and a valid static directory. Support the application wants to serve a static file in a URL that does not have a prefix /aalam/base/s/, or if the file is not on the usual place that the framework can find, it can inform the framework about this static resource through this attribute.

    This attribute needs to be set by the request action handler. The value to this attribute must be a dictionary with two keys

    1. path - The path to the resource
    2. resource - A unique name of this resource. This name should not clash with any of the other static resources.

    Ex.

    request.static_file = {"path": "/absolute/path/to/the/static/file",
                           "resource": "should be a unique resource"}
    

    By doing so, the framework will take care of sending the response.

  • deserializer

    Name of the method to deserialize the input content for a URL. This method should accept one parameter and should return a dictionary object. The returned dictionary object will be passed to the action method as kwargs. For example, if a URL /aalam/users accepts a json input data like

    {
        "name": "Some name",
        "age": 50,
        "occupation": "Some occupation",
    }
    

    the action callback should be like

    def action_handler(self, request, name=None, age=None, occupation=None):
        # Do something here
        pass
    

    or it can be like

    def action_handler(self, request, **kwargs):
        # Do something here
        pass
    
  • serializer

    Name of the method to serialize the data returned by action method to return to the client. This method accepts two parameters in the following order

    1. Data object returned by action method
    2. Webob response object.

    The data has to be serialized to it’s correct format and will set the in the response body. Any manipulation to be done on the response object like modifying headers, can be done here. This will be called only when the action method returns a non-null value. The default serializer is the json_serializer defined in the BaseHandler class.

    Sample code registering the argument can be seen below

    from aalam_common.wsgi import BaseHandler
    from aalam_common.role_mgmt import Permissions
    
    class UserHandler(BaseHandler):
        def create_user_details(self, request, email_id, **kwargs):
            name = kwargs.get("name", None)
            occupation = kwargs.get("occupation", None)
            age = kwargs.get("age", None)
    
    def routes_callback(mapper):
        user_handler = UserHandler()
        mapper.connect("/aalam/base/user/{email_id}",
                       handler=user_handler,
                       action="create_user_details",
                       conditions={"method": ["PUT"]},
                       permissions=Permissions.deny_anon(),
                       serializer="html_serializer",
                       deserializer="xml_serializer")
    

    The above url registration, registers a ‘PUT’ method for the path that matches /aalam/base/user/{email_id}. This URL has an input data in XML format and the input data is deserializer by xml_serializer method in the user_handler object. It also sends a html response that is serialized by the html_serializer method of user_handler object. This URL is not permitted for the anonymous users as described the permissions argument. create_user_details method processes the functionality of this URL.

Submappers

Submapper is a concept where in if many URLs have the same set of mapper arguments, it can be grouped in one sub mapper object and the varying arguments can be connected to that sub mapper object. For example,

/aalam/base/users
/aalam/base/user/{email_id}
/aalam/base/user/{email_id}/status/{status}

all have the same ‘handler’. Instead of passing the same handler object for every mapper.connect(), on can create a submapper object with the handler argument and connect the url to the submapper object. A submapper can be used like

with mapper.submapper(handler=UserHandler(),
                      permissions=Permissions.deny_anon()) as sub_mapper:
    sub_mapper.connect("/aalam/base/users", action="get_users",
                       conditions={"method": ["GET"]})
    sub_mapper.connect("/aalam/base/user/email_id}",
                       action="get_user",
                       conditions={"method": ["GET"]})

Base Handler

The base handler for all the URL handler objects can be imported from aalam_common.wsgi. The base handler should be inherited by the handler objects and the action/serializer/deserializer methods for the handler object should be defined in it.

Input content type of application/json can be automatically deserialized by BaseHandler.json_deserializer(). If the input content type is not json and there is no deserializer set for the URL, the input content will be ignored.

BaseHandler has an inbuilt html_serizlier which accepts a string data. It sends minified html content. This reformats the HTML content internally, if the html data is desired to be sent in its original form, a custom serializer should be defined.

Action Handler

Action handlers are the methods that process the logic of any URL. This is set in the action argument while registering a URL and is mandatory for all URL registrations.

Action handlers are expected to either return some data or raise a webob exception.

If data is a tuple, the first parameter should be an integer, which will be the status code that needs to be set in the response. The second parameter is the output data. If data is not a tuple then it is treated as the output data.

Output data will be serialized on to the response by the serializer set for the URL. If no serializers are set and output data is not None, aalam_common.wsgi.BaseHandler:json_serializer will be used.

If there is an unexpected exception arising from the action handler, the client will receive 500 Internal Server Error response.

The first parameter to the ‘action’ method is object whose class inherits Webob request. In addition to the attributes of the webob request, one can access the following attributes.

  • sqa_session
An sqlalchemy session to the MYSQL database of this application. This session is established specifically for this request. If the request returns a status code from the range of the 400 - 599, any transaction pending on the session will not be committed to the database.
  • auth
The authentication parameters. See authentication for more details.
  • user_perms
Permissions enabled for the user requesting this request. This will be a list permission ids. aalam_common.role_mgmt module has the following helper methods that makes use of this attribute.
  • is_user_admin(email_id or request-object) to check if the user requesting the url is an administrator.
  • is_client_authorized(request, permission_group_name, permission_name) to check if the user requesting this url has a needed permission.

Route permissions

The permissions attribute for the URL registration should be an object of class aalam_common.role_mgmt.Permissions. The permissions object describes the rules and the list of permissions for a client to access a URL. An Administrator user can however be allowed to access any URL and the permissions object will not be applicable to an Administrator

The following static methods defines list of applicable permissions and the condition on the permissions.

  • all(*args)
This static method accepts the list of permissions in the form “<permission-group-name>/<permission-name>”. It returns a Permissions object. The object, when used by the URL, will allow clients with all the permissions in *args to access the URL.
  • any(*args)
This static method accepts the list of permissions in the form “<permission-group-name>/<permission-name>”. It returns a Permissions object. This object, when used by the URL, will allow any client with atleast one permission in *args to access the URL.

Other rules can be applied on a URL by the following method. The following methods return the permissions object so that the rules can be chained like

Permissions.any('pg1/p1', 'pg1/p2').deny_anon().deny_ext().deny_exc('aalam/base')

or like

Permissions().deny_anon().deny_ext().deny_exc('aalam/base')
  • deny_anon(self)
This method creates a rule for the URL to be denied for all anonymous requests. An anonymous request means that the client accessing this URL has not logged in or does not have a valid authentication token.
  • deny_ext(self)
This method creates a rule for the URL to be denied to all the external requests. External requests are the requests that are originating from an authenticated remote program. This will be used in the future.
  • deny_exc(self, *args)
This method creates a rule for the URL to be denied to a list of applications running internally and requesting this URL. Every list member should of the form <provider-code>/<app-code>. It can optionally be Permission.ALL_APPS which means it is denied for all the applications running internally.

If the route permissions does not have any permissions defined only the rules will be applicable.

If the URL cannot choose a permissions statically, or if it wants to authorize based on dynamic condition like the user requesting it, it can do this check in the action handler. For example,

GET /aalam/base/user/{user_email}

In the above URL, if user_email is self, any user can access it. But if user_email is a valid email_id, it will only be allowed to users with “Users/manage” permissions. This kind of check can be done in the action handler like below.

def get_user_details(self, request, user_email):
    if user_email == 'self':
        (_, user_email) = aalam_common.auth.get_auth_user_id(
            request, deny_anon=True)
    else:
        if not aalam_common.role_mgmt.is_client_authorized(
                request, "Users", "manage"):
            raise webob.exc.HTTPForbidden()