Codentium

On the topic of low-level programming.

Introduction

Flask-Gandalf is an extension library that adds fine-grained authorisation to the Flask microframework allowing views to be protected by requiring that the user of the view has certain rights. This works by instantiating a Gandalf object, by implementing the UserMixin and the SecurityContext to be able to check if the user is authenticated, belongs to a certain group and to check whether the user does have a certain permission token and finally by protecting views by stating what specific rights are required to access that view.

/static/img/flask-gandalf.png

Example

First we have to import and instantiate a Gandalf object for our Flask application:

from flask_gandalf import Gandalf

gandalf = Gandalf()

Then we will have to implement various models. Let's first start with the User model which implements both a UserMixin and a SecurityContext. Since we assume that any User instance is an existing account in our database that has been signed in, the is_authenticated method always return True. The in_group method checks if there is a group with the name specified and whether the user is in that group. Further, the has_permission method checks if a UserPermission exists with the token name specified and whether the current user has that permission:

class User(db.Model, UserMixin, SecurityContext):
    id = db.Column(db.Integer, primary_key=True)
    groups = db.relationship('Group', secondary=user_groups, back_populates='users')

    @staticmethod
    def has_permission(token):
        if db.session.query(db.exists().where(db.and_(
            UserPermission.token==token,
            UserPermission.user_id==current_user.id))).scalar():
            return True

    def is_authenticated(self):
        return True

    def in_group(self, name):
        return db.session.query(db.exists().where(db.and_(
            Group.name==name.lower(),
            Group.id==user_groups.c.group_id,
            user_groups.c.user_id==current_user.id))).scalar()

After implementing the User model, we will be implementing the Group model which also implements a SecurityContext. Similar to the User model, this model implements a has_permission method that checks if there is a GroupPermission with the token name specified and whether any of the groups the current user is part of has that permission. If no groups have been found, then it checks whether the current user has that permission by calling the has_permission method of the User security context:

class Group(db.Model, SecurityContext):
    @staticmethod
    def has_permission(token):
        if db.session.query(db.exists().where(db.and_(
                GroupPermission.token==token,
                GroupPermission.group_id==Group.id,
                Group.id==user_groups.c.group_id,
                user_groups.c.user_id==current_user.id))).scalar():
            return True

        return User.has_permission(token)

The final model that we implement is the BlogPost model that also implements a SecurityContext. With this security context the permission tokens can be much more fine-grained as permissions can be granted for specific blog posts using their blog postID. For instance, by having the blog:edit:1234 permission token, the user is allowed to modify the blog post with ID 1234. In this case the has_permission method splits up such a permission token to look up whether the current user is the owner of that blog post. If not then the look up will be delegated to the Group and User security contexts:

class BlogPost(db.Model, SecurityContext):
    @staticmethod
    def has_permission(token):
        tokens = token.split(':')

        if len(tokens) < 3:
            return Group.has_permission(token)

        post_id = tokens[-1]
        post = BlogPost.query.get(post_id) or abort(404)

        if post.owner_id == current_user.id:
            return True

        return Group.has_permission(token)

Using gandalf.requires we can then specify the rights required to access certain views. When the user sends a GET-request, we should check whether the user has the right to view the blog post by checking whether the user has the blog:view:{post_id} permission token, where post_id is the post ID specified in the URL route. Similarly, when the user sends a POST-request, we check whether the user has the right to edit the blog post:

@app.route('/blog/<int:post_id>', methods=('GET', 'POST'))
@gandalf.requires(rights.Permission(BlogPost, 'blog:view:{post_id}'), methods=('GET', 'POST'))
@gandalf.requires(rights.Permission(BlogPost, 'blog:edit:{post_id}'), methods=('POST'))
def view_blog_post(post_id):
    pass

More complex requirements can be formulated by using rights.AllOf, rights.NoneOf and rights.AnyOf logical constructs. An example of this would be a page where the user can sign up for a new account. As the user is signing up for a new account, it doesn't make a lot of sense to sign up for one, if the user has already been authenticated. However, at the same time we want to require that the user either has the permission token user:create to create a new account or that the user is visting the sign up page with an invite token located in the URL:

@user_view.route('/sign-up', methods=('GET', 'POST'))
@gandalf.requires(rights.AllOf(
    rights.NoneOf(rights.Authenticated()),
    rights.AnyOf(
        rights.Permission(Group, 'user:create'),
        rights.Argument('invite')
    )
), methods=('GET', 'POST'))
def sign_up():
    pass