September 06, 2010
In this template the familiar auth’n’auth-supporting identity model is expressed in SQLAlchemy’s declarative modelling DSL and is presented for administration via the FormAlchemy model WebUI (see included screenshots for visual detail of the WebUI).
This is a fairly lightweight template that implements the standard authentication and authorization approach to be seen in other Shabti auth'n'auth templates. The “humanoid” template foregoes the abstractional modelling power of elixir in order to instead explore the delights of SQLAlchemy’s declarative modelling DSL which offers some selected modelling abstractions above that of the vanilla SQLAlchemy ORM.
The template takes full advantage of the FormAlchemy model WebUI to achieve a useful economy of expression.
Warning
In an effort to cut to the chase with respect to password cryptography and achieve maximum developer comfort, this template uses Blowfish password hashing courtesy of the bcrypt library. This introduces a necessary dependency (see “Dependencies”, below).
Note
shabti_humanoid source code is in the bitbucket code repository
The following description is taken from the Formalchemy project web site ...
“FormAlchemy greatly speeds development with SQLAlchemy mapped classes (models) in a HTML forms environment.
FormAlchemy eliminates boilerplate by autogenerating HTML input fields from a given model. FormAlchemy will try to figure out what kind of HTML code should be returned by introspecting the model’s properties and generate ready-to-use HTML code that will fit the developer’s application.
Of course, FormAlchemy can’t figure out everything, i.e, the developer might want to display only a few columns from the given model. Thus, FormAlchemy is also highly customizable.”
See also
For further details, see the project documentation
In order to use the template, you need to easy-install bcrypt, easy-install FormAlchemy and also easy-install fa.jquery, (an accompanying FormAlchemy JQuery enhancement package).
After successfully installing Shabti, additional paster templates will be available. Simply create a Shabti-configured project by specifying that paster should use the shabti_humanoid template:
$ paster create -t shabti_humanoid myproj
These are the option dialogue choices appropriate for the Shabti humanoid template — which uses mako templates and (obviously, for this application), SQLAlchemy is not optional ...
(mako/genshi/jinja/etc: Template language) ['mako']:
(True/False: Include SQLAlchemy 0.5 configuration) [False]: True
(True/False: Setup default appropriate for Google App Engine) [False]:
Once the project has been created, navigate to the project directory and run the (brief) test suite:
$ nosetests
8 tests should be executed successfully. If the tests succeed, the next step is to initialise the back-end store by running the project setup script:
$ paster setup-app development.ini
Once the store has been inialised, start the Pylons web app with:
$ paster serve --reload development.ini
The Shabti humanoid auth’n’auth template’s variant on the standard Pylons welcome screen is browsable at at http://localhost:5000/ ...
Unauthenticated GETs attempting to access the protected /demo/privindex URL are automatically diverted to the sign-in page...
and, on successful authentication, users are automatically forwarded to the original destination...
Paster can be stopped and started and the authenticated state persists across paster invocations of the app until either the user trashes the state by visiting /login/signout or, on the server, the session cache is deleted:
$ rm -rf data/sessions/*
Preservation of authenticated state is cookie-based and on the server it is persisted via Beaker-stored sessions
model/user.py
The identity model is basically functional but presents a deliberately simple profile because it is mainly intended to act as a starting point for further development.
For example, the User class only has the bare minimum of fields
class User(Base):
"""
Reasonably basic User definition. Probably would want additional
attributes."""
__tablename__ = 'user'
user_id = Column(Integer, autoincrement=True, primary_key=True)
username = Column(Unicode(16), unique=True)
displayname = Column(Unicode(255))
email = Column(Unicode(255))
_password = Column('password', Unicode(80))
password_check = Column(Unicode(80))
active = Column(Boolean(), default=False)
created = Column(DateTime(), default=datetime.datetime.utcnow())
The Group is equally sparse in its modelling:
class Group(Base):
"""An ultra-simple group definition."""
__tablename__ = 'group'
group_id = Column(Integer, autoincrement=True, primary_key=True)
name = Column(Unicode(16), unique=True)
description = Column(Unicode(255))
active = Column(Boolean(), default=False)
created = Column(DateTime(), default=datetime.utcnow())
users = relation('User', secondary=user_group_table,
backref='groups')
Whilst the Permission is verging on the laconic:
class Permission(Base):
"""A relationship that determines what each Group can do"""
__tablename__ = 'permission'
permission_id = Column(Integer, autoincrement=True, primary_key=True)
name = Column(Unicode(16), unique=True)
description = Column(Unicode(255))
groups = relation(Group, secondary=group_permission_table,
backref='permissions')
It is the responsibility of the developer to add additional fields according to the domain requirements (e.g. address fields or phone number).
A gradual concensus is forming which sees the default (and elderly) Shabti password encryption (using SHA1) as insufficiently stringent to satisfy some important security requirements and a more stringent approach is encouraged.
The Pylons HQ web site adopts just such a more stringent approach (browse the complete controller code) in this password hash function — implemented as a @staticmethod of the User model entity:
@staticmethod
def hash_password(plain_text):
"""Returns a crypted/salted password field
The salt is stored in front of the password, for per user
salts.
"""
if isinstance(plain_text, unicode):
plain_text = plain_text.encode('utf-8')
password_salt = sha.new(os.urandom(60)).hexdigest()
crypt = sha.new(plain_text + password_salt).hexdigest()
return password_salt + crypt
The addition of a salt renders the encryption appreciably stronger.
So, for the identity model in the Shabti humanoid template, a even more stringent approach has been taken, that of completely replacing sha with bcrypt:
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
hashed_password = bcrypt.hashpw(password_8bit, bcrypt.gensalt())
# Make sure the hashed 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
controllers/demo.py
When the architecture of access is sympathetic to the information architecture of the site, the @authorize() decorator fits naturally into place (details of the decorator code appear in a later section):
class DemoController(BaseController):
# Need to protect an entire controller?
# Decorating __before__ protects all actions
# @authorize(SignedIn())
def __before__(self):
pass
def index(self):
c.users = meta.Session.query(User).all()
c.groups = meta.Session.query(Group).all()
c.permissions = meta.Session.query(Permission).all()
# If signed in, get details
c.user = auth.get_user()
c.title = 'Public'
return render('test.mako')
# Need to protect just a single action?
# Do it like this ....
@authorize(SignedIn())
def privindex(self):
c.users = meta.Session.query(User).all()
c.groups = meta.Session.query(Group).all()
c.permissions = meta.Session.query(Permission).all()
# Use this for obviousness
# c.user = auth.get_user()
# or this for directness
c.user = request.environ['AUTH_USER']
c.title = 'Private'
return render('test.mako')
When the content permissions architecture cuts across the information architecture and into the marshalling of the model data, permissions checks at controller and action level are too coarse, more finely-grained sub-action permissions tests are required. Three handy authorisation helpers are provided ready for use:
# Auth helpers
def signed_in():
return permissions.SignedIn().check()
def in_group(group_name):
return permissions.InGroup(group_name).check()
def has_permission(perm):
return permissions.HasPermission(perm).check()
The various permission objects used in the helpers are separated out to a dedicated source file:
lib/permissions.py
The template creates a set of permission classes (located in lib/permissions.py in the Shabti application package) which can be used at the controller, action or sub-action level, or even as helpers in templates.
# Common permissions. Permission classes must have a 'check'
# method which returns True or False.
class SignedIn(object):
def check(self):
return (get_user() is not None)
class InGroup(object):
def __init__(self, group_name):
self.group_name = group_name
def check(self):
group = model.Group.filter_by(
name = self.group_name, active = True)
if group and get_user() in group.members:
return True
return False
class HasPermission(object):
def __init__(self, permission):
self.permission = permission
def check(self):
user = get_user()
if user and user.has_permission(self.permission):
return True
return False
These classes can be easily extended. Custom alternatives can be created in conjunction with the three Elixir identity classes (User, Group and Permission) that store the relevant authentication and authorization information to the database (they are located in lib/model/identity.py).
lib/decorators.py @authorize() / @in_group()
Two types of approach are provided in lib/decorators.py: the @authorize() decorator inherited by Shabti from the original tesla-pylons-elixir project and a new “Solon” approach taken in Ben Bangert’s “Kai”, the Pylons app that implements PylonsHQ:
# Approach #1: "authorize" decorator, inherited from tesla-pylons-elixir:
# http://bit.ly/8PplKF
def authorize(permission):
"""Decorator for authenticating individual actions. Takes a permission
instance as argument(see lib/permissions.py for examples)"""
def wrapper(func, self, *args, **kw):
if permission.check():
return func(self, *args, **kw)
pylons.session['redirect'] = \
pylons.request.environ['pylons.routes_dict']
pylons.session.save()
redirect_to_login()
return decorator(wrapper)
# Approach #2, "Solon decorators", copied over from kai,
# the Pylons app that implements PylonsHQ:
# http://bitbucket.org/bbangert/kai/src/tip/kai/lib/decorators.py
def in_group(group):
"""Requires a user to be logged in, and the group specified"""
def wrapper(func, *args, **kwargs):
user = pylons.tmpl_context.user
if not user:
log.debug("No user logged in for permission restricted function")
abort(401, "Not Authorized")
if user.in_group(group):
log.debug("User %s verified in group %s", user, group)
return func(*args, **kwargs)
else:
log.debug("User %s not in group %s", user, group)
abort(401, "Not Authorized")
return decorator(wrapper)
@decorator
def logged_in(func, *args, **kwargs):
if not pylons.tmpl_context.user:
abort(401, "Not Authorized")
else:
return func(*args, **kwargs)
and which can be used in conjunction with the permissions objects to provide controller and action-level permissions checking (mentioned previously but an example included here for narrative convenience):
# controller/person.py
@authorize(SignedIn())
def index(self):
users = Session.query(User).all()
[...]
And an example of “Solon” decorator use:
@in_group('admin')
def new(self):
return render('/articles/new.mako')
The Kai source code (e.g. articles.py) provides other examples of @in_group() usage.
template/demo.mak
Just a relevant auth-related snippet demonstrating that the database entity is conveniently available during template processing:
<p>
<a href="/demo/index">Public</a> ::
<a href="/demo/privindex">Private</a>
</p>
<p>
Welcome ${c.user.username if c.user else "Anonymous"|n}.
<a href="/login/signout">Sign out</a> ::
<a href="/login/signin">Sign in</a>.
</p>
author: | Graham Higgins <gjh@bel-epa.com> |
---|
September 06, 2010