Permissions with Flask-Principal

Flask-Potion includes a permission system. The permissions system is built on Flask-Principal. and enabled by decorating a manager.RelationalManager with contrib.principals.principals, which returns a class extending both the manager and contrib.principals.PrincipalMixin.

Permissions are specified as a dict in Meta.permissions.

Defining Permissions

There are four basic actions — read, create, update, delete — for which permissions must be defined. Additional virtual actions can be declared for various purposes.

For example, the default permission declaration looks somewhat like this:

class Meta:
    permissions = {
        'read': 'yes',
        'create': 'no',
        'update': 'create',
        'delete': 'update'
    }

Patterns and Needs they produce:

Pattern Matches Description
{action} a key in the permissions dict If

equal to the action it is declared for — e.g. {'create': 'create'} — evaluate to:

HybridItemNeed({action}, resource_name)

Otherwise re-use needs from other action.

{role} not a key in the permissions dict RoleNeed({role})
{action}:{field} *:* Copy {action} permissions from ToOne linked resource at {field}.
user:{field} user:* UserNeed(item.{field}.id) for ToOne fields.
no, nobody no Do not permit.
yes, everybody yes Always permit.

Note

When protecting an ItemRoute, read access permissions, and updates using the resource manager are checked automatically; for other actions, permissions have to be checked manually from within the function. The manager has helper functions such as PrincipalMixin.can_update_item() to facilitate this.

Example API with permissions

Changed in version 0.11: The PrincipalManager extending SQLAlchemyManager has been replaced by a principals() class-decorator.

We’re going to go ahead and create an example API using PrincipalMixin with Flask-Login for authentication. Since there are quite a few moving parts, this example is split up into several sections.

Our example is a simple blog with articles and comments. First, let’s create the database models:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from sqlalchemy.orm import relationship

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret'  # XXX replace with actual secret and don't keep it in source code

db = SQLAlchemy(app)


class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(), nullable=False)
    is_admin = db.Column(db.Boolean(), default=False)
    is_editor = db.Column(db.Boolean(), default=False)


class Article(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    author_id = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False)
    author = relationship(User)
    content = db.Column(db.Text)


class Comment(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    article_id = db.Column(db.Integer, db.ForeignKey(Article.id), nullable=False)
    author_id = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False)
    article = relationship(Article)
    author = relationship(User)
    message = db.Column(db.Text)


db.create_all()

We’re going to use Flask-Login to authenticate requests using Basic Authentication:

from flask_login import LoginManager, current_user

login_manager = LoginManager(app)


@login_manager.request_loader
def load_user_from_request(request):
    if request.authorization:
        username, password = request.authorization.username, request.authorization.password

        # XXX replace this with an actual password check.
        if username == password:
            return User.query.filter_by(username=username).first()
    return None

This is where Flask-Principal comes in. With every request it adds the needs the identity should provide. Authenticated users are given a user need and maybe some role needs. If this example had some top-level object based permissions (think groups, projects, teams, etc.) they would also be added here.

from flask_principal import Principal, Identity, UserNeed, AnonymousIdentity, identity_loaded, RoleNeed

principals = Principal(app)

@principals.identity_loader
def read_identity_from_flask_login():
    if current_user.is_authenticated():
        return Identity(current_user.id)
    return AnonymousIdentity()


@identity_loaded.connect_via(app)
def on_identity_loaded(sender, identity):
    if not isinstance(identity, AnonymousIdentity):
        identity.provides.add(UserNeed(identity.id))

        if current_user.is_editor:
            identity.provides.add(RoleNeed('editor'))

        if current_user.is_admin:
            identity.provides.add(RoleNeed('admin'))

Finally, we create our API with the login_required decorator from Flask-Login.

from flask_login import login_required
from flask_potion import fields, signals, Api, ModelResource
from flask_potion.contrib.alchemy import SQLAlchemyManager
from flask_potion.contrib.principals import principals

api = Api(app, decorators=[login_required])

class PrincipalResource(ModelResource):
    class Meta:
        manager = principals(SQLAlchemyManager)


class UserResource(PrincipalResource):
    class Meta:
        model = User


class ArticleResource(PrincipalResource):
    class Schema:
        author = fields.ToOne('user')

    class Meta:
        model = Article
        read_only_fields = ['author']
        permissions = {
            'create': 'editor',
            'update': ['user:author', 'admin']
        }


class CommentResource(PrincipalResource):
    class Schema:
        article = fields.ToOne('article')
        author = fields.ToOne('user')

    class Meta:
        model = Comment
        read_only_fields = ['author']
        permissions = {
            'create': 'anybody',
            'update': 'user:author',
            'delete': ['update:article', 'admin']
        }

