PylonsHQ.

Layout: Fixed-width

Authorization with repoze.what

If you get stuck while following this HOWTO, you can always download a Sample application, ask on #repoze or ask on the Repoze or Pylons mailing lists.

Overview

repoze.what is an authorization framework for WSGI applications, based on repoze.who as of v1.X (which deals with authentication and identification). It's the default authorization framework in TurboGears 2.

It is similar to AuthKit in that it enables an authorization system based on the groups to which the user belongs and the permissions granted to such groups by loading these groups and permissions into the request on the way in to the downstream WSGI application, but there are many differences between AuthKit and repoze.what (first of all, AuthKit deals with authentication, identification and authorization).

And on the other hand, it enables you to manage your groups and permissions from the application itself or another program, under a backend-independent API. For example, it would be easy for you to switch from one back-end to another, and even use this framework to migrate the data.

This is just the authorization pattern it supports out-of-the-box, but you can may it support other authorization patterns with your own so-called predicates. It's highly extensible, so it's very unlikely that it will get in your way – Among other things, you can extend it to check for many conditions (such as checking that the user comes from a given country, based on her IP address, for example).

Features

  • Authorization only. It doesn't try to be an all-in-one auth* monster – it will only do authorization and nothing else.
  • Highly extensible. It's been created with extensibility in mind, so that it won't get in your way and you can control authorization however you want or need, either with official components, third party plugins or your own plugins.
  • Fully documented. If it's not described in the manual, it doesn't exist. Everything is documented along with examples.
  • Reliable. Its developer is committed to keep the code coverage of the unit test suite at 100%.
  • Control access to any resource. Although it's only recommended to control authorization on action controllers, you can also use it to restrict access to other things in your package (e.g., only allow access to a database table if the current user is the admin).
  • If you use the groups/permissions-based authorization pattern, your application's groups and permissions may be stored in an SQLAlchemy or Elixir-managed database, in ``.ini`` files or in XML files (although you may also create your own adapters!).
  • The only requirement is that you use the powerful and extensible repoze.who authentication framework (which can be configured for you with the quickstart plugin).
  • It works with Python 2.4, 2.5 and 2.6.

Installing it

We are going to install repoze.what along with one of its official plugins, which provide a nice integration with Pylons applications:

easy_install repoze.what-pylons

Configuring

Using the Quickstart plugin

repoze.what v1.X depends on repoze.who and it has a plugin which configures authentication/identification and authorization in one go, and that's the quickstart plugin. We're going to use here to get started quickly.

The Quickstart will work if you store your application's users, groups and permissions in a SQLAlchemy-managed database, which is the most common situation.

1) Install it

First of all, install it (as of this writing, the Quickstart plugin is defined by the SQL plugin):

easy_install repoze.what-quickstart

2) Define your data model

Your User, Group and Permission models (or whatever you decide to name them) may be defined as in model_sa_example.py. We will assume they are define in your_application.model.auth.

The sample model uses the SQLAlchemy declarative method

You have to keep in mind that the sample SQLAlchemy model definition attached to this page uses the declarative method, not the old/regular one where tables and classes are defined independently and them mapped one another.

As a consequence, you have to define your own base class. The sample model assumes that it's in your_application.model.meta.

So, your your_application.model.meta and your_application.model.__init__ modules may look like this, respectively:

your_application.model.meta

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from sqlalchemy.ext.declarative import declarative_base

__all__ = ['engine', 'Session', 'DeclarativeBase', 'metadata']

# SQLAlchemy database engine.  Updated by model.init_model()
engine = None

# SQLAlchemy session manager.  Updated by model.init_model()
Session = None

# Global metadata. If you have multiple databases with overlapping table
# names, you'll need a metadata for each database
DeclarativeBase = declarative_base()
metadata = DeclarativeBase.metadata

your_application.model._init_

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext.declarative import declarative_base

from your_application.model import meta

