tushman.io

The musings of an insecure technologist

Explaining Bouncer and Method_decorators

Thank you Boston Python for the opportunity to present at “July Presentation Night: What I Built at Work”.

I would like to elaborate on one of the questions asked during the Q&A after my presentation. It was a question about bouncer.

When I shared the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from bouncer import authorization_method
from bouncer.constants import READ, EDIT, MANAGE, ALL

@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)

Someone asked: what is user and they? It was a really good question and deserved a better explanation.

The first thing to consider is @authorization_method This is a method decorator,a really nice Python feature — particularly when you are writing framework code.

A method decorator is a method that takes in a method as an argument and returns a mutated method. (pause to re-read that)

Let’s take a look at this specific implementation:

1
2
3
4
5
6
7
8
9
10
11
12
# in bouncer/__init__.py

def get_authorization_method():
    return _authorization_method

_authorization_method = None

def authorization_method(original_method):
    """The method that will be injected into the authorization target to perform authorization"""
    global _authorization_method
    _authorization_method = original_method
    return original_method

So in the instance of our authorization_method, we receive a function and store it in the global variable _authorization_method. We can make of use of this function later in application’s execution.

For example. In my talk I showed the can method:

1
2
3
4
5
6
7
8
9
10
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

can is defined as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
def can(user, action, subject):
    """Checks if a given user has the ability to perform the action on a subject

    :param user: A user object
    :param action: an action string, typically 'read', 'edit', 'manage'.  Use bouncer.constants for readability
    :param subject: the resource in question.  Either a Class or an instance of a class.  Pass the class if you
                    want to know if the user has general access to perform the action on that type of object.  Or
                    pass a specific object, if you want to know if the user has the ability to that specific instance

    :returns: Boolean
    """
    ability = Ability(user, get_authorization_method())
    return ability.can(action, subject)

When “can” is called, it builds an Ability using the logic in method we decorated (stored) with @authorization_method

Having said that, let me explain what they and they.can is.

they is a RuleList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# in bouncer/models.py

class RuleList(list):
    def append(self, *item_description_or_rule, **kwargs):
        # Will check it a Rule or a description of a rule
        # construct a rule if necessary then append
        if len(item_description_or_rule) == 1 and isinstance(item_description_or_rule[0], Rule):
            item = item_description_or_rule[0]
            super(RuleList, self).append(item)
        else:
            # try to construct a rule
            item = Rule(True, *item_description_or_rule, **kwargs)
            super(RuleList, self).append(item)

    # alias append
    # so you can do things like this:
    #     @authorization_method
    # def authorize(user, they):
    #
    #     if user.is_admin:
    #         # self.can_manage(ALL)
    #         they.can(MANAGE, ALL)
    #     else:
    #         they.can(READ, ALL)
    #
    #         def if_author(article):
    #             return article.author == user
    #
    #         they.can(EDIT, Article, if_author)
    can = append

RuleList is a python list with two tweaks:

  1. override append to handle inputing of Rules or something I can construct into a rule
  2. alias append can = append which allows us to have the desired syntax they.can(READ, ALL)

I am pretty pleased with this; I really like the they.can(READ, ALL) syntax. Some may argue that it is not pythonic since I could be more explicit — but in this case I think ease of readability trumps style.

But if you don’t agree, no worries you can use the following equivalent syntax:

1
2
3
4
5
6
7
8
9
10
@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)

Both work!

Hopefully this clarifies things. Feel free to ping me with additional questions.


Addendum

There has been a fair bit of discussion in my office about the grammatically correctness of they. Uncannily, xkcd comes to the rescue once again:

image

Comments