In the last chapter we built :term:`authentication` into our wiki. We also
went one step further and used the
request.user object to perform some
explicit :term:`authorization` checks. This is fine for a lot of applications,
but :app:`Pyramid` provides some facilities for cleaning this up and decoupling
the constraints from the view function itself.
We will implement access control with the following steps:
- Update the :term:`authentication policy` to break down the :term:`userid`
into a list of :term:`principals <principal>` (
- Define an :term:`authorization policy` for mapping users, resources and
- Add new :term:`resource` definitions that will be used as the :term:`context`
for the wiki pages (
- Add an :term:`ACL` to each resource (
- Replace the inline checks on the views with :term:`permission` declarations
Add user principals
A :term:`principal` is a level of abstraction on top of the raw :term:`userid` that describes the user in terms of its capabilities, roles, or other identifiers that are easier to generalize. The permissions are then written against the principals without focusing on the exact user involved.
:app:`Pyramid` defines two builtin principals used in every application:
:attr:`pyramid.security.Everyone` and :attr:`pyramid.security.Authenticated`.
On top of these we have already mentioned the required principals for this
application in the original design. The user has two possible roles:
basic. These will be prefixed by the string
role: to avoid clashing
with any other types of principals.
Open the file
tutorial/security.py and edit it as follows:
Only the highlighted lines need to be added.
Note that the role comes from the
User object. We also add the
as a principal for when we want to allow that exact user to edit pages which
they have created.
Add the authorization policy
We already added the :term:`authorization policy` in the previous chapter because :app:`Pyramid` requires one when adding an :term:`authentication policy`. However, it was not used anywhere, so we'll mention it now.
In the file
tutorial/security.py, notice the following lines:
We're using the :class:`pyramid.authorization.ACLAuthorizationPolicy`, which
will suffice for most applications. It uses the :term:`context` to define the
mapping between a :term:`principal` and :term:`permission` for the current
request via the
Add resources and ACLs
Resources are the hidden gem of :app:`Pyramid`. You've made it!
Every URL in a web application represents a :term:`resource` (the "R" in Uniform Resource Locator). Often the resource is something in your data model, but it could also be an abstraction over many models.
Our wiki has two resources:
NewPage. Represents a potential
Pagethat does not exist. Any logged-in user, having either role of
editor, can create pages.
PageResource. Represents a
Pagethat is to be viewed or edited.
editorusers, as well as the original creator of the
Page, may edit the
PageResource. Anyone may view it.
The wiki data model is simple enough that the
PageResource is mostly
redundant with our
models.Page SQLAlchemy class. It is completely valid
to combine these into one class. However, for this tutorial, they are
explicitly separated to make clear the distinction between the parts about
which :app:`Pyramid` cares versus application-defined objects.
There are many ways to define these resources, and they can even be grouped into collections with a hierarchy. However, we're keeping it simple here!
Open the file
tutorial/routes.py and edit the following lines:
The highlighted lines need to be edited or added.
NewPage class has an
__acl__ on it that returns a list of mappings
from :term:`principal` to :term:`permission`. This defines who can do what
with that :term:`resource`. In our case we want to allow only those users with
the principals of either
role:basic to have the
NewPage is loaded as the :term:`context` of the
add_page route by
factory on the route:
PageResource class defines the :term:`ACL` for a
Page. It uses an
Page object to determine who can do what to the page.
PageResource is loaded as the :term:`context` of the
edit_page routes by declaring a
factory on the routes:
Add view permissions
At this point we've modified our application to load the
including the actual
Page model in the
PageResource is now the :term:`context` for all
edit_page views. Similarly the
NewPage will be the context for the
Open the file
First, you can drop a few imports that are no longer necessary:
view_page view to declare the
view permission, and remove the
explicit checks within the view:
The work of loading the page has already been done in the factory, so we can
just pull the
page object out of the
PageResource, loaded as
request.context. Our factory also guarantees we will have a
Page, as it
HTTPNotFound exception if no
Page exists, again simplifying
the view logic.
edit_page view to declare the
add_page view to declare the
pagename here is pulled off of the context instead of
request.matchdict. The factory has done a lot of work for us to hide the
actual route pattern.
The ACLs defined on each :term:`resource` are used by the :term:`authorization
policy` to determine if any :term:`principal` is allowed to have some
:term:`permission`. If this check fails (for example, the user is not logged
in) then an
HTTPForbidden exception will be raised automatically. Thus
we're able to drop those exceptions and checks from the views themselves.
Rather we've defined them in terms of operations on a resource.
tutorial/views/default.py should look like the following:
Viewing the application in a browser
We can finally examine our application in a browser (See :ref:`wiki2-start-the-application`). Launch a browser and visit each of the following URLs, checking that the result is as expected:
- http://localhost:6543/ invokes the
view_wikiview. This always redirects to the
view_pageview of the
FrontPagepage object. It is executable by any user.
- http://localhost:6543/FrontPage invokes the
view_pageview of the
FrontPagepage object. There is a "Login" link in the upper right corner while the user is not authenticated, else it is a "Logout" link when the user is authenticated.
- http://localhost:6543/FrontPage/edit_page invokes the
edit_pageview for the
FrontPagepage object. It is executable by only the
editoruser. If a different user (or the anonymous user) invokes it, then a login form will be displayed. Supplying the credentials with the username
editorwill display the edit page form.
- http://localhost:6543/add_page/SomePageName invokes the
add_pageview for a page. If the page already exists, then it redirects the user to the
edit_pageview for the page object. It is executable by either the
basicuser. If a different user (or the anonymous user) invokes it, then a login form will be displayed. Supplying the credentials with either the username
editor, or username
basic, will display the edit page form.
- http://localhost:6543/SomePageName/edit_page invokes the
edit_pageview for an existing page, or generates an error if the page does not exist. It is editable by the
basicuser if the page was created by that user in the previous step. If, instead, the page was created by the
editoruser, then the login page should be shown for the
- After logging in (as a result of hitting an edit or add page and submitting
the login form with the
editorcredentials), we'll see a "Logout" link in the upper right hand corner. When we click it, we're logged out, redirected back to the front page, and a "Login" link is shown in the upper right hand corner.