.. _permissions:
===================================
Permissions with *Flask-Principal*
===================================
.. module:: flask_potion
Flask-Potion includes a permission system. The permissions system is
built on `Flask-Principal `_.
and enabled by decorating a :class:`manager.RelationalManager` with :class:`contrib.principals.principals`, which returns a class
extending both the manager and :class:`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:
.. code-block:: python
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 :class:`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
:meth:`PrincipalMixin.can_update_item` to facilitate this.
Example API with permissions
============================
.. versionchanged:: 0.11
The ``PrincipalManager`` extending ``SQLAlchemyManager`` has been replaced by a :meth:`principals` class-decorator.
We're going to go ahead and create an example API using :class:`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:
.. code-block:: python
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*:
.. code-block:: python
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.
.. code-block:: python
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*.
.. code-block:: python
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:
.. code-block:: python
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
.. code-block:: bash
http --auth editorA:editorA :5000/article content=foo
.. code-block:: http
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 :class:`flask_principal.ItemNeed`, for object-specific permissions. You would want to use them on something important, such as this *project* resource:
.. code-block:: python
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 :class:`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.
:class:`PrincipalMixin` class
===============================
.. module:: flask_potion.contrib.principals
.. autoclass:: PrincipalMixin
:members:
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 :class:`HybridNeed` and :class:`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.
.. module:: flask_potion.contrib.principals.needs
.. autoclass:: HybridNeed
:members:
.. module:: flask_potion.contrib.principals.permission
.. autoclass:: HybridPermission
:members: