bouncer
=======

Simple Declarative Authentication DSL inspired by Ryan Bates’ excellent
cancan library

.. image:: https://travis-ci.org/jtushman/bouncer.svg?branch=master
    :alt: travis-badge
    :target: https://travis-ci.org/jtushman/bouncer

Introduction
------------

Meet **bouncer**.

**bouncer** is your faithful servant. Big, burly, trustworthy – smarter
than he looks, but very effective in what he does. You just have to talk
simply to him. For example:

.. code:: python

    from bouncer import authorization_method
    from bouncer.constants import *

    @authorization_method
    def authorize(user, they):

        if user.is_admin:
            they.can(MANAGE, ALL)
        else:
            they.can(READ, ALL)
            they.cannot(READ, 'TopSecretDocs')

            def if_author(article):
                return article.author == user

            they.can(EDIT, 'Article', if_author)


And once you have that setup, you can ask questions like:

.. code:: python

    jonathan = User(name='jonathan',admin=False)
    marc = User(name='marc',admin=False)

    article = Article(author=jonathan)

    print can(jonathan, EDIT, article)   # True
    print can(marc, EDIT, article)       # False

    # Can Marc view articles in general?
    print can(marc, VIEW, Article)       # True

Installation
------------

``pip install bouncer``

Defining Abilities
------------------

@authorization_method
~~~~~~~~~~~~~~~~~~~~~
User permissions are defined in a method decorated with
``@authorize_method``

A simple setup looks like so …

.. code:: python

    @authorization_method
    def authorize(user, they):

        if user.is_admin:
            they.can(MANAGE, ALL)
        else:
            they.can(READ, ALL)

            def if_author(article):
                return article.author == user

            they.can(EDIT, Article, if_author)


If you do not think the “they.can” is pythonic enough you can use the ``append`` syntax

.. code:: python

    @authorization_method
    def authorize(user, abilities):

        if user.is_admin:
            abilities.append(MANAGE, ALL)
        else:
            abilities.append(READ, ALL)

            # See I am using a string here
            abilities.append(EDIT, 'Article', author=user)
            
Alternative syntax
~~~~~~~~~~~~~~~~~~

``dict`` syntax
^^^^^^^^^^^^^^^
You can also use an alternative ``dict`` syntax. The following is equivalent to above:

.. code:: python

    @authorization_method
    def authorize(user, they):

        if user.is_admin:
            they.can(MANAGE, ALL)
        else:
            they.can(READ, ALL)
            they.can(EDIT, Article, author=user)

You can add multiple conditions to the ``dict``:

.. code:: python

    they.can(READ, Article, published=True, active=True)

Strings instead of classes
^^^^^^^^^^^^^^^^^^^^^^^^^^
You can use strings instead of classes. This means you do not need to import a bunch of files you are not using in initialization

.. code:: python

    @authorization_method
    def authorize(user, they):

        if user.is_admin:
            they.can(MANAGE, ALL)
        else:
            they.can(READ, ALL)

            # Notice that I am using a string here
            they.can(EDIT, 'Article', author=user)


Combining Rules
^^^^^^^^^^^^^^^
You can (are encouraged to) combine similar rules on a single line:

.. code:: python

    they.can((EDIT,READ,DELETE),(Article,Photo))
    
Combining Abilities
^^^^^^^^^^^^^^^^^^^

It is possible to define multiple abilites for the same resource. This is
particularly useful in combination with the ``cannot`` method

.. code:: python

    they.can(MANAGE, ALL)
    then.cannot(DELETE, ('USER', 'ACCOUNT'))

Checking Abilities
------------------
There are two main ways for checking for authorization.  ``can`` (and its brother ``cannot``) and ``ensure``

* ``can`` returns a boolean
* ``ensure`` will raise an ``AccessDenied`` Exception

.. code:: python

    from bouncer import can, ensure
    from bouncer.constants import *

    jonathan = User(name='jonathan',admin=False)

    # can jonathan edit articles in general
    can(jonathan, EDIT, Article)

    # ensure jonathan edit articles in general -- otherwise we are going to throw an exception
    ensure(jonathan, EDIT, Article)

    article = Article(author=jonathan)

    # can jonathan delete this specific article
    can(jonathan, EDIT, article)
    
Decorating your User Model
~~~~~~~~~~~~~~~~~~~~~~~~~~
Optionally, you can add helper methods into your User model by using ``@authorization_target``

For example:

.. code:: python

    from bouncer import authorization_target

    @authorization_target
    class User(object):

        def __init__(self, **kwargs):
            self.id = kwargs.get('id', 1)
            self.name = kwargs.get('name', '')
            self.admin = kwargs.get('admin', False)
            pass
    
        @property
        def is_admin(self):
            return self.admin
    
    jonathan = User(name='jonathan',admin=False)
    marc = User(name='marc',admin=False)

    article = Article(author=jonathan)

    print jonathan.can(EDIT,article)   # True
    print marc.can(EDIT,article)       # False

Flask
-----

If you use Flask, I am currently working on a Flask extension – follow
its progress here: `flask-bouncer`_.

Questions / Issues
------------------

Feel free to ping me on twitter: `@tushman`_
or add issues or PRs at https://github.com/jtushman/bouncer
    
.. _flask-bouncer: https://github.com/jtushman/flask-bouncer
.. _@tushman: http://twitter.com/tushman