api.add_resource(UserResource)
api.add_resource(ArticleResource)
api.add_resource(CommentResource)

# add the author to articles & comments when they are created:
@signals.before_create.connect_via(ANY)
def before_create_article_comment(sender, item):
    if issubclass(sender, (ArticleResource, CommentResource)):
        item.author_id = current_user.id

We’ve implemented the following permissions:

  • only editors can create articles
  • articles can be updated or deleted by either their authors or by admins
  • comments can be created by anyone who is authenticated
  • comments can updated only by the person who wrote the comment, but deleted both by admins and the author of the article

Now we just need to start the app:

if __name__ == '__main__':
    # add some example users & run the application
    db.session.add(User(username='editorA', is_editor=True))
    db.session.add(User(username='editorB', is_editor=True))
    db.session.add(User(username='admin', is_admin=True))
    db.session.add(User(username='user'))
    db.session.commit()

    app.run()

You can find the complete example code on GitHub under:

examples/permissions_example.py
http --auth editorA:editorA :5000/article content=foo
HTTP/1.0 200 OK
Content-Length: 71
Content-Type: application/json
Date: Sun, 08 Feb 2015 10:48:03 GMT
Server: Werkzeug/0.9.6 Python/3.3.2
Set-Cookie: session=.eJyrVorPTFGyUjK3SEw0TDOzSDRPtDRJtUxNMzZKM0pNNE4zNks1TU6zVNJRykxJzSvJLKnUSywtyYgvqSxIVbLKK83JQZIBGWVYCwBQWxtd.B7jQYw.Nhh6qE-h5WrGPfsYibXnDzCaJQM; HttpOnly; Path=/

{
    "$uri": "/article/2",
    "author": {
        "$ref": "/user/1"
    },
    "content": "foo"
}

Object-based permissions

The example above did already sort of touch on object-based permissions, with the 'user:author' pattern that restricts access to the user who has authored a comment or article. We’ve also used permissions options, with more than one need potentially providing access. Finally, you have seen a hint of cascading object-based permissions with the 'update:article' pattern that conditions access to the permissions on a relation.

There is another permission layer, building on flask_principal.ItemNeed, for object-specific permissions. You would want to use them on something important, such as this project resource:

class ProjectResource(ModelResource):
    class Meta:
        manager = principals(SQLAlchemyManager)
        model = Project
        permissions = {
            'create': 'anybody',
            'update': 'manage',
            'manage': 'manage'
        }

To update a project, your identity needs this need:

ItemNeed('manage', PROJECT_ID, 'project')

The pair {'manage': 'manage'} makes manage a new virtual action, which is why the flask_principals.ItemNeed wants a 'manage' permission. We could also have written {'update': 'update'} — then the required need would have been:

ItemNeed('update', PROJECT_ID, 'project')

With cascading permissions, role-based, user-based, and object-based permissions you should now have all the tools to implement all sorts of complex permissions setups.

PrincipalMixin class

class flask_potion.contrib.principals.PrincipalMixin(*args, **kwargs)
get_permissions_for_item(item)

Returns a dictionary of evaluated permissions for an item. :param item: :return: Dictionary in the form {operation: bool, ..}

can_create_item(item)

Looks up permissions on whether an item may be created. :param item:

can_update_item(item, changes=None)

Looks up permissions on whether an item may be updated. :param item: :param changes: dictionary of changes

can_delete_item(item)

Looks up permissions on whether an item may be deleted. :param item:

Efficiency

Those who have worked with Flask-Principal know that it is on its own not well-suited for object-based permissions where large numbers of objects are involved, because each permission has to be loaded into memory as ItemNeed at the start of the session.

The permission system built into Potion introduces the HybridNeed and HybridPermission classes to solve this issue. They can either be evaluated directly or be applied to SQLAlchemy queries, and are therefore efficient with any number of object-based permissions.

class flask_potion.contrib.principals.needs.HybridNeed

HybridNeed base class. Hybrid needs can both be evaluated directly or produce an expression for use with SQLAlchemy.

class flask_potion.contrib.principals.permission.HybridPermission(*needs)

Hybrid flask_principal.Permission that evaluates both regular and hybrid needs.

allows(identity)

Determines whether a given identity meets this permission.

Parameters:identity (flask_principal.Identity) – An identity with a set of provided needs
can(item=None)

Depending on whether or not item is given, this function either:

  • evaluates all regular needs needs
  • also evaluates the hybrid needs against the item

If any of the needs are met, the function returns True.

Parameters:item – SQLAlchemy model instance