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