PylonsHQ.

Layout: Fixed-width

Pylons 1.0 and repoze.what

Skip to end of metadata
Go to start of metadata

Most of the code is ported from the Pylons Cookbook tutorial: http://wiki.pylonshq.com/display/pylonscookbook/Authorization+with+repoze.what

Although the cookbook tutorial is in-depth and very well written, I had to redo some of the stuff to work properly on the latest Pylons and repoze version releases.

I used the following component versions for my Pylons project:

  • Pylons v1.0
  • repoze.what v1.0.9
  • repoze.what-pylons v1.0
  • repoze.what-quickstart v1.0.8

Installing

We will be using SQLAlchemy powered Pylons project. So I am assuming that before we proceed with the below steps, we already have a fresh project configured with SQLAlchemy support

In order for authorization to work, we first need to install a long list of repoze dependent packages. viz. repoze.who, repoze.who-friendlyform, repoze.what, repoze.what-pylons, repoze.what-quickstart, repoze.what-plugins-sql and repoze.who.plugins.sa

But luckily all of them can be easily installed by just installing “repoze.what-pylons” and “repoze.what-quickstart” since all the other packages get automatically installed with them.

So lets install them here:

easy_install repoze.what-pylons

easy_install repoze.what-quickstart

Define data model for Users, groups and permissions

We need to define three tables/classes to handle our users viz. User, Group and Permission. Lets define them in myproject > model > auth.py

Note that I have used the classical SQLAlchemy syntax to create my model as opposed to the declarative syntax used in the Cookbook tutorial as it gives me more control on my class definitions. You can choose any way you like.

So here is how my myproject.model.auth looks like:

  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
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
"""
SQLAlchemy-powered model definitions for repoze.what SQL plugin.
Sets up Users, Groups and Permissions
"""
from sqlalchemy import Table, ForeignKey, Column
from sqlalchemy.types import Unicode, UnicodeText, Integer, Date, CHAR
from sqlalchemy import orm
from myproject.model.meta import metadata
import os
from hashlib import sha1

group_table = Table('group', metadata,
    Column('id', Integer(), primary_key=True),
    Column('name', Unicode(255), unique=True, nullable=False),
)

permission_table = Table('permission', metadata,
    Column('id', Integer(), primary_key=True),
    Column('name', Unicode(255), unique=True, nullable=False),
)

user_table = Table('user', metadata,
    Column('id', Integer(), primary_key=True),
    Column('username', Unicode(255), unique=True, nullable=False),
    Column('email', Unicode(255), unique=True, nullable=False),
    Column('password', Unicode(80), nullable=False),
    Column('fullname', Unicode(255), nullable=False),
)

# This is the association table for the many-to-many relationship between
# groups and permissions.
group_permission_table = Table('group_permission', metadata,
    Column('group_id', Integer, ForeignKey('group.id')),
    Column('permission_id', Integer, ForeignKey('permission.id')),
)

# This is the association table for the many-to-many relationship between
# groups and users
user_group_table = Table('user_group', metadata,
    Column('user_id', Integer, ForeignKey('user.id')),
    Column('group_id', Integer, ForeignKey('group.id')),
)

class Group(object):
    pass

class Permission(object):
    pass

class User(object):

    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

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

# Map SQLAlchemy table definitions to python classes
orm.mapper(Group, group_table, properties={
    'permissions':orm.relation(Permission, secondary=group_permission_table),
    'users':orm.relation(User, secondary=user_group_table),
})
orm.mapper(Permission, permission_table, properties={
    'groups':orm.relation(Group, secondary=group_permission_table),
})
orm.mapper(User, user_table, properties={
    'groups':orm.relation(Group, secondary=user_group_table),
})

We don’t need any changes to meta.py but here is how my myproject.model.meta looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
"""Creates SQLAlchemy Metadata and Session object"""
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy import MetaData

__all__ = ['Base', 'Session']

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

metadata = MetaData()

We now import the User, Group and Permission classes inside our myproject.model._init_.py.
Apart from that, we don’t need to make any changes to _init_.py . Also note that I didn’t import the declarative_base in my init module since I don’t use it in my model definition.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
""" Creates or imports Application models"""
import sqlalchemy as sa
from sqlalchemy import orm
from myproject.model import meta
from myproject.model.auth import User, Group, Permission

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)

    # We are using SQLAlchemy 0.5 so transactional=True is replaced by
    # autocommit=False
    sm = orm.sessionmaker(autoflush=True, autocommit=False, bind=engine)

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

Add the middleware app to your application

Now that we have created the model class/table definitions inside our model/auth.py module, we need a way to inform repoze.what about its existence.

For this we use the setup_sql_auth() function as described here: http://what.repoze.org/docs/plugins/quickstart/ . We call the setup_sql_auth() function with appropriate parameters inside myproject/lib/auth.py add_auth() function.

