,
{'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|