.. _shabti_auth_repozewho: |today| .. _shabti-auth-repozewho: **shabti_auth_repozewho** -- authentication using repoze.who ============================================================ .. rubric :: This template brings repoze.who's authentication and repoze.what's authorisation functions to Pylons. Gustavo Narea's ``quickstart`` plugin for repoze.who usefully integrates repoze.who authentication with the standard Shabti identity model expressed in elixir, making this template immediately usable for extension and development. About repoze.who auth --------------------- Chris McDonough's ``repoze.who`` runs as middleware. The overview shown below is lifted straight from the `repoze.who docs `_ . .. note :: **Overview** repoze.who is an identification and authentication framework for arbitrary WSGI applications. It acts as WSGI middleware. repoze.who is inspired by Zope 2’s Pluggable Authentication Service (PAS) (but repoze.who is not dependent on Zope in any way; it is useful for any WSGI application). It provides no facility for authorization (ensuring whether a user can or cannot perform the operation implied by the request). This is considered to be the domain of the WSGI application. It attempts to reuse implementations from paste.auth for some of its functionality. This template follows the same approach as the basic :ref:`shabti-auth` template and re-uses the majority of the code in that template. User authentication is handled by the middleware ``repoze.who`` module. After the user performs a successful authentication, the ``request.environ`` dict gains a new key: ``repoze.who.identity``, itself a dictionary with the user key retrieving the corresponding elixir model entity object, this binding persists until the user explicitly signs off. The :func:`@authorize` decorator from :ref:`shabti-auth` is swapped for a :func:`@require` decorator stolen^W adapted from TurboGears2 and is immediately available for use in applying authorization constraints on access to controllers and/or actions. Persistence of user, group and permission data is handled in the application's elixir-mediated model and is amenable to manipulation via the usual REST controller approach. .. note :: shabti_auth_repozewho source code is in the `bitbucket code repository `_ (*Credit where credit's due Dept: most of this template is based on* `Gustavo Narea `_'s *work on the repoze plugins and the successful functioning of the template application is due to key advice given by* `Chris McDonough `_.) 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 auth repozewho template: .. code-block :: bash $ paster create -t shabti_auth_repozewho 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 repoze.who auth template's variant on the standard Pylons welcome screen is browsable at at ``http://localhost:5000/`` ... Welcome screen ^^^^^^^^^^^^^^ .. image :: images/shabti_auth_repozewho_welcome.jpg signin page ^^^^^^^^^^^ .. image :: images/shabti_auth_repozewho_signin.jpg shabti model unauthenticated ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. image :: images/shabti_auth_repozewho_model_unauth.jpg shabti model authenticated ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. image :: images/shabti_auth_repozewho_model_auth.jpg Authentication and authorisation -------------------------------- Authentication via GETting a standard login page ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In the case of a standard login page, requests directed to the URL are detected by the repoze.who middleware (when appropriately configured) which intercepts the request and handles the login process, directly accessing the Elixir identity model and validating the entered password. Upon successful authentication the user is re-routed to a (configured) post-login URL. Successful authentication sequence for a standard login page ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Lengthy but highly informative as it shows the various stages of the repoze.who/what middleware process of relating plugin resources to the characteristics and requirements of the request and the particular type of authentication in play (authenticating an XMLRPC request would require different handling to that of a browser-originated requeste) Rendering the login form .. code-block :: text -- repoze.who request started (/login/index) -- request classification: browser identifier plugins registered [, ] identifier plugins matched for classification "browser": [, ] no identity returned from (None) no identity returned from (None) identities found: [] no identities found, not authenticating no challenge required -- repoze.who request ended (/login/index) -- Going to to /login/signin (as configured to do so) with username and password entered in the login form. .. code-block :: text -- repoze.who request started (/login/signin) -- request classification: browser identifier plugins registered [, ] identifier plugins matched for classification "browser": [, ] identity returned from : {'login': 'admin', 'password': 'admin'} no identity returned from (None) identities found: [(, {'login': 'admin', 'password': 'admin'})] The authenticator plugin uses SQLachemy to access the identity model, perform password validation and retrieve the entity from the database. .. code-block :: text authenticator plugins registered [] authenticator plugins matched for classification "browser": [] userid returned from : "admin" identities authenticated: [((0, 0), , , {'login': 'admin', 'password': 'admin', 'repoze.who.userid': 'admin'}, 'admin')] Group and Permissions information is also retrieved from the database and the entity is fully populated with data. Three cookies are now available for use as state-preserving store. .. code-block :: text User belongs to the following groups: (u'Administrators',) User has the following permissions: () static downstream application replaced with 302 Found The resource was found at /login/welcome?__foo=0 no challenge required remembering via headers from : [('Set-Cookie', 'authtkt=d075d12ec8606124b4f6adeb8838ade049923f60admin!; Path=/'), ('Set-Cookie', 'authtkt=d075d12ec8606124b4f6adeb8838ade049923f60admin!; Path=/; Domain=localhost:5000'), ('Set-Cookie', 'authtkt=d075d12ec8606124b4f6adeb8838ade049923f60admin!; Path=/; Domain=.localhost:5000')] -- repoze.who request ended (/login/signin) -- Login was successful, cookies baked and consumed, so the user is re-routed to the post-login URL. .. code-block :: text -- repoze.who request started (/login/welcome) -- request classification: browser identifier plugins registered [, ] identifier plugins matched for classification "browser": [, ] no identity returned from (None) identity returned from : {'tokens': [''], 'timestamp': 1234321248, 'repoze.who.userid': 'admin', 'userdata': ''} identities found: [(, {'tokens': [''], 'timestamp': 1234321248, 'repoze.who.userid': 'admin', 'userdata': ''})] authenticator plugins registered [] authenticator plugins matched for classification "browser": [] userid preauthenticated by : "admin" (repoze.who.userid set) identities authenticated: [((0, 0), None, , {'tokens': [''], 'timestamp': 1234321248, 'repoze.who.userid': 'admin', 'userdata': ''}, 'admin')] User belongs to the following groups: (u'Administrators',) User has the following permissions: () no challenge required -- repoze.who request ended (/login/welcome) -- The important point to note is that the User's group membership details have been retrieved from the database-held identity model and are ready for immediate use when checking authorisation. The other point to note is that processing was all done upstream of the application, obviating any necessity for the developer to write authorisation-handling code in the controller. Extending the capability and range of auth'n'auth coverage is largely effected by adding the requisite plugin (e.g. the :class:SQLAlchemyAuthenticatorPlugin referenced above) along with the appropriate configuration directives. In-line or "URL-forwarding" Authentication ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When an authenticated user agent fails to present credentials when accessing a controlled resource, in-line authentication can be more than just a user convenience, it can significantly improve usability. When attempting to access the resource, the user's request is detected, authentication and authorisation credentials are checked and, on failure, the user is automatically routed to a login page. Upon successful authentication, credentials are available to be presented and, if duly authenticated and authorised, the user is returned to the originally-requested resource. Again, all this is handled upstream, the main task of the developer is to express the permissions architecture as permutations of access roles or rules and issue a 401 response when the proffered credentials are inadequate. This can be done with a decorator such as TurboGears' :func:`@require` decorator (TG2 uses repoze.who as its authentication component) .. code-block :: python @require(Any(has_permission('edit-posts'), is_user('admin'))) The shabti template uses a helper to perform sub-action permissions checks. The various fragments of the links in the calling chain are shown below: In-line authentication sequencing in the template +++++++++++++++++++++++++++++++++++++++++++++++++ **controllers/demo.py** .. code-block :: python def privindex(self): if not h.signed_in(): abort(401, 'You are not authenticated') # [ ... ] return render('test.mak') **lib/helpers.py** - *signed_in* .. code-block :: python def signed_in(): return permissions.SignedIn().check() **lib/permissions.py** - *SignedIn().check()* .. code-block :: python class SignedIn(object): def check(self): return (get_user() is not None) **lib/auth/__init__.py** - *get_user* .. code-block :: python def get_user(): if 'repoze.who.identity' in request.environ: return request.environ.get('repoze.who.identity')['user'] else: return None The repoze.who middleware detects the outgoing 401 emitted by the controller action, records the origin of the 401 and creates a new URL to re-route the user to a login form. The original auth-triggering URL is urlencoded and inserted into the QUERY_STRING as the value of the attribute ``came_from``. .. code-block :: text http://localhost:5000/login/index\ ?came_from=http%3A%2F%2Flocalhost%3A5000%2Fdemo%2Fprivindex (*formatted over two lines for presentation purposes*) Failed login attempts are optionally re-routed or just return to the login form. The repoze.who identity checker that generates the form also accepts either a form to use instead of the default login form or a callable which generates a form to be used in place of the default. Notes on the template --------------------- **development.ini** repoze.who configuration options are typically added in :file:`development.ini`, albeit it could just be a reference to a separate .ini file: .. code-block :: ini who.config_file = %(here)s/{{package}}/config/who.ini who.log_level = debug who.log_file = stdout In this instance however, the SQLAlchemy-enabled plugin used in the shabti :dfn:`auth_repozewho` template abandons the .ini file in favour of a configuration implemented in Python. It's something of a trade-off of the convenience of a SQLAlchemy-backed auth'n'auth solution for a very slight increase in configuration complexity. Separating the configuration of the repoze.who middleware results in a cleaner middleware.py file: **config/middleware.py** repoze.who runs as middleware, so the standard Pylons middleware file needs augmenting with the app (defined in :file:`lib/auth/__init__.py`), here expressed as a diff: .. code-block :: diff --- orig.middleware.py_tmpl +++ new.middleware.py_tmpl @@ -47,6 +47,10 @@ # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares) + # Import and add ready-configured repoze.what quickstart app + from {{package}}.lib.auth import add_auth + app = add_auth(app) + if asbool(full_stack): # Handle Python exceptions app = ErrorHandler(app, global_conf, **config['pylons.errorware']) **lib/auth/__init__.py** The first step is to bind values to the variables that will be provided as arguments to :func:`setup_sql_auth`. This is a good example of the difference between the ``.ini`` config file approach and this one, (of effecting the configuration within the friendly confines of an executable Python source file). In a ``.ini`` approach, the model entity classes would have to be specified by using strings (e.g. "User", "Group"), to be later turned into actual references to the object's class. The "translations" dictionary maps the repoze identity model :class:`User`, :class:`Group`, :class:`Permission` and password validation function to the field names and function that are actually used in the standard Shabti elixir identity model. With repoze.who handling access to the database, the amount of code in the standard :func:`get_user` function is usefully reduced. .. code-block :: python user_name = 'username' user_class = User group_class = Group permission_class = Permission dbsession = Session translations={'user_name': 'username', 'users': 'users', 'group_name': 'name', 'groups': 'groups', 'permission_name': 'name', 'permissions': 'permissions', 'validate_password': 'validate_password' } def add_auth(app, skip_authentication): """Add authentication and authorization middleware to the ``app``.""" return setup_sql_auth(app, user_class, group_class, permission_class, dbsession, # form_plugin=loginform, --not required form_identifies=True, cookie_secret='secretsquirrel', cookie_name='authtkt', login_url='/login/index', post_login_url='/login/welcome_back', post_logout_url='/login/see_you_later', login_counter_name='__logins', translations=translations, skip_authentication=skip_authentication) def get_user(): """Return the current user's database object.""" if 'repoze.who.identity' in request.environ: return request.environ.get('repoze.who.identity')['user'] else: return None .. note:: The URLs "/login_handler" and "/login_handler" are special, they act as a kind of "pre-named route". Because repoze.who runs before any of the Pylons app routines are called, no routes have been yet defined, which is why these two key routes are "firm-coded", i.e. configurable at application startup but not thereafter. **lib/decorators.py** The :func:`authorize` decorator that is used other Shabti templates is replaced in the ``auth_repozewho`` template by the :func:`@require` decorator, taken directly from TurboGears trunk: .. code-block :: python from repoze.what.authorize import check_authorization, NotAuthorizedError def require(predicate): """ Make repoze.what verify that the predicate is met. :param predicate: A repoze.what predicate. :return: The decorator that checks authorization. """ @decorator def check_auth(func, *args, **kwargs): environ = request.environ try: check_authorization(predicate, environ) except NotAuthorizedError, reason: # TODO: We should warn the user # flash(reason, status='warning') raise HTTPUnauthorized() return func(*args, **kwargs) return check_auth .. seealso :: The ``predicate`` param specified in the docstring relates to repoze.who predicates, a repoze.who plug-in provides some `useful basic predicates`__ as a starter. More information generally about creating and using repoze.who predicates can be found in the `Controlling access with predicates`__ page in the `repoze.who manual`__ and in the `repoze.what`__ manual. .. __: http://static.repoze.org/whatdocs/Manual/Predicates/index.html#term-predicate-checker .. __: http://static.repoze.org/whatdocs/Manual/Predicates.html .. __: http://static.repoze.org/whodocs/ .. __: http://static.repoze.org/whatdocs/Manual/index.html **controllers/demo.py** The :func:`@require` decorator allows access rules to be imposed and the repoze-added identity is immediately retrievable from the environment: .. code-block :: python @require(Any(has_permission('edit-posts'), is_user('admin'))) def privindex(self, id): user = req.environ.get('repoze.who.identity') if not IsLicensedTo(user,id): abort(401, 'You are totally not authenticated') c.title = 'Test' c.identity = user c.dataobj = Session.query(MyEntity).filter_by(id=id).one() return render('template.mak') **templates/test.mak** Identity usage in the mako template, for eye-watering completeness ... .. code-block :: html+mako % if c.identity is not UNDEFINED:
Authenticated: ${c.identity.username}
% else:
Not Authenticated
% endif :author: Graham Higgins |today|