# 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](https://routes.readthedocs.io/en/latest/) module version 2.2.0 to manage the URL routing. **Every URL to be accessed should start with `///`** where [``](http://docs.aalam.io/_/apps/latest/definitions.html#provider-code) and [``](http://docs.aalam.io/_/apps/latest/definitions.html#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. [handler]: #action-handler Mapper Arguments ---------------- When a URL is registered, following kwargs will be used by the framework. - **handler** Object of a class that inherits [BaseHandler](#base-handler) 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](#action-handler) - **permissions** [Permissions](#route-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][wres] 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][wexc]. 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][wreq]. 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](auth.md) 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. [wreq]: http://docs.webob.org/en/stable/api/request.html "Webob request" [wres]: http://docs.webob.org/en/stable/api/request.html "Webob response" [wexc]: http://docs.webob.org/en/stable/api/exceptions.html "webob exception" 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 "/". 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 "/". 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 `/`. > 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() ```