.. _shabti_auth: Shabti's auth'n'auth |today| .. _shabti-auth: **shabti_auth** -- boilerplate authentication ============================================= .. rubric :: The ``shabti_auth`` template includes the additions from the :ref:`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 (:class:`User`, :class:`Group` and :class:`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 :func:`@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 :func:`@authorize()` decorator in use is shown below: .. code-block :: python 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 :func:`@authorize()` decorator. **/lib/decorators.py** .. code-block :: python 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 :meth:`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 :func:`login()` function and because sign-in state is now available, the :func:`login()` function routes the request to the original destination. The :func:`@authorize()` decorator takes an instance of a permission class as an argument; :class:`SignedIn` and :class:`InGroup` respectively in the examples shown above. An example set of basic permission classes are defined in ``lib/permissions.py``. Applying the :func:`@authorize()` decorator to the controller's :meth:`__before__` action imposes access control on every action in the controller: .. code-block :: python class DemoController(BaseController): @authorize(InGroup('administrators')) def __before__(self): pass The Permissions ^^^^^^^^^^^^^^^ SignedIn ++++++++ .. code-block :: python class SignedIn(object): def check(self): return (get_user() is not None) InGroup ++++++++ .. code-block :: python 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 +++++++++++++ .. code-block :: python 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 :class:`User` object via: .. code-block :: python 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: .. code-block :: bash $ paster create -t shabti_auth myproj These are the option dialogue choices appropriate for the Shabti auth template --- which uses mako templates and SQLAlchemy ... .. code-block :: bash (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: .. code-block :: bash $ paster setup-app development.ini If successful, the setup script will stream the log of database transactions to stdout, e.g.: .. code-block :: sql 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: .. code-block :: bash $ 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 ^^^^^^^^^^^^^^ .. image :: images/shabti_auth_welcome.jpg shabti model unauthenticated ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Follow ``/demo/index`` to see the public view: .. image :: 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) .. image :: 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".) .. image :: 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: .. code-block :: bash $ 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 :class:`User` class only has the bare minimum of fields such as username and password. .. code-block :: python 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 :class:`Group` is equally sparse in its modelling: .. code-block :: python 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 :class:`Permission` is verging on the laconic: .. code-block :: python 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: .. code-block :: python @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. .. __: http://docs.python.org/library/sha.html **controllers/demo.py** When the architecture of access is sympathetic to the information architecture of the site, the :func:`@authorize` decorator fits naturally into place (details of the decorator code appear in a later section): .. code-block :: python 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: .. code-block :: python # 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 :file:`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. .. code-block :: python 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 (:class:`User`, :class:`Group` and :class:`Permission`) that store the relevant authentication and authorization information to the database (they are located in :file:`lib/model/identity.py`). **lib/decorators.py** :func:`@authorize` An :func:`@authorize` decorator is provided, located in :file:`lib/decorators.py`: .. code-block :: python 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): .. code-block :: python # 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: .. code-block :: html+mako

Public :: Private

Welcome ${c.user.username if c.user else "Anonymous"|n}. Sign out :: Sign in.

:author: Graham Higgins |today|