summaryrefslogtreecommitdiff
path: root/docs/tutorials/wiki2/authorization.rst
diff options
context:
space:
mode:
authorChristoph Zwerschke <cito@online.de>2016-04-19 20:07:12 +0200
committerChristoph Zwerschke <cito@online.de>2016-04-19 20:07:12 +0200
commit3629c49e46207ff5162a82883c14937e6ef4c186 (patch)
tree1306181202cb8313f16080789f5b9ab1eeb61d53 /docs/tutorials/wiki2/authorization.rst
parent804ba0b2f434781e77d2b5191f1cd76a490f6610 (diff)
parent6c16fb020027fac47e4d2e335cd9e264dba8aa3b (diff)
downloadpyramid-3629c49e46207ff5162a82883c14937e6ef4c186.tar.gz
pyramid-3629c49e46207ff5162a82883c14937e6ef4c186.tar.bz2
pyramid-3629c49e46207ff5162a82883c14937e6ef4c186.zip
Merge remote-tracking branch 'refs/remotes/Pylons/master'
Diffstat (limited to 'docs/tutorials/wiki2/authorization.rst')
-rw-r--r--docs/tutorials/wiki2/authorization.rst462
1 files changed, 207 insertions, 255 deletions
diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst
index 76ce4b83f..234f40e3b 100644
--- a/docs/tutorials/wiki2/authorization.rst
+++ b/docs/tutorials/wiki2/authorization.rst
@@ -1,311 +1,263 @@
.. _wiki2_adding_authorization:
====================
-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.
-:app:`Pyramid` provides facilities for :term:`authorization` and
-:term:`authentication`. We'll make use of both features to provide security
-to our application.
-
-We will add an :term:`authentication policy` and an
-:term:`authorization policy` to our :term:`application
-registry`, add a ``security.py`` module, create a :term:`root factory`
-with an :term:`ACL`, and add :term:`permission` declarations to
-the ``edit_page`` and ``add_page`` views.
-
-Then we will add ``login`` and ``logout`` views, and modify the
-existing views to make them return a ``logged_in`` flag to the
-renderer.
-
-Finally, we will add a ``login.pt`` template and change the existing
-``view.pt`` and ``edit.pt`` to show a "Logout" link when not logged in.
-
-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/>`_.
-
-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. 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 a new class we create
-inside our ``models.py`` file. Add the following statements to your
-``models.py`` file:
-
-.. literalinclude:: src/authorization/tutorial/models.py
- :lines: 3-4,45-49
- :linenos:
- :language: python
+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.
-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.
-
-.. 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`` argument to
- :meth:`pyramid.config.Configurator.add_route` for more info.
-
-We'll pass the ``RootFactory`` we created in the step above in as the
-``root_factory`` argument to a :term:`Configurator`.
-
-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*.
-
-We'll change our ``__init__.py`` file to enable an
-``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy`` to enable
-declarative security checking. We need to import the new policies:
-
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 2-3,8
- :linenos:
- :language: python
+We will implement access control with the following steps:
-Then, we'll add those policies to the configuration:
+* Update the :term:`authentication policy` to break down the :term:`userid`
+ into a list of :term:`principals <principal>` (``security.py``).
+* Define an :term:`authorization policy` for mapping users, resources and
+ permissions (``security.py``).
+* Add new :term:`resource` definitions that will be used as the :term:`context`
+ for the wiki pages (``routes.py``).
+* Add an :term:`ACL` to each resource (``routes.py``).
+* Replace the inline checks on the views with :term:`permission` declarations
+ (``views/default.py``).
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 15-21
- :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
-``groupfinder`` function in the current directory's ``security.py`` file. We
-haven't added that module yet, but we're about to.
+Add user principals
+-------------------
-We'll also change ``__init__.py``, adding a call to
-:meth:`pyramid.config.Configurator.add_view` that points at our ``login``
-:term:`view callable`. This is also known as a :term:`forbidden view`:
+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.
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 25,41-43
- :linenos:
- :language: python
-
-A forbidden view configures our newly created login view to show up when
-:app:`Pyramid` detects that a view invocation can not be authorized.
+: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: ``editor``
+or ``basic``. These will be prefixed by the string ``role:`` to avoid clashing
+with any other types of principals.
-A ``logout`` :term:`view callable` will allow users to log out later:
+Open the file ``tutorial/security.py`` and edit it as follows:
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 26,34
+.. literalinclude:: src/authorization/tutorial/security.py
:linenos:
+ :emphasize-lines: 3-6,17-24
:language: python
-We'll also add ``permission`` arguments with the value ``edit`` to the
-``edit_page`` and ``add_page`` views. 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 context.
+Only the highlighted lines need to be added.
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 37-40
- :linenos:
- :language: python
+Note that the role comes from the ``User`` object. We also add the ``user.id``
+as a principal for when we want to allow that exact user to edit pages which
+they have created.
-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.
-Viewing Your Changes
-~~~~~~~~~~~~~~~~~~~~
+Add the authorization policy
+----------------------------
-When we're done configuring a root factory, adding an authorization policy,
-and adding views, your application's ``__init__.py`` will look like this:
+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.
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :linenos:
+In the file ``tutorial/security.py``, notice the following lines:
+
+.. literalinclude:: src/authorization/tutorial/security.py
+ :lines: 38-40
+ :lineno-match:
+ :emphasize-lines: 2
:language: python
-Adding ``security.py``
+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 ``__acl__``.
+
+
+Add resources and ACLs
----------------------
-Add a ``security.py`` module within your package (in the same directory as
-:file:`__init__.py`, :file:`views.py`, etc.) with the following content:
+Resources are the hidden gem of :app:`Pyramid`. You've made it!
-.. literalinclude:: src/authorization/tutorial/security.py
+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:
+
+#. A ``NewPage``. Represents a potential ``Page`` that does not exist. Any
+ logged-in user, having either role of ``basic`` or ``editor``, can create
+ pages.
+
+#. A ``PageResource``. Represents a ``Page`` that is to be viewed or edited.
+ ``editor`` users, as well as the original creator of the ``Page``, may edit
+ the ``PageResource``. Anyone may view it.
+
+.. note::
+
+ 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:
+
+.. literalinclude:: src/authorization/tutorial/routes.py
:linenos:
+ :emphasize-lines: 1-11,17-
: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.
-
-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 the 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:
+The highlighted lines need to be edited or added.
+
+The ``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:editor`` or ``role:basic`` to have the
+``create`` permission:
+
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 30-38
+ :lineno-match:
+ :emphasize-lines: 5-9
:language: python
-Changing Existing Views
------------------------
+The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` route by
+declaring a ``factory`` on the route:
-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:
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 18-19
+ :lineno-match:
+ :emphasize-lines: 1-2
+ :language: python
-.. ignore-next-block
-.. code-block:: python
- :linenos:
+The ``PageResource`` class defines the :term:`ACL` for a ``Page``. It uses an
+actual ``Page`` object to determine *who* can do *what* to the page.
- from pyramid.security import authenticated_userid
- logged_in = authenticated_userid(request)
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 47-
+ :lineno-match:
+ :emphasize-lines: 5-10
+ :language: python
-We'll then change the return value of these views to pass the `resulting
-`logged_in`` value to the template, e.g.:
+The ``PageResource`` is loaded as the :term:`context` of the ``view_page`` and
+``edit_page`` routes by declaring a ``factory`` on the routes:
-.. ignore-next-block
-.. code-block:: python
- :linenos:
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 17-21
+ :lineno-match:
+ :emphasize-lines: 1,4-5
+ :language: python
- return dict(page = context,
- content = content,
- logged_in = logged_in,
- edit_url = edit_url)
-Adding the ``login.pt`` Template
---------------------------------
+Add view permissions
+--------------------
-Add a ``login.pt`` template to your templates directory. It's
-referred to within the login view we just added to ``login.py``.
+At this point we've modified our application to load the ``PageResource``,
+including the actual ``Page`` model in the ``page_factory``. The
+``PageResource`` is now the :term:`context` for all ``view_page`` and
+``edit_page`` views. Similarly the ``NewPage`` will be the context for the
+``add_page`` view.
-.. literalinclude:: src/authorization/tutorial/templates/login.pt
- :language: xml
+Open the file ``tutorial/views/default.py``.
-Change ``view.pt`` and ``edit.pt``
-----------------------------------
+First, you can drop a few imports that are no longer necessary:
+
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :lines: 5-7
+ :lineno-match:
+ :emphasize-lines: 1
+ :language: python
-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.
+Edit the ``view_page`` view to declare the ``view`` permission, and remove the
+explicit checks within the view:
-To do so we'll add this to both templates within the ``<div id="right"
-class="app-welcome align-right">`` div:
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :lines: 18-23
+ :lineno-match:
+ :emphasize-lines: 1-2,4
+ :language: python
-.. code-block:: xml
+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
+raises the ``HTTPNotFound`` exception if no ``Page`` exists, again simplifying
+the view logic.
- <span tal:condition="logged_in">
- <a href="${request.application_url}/logout">Logout</a>
- </span>
+Edit the ``edit_page`` view to declare the ``edit`` permission:
-Seeing Our Changes To ``views.py`` and our Templates
-----------------------------------------------------
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :lines: 38-42
+ :lineno-match:
+ :emphasize-lines: 1-2,4
+ :language: python
-Our ``views.py`` module will look something like this when we're done:
+Edit the ``add_page`` view to declare the ``create`` permission:
-.. literalinclude:: src/authorization/tutorial/views.py
- :linenos:
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :lines: 52-56
+ :lineno-match:
+ :emphasize-lines: 1-2,4
:language: python
-Our ``edit.pt`` template will look something like this when we're done:
+Note 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.
-.. literalinclude:: src/authorization/tutorial/templates/edit.pt
- :language: xml
+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.
-Our ``view.pt`` template will look something like this when we're done:
+The final ``tutorial/views/default.py`` should look like the following:
-.. literalinclude:: src/authorization/tutorial/templates/view.pt
- :language: xml
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :linenos:
+ :language: python
-Viewing the Application in a Browser
+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.
-
-- 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.
+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_wiki`` view. This always
+ redirects to the ``view_page`` view of the ``FrontPage`` page object. It
+ is executable by any user.
+
+- http://localhost:6543/FrontPage invokes the ``view_page`` view of the
+ ``FrontPage`` page 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_page`` view for
+ the ``FrontPage`` page object. It is executable by only the ``editor`` user.
+ If a different user (or the anonymous user) invokes it, then a login form
+ will be displayed. Supplying the credentials with the username ``editor`` and
+ password ``editor`` will display the edit page form.
+
+- http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for
+ a page. If the page already exists, then it redirects the user to the
+ ``edit_page`` view for the page object. It is executable by either the
+ ``editor`` or ``basic`` user. 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`` and password ``editor``, or username
+ ``basic`` and password ``basic``, will display the edit page form.
+
+- http://localhost:6543/SomePageName/edit_page invokes the ``edit_page`` view
+ for an existing page, or generates an error if the page does not exist. It is
+ editable by the ``basic`` user if the page was created by that user in the
+ previous step. If, instead, the page was created by the ``editor`` user, then
+ the login page should be shown for the ``basic`` user.
+
+- 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, redirected
+ back to the front page, and a "Login" link is shown in the upper right hand
+ corner.