def init_model(engine):
    """Call me before using any of the tables or classes in the model"""
    ## Reflected tables must be defined and mapped here
    #global reflected_table
    #reflected_table = sa.Table("Reflected", meta.metadata, autoload=True,
    #                           autoload_with=engine)
    #orm.mapper(Reflected, reflected_table)

    sm = orm.sessionmaker(autoflush=True, autocommit=False, bind=engine)

    meta.engine = engine
    meta.Session = orm.scoped_session(sm)

from your_application.model.auth import User, Group, Permission

3) Add the middleware to your application

To make things clear, create a module to handle auth-related stuff. It can be your-application.lib.auth.

Then define the add_auth function in the module in question:

Contents of your-application/lib/auth.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from repoze.what.plugins.quickstart import setup_sql_auth

from your_application.model.meta import Session
from your_application.model.auth import User, Group, Permission


def add_auth(app):
    """
    Add authentication and authorization middleware to the ``app``.

    We're going to define post-login and post-logout pages to do some cool things.
    
    """
    return setup_sql_auth(app, User, Group, Permission, Session,
                          post_login_url='/welcome_back', post_logout_url='/see_you_later')

Note that we're configuring authentication so that when the user logs in, she's redirected to /welcome_back, and when the user logs out, she's redirected to /see_you_later. Both paths are served by your application.

On the other hand, there are two more URL paths involved: Where the user-submitted login form is processed (/login_handler) and where she logs out (and the session cookie is discarded; at /logout). These paths are never handled by your application, but by repoze.who (specifically, by its FriendlyFormPlugin plugin). If you wanted to customize these paths, your add_auth() function would look like this:

add_auth() with custom login and logout handler paths

1
2
3
4
5
def add_auth(app):
    return setup_sql_auth(app, User, Group, Permission, Session,
                          post_login_url='/welcome_back', post_logout_url='/see_you_later',
                          login_handler='/custom_login_handler',
                          logout_handler='/custom_logout_handler')

Now it's time to use add_auth() to add authentication, identification and authorization middleware to your application. So go to your-application/config/middleware.py and add the following line after the message "# CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)":

Contents of your-application/config/middleware.py

1
2
3
4
5
6
7
8
# ...
from your_application.lib.auth import add_auth
# ...
def make_app(global_conf, full_stack=True, **app_conf):
    # ...
    # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
    app = add_auth(app)
    # ...

The nice thing about using the add_auth function is that as your authentication/authorization settings become more complex, you'd only have to update that function (not your-application/config/middleware.py itself).

4) Define the login action and template

In our add_auth function we specified that we wanted post-login and post-logout pages. You can define them using the code below:

Post-login and post-logout actions

 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# (...)

from pylons import request, tmpl_context as c
from pylons.controllers.util import redirect_to

from pylonsproject.lib.base import BaseController, render
from pylonsproject.lib.helpers import flash

from routes.util import url_for

# (...)

class RootController(BaseController):
    # (...)

    def login(self):
        """This is where the login form should be rendered."""
        # Without the login counter, we won't be able to tell if the user has
        # tried to log in with the wrong credentials
        login_counter = request.environ['repoze.who.logins']
        if login_counter > 0:
            flash('Wrong credentials')
        c.login_counter = login_counter
        c.came_from = request.params.get('came_from') or url_for('/')
        return render('login.html')

    def welcome_back(self):
        """
        Greet the user if he logged in successfully or redirect back to the login
        form otherwise.
        
        """
        identity = request.environ.get('repoze.who.identity')
        came_from = str(request.params.get('came_from', '')) or url_for('/')
        if not identity:
            # The user provided the wrong credentials
            login_counter = request.environ['repoze.who.logins'] + 1
            redirect_to(url_for('/login', came_from=came_from,
                                __logins=login_counter))
        userid = identity['repoze.who.userid']
        flash('Welcome back, %s!' % userid)
        redirect_to(url_for(came_from))

    def see_you_later(self):
        """Say goodbye to the user, she just logged out"""
        flash('We hope to see you soon!')
        came_from = str(request.params.get('came_from', '')) or url_for('/')
        redirect_to(url_for(came_from))

