Repoze.who/Repoze.what with Pylons (step by step)
After working through quite a bit of the documentation on the pylons site and the repoze site, I didn’t really find a step by step guide to get repoze.who/repoze.what working with Pylons.
Some of the references used:
* http://code.gustavonarea.net/repoze.what-pylons/Manual/Protecting.html
* http://wiki.pylonshq.com/display/pylonscookbook/Authorization+with+repoze.what
Thanks to my nephew Qwait for overloading ActionProtector to intercept 403s and give an additional chance to authenticate.
wget http://pylonshq.com/download/1.0/go-pylons.py python go-pylons.py --no-site-packages pylons cd pylons source bin/activate easy_install repoze.what-quickstart easy_install repoze.what-pylons easy_install mysql-python paster create -t pylons project cd project
Because of the association table and the cascade, you must use MySQL or Postgresql rather than SQLite. You also might need to modify development.ini [server:main] host/port.
cd project
config/middleware.py:
# CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares) from project.lib.auth import add_auth app = add_auth(app, config)
config/routing.py:
map.connect('/login', controller='login', action='login') map.connect('/login/submit', controller='login', action='login_handler') map.connect('/login/continue', controller='login', action='post_login') map.connect('/logout/continue', controller='login', action='post_logout') map.connect('/logout', controller='login', action='logout_handler')
lib/auth.py: – modified to intercept 403 and provide chance to authenticate
from pylons import response, url from pylons.controllers.util import redirect from repoze.what.plugins.quickstart import setup_sql_auth from repoze.what.plugins import pylonshq import project.lib.helpers as h from project.model.meta import Session from project.model.auth import AuthUser, AuthGroup, AuthPermission def add_auth(app, config): return setup_sql_auth( app, AuthUser, AuthGroup, AuthPermission, Session, login_handler = '/login/submit', logout_handler = '/logout', post_login_url = '/login/continue', post_logout_url = '/logout/continue', cookie_secret = 'my_secret_word', translations = { 'user_name' : 'username', 'groups' : 'auth_groups', 'group_name' : 'name', 'permissions' : 'auth_permissions', 'permission_name' : 'name' } ) def redirect_auth_denial(reason): if response.status_int == 401: message = 'You are not logged in.' message_type = 'warning' else: message = 'You do not have the permissions to access this page.' message_type = 'error' h.flash(message, message_type) redirect(url('/login', came_from=url.current())) class ActionProtector(pylonshq.ActionProtector): default_denial_handler = staticmethod(redirect_auth_denial)
model/auth.py:
from sqlalchemy import * from sqlalchemy.databases import mysql from sqlalchemy.orm import relation, backref, synonym from sqlalchemy.orm.exc import NoResultFound from project.model.meta import Base import os from hashlib import sha1 from datetime import datetime group_permission_table = Table('auth_group_permissions', Base.metadata, Column('group_id', mysql.BIGINT(20, unsigned=True), ForeignKey('auth_groups.id', onupdate='CASCADE', ondelete='CASCADE')), Column('permission_id', mysql.BIGINT(20, unsigned=True), ForeignKey('auth_permissions.id', onupdate='CASCADE', ondelete='CASCADE')) ) user_group_table = Table('auth_user_groups', Base.metadata, Column('user_id', mysql.BIGINT(20, unsigned=True), ForeignKey('auth_users.id', onupdate='CASCADE', ondelete='CASCADE')), Column('group_id', mysql.BIGINT(20, unsigned=True), ForeignKey('auth_groups.id', onupdate='CASCADE', ondelete='CASCADE')) ) class AuthGroup(Base): __tablename__ = 'auth_groups' id = Column(mysql.BIGINT(20, unsigned=True), primary_key=True, autoincrement=True) name = Column(Unicode(80), unique=True, nullable=False) created = Column(mysql.DATE()) users = relation('AuthUser', secondary=user_group_table, backref='auth_groups') def __repr__(self): return '<group: name=%s>' % self.name def __unicode__(self): return self.name class AuthUser(Base): __tablename__ = 'auth_users' id = Column(mysql.BIGINT(20, unsigned=True), primary_key=True, autoincrement=True) username = Column(Unicode(80), nullable=False) _password = Column('password', Unicode(80), nullable=False) @property def permissions(self): perms = set() for g in self.groups: perms = perms | set(g.permissions) return perms def _set_password(self, password): 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() if not isinstance(hashed_password, unicode): hashed_password = hashed_password.decode('UTF-8') self._password = hashed_password def _get_password(self): return self._password password = synonym('_password', descriptor=property(_get_password, _set_password)) def validate_password(self, password): hashed_pass = sha1() hashed_pass.update(password + self.password[:40]) return self.password[40:] == hashed_pass.hexdigest() def __repr__(self): return '<user: id="%s" username="%s" email="%s">' % (self.id, self.username, self.email) def __unicode__(self): return self.username class AuthPermission(Base): __tablename__ = 'auth_permissions' id = Column(mysql.BIGINT(20, unsigned=True), primary_key=True, autoincrement=True) name = Column(Unicode(80), unique=True, nullable=False) description = Column(mysql.TEXT()) groups = relation(AuthGroup, secondary=group_permission_table, backref='auth_permissions') def __unicode__(self): return self.permission_name
controllers/login.py:
from pylons import request, response, session, tmpl_context, config, url from pylons.controllers.util import redirect from project.lib.base import BaseController, render from project.lib.helpers import flash class LoginController(BaseController): def login(self): login_counter = request.environ['repoze.who.logins'] if login_counter > 0: flash('Wrong credentials') tmpl_context.login_counter = login_counter tmpl_context.came_from = request.params.get('came_from') or url('/') return render('login.mako') def login_handler(self): pass def post_login(self): identity = request.environ.get('repoze.who.identity') came_from = str(request.params.get('came_from', '')) or url('/') if not identity: login_counter = request.environ['repoze.who.logins'] + 1 redirect(url('/login', came_from=came_from, __logins=login_counter)) redirect(came_from) def logout_handler(self): pass def post_logout(self): redirect('/')
templates/login.mako:
<% messages = h.flash.pop_messages() %> % if messages: <div class="flash"> % for message in messages: <p class="${message.category}">${message} % endfor </div> % endif <form action="${h.url('/login/submit', came_from=tmpl_context.came_from, __logins=tmpl_context.login_counter)}" method="POST"> <label for="login">Username:<input type="text" id="login" name="login" /><br /> <label for="password">Password:<input type="password" id="password" name="password" /> <input type="submit" value="Login" /> </form>
controllers/root.py:
from pylons import request, response, session, tmpl_context, config #from repoze.what.plugins.pylonshq import ActionProtector, ControllerProtector from project.lib.auth import ActionProtector from repoze.what.predicates import is_user, has_permission, in_group from project.lib.base import BaseController, render class RootController(BaseController): def index(self): return render('index.mako') @ActionProtector(is_user('test')) def user(self): return render('loggedin.mako') @ActionProtector(is_user('nottest')) def notuser(self): return render('loggedin.mako') @ActionProtector(in_group('admin')) def admin(self): return render('loggedin.mako') @ActionProtector(has_permission('edit')) def edit(self): return render('loggedin.mako')
lib/helpers.py:
from pylons import url from webhelpers.pylonslib import Flash as _Flash flash = _Flash()
websetup.py: – after the Session, Base import:
from project.model.auth import *
Setup/Create the database, start paster
paster setup-app development.ini paster serve --reload development.ini
createuser.py:
#!/usr/bin/python2.6 from sqlalchemy import create_engine engine = create_engine('mysql://user:pass@localhost/dbname', echo=True) from sqlalchemy.orm import sessionmaker Session = sessionmaker(bind=engine) session = Session() from project.model.auth import * u = AuthUser() u.username = u'test' u.password = u'test' session.add(u) g = AuthGroup() g.name = u'admin' g.users.append(u) session.add(g) p = AuthPermission() p.name = u'edit' p.groups.append(g) session.add(p) session.commit()