:app:`Pyramid` provides facilities for :term:`authentication` and :term:`authorization`. We'll make use of both features to provide security to our application. Our application currently allows anyone with access to the server to view, edit, and add pages to our wiki. We'll change our application to allow only people whom possess a specific username (editor) to add and edit wiki pages but we'll continue allowing anyone with access to the server to view pages.
We will do the following steps:
- Add a :term:`root factory` with an :term:`ACL` (
models.py
). - Add an :term:`authentication policy` and an :term:`authorization policy`
(
__init__.py
). - Add an authentication policy callback (new
security.py
module). - Add
login
andlogout
views (views.py
). - Add :term:`permission` declarations to the
edit_page
andadd_page
views (views.py
). - Make the existing views return a
logged_in
flag to the renderer (views.py
). - Add a login template (new
login.pt
). - Add a "Logout" link to be shown when logged in and viewing or editing a page
(
view.pt
,edit.pt
).
The source code for this tutorial stage can be browsed at http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/authorization/.
Open models.py
and add the following statements:
.. literalinclude:: src/authorization/tutorial/models.py :lines: 1-4,35-39 :linenos: :language: python
We're going to start to use a custom :term:`root factory` within our
__init__.py
file. The objects generated by the root factory will be used
as the :term:`context` of each request to our application. We do this to
allow :app:`Pyramid` declarative security to work properly. The context
object generated by the root factory during a request will be decorated with
security declarations. When we begin to use a custom root factory to generate
our contexts, we can begin to make use of the declarative security features
of :app:`Pyramid`.
We'll modify our __init__.py
, passing in a :term:`root factory` to our
:term:`Configurator` constructor. We'll point it at the new class we created
inside our models.py
file.
The RootFactory
class we've just added will be used by :app:`Pyramid` to
construct a context
object. The context is attached to the request
object passed to our view callables as the context
attribute.
The context object generated by our root factory will possess an __acl__
attribute that allows :data:`pyramid.security.Everyone` (a special principal)
to view all pages, while allowing only a :term:`principal` named
group:editors
to edit and add pages. The __acl__
attribute attached
to a context is interpreted specially by :app:`Pyramid` as an access control
list during view callable execution. See :ref:`assigning_acls` for more
information about what an :term:`ACL` represents.
We'll pass the RootFactory
we created in the step above in as the
root_factory
argument to a :term:`Configurator`.
We're going to be making several changes to our __init__.py
file which
will help us configure an authorization policy.
For any :app:`Pyramid` application to perform authorization, we need to add a
security.py
module (we'll do that shortly) and we'll need to change our
__init__.py
file to add an :term:`authentication policy` and an
:term:`authorization policy` which uses the security.py
file for a
callback.
We'll enable an AuthTktAuthenticationPolicy
and an ACLAuthorizationPolicy
to implement declarative security checking. Open tutorial/__init__.py
and
add these import statements:
.. literalinclude:: src/authorization/tutorial/__init__.py :lines: 2-3,7 :linenos: :language: python
Now add those policies to the configuration:
.. literalinclude:: src/authorization/tutorial/__init__.py :lines: 16-22 :linenos: :language: python
Note that the
:class:`pyramid.authentication.AuthTktAuthenticationPolicy` constructor
accepts two arguments: secret
and callback
. secret
is a string
representing an encryption key used by the "authentication ticket" machinery
represented by this policy: it is required. The callback
is a
groupfinder
function in the current directory's security.py
file. We
haven't added that module yet, but we're about to.
When we're done configuring a root factory, adding a authentication and
authorization policies, and adding routes for /login
and /logout
,
your application's __init__.py
will look like this:
.. literalinclude:: src/authorization/tutorial/__init__.py :linenos: :language: python
Add a tutorial/security.py
module within your package (in the same
directory as :file:`__init__.py`, :file:`views.py`, etc.) with the
following content:
.. literalinclude:: src/authorization/tutorial/security.py :linenos: :language: python
The groupfinder
function defined here is an :term:`authentication policy`
"callback"; it is a callable that accepts a userid and a request. If
the userid exists in the system, the callback will return a sequence
of group identifiers (or an empty sequence if the user isn't a member
of any groups). If the userid does not exist in the system, the
callback will return None
. In a production system, user and group
data will most often come from a database, but here we use "dummy"
data to represent user and groups sources. Note that the editor
user is a member of the group:editors
group in our dummy group
data (the GROUPS
data structure).
We've given the editor
user membership to the group:editors
by
mapping him to this group in the GROUPS
data structure (GROUPS =
{'editor':['group:editors']}
). Since the groupfinder
function
consults the GROUPS
data structure, this will mean that, as a
result of the ACL attached to the root returned by the root factory,
and the permission associated with the add_page
and edit_page
views, the editor
user should be able to add and edit pages.
To our views.py
we'll add a login
view callable which renders a login
form and processes the post from the login form, checking credentials.
We'll also add a logout
view callable to our application and
provide a link to it. This view will clear the credentials of the
logged in user and redirect back to the front page.
The login
view callable will look something like this:
.. literalinclude:: src/authorization/tutorial/views.py :lines: 89-115 :linenos: :language: python
The logout
view callable will look something like this:
.. literalinclude:: src/authorization/tutorial/views.py :lines: 117-121 :linenos: :language: python
The login
view callable is decorated with two decorators, a
@view_config
decorators, which associates it with the login
route,
the other a @forbidden_view_config
decorator which turns it in to an
:term:`exception view` when Pyramid raises a
:class:`pyramid.httpexceptions.HTTPForbidden` exception. The one which
associates it with the login
route makes it visible when we visit
/login
. The other one makes it a :term:`forbidden view`. The forbidden
view is displayed whenever Pyramid or your application raises an
HTTPForbidden exception. In this case, we'll be relying on the forbidden
view to show the login form whenver someone attempts to execute an action
which they're not yet authorized to perform.
The logout
view callable is decorated with a @view_config
decorator
which associates it with the logout
route. This makes it visible when we
visit /login
.
We'll need to import some stuff to service the needs of these two functions:
the pyramid.view.forbidden_view_config
class, a number of values from the
pyramid.security
module, and a value from our newly added
tutorial.security
package. Add the following import statements to the
head of the views.py
file:
.. literalinclude:: src/authorization/tutorial/views.py :lines: 9-18,24-25 :linenos: :language: python
Then we need to change each of our view_page
, edit_page
and
add_page
view callables in views.py
. Within each of these views,
we'll need to pass a "logged in" parameter to its template. We'll add
something like this to each view body:
from pyramid.security import authenticated_userid
logged_in = authenticated_userid(request)
We'll then change the return value of these views to pass the resulting `logged_in` value to the template, e.g.:
return dict(page = page,
content = content,
logged_in = logged_in,
edit_url = edit_url)
We'll also need to add a permission
value to the @view_config
decorator for each of the add_page
and edit_page
view callables. For
each, we'll add permission='edit'
, for example:
@view_config(route_name='edit_page', renderer='templates/edit.pt',
permission='edit')
See the permission='edit'
we added there? This indicates that the view
callables which these views reference cannot be invoked without the
authenticated user possessing the edit
permission with respect to the
current :term:`context`.
Adding these permission
arguments causes Pyramid to make the assertion
that only users who possess the effective edit
permission at the time of
the request may invoke those two views. We've granted the group:editors
principal the edit
permission at the root model via its ACL, so only the
a user whom is a member of the group named group:editors
will able to
invoke the views associated with the add_page
or edit_page
routes.
Add a login.pt
template to your templates directory. It's
referred to within the login view we just added to views.py
.
.. literalinclude:: src/authorization/tutorial/templates/login.pt :language: xml
We'll also need to change our edit.pt
and view.pt
templates to
display a "Logout" link if someone is logged in. This link will
invoke the logout view.
To do so we'll add this to both templates within the <div id="right"
class="app-welcome align-right">
div:
<span tal:condition="logged_in">
<a href="${request.application_url}/logout">Logout</a>
</span>
Our views.py
module will look something like this when we're done:
.. literalinclude:: src/authorization/tutorial/views.py :linenos: :language: python
Our edit.pt
template will look something like this when we're done:
.. literalinclude:: src/authorization/tutorial/templates/edit.pt :language: xml
Our view.pt
template will look something like this when we're done:
.. literalinclude:: src/authorization/tutorial/templates/view.pt :language: xml
We can finally examine our application in a browser. The views we'll try are as follows:
- Visiting
http://localhost:6543/
in a browser invokes theview_wiki
view. This always redirects to theview_page
view of the FrontPage page object. It is executable by any user. - Visiting
http://localhost:6543/FrontPage
in a browser invokes theview_page
view of the FrontPage page object. - Visiting
http://localhost:6543/FrontPage/edit_page
in a browser invokes the edit view for the FrontPage object. It is executable by only theeditor
user. If a different user (or the anonymous user) invokes it, a login form will be displayed. Supplying the credentials with the usernameeditor
, passwordeditor
will display the edit page form. - Visiting
http://localhost:6543/add_page/SomePageName
in a browser invokes the add view for a page. It is executable by only theeditor
user. If a different user (or the anonymous user) invokes it, a login form will be displayed. Supplying the credentials with the usernameeditor
, passwordeditor
will display the edit page form. - After logging in (as a result of hitting an edit or add page
and submitting the login form with the
editor
credentials), we'll see a Logout link in the upper right hand corner. When we click it, we're logged out, and redirected back to the front page.