Then you can define your login form as (if you're using Genshi):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
                    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:py="http://genshi.edgewall.org/"
    xmlns:xi="http://www.w3.org/2001/XInclude">

<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
<title>Login Form</title>
</head>

<body>
  <h2>Please log in</h2>

  <form action="${h.url_for('/login_handler', came_from=c.came_from, __logins=c.login_counter)}"
        method="POST">
    <label for="login">Username:</label><input type="text" id="login" name="login" /><br/>
    <label for="password">Password:</label><input type="password" id="password" name="password" />
    <input type="submit" value="Login" />
  </form>
</body>
</html>

Note that the form is submitted to the login handler.

5) Add an initial User, Group and Permission

You'll need an initial User, Group and Permission to try out your setup. You can add them like so, either in an interactive python session, or via a script.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from sqlalchemy import create_engine
engine = create_engine('<your sqlalchemy url>', echo=True)
from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine)
session = Session()

u = User()
u.user_name = u'admin'
u.password = u'admin'
session.add(u)
g = Group()
g.group_name = u'admin_group'
g.users.append(u)
session.add(g)
p = Permission()
p.permission_name = u'admin_permission'
p.groups.append(g)
session.add(p)
session.commit()

Customization (optional)

Anything here can be customized. Check the documentation to learn how to do it.

Configuring without the Quickstart

If you don't use the Quickstart, then you have to define repoze.who and repoze.what by yourself. Check the repoze.what documentation to learn how to do that.

Protecting your controllers and controller actions

How to protect your controller actions

repoze.what-pylons provides a function decorator which you can use to protect your controller actions, which can be used as in the following example:

Protecting actions with @ActionProtector

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from repoze.what.predicates import has_permission
from repoze.what.plugins.pylonshq import ActionProtector

from pylonsproject.lib.base import BaseController, render
from pylonsproject.lib.helpers import flash

class RootController(BaseController):
    
    # (...)

    @ActionProtector(has_permission('edit-posts'))
    def edit_posts(self):
        flash("If you can see this, that's because you can edit posts")
        return render('main.html')

How to protect your controllers

repoze.what-pylons provides a function decorator which you can use to protect your controllers, which can be used as in the following example:

Protecting controllers with @ControllerProtector

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from repoze.what.predicates import has_permission
from repoze.what.plugins.pylonshq import ControllerProtector

from pylonsproject.lib.base import BaseController, render
from pylonsproject.lib.helpers import flash


@ControllerProtector(has_permission('manage'))
class PanelController(BaseController):

    def index(self):
        flash("If you can see this, that's because you are an administrator")
        return render('main.html')

    def something_else(self):
        flash("If you can see this, that's because you are an administrator")
        return render('main.html')

Python < 2.6 users

Class decorators were introduced in Python 2.6, so if you're using an older version, you can use @ControllerProtector as in:

Protecting controllers with @ControllerProtector in Python 2.4/2.5

1
2
3
4
# (...)
class PanelController(BaseController):
    # (...)
PanelController = ControllerProtector(has_permission('manage'))(PanelController)

Another way to get controller-wide authorization is to decorate Controller.__before__ with the ActionProtector decorator. In fact, this is what @ControllerProtector does under the hood.

Using denial handlers

By default, an authorization denial triggers one of the following actions:

  • If the user is anonymous, repoze.who will perform a challenge (e.g., a login form will be displayed).
  • If the user is authenticated, a page whose HTTP status code is 403 will be served.

If you want to override the default behavior when authorization is denied, you have define a so-called "denial handler". A denial handler is a callable which receives one positional argument (which is the message that describes why authorization is denied; this is, the relevant repoze.what predicate message) and is called only when authorization is denied:

Sample denial handler

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# This is pylonsproject.anotherpackage

