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

Reply via email to