Skip to content
This repository
tag: 1.0a10
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 631 lines (473 sloc) 24.339 kb

Adding Authorization

Our application currently allows anyone with access to the server to view, edit, and add pages to our wiki. For purposes of demonstration 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. :app:`Pyramid` provides facilities for authorization and authentication. We'll make use of both features to provide security to our application.

System Message: ERROR/3 (<string>, line 7); backlink

Unknown interpreted text role "app".

The source code for this tutorial stage can be browsed at http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/authorization/.

Changing __init__.py For Authorization

We're going to be making several changes to our __init__.py file which will help us configure an authorization policy.

Adding A Root Factory

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. In order for :app:`Pyramid` declarative security to work properly, the context object generated during a request must 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`.

System Message: ERROR/3 (<string>, line 29); backlink

Unknown interpreted text role "term".

System Message: ERROR/3 (<string>, line 29); backlink

Unknown interpreted text role "term".

System Message: ERROR/3 (<string>, line 29); backlink

Unknown interpreted text role "app".

System Message: ERROR/3 (<string>, line 29); backlink

Unknown interpreted text role "app".

We'll modify our __init__.py, passing in a :term:`root factory` to our :term:`Configurator` constructor. We'll point it at a new class we create inside our models.py file. Add the following statements to your models.py file:

System Message: ERROR/3 (<string>, line 38); backlink

Unknown interpreted text role "term".

System Message: ERROR/3 (<string>, line 38); backlink

Unknown interpreted text role "term".
from pyramid.security import Allow
from pyramid.security import Everyone

class RootFactory(object):
    __acl__ = [ (Allow, Everyone, 'view'),
                (Allow, 'group:editors', 'edit') ]
    def __init__(self, request):
        pass

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.

System Message: ERROR/3 (<string>, line 54); backlink

Unknown interpreted text role "app".

All of our context objects 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.

System Message: ERROR/3 (<string>, line 59); backlink

Unknown interpreted text role "data".

System Message: ERROR/3 (<string>, line 59); backlink

Unknown interpreted text role "term".

System Message: ERROR/3 (<string>, line 59); backlink

Unknown interpreted text role "app".

System Message: ERROR/3 (<string>, line 59); backlink

Unknown interpreted text role "ref".

System Message: ERROR/3 (<string>, line 59); backlink

Unknown interpreted text role "term".

We'll pass the RootFactory we created in the step above in as the root_factory argument to a :term:`Configurator`.

System Message: ERROR/3 (<string>, line 73); backlink

Unknown interpreted text role "term".

Configuring 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.

System Message: ERROR/3 (<string>, line 79); backlink

Unknown interpreted text role "app".

System Message: ERROR/3 (<string>, line 79); backlink

Unknown interpreted text role "term".

System Message: ERROR/3 (<string>, line 79); backlink

Unknown interpreted text role "term".

We'll change our __init__.py file to enable an AuthTktAuthenticationPolicy and an ACLAuthorizationPolicy to enable declarative security checking. We'll also change __init__.py to add a :meth:`pyramid.config.Configurator.add_view` call to points at our login :term:`view callable`, also known as a :term:`forbidden view`. This configures our newly created login view to show up when :app:`Pyramid` detects that a view invocation can not be authorized. Also, we'll add view_permission arguments with the value edit to the edit_page and add_page routes. This indicates that the view callables which these routes reference cannot be invoked without the authenticated user possessing the edit permission with respect to the current context.

System Message: ERROR/3 (<string>, line 85); backlink

Unknown interpreted text role "meth".

System Message: ERROR/3 (<string>, line 85); backlink

Unknown interpreted text role "term".

System Message: ERROR/3 (<string>, line 85); backlink

Unknown interpreted text role "term".

System Message: ERROR/3 (<string>, line 85); backlink

Unknown interpreted text role "app".

This makes 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.

Viewing Your Changes

When we're done configuring a root factory, adding an authorization policy, and adding views, your application's __init__.py will look like this:

System Message: ERROR/3 (<string>, line 110)

Unknown directive type "literalinclude".

.. literalinclude:: src/authorization/tutorial/__init__.py
   :linenos:
   :language: python

Note that 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 string, representing a :term:`dotted Python name`, which points at the groupfinder function in the current directory's security.py file. We haven't added that module yet, but we're about to.

System Message: ERROR/3 (<string>, line 114); backlink

Unknown interpreted text role "class".

System Message: ERROR/3 (<string>, line 114); backlink

Unknown interpreted text role "term".

Adding security.py

Add a security.py module within your package (in the same directory as :file:`__init__.py`, :file:`views.py`, etc) with the following content:

System Message: ERROR/3 (<string>, line 127); backlink

Unknown interpreted text role "file".

System Message: ERROR/3 (<string>, line 127); backlink

Unknown interpreted text role "file".

System Message: ERROR/3 (<string>, line 130)

Unknown directive type "literalinclude".

.. literalinclude:: src/authorization/tutorial/security.py
   :linenos:
   :language: python

The groupfinder 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).

System Message: ERROR/3 (<string>, line 134); backlink

Unknown interpreted text role "term".

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.

Adding Login and Logout Views

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.

We'll add a different file (for presentation convenience) to add login and logout view callables. Add a file named login.py to your application (in the same directory as views.py) with the following content:

System Message: ERROR/3 (<string>, line 168)

Unknown directive type "literalinclude".

.. literalinclude:: src/authorization/tutorial/login.py
   :linenos:
   :language: python

Changing Existing Views

Then we need to change each of our view_page, edit_page and add_page views in views.py to pass a "logged in" parameter to its template. We'll add something like this to each view body:

System Message: ERROR/3 (<string>, line 180)

Error in "code-block" directive: unknown option: "linenos".

.. code-block:: python
   :linenos:

   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.:

System Message: ERROR/3 (<string>, line 190)

Error in "code-block" directive: unknown option: "linenos".

.. code-block:: python
   :linenos:

   return dict(page = context,
               content = content,
               logged_in = logged_in,
               edit_url = edit_url)

Adding the login.pt Template

Add a login.pt template to your templates directory. It's referred to within the login view we just added to login.py.

System Message: ERROR/3 (<string>, line 204)

Unknown directive type "literalinclude".

.. literalinclude:: src/authorization/tutorial/templates/login.pt
   :language: xml
   :tab-width: 2

Change view.pt and edit.pt

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>

Viewing the Application in a Browser

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 the view_wiki view. This always redirects to the view_page view of the FrontPage page object. It is executable by any user.
  • Visiting http://localhost:6543/FrontPage in a browser invokes the view_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 the editor user. If a different user (or the anonymous user) invokes it, a login form will be displayed. Supplying the credentials with the username editor, password editor 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 the editor user. If a different user (or the anonymous user) invokes it, a login form will be displayed. Supplying the credentials with the username editor, password editor will display the edit page form.

Seeing Our Changes To views.py and our Templates

Our views.py module will look something like this when we're done:

System Message: ERROR/3 (<string>, line 256)

Unknown directive type "literalinclude".

.. literalinclude:: src/authorization/tutorial/views.py
   :linenos:
   :language: python

Our edit.pt template will look something like this when we're done:

System Message: ERROR/3 (<string>, line 262)

Unknown directive type "literalinclude".

.. literalinclude:: src/authorization/tutorial/templates/edit.pt
   :language: xml
   :tab-width: 2

Our view.pt template will look something like this when we're done:

System Message: ERROR/3 (<string>, line 268)

Unknown directive type "literalinclude".

.. literalinclude:: src/authorization/tutorial/templates/view.pt
   :language: xml
   :tab-width: 2

Revisiting the Application

When we revisit the application in a browser, and log 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.

Overall flow of an authentication

Now that you have seen all the pieces of the authentication mechanism, here are some examples that show how they all work together.

  1. Failed login: The user requests /FrontPage/edit_page. The site presents the login form. The user enters editor as the login, but enters an invalid password bad. The site redisplays the login form with the message "Failed login". See :ref:`failed_login`.

    System Message: ERROR/3 (<string>, line 290); backlink

    Unknown interpreted text role "ref".

  2. The user again requests /FrontPage/edit_page. The site presents the login form, and this time the user enters login editor and password editor. The site presents the edit form with the content of /FrontPage. The user makes some changes and saves them. See :ref:`good_login`.

    System Message: ERROR/3 (<string>, line 296); backlink

    Unknown interpreted text role "ref".

  3. The user again revisits /FrontPage/edit_page. The site goes immediately to the edit form without requesting credentials. See :ref:`revisit`.

    System Message: ERROR/3 (<string>, line 302); backlink

    Unknown interpreted text role "ref".

  4. The user clicks the Logout link. See :ref:`logging_out`.

    System Message: ERROR/3 (<string>, line 306); backlink

    Unknown interpreted text role "ref".

Failed login

The process starts when the user enters URL http://localhost:6543/FrontPage/edit_page. Let's assume that this is the first request ever made to the application and the page database is empty except for the Page instance created for the front page by the initialize_sql function in :file:`models.py`.

System Message: ERROR/3 (<string>, line 313); backlink

Unknown interpreted text role "file".

This process involves two complete request/response cycles.

  1. From the front page, the user clicks :guilabel:`Edit page`. The request is to /FrontPage/edit_page. The view callable is login.login. The response is the login.pt template with blank fields.

    System Message: ERROR/3 (<string>, line 322); backlink

    Unknown interpreted text role "guilabel".

  2. The user enters invalid credentials and clicks :guilabel:`Log in`. A POST request is sent to /FrontPage/edit_page. The view callable is again login.login. The response is the login.pt template showing the message "Failed login", with the entry fields displaying their former values.

    System Message: ERROR/3 (<string>, line 327); backlink

    Unknown interpreted text role "guilabel".

Cycle 1:

  1. During URL dispatch, the route '/{pagename}/edit_page' is considered for matching. The associated view has a view_permission='edit' permission attached, so the dispatch logic has to verify that the user has that permission or the route is not considered to match.

    The context for all route matching comes from the configured root factory, :meth:`RootFactory` in :file:`models.py`. This class has an __acl__ attribute that defines the access control list for all routes:

    System Message: ERROR/3 (<string>, line 341); backlink

    Unknown interpreted text role "meth".

    System Message: ERROR/3 (<string>, line 341); backlink

    Unknown interpreted text role "file".

    __acl__ = [ (Allow, Everyone, 'view'),
                (Allow, 'group:editors', 'edit') ]
    

    In practice, this means that for any route that requires the edit permission, the user must be authenticated and have the group:editors principal or the route is not considered to match.

  2. To find the list of the user's principals, the authorization first policy checks to see if the user has a paste.auth.auth_tkt cookie. Since the user has never been to the site, there is no such cookie, and the user is considered to be unauthenticated.

  3. Since the user is unauthenticated, the groupfinder function in :file:`security.py` is called with None as its userid argument. The function returns an empty list of principals.

    System Message: ERROR/3 (<string>, line 360); backlink

    Unknown interpreted text role "file".

  4. Because that list does not contain the group:editors principal, the '/{pagename}/edit_page' route's edit permission fails, and the route does not match.

  5. Because no routes match, the forbidden view callable is invoked: the login function in module login.py.

  6. Inside the login function, the value of login_url is http://localhost:6543/login, and the value of referrer is http://localhost:6543/FrontPage/edit_page.

    Because request.params has no key for 'came_from', the variable came_from is also set to http://localhost:6543/FrontPage/edit_page. Variables message, login, and password are set to the empty string.

    Because request.params has no key for 'form.submitted', the login function returns this dictionary:

    {'message': '', 'url':'http://localhost:6543/login',
     'came_from':'http://localhost:6543/FrontPage/edit_page',
     'login':'', 'password':''}
    
  7. This dictionary is used to render the login.pt template. In the form, the action attribute is http://localhost:6543/login, and the value of came_from is included in that form as a hidden field by this line in the template:

    <input type="hidden" name="came_from" value="${came_from}"/>
    

Cycle 2:

  1. The user enters incorrect credentials and clicks the :guilabel:`Log in` button, which does a POST request to URL http://localhost:6543/login. The name of the :guilabel:`Log in` button in this form is form.submitted.

    System Message: ERROR/3 (<string>, line 400); backlink

    Unknown interpreted text role "guilabel".

    System Message: ERROR/3 (<string>, line 400); backlink

    Unknown interpreted text role "guilabel".

  2. The route with pattern '/login' matches this URL, so control is passed again to the login view callable.

  3. The login_url and referrer have the same value this time (http://localhost:6543/login), so variable referrer is set to '/'.

    Since request.params does have a key 'form.submitted', the values of login and password are retrieved from request.params.

    Because the login and password do not match any of the entries in the USERS dictionary in security.py, variable message is set to 'Failed login'.

    The view callable returns this dictionary:

    {'message':'Failed login',
     'url':'http://localhost:6543/login', 'came_from':'/',
     'login':'editor', 'password':'bad'}
    
  4. The login.pt template is rendered using those values.

Successful login

In this scenario, the user again requests URL /FrontPage/edit_page.

This process involves four complete request/response cycles.

  1. The user clicks :guilabel:`Edit page`. The view callable is login.login. The response is template login.pt, with all the fields blank.

    System Message: ERROR/3 (<string>, line 438); backlink

    Unknown interpreted text role "guilabel".

  2. The user enters valid credentials and clicks :guilabel:`Log in`. The view callable is login.login. The response is a redirect to /FrontPage/edit_page.

    System Message: ERROR/3 (<string>, line 442); backlink

    Unknown interpreted text role "guilabel".

  3. The view callable is views.edit_page. The response renders template edit.pt, displaying the current page content.

  4. The user edits the content and clicks :guilabel:`Save`. The view callable is views.edit_page. The response is a redirect to /FrontPage.

    System Message: ERROR/3 (<string>, line 450); backlink

    Unknown interpreted text role "guilabel".

Execution proceeds as in :ref:`failed_login`, up to the point where the password editor is successfully matched against the value from the USERS dictionary.

System Message: ERROR/3 (<string>, line 454); backlink

Unknown interpreted text role "ref".

Cycle 2:

  1. Within the login.login view callable, the value of login_url is http://localhost:6543/login, and the value of referrer is '/', and came_from is http://localhost:6543/FrontPage/edit_page when this block is executed:

    if USERS.get(login) == password:
        headers = remember(request, login)
        return HTTPFound(location=came_from, headers=headers)
    
  2. Because the password matches this time, :mod:`pyramid.security.remember` returns a sequence of header tuples that will set a paste.auth.auth_tkt authentication cookie in the user's browser for the login 'editor'.

    System Message: ERROR/3 (<string>, line 472); backlink

    Unknown interpreted text role "mod".

  3. The HTTPFound exception returns a response that redirects the browser to http://localhost:6543/FrontPage/edit_page, including the headers that set the authentication cookie.

Cycle 3:

  1. Route pattern '/{pagename}/edit_page' matches this URL, but the corresponding view is restricted by an 'edit' permission.

  2. Because the user now has an authentication cookie defining their login name as 'editor', the groupfinder function is called with that value as its userid argument.

  3. The groupfinder function returns the list ['group:editors']. This satisfies the access control entry (Allow, 'group:editors', 'edit'), which grants the edit permission. Thus, this route matches, and control passes to view callable edit_page.

  4. Within edit_page, name is set to 'FrontPage', the page name from request.matchdict['pagename'], and page is set to an instance of :class:`models.Page` that holds the current content of FrontPage.

    System Message: ERROR/3 (<string>, line 497); backlink

    Unknown interpreted text role "class".

  5. Since this request did not come from a form, request.params does not have a key for 'form.submitted'.

  6. The edit_page function calls :meth:`pyramid.security.authenticated_userid` to find out whether the user is authenticated. Because of the cookies set previously, the variable logged_in is set to the userid 'editor'.

    System Message: ERROR/3 (<string>, line 506); backlink

    Unknown interpreted text role "meth".

  7. The edit_page function returns this dictionary:

    {'page':page, 'logged_in':'editor',
     'save_url':'http://localhost:6543/FrontPage/edit_page'}
    
  8. Template :file:`edit.pt` is rendered with those values. Among other features of this template, these lines cause the inclusion of a :guilabel:`Logout` link:

    System Message: ERROR/3 (<string>, line 517); backlink

    Unknown interpreted text role "file".

    System Message: ERROR/3 (<string>, line 517); backlink

    Unknown interpreted text role "guilabel".

    <span tal:condition="logged_in">
      <a href="${request.application_url}/logout">Logout</a>
    </span>
    

    For the example case, this link will refer to http://localhost:6543/logout.

    These lines of the template display the current page's content in a form whose action attribute is http://localhost:6543/FrontPage/edit_page:

    <form action="${save_url}" method="post">
      <textarea name="body" tal:content="page.data" rows="10" cols="60"/>
      <input type="submit" name="form.submitted" value="Save"/>
    </form>
    

Cycle 4:

  1. The user edits the page content and clicks :guilabel:`Save`.

    System Message: ERROR/3 (<string>, line 539); backlink

    Unknown interpreted text role "guilabel".

  2. URL http://localhost:6543/FrontPage/edit_page goes through the same routing as before, up until the line that checks whether request.params has a key 'form.submitted'. This time, within the edit_page view callable, these lines are executed:

    page.data = request.params['body']
    session.add(page)
    return HTTPFound(location = route_url('view_page', request,
                                          pagename=name))
    

    The first two lines replace the old page content with the contents of the body text area from the form, and then update the page stored in the database. The third line causes a response that redirects the browser to http://localhost:6543/FrontPage.

Revisiting after authentication

In this case, the user has an authentication cookie set in their browser that specifies their login as 'editor'. The requested URL is http://localhost:6543/FrontPage/edit_page.

This process requires two request/response cycles.

  1. The user clicks :guilabel:`Edit page`. The view callable is views.edit_page. The response is edit.pt, showing the current page content.

    System Message: ERROR/3 (<string>, line 570); backlink

    Unknown interpreted text role "guilabel".

  2. The user edits the content and clicks :guilabel:`Save`. The view callable is views.edit_page. The response is a redirect to /Frontpage.

    System Message: ERROR/3 (<string>, line 574); backlink

    Unknown interpreted text role "guilabel".

Cycle 1:

  1. The route with pattern /{pagename}/edit_page matches the URL, and because of the authentication cookie, groupfinder returns a list containing the group:editors principal, which models.RootFactory.__acl__ uses to grant the edit permission, so this route matches and dispatches to the view callable :meth:`views.edit_page`.

    System Message: ERROR/3 (<string>, line 580); backlink

    Unknown interpreted text role "meth".

  2. In edit_page, because the request did not come from a form submission, request.params has no key for 'form.submitted'.

  3. The variable logged_in is set to the login name 'editor' by calling authenticated_userid, which extracts it from the authentication cookie.

  4. The function returns this dictionary:

    {'page':page,
     'save_url':'http://localhost:6543/FrontPage/edit_page',
     'logged_in':'editor'}
    
  5. Template :file:`edit.pt` is rendered with the values from that dictionary. Because of the presence of the 'logged_in' entry, a :guilabel:`Logout` link appears.

    System Message: ERROR/3 (<string>, line 601); backlink

    Unknown interpreted text role "file".

    System Message: ERROR/3 (<string>, line 601); backlink

    Unknown interpreted text role "guilabel".

Cycle 2:

  1. The user edits the page content and clicks :guilabel:`Save`.

    System Message: ERROR/3 (<string>, line 607); backlink

    Unknown interpreted text role "guilabel".

  2. The POST operation works as in :ref:`good_login`.

    System Message: ERROR/3 (<string>, line 609); backlink

    Unknown interpreted text role "ref".

Logging out

This process starts with a request URL http://localhost:6543/logout.

  1. The route with pattern '/logout' matches and dispatches to the view callable logout in :file:`login.py`.

    System Message: ERROR/3 (<string>, line 619); backlink

    Unknown interpreted text role "file".

  2. The call to :meth:`pyramid.security.forget` returns a list of header tuples that will, when returned with the response, cause the browser to delete the user's authentication cookie.

    System Message: ERROR/3 (<string>, line 622); backlink

    Unknown interpreted text role "meth".

  3. The view callable returns an HTTPFound exception that redirects the browser to named route view_wiki, which will translate to URL http://localhost:6543. It also passes along the headers that delete the authentication cookie.

Something went wrong with that request. Please try again.