from pylons import request, response
from pylons.controllers.util import abort

from pylonsproject.lib.helpers import flash

def cool_denial_handler(reason):
    # When this handler is called, response.status has two possible values:
    # 401 or 403.
    if response.status.startswith('401'):
        message = 'Oops, you have to login: %s' % reason
    else:
        identity = request.environ['repoze.who.identity']
        userid = identity['repoze.who.userid']
        message = "Come on, %s, you know you can't do that: %s" % (userid,
                                                                   reason)
    flash(message)
    abort(response.status_int, comment=reason)

Note that in the denial handler above we have to abort(), otherwise we'd be granting access to the request denied by repoze.what. Note that this is a feature, not a bug: In some situations you may not want to abort (e.g., you may want to redirect).

And you can use it as in:

Using denial handlers

 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
from repoze.what.predicates import has_permission
from repoze.what.plugins.pylonshq import ActionProtector, ControllerProtector

from pylonsproject.anotherpackage import cool_denial_handler
from pylonsproject.lib.helpers import flash

@ControllerProtector(has_permission('manage'), cool_denial_handler)
class PanelController(BaseController):

    def index(self):
        flash("If you can see this, that's because you are an administrator")
        return render('main.html')

    def something_else(self):
        flash("If you can see this, that's because you are an administrator")
        return render('main.html')

# (...)

class RootController(BaseController):

    # (...)

    @ActionProtector(has_permission('edit-articles'), cool_denial_handler)
    def edit_article(self, article_id):
        flash("If you can see this, that's because you can edit posts")
        return render('main.html')

# (...)

Then, when authorization is denied:

  • If the user is anonymous, she should be served a web page which contains a login form and a message that starts with "Oops, you have to login (...)". The status code of such a response is up to the repoze.who challenger.
  • If the user is authenticated, she should be served a web page that contains a message that starts with "Come on, username, you know (..)" and whose HTTP status code is 403.

Creating application-specific protectors

Sometimes you may need to customize the controller and controller action protectors in many places within your application (or in the whole application). All you have to do is subclass the relevant protector.

For example, if we use the cool_denial_handler function above very often, then we should create controller and controller action protectors which use that handler by default:

Custom controller and action protectors

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# This is pylonsproject.yetanotherpackage

from repoze.what.plugins.pylonshq import ActionProtector, ControllerProtector
from pylonsproject.anotherpackage import cool_denial_handler

class CoolActionProtector(ActionProtector):
    default_denial_handler = staticmethod(cool_denial_handler)

class CoolControllerProtector(ControllerProtector):
    default_denial_handler = staticmethod(cool_denial_handler)

Then our controllers would look like this:

Custom protectors in our controllers

 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
31
from repoze.what.predicates import has_permission

from pylonsproject.yetanotherpackage import CoolActionProtector, CoolControllerProtector

@CoolControllerProtector(has_permission('manage'))
class PanelController(BaseController):

    def index(self):
        flash("If you can see this, that's because you are an administrator")
        return render('main.html')

    def something_else(self):
        flash("If you can see this, that's because you are an administrator")
        return render('main.html')

# (...)

class RootController(BaseController):

    @CoolActionProtector(has_permission('edit-articles'))
    def edit_article(self, article_id):
        return render('main.html')

class RootController(YourBaseController):

    # (...)

    @ActionProtector(has_permission('edit-articles'), cool_denial_handler)
    def edit_article(self, article_id):
        flash("If you can see this, that's because you can edit posts")
        return render('main.html')

And every time authorization is denied, the cool_denial_handler function will be called.

Sample application

There's a sample, working Pylons application created based on this tutorial, which you can download as a compressed file through its project page on Bitbucket: http://bitbucket.org/Gustavo/whatpylonsproject/overview/

Getting more information

