Python Tornado and decorator

I had some time with python Tornado recently. Despite if it’s less cool compare to Java Jersey, it’s based on Python, which means sometimes it’s -really- less boring than Java.

Based on this article, we will go threw the basic HTTP Auth handled on Tornado, but also some role check, allowing to quickly draw the base of a secured system in Tornado (and even other framework can take benefits of this…).
Of course, like we do usually, the decorator will be our weapons of choice, because it’s cool.

HTTP Basic Auth

Like always, HTTP Basic Auth is a simple HTTP parameter named Authorisation sended threw inside request header, and like everywhere else, we need to get headers to get it.

Python can easily do the job here, as it provide base64 module by default, I provide here a little bit modified version compare to the article above:

import base64

def _checkAuth(login, password):
    ''' Check user can access or not to this element '''
    # TODO: return None if user is refused
    # TODO: do database check here, to get user.
    return {
        'login': 'okay',
        'password': 'okay',
        'role': 'okay'
    }

def httpauth(handler_class):
    ''' Handle Tornado HTTP Basic Auth '''
    def wrap_execute(handler_execute):
        def require_auth(handler, kwargs):
            auth_header = handler.request.headers.get('Authorization')

            if auth_header is None or not auth_header.startswith('Basic '):
                handler.set_status(401)
                handler.set_header('WWW-Authenticate', 'Basic realm=Restricted')
                handler._transforms = []
                handler.finish()
                return False

            auth_decoded    = base64.decodestring(auth_header[6:])
            login, password = auth_decoded.split(':', 2)
            auth_found      = _checkAuth(login, password)

            if auth_found is None:
                handler.set_status(401)
                handler.set_header('WWW-Authenticate', 'Basic realm=Restricted')
                handler._transforms = []
                handler.finish()
                return False
            else:
                handler.request.headers.add('auth', auth_found)

            return True

        def _execute(self, transforms, *args, **kwargs):
            if not require_auth(self, kwargs):
                return False
            return handler_execute(self, transforms, *args, **kwargs)

        return _execute

    handler_class._execute = wrap_execute(handler_class._execute)
    return handler_class

The two main difference resides in the way this decorator expose content, and the simple _checkAuth to implement your own database access.
Why I change this, was simply because the original article (at least code provided do this on my system), needs to have after, on every request using it, kwargs as parameter. Which I don’t really want -to keep things clear-. Also, the code was giving back login/password, which is also something I don’t want to retrieve usually, as I consider to have a single auth system, I can directly work with user found in db…

The most simple hello word would be:

@httpauth
class SessionCreateHandler(tornado.web.RequestHandler):
    def get(self):
        # Contains user found in previous auth
        print self.request.headers.get('auth')
        self.write('ok')

Now we see the interest: the user is embed into request headers, we can still access it, but it’s not needed anymore to keep a trace of external parameters.

Good.

But not enough, I also need a role check system, even a basic one will be usefull to remove many code portion. Let’s do it!

Role check

We will keep this idea of decorator, but this time for role check. We of course, assume we are already using the auth above.

# The _checkAuth should return a user object, and this
# configure which property from that objet get the 'role'
_userRolePropertyName = 'role'

def _checkRole(role, roles):
    ''' Check given role is inside or equals to roles '''
    # Roles is a list not a single element
    if isinstance(roles, list):
        found = False
        for r in roles:
            if r == role:
                found = True
                break

        if found == True:
            return True

    # Role is a single string
    else:
        if role == roles:
            return True

    return False


def allowedRole(roles = None):
    def decorator(func):
        def decorated(self, *args, **kwargs):
            user = self.request.headers.get('auth')

            # User is refused
            if user is None:
                raise Exception('Cannot proceed role check: user not found')

            role = user[_userRolePropertyName]

            if _checkRole(role, roles) == False:
                self.set_status(403)
                self._transforms = []
                self.finish()
                return None

            return func(self, *args, **kwargs)
        return decorated
    return decorator


def refusedRole(roles = None):
    def decorator(func):
        def decorated(self, *args, **kwargs):
            user = self.request.headers.get('auth')

            # User is refused
            if user is None:
                raise Exception('Cannot proceed role check: user not found')

            role = user[_userRolePropertyName]

            if _checkRole(role, roles) == True:
                self.set_status(403)
                self._transforms = []
                self.finish()
                return None

            return func(self, *args, **kwargs)
        return decorated
    return decorator

I decide to provide both method: allow and refuse, because sometimes we just don’t need to allow everything, just refuse few. The system takes the previously setted auth parameter found in header. And expose it to role check. Pay attention to _userRolePropertyName which may need to be adapted to your system.
Let’s see the example related (based on the previous one):

@httpauth
class SessionCreateHandler(tornado.web.RequestHandler):
    @allowedRole('administrator')
    def get(self):
        # Contains user found in previous auth
        print self.request.headers.get('auth')
        self.write('ok')

You can of course submit list also:

@httpauth
class SessionCreateHandler(tornado.web.RequestHandler):
    @allowedRole(['administrator', 'super-administrator'])
    def get(self):
        # Contains user found in previous auth
        print self.request.headers.get('auth')
        self.write('ok')

With those two decorators, you are able to provide a powerfull base to tornado for handling a highly customizable security check.

Final words

Like in Java (named annotation), Python’s decorator can be a powerfull tool to automate part of code, without populating functions. As it separate from rest of code, you can easily see them and check if they are well placed, and handling everything well.

So, now, you just need to jump into this marvellous word!

Publicités

6 Commentaires

  1. Thank you for this nice article.
    I will also use it 🙂

  2. A reblogué ceci sur ArtyProget a ajouté:
    Very nice article for fans of Tornado

  3. kota

    does it work for websockets?

    • deisss

      The decorator trick itself yes, the Basic Auth is more complicated. As of Socks-tornado hide many details, getting access to such parameters is tricky. On the other side, you can create your own wrapper for Sockjs and then, have a parameter which will act like Basic Auth one…

      Hope this helps…

  4. Your code failed in python 3.2, so i changed:
    auth_decoded = base64.decodestring(auth_header[6:])
    to:
    auth_decoded = base64.b64decode(auth_header[6:].encode(« ascii »)).decode(« ascii »)

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :