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.

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

  1. all: Any user, authenticated or anonymous (required)
  2. anon: Anonymous user (required)
  3. user: Logged in user or user token (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
  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, set_roles

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

class DeclaredAttrMixin(object):
    # The ugly way to work with declared_attr
    @declared_attr
    def mixed_in1(cls):
        return set_roles(db.Column(db.Unicode(250)),
            rw={'owner'})

    # The clean way to work with declared_attr
    @declared_attr
    @declared_attr_roles(rw={'owner', 'editor'}, read={'all'})
    def mixed_in2(cls):
        return db.Column(db.Unicode(250))


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

    # Approach one, declare roles in advance.
    # 'all' is a special role that is always granted from the base class

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

    # Approach two, annotate roles on the attributes.
    # These annotations always add to anything specified in __roles__

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

    title = db.Column(db.Unicode(250))
    set_roles(title, write={'owner', 'editor'})  # Grant 'owner' and 'editor' write but not read access

    @set_roles(call={'all'})  # 'call' is an alias for 'read', to be used for clarity
    def hello(self):
        return "Hello!"

    # Your model is responsible for granting roles given a user or
    # user token. The format of tokens is not specified by RoleMixin.

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

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, calls are controlled by the read action. Care should be taken when the callable mutates the object.

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.roles.RoleMixin

Provides methods for role-based access control.

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

__roles__ = {
    'role_name': {
        'read': {'attr1', 'attr2'}
        'write': {'attr1', 'attr2'}
        },
    }
access_for(roles=None, user=None, token=None)

Return a proxy object that limits read and write access to attributes based on the user’s roles. If the roles parameter isn’t provided, but a user or token is provided instead, roles_for() is called:

# This typical call:
obj.access_for(user=current_user)
# Is shorthand for:
obj.access_for(roles=obj.roles_for(user=current_user))
make_token_for(user, roles=None, token=None)

Generate a token for the specified user that grants access to this object alone, with either all roles available to the user, or just the specified subset. If an existing token is available, add to it.

This method should return None if a token cannot be generated. Must be implemented by subclasses.

roles_for(user=None, token=None)

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

The role all is always granted. If either user or token is specified, the role user is granted. If neither, anon is granted.

users_with(roles)

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

Must be implemented by subclasses.

coaster.roles.set_roles(obj=None, rw=None, call=None, read=None, write=None)

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)
set_roles(id, read={'all'})

@set_roles(read={'all'})
@hybrid_property
def url_id(self):
    return str(self.id)
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. Due to technical limitations, call is just an alias for read. Any role with read access to a method can also call it
  • read (set) – Roles which get read access to the decorated attribute
  • write (set) – Roles which get write access to the decorated attribute
coaster.roles.declared_attr_roles(rw=None, call=None, read=None, write=None)

Equivalent of set_roles() for use with @declared_attr:

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

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