Role-based access control

Coaster provides a RoleMixin class that can be used to define role-based access control to the attributes and methods of any SQLAlchemy model. RoleMixin is a base class for BaseMixin and applies to all derived classes. Access is defined as one of ‘call’ (for methods), ‘read’ or ‘write’ (both for attributes).

Roles are freeform string tokens. A model may freely define and grant roles to actors (users and sometimes client apps) based on internal criteria. The following standard tokens are recommended. Required tokens are granted by RoleMixin itself.

  1. all: Any actor, authenticated or anonymous (required)
  2. anon: Anonymous actor (required)
  3. auth: Authenticated actor (required)
  4. creator: The creator of an object (may or may not be the current owner)
  5. owner: The current owner of an object
  6. author: Author of the object’s contents (all creators are authors)
  7. editor: Someone authorised to edit the object
  8. reader: Someone authorised to read the object (assuming it’s not public)

Example use:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from coaster.sqlalchemy import BaseMixin, with_roles

app = Flask(__name__)
db = SQLAlchemy(app)

class ColumnMixin(object):
    '''
    Mixin class that offers some columns to the RoleModel class below,
    demonstrating two ways to use with_roles.
    '''
    @with_roles(rw={'owner'})
    def mixed_in1(cls):
        return db.Column(db.Unicode(250))

    @declared_attr
    def mixed_in2(cls):
        return with_roles(db.Column(db.Unicode(250)),
            rw={'owner'})


class RoleModel(ColumnMixin, RoleMixin, db.Model):
    __tablename__ = 'role_model'

    # The low level approach is to declare roles in advance.
    # 'all' is a special role that is always granted from the base class.
    # Avoid this approach because you may accidentally lose roles if a
    # subclass does not copy __roles__ from parent classes.

    __roles__ = {
        'all': {
            'read': {'id', 'name', 'title'}
        }
    }

    # Recommended: annotate roles on the attributes using ``with_roles``.
    # These annotations always add to anything specified in ``__roles__``.

    id = db.Column(db.Integer, primary_key=True)
    name = with_roles(db.Column(db.Unicode(250)),
        rw={'owner'})  # Specify read+write access

    # ``with_roles`` can also be called later. This is required for
    # properties, where roles must be assigned after the property is
    # fully described.

    _title = db.Column('title', db.Unicode(250))

    @property
    def title(self):
        return self._title

    @title.setter
    def title(self, value):
        self._title = value

    # This grants 'owner' and 'editor' write but not read access
    title = with_roles(title, write={'owner', 'editor'})

    # ``with_roles`` can be used as a decorator on methods, in which case
    # access is controlled with the 'call' action.

    @with_roles(call={'all'})
    def hello(self):
        return "Hello!"

    # Your model is responsible for granting roles given an actor or anchors
    # (an iterable).

    def roles_for(self, actor=None, anchors=()):
        # Calling super give us a result set with the standard roles
        result = super(RoleModel, self).roles_for(actor, anchors)
        if 'owner-secret' in anchors:
            result.add('owner')  # Grant owner role
        return result
class coaster.sqlalchemy.roles.RoleAccessProxy(obj, roles)[source]

A proxy interface that wraps an object and provides passthrough read and write access to attributes that the specified roles have access to. Consults the __roles__ dictionary on the object for determining which roles can access which attributes. Provides both attribute and dictionary interfaces.

Note that if the underlying attribute is a callable and is specified with the ‘call’ action, it will be available via attribute access but not dictionary access.

RoleAccessProxy is typically accessed directly from the target object via access_for() (from RoleMixin).

Example:

proxy = RoleAccessProxy(obj, roles={'writer'})
proxy.attr1
proxy.attr1 = 'new value'
proxy['attr2'] = 'new value'
dict(proxy)
Parameters:
  • obj – The object that should be wrapped with the proxy
  • roles – A set of roles to determine what attributes are accessible
class coaster.sqlalchemy.roles.RoleMixin[source]

Provides methods for role-based access control.

Subclasses must define a __roles__ dictionary with roles and the attributes they have call, read and write access to:

__roles__ = {
    'role_name': {
        'call': {'meth1', 'meth2'},
        'read': {'attr1', 'attr2'},
        'write': {'attr1', 'attr2'},
        },
    }

The with_roles() decorator is recommended over __roles__.

access_for(roles=None, actor=None, anchors=[])[source]

Return a proxy object that limits read and write access to attributes based on the actor’s roles. If the roles parameter isn’t provided, roles_for() is called with the other parameters:

# This typical call:
obj.access_for(actor=current_auth.actor)
# Is shorthand for:
obj.access_for(roles=obj.roles_for(actor=current_auth.actor))
actors_with(roles)[source]

Return an iterable of all actors who have the specified roles on this object. The iterable may be a list, tuple, set or SQLAlchemy query.

Must be implemented by subclasses.

current_access()[source]

Wraps access_for() with current_auth to return a proxy for the currently authenticated user.

current_roles

InspectableSet containing currently available roles on this object, using current_auth. Use in the view layer to inspect for a role being present:

if obj.current_roles.editor:
pass

{% if obj.current_roles.editor %}…{% endif %}

This property is also available in RoleAccessProxy.

roles_for(actor=None, anchors=())[source]

Return roles available to the given actor or anchors on this object. The data type for both parameters are intentionally undefined here. Subclasses are free to define them in any way appropriate. Actors and anchors are assumed to be valid.

The role all is always granted. If actor is specified, the role auth is granted. If not, anon is granted.

Subclasses overriding roles_for() must always call super() to ensure they are receiving the standard roles. Recommended boilerplate:

def roles_for(self, actor=None, anchors=()):
    roles = super(YourClass, self).roles_for(actor, anchors)
    # 'roles' is a set. Add more roles here
    # ...
    return roles
coaster.sqlalchemy.roles.with_roles(obj=None, rw=None, call=None, read=None, write=None)[source]

Convenience function and decorator to define roles on an attribute. Only works with RoleMixin, which reads the annotations made by this function and populates __roles__.

Examples:

id = db.Column(Integer, primary_key=True)
with_roles(id, read={'all'})

@with_roles(read={'all'})
@hybrid_property
def url_id(self):
    return str(self.id)

When used with properties, with_roles must always be applied after the property is fully described:

@property
def title(self):
    return self._title

@title.setter
def title(self, value):
    self._title = value

# Either of the following is fine, since with_roles annotates objects
# instead of wrapping them. The return value can be discarded if it's
# already present on the host object:

with_roles(title, read={'all'}, write={'owner', 'editor'})
title = with_roles(title, read={'all'}, write={'owner', 'editor'})
Parameters:
  • rw (set) – Roles which get read and write access to the decorated attribute
  • call (set) – Roles which get call access to the decorated method
  • read (set) – Roles which get read access to the decorated attribute
  • write (set) – Roles which get write access to the decorated attribute
coaster.sqlalchemy.roles.declared_attr_roles(rw=None, call=None, read=None, write=None)[source]

Equivalent of with_roles() for use with @declared_attr:

@declared_attr
@declared_attr_roles(read={'all'})
def my_column(cls):
    return Column(Integer)

While with_roles() is always the outermost decorator on properties and functions, declared_attr_roles() must appear below @declared_attr to work correctly.

Deprecated since version 0.6.1: Use with_roles() instead. It works for declared_attr since 0.6.1