September 06, 2010

shabti_auth – boilerplate authentication

The shabti_auth template includes the additions from the shabti default template (i.e. Elixir on SQLAlchemy) and adds boilerplate authentication and authorisation code as a basic design pattern to help developers get started.

“Boilerplate auth’n’auth” includes:

  • controller-level permission-handling
  • action-level permission handling (decorators)
  • authentication helpers
  • basic identity classes (User, Group and Permission)

The idea behind the shabti_auth template is to do the 80% hard/repetitive work that characterises most auth’n’auth requirements and to stay out of the developer’s way during the more intensive development of the last 20% of the task.

Note

shabti_auth source code is in the bitbucket code repository

How the boilerplate auth’n’auth works

The code for the boilerplate auth’n’auth is inherited virtually unchanged from the original Tesla code. It has the inestimable merit of being an elegantly straightforward approach to controlling user access to resources.

Access control is implemented by prepending an @authorize() decorator to the controller action for which access is to be controlled. The decorator wraps the controller action and handles both authentication (checking or requiring user sign-in) and authorisation (checking permission to access and/or change the state of resources). An example of the @authorize() decorator in use is shown below:

class DemoController(BaseController):

    @authorize(SignedIn())
    def index(self):
        users = Session.query(User).all()
        # [...]

Authentication and authorisation state is communicated to controller actions via the setting and/or unsetting of environment variables, the values of which are picked up later by the controller action when it is called by the @authorize() decorator.

/lib/decorators.py

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)

The True or False value returned by the permission instance’s check() method determines whether or not the controller action is executed. If the permission check succeeds (returned value True), the controller action is called and the result is returned.

If the permission check fails (returned value False) then the original URL (in pylons.routes_dict) is saved to session['redirect'], then the request is redirected to the login controller.

The login controller checks the values of the user sign-in environment variables (e.g. REMOTE_USER)

If the sign-in state is deemed acceptable, the request is simply routed to the original destination as retrieved from the session['redirect'] environment variable. If the sign-in state is missing, the request is routed to a sign-in function.

If the sign-in returns successfully then the request is routed to the login() function and because sign-in state is now available, the login() function routes the request to the original destination.

The @authorize() decorator takes an instance of a permission class as an argument; SignedIn and InGroup respectively in the examples shown above. An example set of basic permission classes are defined in lib/permissions.py.

Applying the @authorize() decorator to the controller’s __before__() action imposes access control on every action in the controller:

class DemoController(BaseController):

    @authorize(InGroup('administrators'))
    def __before__(self):
        pass

The Permissions

SignedIn

class SignedIn(object):

    def check(self):
        return (get_user() is not None)

InGroup

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

HasPermission

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

The Process

Signing in

The @authorize(SignedIn()) decorator wraps a demo/privindex action, rerouting all non-signed-in requests to the index action of the login controller. Successful sign-in results in the binding of two session variables:

  1. the user id is bound to the session variable AUTH_USER_ID, this variables both acts as an signedin stamp and provides a fast and convenient means of retrieving the corresponding User object via:

    user = request.environ['AUTH_USER']
    
  2. the routes dict is bound to the session variable redirect, rendering it accessible to the login function which, after successful authentication, retrieves the original destination from the session variable and redirects the user to it.

Signing out

Using the template

After successfully installing Shabti, additional paster templates will be available. Simply create a Shabti-configured project by specifying that paster should use the shabti template:

$ paster create -t shabti_auth myproj

These are the option dialogue choices appropriate for the Shabti auth template — which uses mako templates and SQLAlchemy ...

(mako/genshi/jinja/etc: Template language) ['mako']:
(True/False: Include SQLAlchemy 0.4 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 initialise the store by running the project setup script:

$ paster setup-app development.ini

If successful, the setup script will stream the log of database transactions to stdout, e.g.:

CREATE TABLE user_groups__group_users (
    user_id INTEGER NOT NULL,
    group_id INTEGER NOT NULL,
    PRIMARY KEY (user_id, group_id),
     CONSTRAINT user_groups_fk FOREIGN KEY(user_id)
                    REFERENCES user (id),
     CONSTRAINT group_users_fk FOREIGN KEY(group_id)
                    REFERENCES "group" (id)
)

Once the database has been initialised, start the Pylons web app with:

$ paster serve --reload development.ini

The Shabti standard auth’n’auth template’s variant on the standard Pylons welcome screen is browsable at at http://localhost:5000/ ...

Welcome screen

../_images/shabti_auth_welcome.jpg

shabti model unauthenticated

Follow /demo/index to see the public view:

../_images/shabti_auth_model_unauth.jpg

shabti signin

Follow the private link or go to /demo/privindex and get rerouted here.

(user=admin, pwd=admin)

../_images/shabti_auth_signin.jpg

shabti model authenticated

After successful authentication, you will be greeted with the following model view (which is almost identical to the unauthenticated view except for a crucial difference: the user identity is no longer “Anonymous”.)

../_images/shabti_auth_model_auth.jpg

Paster can be stopped and started and the authenticated state persists across invocations until /login/signout is visited or until:

$ rm -rf data/sessions/*

is executed. Preservation of authenticated state is cookie-based and on the server it is persisted via Beaker-stored sessions

Note

Authenticated state does not survive deletion of the cached session data.

Notes on the template

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 such as username and password.

class User(Entity):
    username = Field(Unicode(30), unique=True)
    password = Field(String(40))
    password_check = Field(String(40))
    email = Field(String(255))
    created = Field(DateTime)
    active = Field(Boolean)
    groups = ManyToMany('Group')

The Group is equally sparse in its modelling:

class Group(Entity):
    name = Field(Unicode(30))
    description = Field(Unicode(255))
    created = Field(DateTime)
    active = Field(Boolean)
    users = ManyToMany('User')
    permissions = ManyToMany('Permission')

Whilst the Permission is verging on the laconic:

class Permission(Entity):
    name = Field(Unicode(30))
    description = Field(Unicode(255))
    groups = ManyToMany('Group',
                         onupdate = 'CASCADE',
                         ondelete = 'CASCADE',
                         uselist = True)

Model development

It is the responsibility of the developer to add additional fields according to the domain requirements (e.g. address fields or phone number).

In addition, the default password encryption (using SHA1) may not be stringent enough to satisfy certain security requirements; if this is the case, then the template-generated code must then be rewritten in order to satisfy more stringent needs.

The Pylons HQ web site uses just such a more stringent approach (browse the complete controller code) in this password hash function — implemented as a @classmethod 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.

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__ protect all actions

    # @authorize(SignedIn())
    def __before__(self):
        pass

    def index(self):
        c.users = Session.query(User).all()
        c.groups = Session.query(Group).all()
        c.permissions = Session.query(Permission).all()
        c.title = 'Public'
        return render('test.mak')

    # Need to protect just a single action?
    # Do it like this ....
    @authorize(InGroup(administrators))
    def privindex(self):
        c.users = Session.query(User).all()
        c.groups = Session.query(Group).all()
        c.permissions = 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.mak')

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.

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()

An @authorize() decorator is provided, located in lib/decorators.py:

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)

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()
    [...]

template/demo.mak

Just an relevant auth-related snippet demonstrating that the database entity is fully available for 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