For one of our projects we used Pylons 1.0 with Repoze.who/Repoze.what, though, we only used groups without permissions. In our application, a member could access one of three controllers based on their membership in a group. There are a number of methods that can be used to set up authentication. One method is to create a thin table with the Primary Key, Username and Password and use an association table to add any additional information. This has the ability to be flexible when you need to add a number of fields that shouldn’t be contained within the AuthUser table. Another method is to use the AuthUser table to hold the related information. This example uses the latter method.
The first changes made are to __init__.py to add the policies and Forbidden view. This view allows us to prompt a user for a username/password when they visit a page that is protected by the ACLs in Pyramid.
auth.py contains our authentication model with the Permission models removed. We put the login/logout views, forbidden view along with the groupfinder and the RootFactory in login.py to consolidate authentication into an auth.py and login.py file for installations in other applications. Last but not least is a simple template to present the user with a login page.
For our RootFactory, we’ve defined three groups, client, manager and admin. Within our __init__.py (or our views if we use add_handler), we can restrict permissions using Pyramid’s authentication.
In our __init__.py, we can use the permission= to specify membership in a group.
config.add_route('admin2', '/admin/', view='project.admin.index', permission='admin', view_renderer='admin_index.jinja2')
If we have used add_handler:
config.add_route('admin2', '/admin/', view='project.admin.index', view_renderer='admin_index.jinja2')
We can use the permissions ACL in the @action decorator:
@action(renderer='admin_index.jinja2', permission='admin')
To access the userid in your views:
from pyramid.security import authenticated_userid
userid = authenticated_userid(request)
At this point, we’ve migrated a Repoze.who/Repoze.what authentication scheme that only used Group membership as its criteria and we have an SQL backed authentication system under Pyramid.
Most of the guidance for this came from:
* http://docs.pylonshq.com/pyramid/dev/tutorials/wiki2/authorization.html
Modifications to __init__.py:
Added to the import section:
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from project.login import forbidden_view, groupfinder
from pyramid.exceptions import Forbidden
Added to the Configurator:
authn_policy = AuthTktAuthenticationPolicy(
'sosecret', callback=groupfinder)
authz_policy = ACLAuthorizationPolicy()
config = Configurator(settings=settings,
root_factory='project.login.RootFactory',
authentication_policy=authn_policy,
authorization_policy=authz_policy)
Added within the begin()/end() block of the Configurator
config.add_view(forbidden_view, context=Forbidden)
config.add_route('login', '/login',
view='project.login.login',
view_renderer='project:templates/login.pt')
config.add_route('logout', '/logout',
view='project.login.logout',)
auth.py:
import transaction
from sqlalchemy import create_engine
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import Unicode
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker
from zope.sqlalchemy import ZopeTransactionExtension
DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
Base = declarative_base()
from sqlalchemy import *
from sqlalchemy.databases import mysql
from sqlalchemy.orm import relation, backref, synonym
from sqlalchemy.orm.exc import NoResultFound
import os
from hashlib import sha1
from datetime import datetime
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 '' % 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)
email = Column(Unicode(80), nullable=False)
contact = Column(Unicode(80), nullable=False)
company = Column(Unicode(80), nullable=False)
groups = relation('AuthGroup', secondary=user_group_table, backref='auth_users')
@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 '' % (self.id, self.username, self.email)
def __unicode__(self):
return self.username
login.py:
from pyramid.httpexceptions import HTTPFound
from pyramid.security import remember
from pyramid.security import forget
from pyramid.security import Allow
from pyramid.security import Everyone
from pyramid.url import route_url
from pyramid.renderers import render_to_response
from project.auth import AuthUser
from project.models import DBSession
def login(request):
dbsession = DBSession()
login_url = route_url('login', request)
referrer = request.url
if referrer == login_url:
referrer = '/' # never use the login form itself as came_from
came_from = request.params.get('came_from', referrer)
message = ''
login = ''
password = ''
if 'form.submitted' in request.params:
login = request.params['login']
password = request.params['password']
auth = dbsession.query(AuthUser).filter(AuthUser.username==login).first()
if auth.validate_password(password):
headers = remember(request, login)
"""
We use the Primary Key as our identifier once someone has authenticated rather than the
username. You can change what is returned as the userid by altering what is passed to
remember.
"""
#headers = remember(request, auth.id)
return HTTPFound(location = came_from,
headers = headers)
message = 'Failed login'
return dict(
message = message,
url = request.application_url + '/login',
came_from = came_from,
login = login,
password = password,
)
def logout(request):
headers = forget(request)
return HTTPFound(location = route_url('root', request),
headers = headers)
def forbidden_view(request):
login_url = route_url('login', request)
referrer = request.url
if referrer == login_url:
referrer = '/' # never use the login form itself as came_from
came_from = request.params.get('came_from', referrer)
return render_to_response('templates/login.pt', dict(
message = '',
url = request.application_url + '/login',
came_from = came_from,
login = '',
password = '',
), request=request)
def groupfinder(userid, request):
dbsession = DBSession()
auth = dbsession.query(AuthUser).filter(AuthUser.id==userid).first()
if auth:
return [('group:%s' % group.name) for group in auth.groups]
class RootFactory(object):
__acl__ = [ (Allow, 'group:client', 'client'),
(Allow, 'group:manager', 'manager'),
(Allow, 'group:admin', 'admin') ]
def __init__(self, request):
self.__dict__.update(request.matchdict)
templates/login.pt:
<!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:tal="http://xml.zope.org/namespaces/tal">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
<title>Authentication Test</title>
<link rel="stylesheet" type="text/css"
href="${request.application_url}/static/style.css" />
</head>
<body>
<h1>Log In</h1>
<div tal:replace="message"/>
<div class="main_content">
<form action="${url}" method="post">
<input type="hidden" name="came_from" value="${came_from}"/>
<input type="text" name="login" value="${login}"/>
<br/>
<input type="password" name="password" value="${password}"/>
<br/>
<input type="submit" name="form.submitted" value="Log In"/>
</form>
</div>
</body>
</html>