We create this additional add_auth() function so that it will be easier for us to add the repoze configured middleware inside our middleware.py configuration.

Calling the setup_sql_auth() function with appropriate parameters is the most important part of the setup process as we can make and break the entire authentication configuration with the vast number of parameters it allows. I will list down the most important parameters we will use while calling this function:

app: Our WSGI application we got from middleware.py.
User: The user class where our user information is stored.
Group: The group class where our user groups are stored.
Permission: The Permission class where our group permissions are stored.
login_url: The URL where the login form is displayed.
post_login_url: The URL where our user is redirected after login. Later, we will see why in our case the login_url and the post_login_url are same.
post_logout_url: The URL where our user is redirected after logging out.
login_handler: The URL where actual login credentials are submitted. This is actually handled by repoze (credential check against the database and setting user session and cookie) so this can be any damn URL you want.
logout_handler: Similar to login_handler is the logout_handler which is handled by repoze. (It basically unsets the user session and the cookie). This can again be any URL.
cookie_secret: We need to provide repoze with a secret to create the cookie against. Since our secret can change for different environments, we add the cookie_secret parameter inside our development.ini [app:main] section and then use it while calling the setup_sql_auth function. Note that for this reason we also pass the config parameter inside the add_auth() function.
translations: Repoze, by default assumes that we use certain pre-defined column names in our table definitions (“user_name” in User table, “group_name” in Group table etc). But in our case, instead of “user_name’, we use “username” and instead of “group_name” we just use “name” for the same column. So we need to inform repoze to override its default assumptions with our custom naming conventions. We do this in the translations parameter dictionary.

Here is how our myproject/lib/auth.py looks like:

 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
from repoze.what.plugins.quickstart import setup_sql_auth
from myproject.model import meta
from myproject.model.auth import User, Group, Permission

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

    We're going to define post-login and post-logout pages
    to do some cool things.

    """
    # we need to provide repoze.what with translations as described here:
    # http://what.repoze.org/docs/plugins/quickstart/
    return setup_sql_auth(app, User, Group, Permission, meta.Session,
                login_url='/account/login',
                post_login_url='/account/login',
                post_logout_url='/',
                login_handler='/account/login_handler',
                logout_handler='/account/logout',
                cookie_secret=config.get('cookie_secret'),
                translations={
                    'user_name': 'username',
                    'group_name': 'name',
                    'permission_name': 'name',
                })

As promised lets add the cookie secret in development.ini [app:main] section

[app:main]
...
#set cookie secret for repoze.what
cookie_secret = 'your-own-secret'
...

Now add the middleware inside myproject/config/middleware.py. Note that we also pass the config variable to the add_auth function.

...
from myproject.lib.auth import add_auth
...
def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
    ...
    # Configure the Pylons environment
    config = load_environment(global_conf, app_conf)
    ...
    # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
    # Add the custom repoze.what middleware app here and pass it the config
    # variable
    app = add_auth(app, config)
...

Add an initial User, Group and Permission

Our middleware is now ready to be used. We will now add some data to our model. Lets create a permission and a group named “admin”. With this in place we can assign certain users to be administrators of our app. For the same reason we also create a user named “admin” and assign this user the “admin” group. We also create a user named “test” and don’t assign any group to this user.

Lets create these users in our myproject/websetup.py file. Note how we use “_set_password()” function to assign passwords to the users. If we set the passwords without using this function, our authentication system won’t work. One can also create the users manually using the built in pylons interpreter “paster shell”

 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
...
log.info("Adding initial users, groups and permissions...")
g = Group()
g.name = u'admin'
meta.Session.add(g)

p = Permission()
p.name = u'admin'
p.groups.append(g)
meta.Session.add(p)

u = User()
u.username = u'admin'
u.fullname = u'admin'
u._set_password('admin')
u.email = u'admin@example.com'
u.groups.append(g)
meta.Session.add(u)

u = User()
u.username = u'test'
u.fullname = u'test'
u._set_password('test')
u.email = u'test@example.com'
meta.Session.add(u)

meta.Session.commit()
...

Define the login action and template

We will now create a controller that will handle user login form and user authorization. Lets create a controller myproject.controllers.account using “paster controller account”

In our controller, we import certain predicates from repoze.what that allows us to handle user authorization.
The “has_permission()” predicate allows us to check if a user has certain permission like “admin”. The not_anonymous() predicate allows us to test whether a user is logged in to our system. Similarly there are other predicates available that we haven’t used here viz. has_all_permissions(), has_any_permission(), is_user(), in_all_groups(), is_anonymous(), in_any_group() and in_group().

We also import the ActionProtector decorator which works along with the predicates to evaluate if they are true. If not, it redirects the user back to the login form. Similar to ActionProtector is ControllerProtector which decorates the entire Controller instead of a specific action.

In our AccountController, we use the login action to render the login form. We had specifically instructed repoze to use the login action to render the login form. We had also informed repoze to redirect the user again to the login action after successful/unsuccessful login because in login action we redirect the user to the welcome page if she is logged in or just render the login form if her login was not successful. Note how we used the ActionProtector to protect the welcome action since we don’t want anonymous users to have access to this action.

We don’t write any fancy action like “see_you_later” for logout, since we asked repoze(using post_logout_url) to redirect the user to index page after logout.

We also create two test actions to test anonymous user(test_user_access) and admin user(test_admin_access) authoriztion.

Here is my controller myproject.controllers.account

 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
import logging
from pylons import request, response, session, tmpl_context as c, url
from pylons.controllers.util import abort, redirect
from myproject.lib.base import BaseController, render
from repoze.what.predicates import not_anonymous, has_permission
from repoze.what.plugins.pylonshq import ActionProtector
from pylons.controllers.util import redirect

log = logging.getLogger(__name__)

class AccountController(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 wrong credentials
        """
        identity = request.environ.get('repoze.who.identity')
        came_from = str(request.GET.get('came_from', '')) or \
                    url(controller='account', action='welcome')
        if identity:
            redirect(url(came_from))
        else:
            c.came_from = came_from
            c.login_counter = request.environ['repoze.who.logins'] + 1
            return render('/derived/account/login.html')

    @ActionProtector(not_anonymous())
    def welcome(self):
        """
        Greet the user if she logged in successfully or redirect back
        to the login form otherwise(using ActionProtector decorator).
        """
        identity = request.environ.get('repoze.who.identity')
        return 'Welcome back %s' % identity['repoze.who.userid']

    @ActionProtector(not_anonymous())
    def test_user_access(self):
        return 'You are inside user section'

    @ActionProtector(has_permission('admin'))
    def test_admin_access(self):
        return 'You are inside admin section'