TODO

  • Explain how to set up the test suite (it's already implemented in the sample application).

Labels

wsgi wsgi Delete
repoze repoze Delete
authorization authorization Delete
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.
  1. May 24, 2009

    Jonas Fietz says:

    You mixed something up in your models, at least they don't authenticate for me. ...

    You mixed something up in your models, at least they don't authenticate for me. Most likely it is the order of the salt and the hashed password in the database. Here is my version of the User-Class

     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
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    class User(DeclarativeBase):
        """
        Reasonably basic User definition. Probably would want additional
        attributes.
    
        """
        __tablename__ = 'user'
    
        user_id = Column(Integer, autoincrement=True, primary_key=True)
    
        user_name = Column(Unicode(16), unique=True)
    
        _password = Column('password', Unicode(80))
    
        def _set_password(self, password):
            """Hash password on the fly."""
            hashed_password = password
    
            if isinstance(password, unicode):
                password_8bit = password.encode('UTF-8')
            else:
                password_8bit = password
    
            salt = sha1()
            salt.update(os.urandom(60))
            hash = sha1()
            hash.update(password_8bit + salt.hexdigest())
            hashed_password = salt.hexdigest() + hash.hexdigest()
    
            # Make sure the hased password is an UTF-8 object at the end of the
            # process because SQLAlchemy _wants_ a unicode object for Unicode
            # fields
            if not isinstance(hashed_password, unicode):
                hashed_password = hashed_password.decode('UTF-8')
    
            self._password = hashed_password
    
        def _get_password(self):
            """Return the password hashed"""
            return self._password
    
        password = synonym('_password', descriptor=property(_get_password,
                                                            _set_password))
    
        def validate_password(self, password):
            """
            Check the password against existing credentials.
    
            :param password: the password that was provided by the user to
                try and authenticate. This is the clear text version that we will
                need to match against the hashed one in the database.
            :type password: unicode object.
            :return: Whether the password is valid.
            :rtype: bool
    
            """
            hashed_pass = sha1()
            hashed_pass.update(password + self.password[:40])
            return self.password[40:] == hashed_pass.hexdigest()
    

  2. May 24, 2009

    Jonas Fietz says:

    Great... Managed to post the wrong version, edit function HELLO? class User(De...

    Great... Managed to post the wrong version, edit function HELLO?

    class User(DeclarativeBase):
        """
        Reasonably basic User definition. Probably would want additional
        attributes.
    
        """
        __tablename__ = 'user'
    
        user_id = Column(Integer, autoincrement=True, primary_key=True)
    
        user_name = Column(Unicode(16), unique=True)
    
        _password = Column('password', Unicode(80))
    
        def _set_password(self, password):
            """Hash password on the fly."""
            hashed_password = password
    
            if isinstance(password, unicode):
                password_8bit = password.encode('UTF-8')
            else:
                password_8bit = password
    
            salt = sha1()
            salt.update(os.urandom(60))
            hash = sha1()
            hash.update(salt.hexdigest() + password_8bit)
            hashed_password = salt.hexdigest() + hash.hexdigest()
            print '*'*20,  salt.hexdigest(), " ", hashed_password[:40]
    
            # Make sure the hased password is an UTF-8 object at the end of the
            # process because SQLAlchemy _wants_ a unicode object for Unicode
            # fields
            if not isinstance(hashed_password, unicode):
                hashed_password = hashed_password.decode('UTF-8')
    
            self._password = hashed_password
    
        def _get_password(self):
            """Return the password hashed"""
            return self._password
    
        password = synonym('_password', descriptor=property(_get_password,
                                                            _set_password))
    
        def validate_password(self, password):
            """
            Check the password against existing credentials.
    
            :param password: the password that was provided by the user to
                try and authenticate. This is the clear text version that we will
                need to match against the hashed one in the database.
            :type password: unicode object.
            :return: Whether the password is valid.
            :rtype: bool
    
            """
            hashed_pass = sha1()
            hashed_pass.update(self._password[:40] + password)
            return self._password[40:] == hashed_pass.hexdigest()


Powered by Pylons - Contact Administrators