diff options
Diffstat (limited to 'docs/tutorials/wiki2/authorization.rst')
| -rw-r--r-- | docs/tutorials/wiki2/authorization.rst | 280 |
1 files changed, 280 insertions, 0 deletions
diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst new file mode 100644 index 000000000..6d8347f79 --- /dev/null +++ b/docs/tutorials/wiki2/authorization.rst @@ -0,0 +1,280 @@ +.. _wiki2_adding_authorization: + +==================== +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. +:mod:`pyramid` provides facilities for *authorization* and +*authentication*. We'll make use of both features to provide security +to our application. + +The source code for this tutorial stage can be browsed at +`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/authorization/ +<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/authorization/>`_. + +Adding A Root Factory +--------------------- + +We're going to start to use a custom :term:`root factory` within our +``run.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 :mod:`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 :mod:`pyramid`. + +Let's modify our ``run.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: + +.. code-block:: python + + 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): + self.__dict__.update(request.matchdict) + +The ``RootFactory`` class we've just added will be used by +:mod:`pyramid` to construct a ``context`` object. The context is +attached to the request object passed to our view callables as the +``context`` attribute. + +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 :mod:`pyramid` as +an access control list during view callable execution. See +:ref:`assigning_acls` for more information about what an :term:`ACL` +represents. + +.. note: Although we don't use the functionality here, the ``factory`` + used to create route contexts may differ per-route as opposed to + globally. See the ``factory`` attribute in + :ref:`route_zcml_directive` for more info. + +We'll pass the ``RootFactory`` we created in the step above in as the +``root_factory`` argument to a :term:`Configurator`. When we're done, +your application's ``run.py`` will look like this. + +.. literalinclude:: src/authorization/tutorial/run.py + :linenos: + :language: python + +Configuring a ``pyramid`` Authorization Policy +------------------------------------------------- + +For any :mod:`pyramid` application to perform authorization, we +need to add a ``security.py`` module and we'll need to change our +``configure.zcml`` file to add an :term:`authentication policy` and an +:term:`authorization policy`. + +Changing ``configure.zcml`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We'll change our ``configure.zcml`` file to enable an +``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy`` to +enable declarative security checking. We'll also change +``configure.zcml`` to add a view stanza which 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 +:mod:`pyramid` detects that a view invocation can not be +authorized. Also, we'll add ``view_permission`` attributes with the +value ``edit`` to the ``edit_page`` and ``add_page`` route +declarations. 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. + +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. + +When you're done, your ``configure.zcml`` will look like so + +.. literalinclude:: src/authorization/tutorial/configure.zcml + :linenos: + :language: xml + +Note that the ``authtktauthenticationpolicy`` tag has two attributes: +``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. + +Adding ``security.py`` +~~~~~~~~~~~~~~~~~~~~~~ + +Add a ``security.py`` module within your package (in the same +directory as "run.py", "views.py", etc) with the following content: + +.. 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). + +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: + +.. 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: + +.. ignore-next-block +.. 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.: + +.. ignore-next-block +.. 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``. + +.. literalinclude:: src/authorization/tutorial/templates/login.pt + :linenos: + :language: xml + +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 +class="main_content">`` div: + +.. code-block:: xml + :linenos: + + <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: + +.. 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 + :linenos: + :language: xml + +Our ``view.pt`` template will look something like this when we're done: + +.. literalinclude:: src/authorization/tutorial/templates/view.pt + :linenos: + :language: xml + +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. + + + |
