This is a conversation that I had with John Miller. He has agreed to
let me post it here so others can benefit.
All comments/questions are welcome. I hope other find this useful.
Krys
--------
Sure :-)
I have attached the model I use as well as my auth.py module that has
the decorators and my auth.py controller (you should rename the files
once they are in their directories :-).
The name of my app is called Marvin. It houses sub-apps, so it's more
of a simple Django-style app container.
The system works like this:
@require_login
@turbogears.expose(html=<your template here>)
def page(self, ...)
# do something
or
@require_role('some_role')
@turbogears.expose(html=<your template here>)
def page(self, ...)
# do something
require_login checks to see of there is a 'user_name' key in the session
variable. If not, it redirects to /login/<original path>, which should
authenticate and redirect back to the original path.
require_role first does require_login, and then checks that the logged
in user has the requested role. if not, it redirects to /denied.
The password_hash function uses PyCrypto's SHA256 module/class, but the
build in md5 work identically (except it returns 32-byte hashes instead
of SHA256's 64 bytes.)
The controller has 3 classes Login, Logout and Denied which I attach to
my app's Root directly. Login shows a login form and checks that the
user exists and matched the password hash. If all is good, it redirects
to the destination. There is a default method there that handles the
destination as a sub-path of login.
Logout just deletes the user_name session variable and shoes a logged
out page. Denied just shoes an access denied page.
My templates are kind of customized so I am only including examples here:
login.kid
<p py:if="warning" py:content="warning" class="bad">Warning message
here.</p>
<form method="post" action=".">
<input type="hidden" name="destination"
py:attrs="value=destination" />
<label for="user_name">User Name:</label>
<input type="text" name="user_name" id="user_name"
py:attrs="value=user_name"/>
<br />
<label for="password">Password:</label>
<input type="password" name="password" id="password"/>
<input type="submit" name="login" value="Login" />
</form>
logout.kid:
<p>You have been successfully logged out.</p>
denied.kid:
<p>Sorry, you are not authorized to access this resource.</p>
Finally, please excuse any bad coding style or bugs, this is certainly
not tested code, but it is simple. That said I like what Lethalman is
doing with authentication too. Once TG gets a standardized
authentication system (or at lease a popular one), I will be seriously
looking to replace my code.
All this said, I welcome all feedback and if others can benefit form my
code, then that makes me happy.
Hope this helps,
Krys
P.S. Mind if I post this message to the TG mailing list?
John Miller wrote:
>Krys Wilken wrote:
>
>
>>For my project I just created a couple tables, Users and Roles and built
>>a very simple authentication system consisting of 2 decorators,
>>require_login and require_role. This meets my needs right now, as I
>>prefer form-based logins over http password pop-ups (basic and digest).
>>However it would be nice to have access to digest and ntlm over ssl.
>>Plugging into Apache could solve that.
>>
>>Anyway, I am just throwing out the idea in case others are trying to
>>figure out a simple way to do things. I'd be willing to share the code
>>if anyone is interested
>>
>>
>
>I'm interested! I'm quite the beginner, but I like the idea of a simple
>login system for TurboGears. Would you be willing to share how you set
>this up with me? Thanks!
>
>John Miller
>
>
>
>
"""This module provides common data objects for all applications, like User and Role."""
from sqlobject import *
from turbogears.database import AutoConnectHub
hub = AutoConnectHub()
class Role(SQLObject):
"""A Role represents is certain permission. Users have roles that define what they are allowed to do."""
_connection = hub
name = StringCol(unique = True, notNone = True, alternateID = True, length = 10)
description = StringCol()
users = RelatedJoin('User')
@staticmethod
def create_default_roles():
"""Creates default roles that are expected to exist for all applications, including Marvin itself."""
Role(name = 'site_admin', description = 'This role is for administrators of the whole Marvin site.')
class User(SQLObject):
"""A User is someone who has a Marvin account and is not anonymous (i.e. has authenticated themself)."""
_connection = hub
class sqlmeta:
table = 'user_' # user is a reserved work in PostgreSQL so we use user_.
user_name = StringCol(unique = True, notNone = True, alternateID = True, length = 10)
password_hash = StringCol(length = 64)
first_name = StringCol(notNone = True, length = 30)
middle_initials = StringCol(length = 3, default = None)
last_name = StringCol(notNone = True, length = 30)
phone_number = StringCol(notNone = True, length = 20)
email_address = StringCol(notNone = True, length = 30)
roles = RelatedJoin('Role')
"""Provides the user interfaces for authentication."""
import turbogears
import cherrypy
from sqlobject import SQLObjectNotFound
from marvin.model import User
from marvin.auth import hash_password
class Login:
"""The /login path. Provides the user interface for logging in."""
@turbogears.expose(html="marvin.templates.auth.login")
def index(self, destination = None, user_name = None, password = None, login = None):
"""The login page. Authenticates user and sets user_name session key."""
if destination is None: # if no destination specified, assume root.
destination = '/'
context = {'destination': destination, 'user_name': user_name, 'warning': ''}
if not user_name: # no POST data or empty user_name, so show login page.
return context
else: # else, user_name provided.
try: # Get user , if they exist.
user = User.byUser_name(user_name)
except SQLObjectNotFound: # User does not exist.
context['warning'] = 'User "%s" not found. Please try again.' % user_name
return context
if hash_password(password) != user.password_hash: # User exists, check the passowrd.
context['warning'] = 'Invalid password. Please try again.' # Bad password
return context
cherrypy.session['user_name'] = user_name # password ok, return requested destination.
raise cherrypy.HTTPRedirect(destination)
@turbogears.expose()
def default(self, *args, **kwargs):
kwargs.setdefault('destination', '/' + '/'.join(args))
return self.index(**kwargs)
class Logout:
"""The /logout path. Provides the user interface for logging out."""
@turbogears.expose(html="marvin.templates.auth.logout")
def index(self):
"""The logout page. Removes the user_name session key."""
if cherrypy.session.has_key('user_name'):
del cherrypy.session['user_name']
return {}
class Denied:
"""The /denied path. Provides the access denied page."""
@turbogears.expose(html="marvin.templates.auth.denied")
def index(self):
"""The "Access Denied" page."""
return {}
"""This module provides functions and decorators for manging authorization."""
from Crypto.Hash import SHA256 as hash
import cherrypy
from sqlobject import SQLObjectNotFound
from marvin.model import User, Role
def _login_check():
"""Check if the session has the user_name keys, otherwise redirect to the login page."""
if not cherrypy.session.has_key('user_name'):
raise cherrypy.HTTPRedirect('/login%s' % cherrypy.request.path) # path has a leading /
def _role_check(role_name):
"""Check if the logged in user has the given role, otherwise redirect ot an "access denied" page."""
user_name = cherrypy.session['user_name']
try:
user = User.byUser_name(user_name)
except SQLObjectNotFound: # if the session has a user name, but the uer does not exist, then reset the session.
del cherrypy.session['user_name']
raise cherrypy.HTTPRedirect(cherrypy.request.path)
try:
role = Role.byName(role_name)
if role not in user.roles:
raise SQLObjectNotFound
except SQLObjectNotFound: # if the requested role does not exist or the user does not have that role, then deny access.
raise cherrypy.HTTPRedirect('/denied/')
def hash_password(password):
"""Returns the password hash for the given password.
This should be the only function the needs changing to implement a newer hash algorithm.
"""
return hash.new(password).hexdigest()
def require_login(function):
"""Decorator that redirects annonymous users to the login page."""
def wrapper(*args, **kwargs):
_login_check()
return function(*args, **kwargs)
wrapper.exposed = True
return wrapper
def require_role(role_name):
"""Decorator that that redirects users who do not have role to an "access denied" page."""
def decorator(function):
def wrapper(*args, **kwargs):
_login_check() # Make sure the user is logged in first.
_role_check(role_name)
return function(*args, **kwargs)
wrapper.exposed = True
return wrapper
return decorator