We finally create a login.html template to render the login form inside myproject > templates > derived > account. The __logins parameter is used to count number of unsuccessful logins.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
% if c.login_counter > 1:
    Incorrect Username or Password
% endif
<form action="${h.url(controller='account', action='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" /><br />
<input type="submit" id="submit" value="Submit" />
</form>

This is it! Our basic authentication and authorization is now in place. You can try access the “test_user_access” and “test_admin_access” actions to test the whole flow.

Protecting Static Files

One out-of-the-box disadvantage of repoze.what compared to AuthKit is that by default, files served by StaticURLParser are not protected. By adding a simple conditional redirect, we can accomplish the same thing.

First, let's make a custom middleware class in myproject.lib.middleware

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
## lib/middleware.py
from repoze.what.predicates import not_anonymous

class RepozeMiddleware(object):
    def __init__(self, app, signin_url):
        self._app = app
        self._signin_url = signin_url
    
    def __call__(self, environ, start_response):
        # need to check path_info to avoid infinite loop
        if not_anonymous().is_met(environ) or environ['PATH_INFO'] == self._signin_url:
            return self._app(environ, start_response)
        else:
            status = "301 Redirect"
            headers = [("Location", self._signin_url),]
            start_response(status, headers)
            return ["Not logged in",]

Next, wrap the app. Here, I'm only wrapping it if we're serving static files. Otherwise, the Pylons app will be solely responsible for handling login/logout. Make sure that signin_url is the same as in myproject/lib/auth.py above - otherwise you'll get an infinite loop. Multiple definitions of constants like this is bad form, and both instances should be defined in a single location in your development.ini.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
## in config/middleware.py

from myproject.lib.middleware import RepozeMiddleware

# ...

    if asbool(static_files):
        # Serve static files
        static_app = StaticURLParser(config['pylons.paths']['static_files'])
        ## add a not_anonymous check to statics
        app = Cascade([static_app, app])
        app = RepozeMiddleware(app, '/account/login')
    ## not sure if order is important        
    app = add_auth(app, config)

Source:

http://sarafsaurabh.wordpress.com/2010/08/10/pylons-authentication-and-authorization-using-repoze-what/

Labels

authorization authorization Delete
repoze repoze Delete
pylons10 pylons10 Delete
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.
  1. Mar 22, 2011

    mani sabri says:

    tnx a lot it works. but I can't find out why every controller in my app redirect...

    tnx a lot it works. but I can't find out why every controller in my app redirects to the aacountcontroller login action and I won't be able to do anything until I login.

    1. Mar 23, 2011

      mani sabri says:

      well I figured out what was the problem. the proposed way for protecting static ...

      well I figured out what was the problem. the proposed way for protecting static files caused that.It seems that putting template css, js and image files in the public dir caused that so I think may be you should change the way you protect your static file by modifying the RepozeMiddleware _call_ method. Beware of the cache of the Firefox(3.6.15 win32)! it will prevent you to see the exact result even if you set it to 0 !! you have to clear it manually.


Powered by Pylons - Contact Administrators