summaryrefslogtreecommitdiff
path: root/docs/tutorials/wiki2/authorization.rst
diff options
context:
space:
mode:
Diffstat (limited to 'docs/tutorials/wiki2/authorization.rst')
-rw-r--r--docs/tutorials/wiki2/authorization.rst501
1 files changed, 179 insertions, 322 deletions
diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst
index 1d810b05b..962e2859c 100644
--- a/docs/tutorials/wiki2/authorization.rst
+++ b/docs/tutorials/wiki2/authorization.rst
@@ -4,375 +4,221 @@
Adding authorization
====================
-: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 that to
-allow only people who are members of a *group* named ``group:editors`` to add
-and edit wiki pages but we'll continue allowing anyone with access to the
-server to view pages.
-
-We will also add a login page and a logout link on all the pages. The login
-page will be shown when a user is denied access to any of the views that
-require permission, instead of a default "403 Forbidden" page.
-
-We will implement the access control with the following steps:
-
-* Add users and groups (``security.py``, a new module).
-* Add an :term:`ACL` (``models.py`` and ``__init__.py``).
-* Add an :term:`authentication policy` and an :term:`authorization policy`
- (``__init__.py``).
-* Add :term:`permission` declarations to the ``edit_page`` and ``add_page``
- views (``views.py``).
-
-Then we will add the login and logout feature:
-
-* Add routes for /login and /logout (``__init__.py``).
-* Add ``login`` and ``logout`` views (``views.py``).
-* Add a login template (``login.pt``).
-* Make the existing views return a ``logged_in`` flag to the renderer
- (``views.py``).
-* Add a "Logout" link to be shown when logged in and viewing or editing a page
- (``view.pt``, ``edit.pt``).
-
-
-Access control
---------------
-
-Add users and groups
-~~~~~~~~~~~~~~~~~~~~
-
-Create a new ``tutorial/tutorial/security.py`` module with the
-following content:
+In the last chapter we built :term:`authentication` into our wiki2. 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>` (``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``).
+
+
+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: ``editor``
+or ``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:
.. literalinclude:: src/authorization/tutorial/security.py
:linenos:
+ :emphasize-lines: 3-6,17-24
:language: python
-The ``groupfinder`` function accepts a userid and a request and
-returns one of these values:
+Only the highlighted lines need to be added.
-- If the userid exists in the system, it 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, it will return ``None``.
+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.
-For example, ``groupfinder('editor', request )`` returns ``['group:editor']``,
-``groupfinder('viewer', request)`` returns ``[]``, and ``groupfinder('admin',
-request)`` returns ``None``. We will use ``groupfinder()`` as an
-:term:`authentication policy` "callback" that will provide the
-:term:`principal` or principals for a user.
-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.
+Add the authorization policy
+----------------------------
-Add an ACL
-~~~~~~~~~~
+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.
-Open ``tutorial/tutorial/models.py`` and add the following import
-statement at the head:
+In the file ``tutorial/security.py``, notice the following lines:
-.. literalinclude:: src/authorization/tutorial/models.py
- :lines: 1-4
- :linenos:
+.. literalinclude:: src/authorization/tutorial/security.py
+ :lines: 38-40
+ :lineno-match:
+ :emphasize-lines: 2
:language: python
-Add the following class definition at the end:
+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__``.
-.. literalinclude:: src/authorization/tutorial/models.py
- :lines: 33-37
- :linenos:
- :lineno-start: 33
- :language: python
-We import :data:`~pyramid.security.Allow`, an action that means that
-permission is allowed, and :data:`~pyramid.security.Everyone`, a special
-:term:`principal` that is associated to all requests. Both are used in the
-:term:`ACE` entries that make up the ACL.
+Add resources and ACLs
+----------------------
-The ACL is a list that needs to be named `__acl__` and be an attribute of a
-class. We define an :term:`ACL` with two :term:`ACE` entries: the first entry
-allows any user the `view` permission. The second entry allows the
-``group:editors`` principal the `edit` permission.
+Resources are the hidden gem of :app:`Pyramid`. You've made it!
-The ``RootFactory`` class that contains the ACL is a :term:`root factory`. We
-need to associate it to our :app:`Pyramid` application, so the ACL is provided
-to each view in the :term:`context` of the request as the ``context``
-attribute.
+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.
-Open ``tutorial/tutorial/__init__.py`` and add a ``root_factory`` parameter to
-our :term:`Configurator` constructor, that points to the class we created
-above:
+Our wiki has two resources:
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 24-25
- :linenos:
- :emphasize-lines: 2
- :lineno-start: 16
- :language: python
+#. A ``NewPage``. Represents a potential ``Page`` that does not exist. Any
+ logged-in user, having either role of ``basic`` or ``editor``, can create
+ pages.
-Only the highlighted line needs to be added.
+#. 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.
-We are now providing the ACL to the application. See :ref:`assigning_acls`
-for more information about what an :term:`ACL` represents.
+.. note::
-.. 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.
+ 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.
-Add authentication and authorization policies
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+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 ``tutorial/tutorial/__init__.py`` and add the highlighted import
-statements:
+Open the file ``tutorial/routes.py`` and edit the following lines:
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 1-7
+.. literalinclude:: src/authorization/tutorial/routes.py
:linenos:
- :emphasize-lines: 2-3,7
+ :emphasize-lines: 1-11,17-
:language: python
-Now add those policies to the configuration:
+The highlighted lines need to be edited or added.
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 21-27
- :linenos:
- :lineno-start: 21
- :emphasize-lines: 1-3,6-7
- :language: python
+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:
-Only the highlighted lines need to be added.
-
-We are enabling an ``AuthTktAuthenticationPolicy``, which is based in an auth
-ticket that may be included in the request. We are also enabling an
-``ACLAuthorizationPolicy``, which uses an ACL to determine the *allow* or
-*deny* outcome for a view.
-
-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 the
-``groupfinder()`` function that we created before.
-
-Add permission declarations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Open ``tutorial/tutorial/views.py`` and add a ``permission='edit'`` parameter
-to the ``@view_config`` decorators for ``add_page()`` and ``edit_page()``:
-
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 60-61
- :emphasize-lines: 1-2
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 30-38
+ :lineno-match:
+ :emphasize-lines: 5-9
:language: python
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 75-76
- :emphasize-lines: 1-2
- :language: python
-
-Only the highlighted lines, along with their preceding commas, need to be
-edited and added.
-
-The result is that only users who possess the ``edit`` permission at the time
-of the request may invoke those two views.
+The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` route by
+declaring a ``factory`` on the route:
-Add a ``permission='view'`` parameter to the ``@view_config`` decorator for
-``view_wiki()`` and ``view_page()`` as follows:
-
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 30-31
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 18-19
+ :lineno-match:
:emphasize-lines: 1-2
:language: python
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 36-37
- :emphasize-lines: 1-2
- :language: python
-
-Only the highlighted lines, along with their preceding commas, need to be
-edited and added.
-
-This allows anyone to invoke these two views.
-
-We are done with the changes needed to control access. The changes that
-follow will add the login and logout feature.
-
-Login, logout
--------------
-
-Add routes for /login and /logout
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Go back to ``tutorial/tutorial/__init__.py`` and add these two routes as
-highlighted:
-
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 30-33
- :emphasize-lines: 2-3
- :language: python
-
-.. note:: The preceding lines must be added *before* the following
- ``view_page`` route definition:
-
- .. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 33
- :language: python
-
- This is because ``view_page``'s route definition uses a catch-all
- "replacement marker" ``/{pagename}`` (see :ref:`route_pattern_syntax`)
- which will catch any route that was not already caught by any route listed
- above it in ``__init__.py``. Hence, for ``login`` and ``logout`` views to
- have the opportunity of being matched (or "caught"), they must be above
- ``/{pagename}``.
-
-Add login and logout views
-~~~~~~~~~~~~~~~~~~~~~~~~~~
+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.
-We'll add a ``login`` view 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.
-
-Add the following import statements to the head of
-``tutorial/tutorial/views.py``:
-
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 9-19
- :emphasize-lines: 1-11
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 47-
+ :lineno-match:
+ :emphasize-lines: 5-10
:language: python
-All the highlighted lines need to be added or edited.
-
-:meth:`~pyramid.view.forbidden_view_config` will be used to customize the
-default 403 Forbidden page. :meth:`~pyramid.security.remember` and
-:meth:`~pyramid.security.forget` help to create and expire an auth ticket
-cookie.
-
-Now add the ``login`` and ``logout`` views at the end of the file:
+The ``PageResource`` is loaded as the :term:`context` of the ``view_page`` and
+``edit_page`` routes by declaring a ``factory`` on the routes:
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 91-123
+.. literalinclude:: src/authorization/tutorial/routes.py
+ :lines: 17-21
+ :lineno-match:
+ :emphasize-lines: 1,4-5
:language: python
-``login()`` has two decorators:
-- a ``@view_config`` decorator which associates it with the ``login`` route
- and makes it visible when we visit ``/login``,
-- a ``@forbidden_view_config`` decorator which turns it into a
- :term:`forbidden view`. ``login()`` will be invoked when a user tries to
- execute a view callable for which they lack authorization. For example, if
- a user has not logged in and tries to add or edit a Wiki page, they will be
- shown the login form before being allowed to continue.
+Add view permissions
+--------------------
-The order of these two :term:`view configuration` decorators is unimportant.
+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.
-``logout()`` is decorated with a ``@view_config`` decorator which associates
-it with the ``logout`` route. It will be invoked when we visit ``/logout``.
+Open the file ``tutorial/views/default.py``.
-Add the ``login.pt`` Template
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+First, you can drop a few imports that are no longer necessary:
-Create ``tutorial/tutorial/templates/login.pt`` with the following content:
-
-.. literalinclude:: src/authorization/tutorial/templates/login.pt
- :language: html
-
-The above template is referenced in the login view that we just added in
-``views.py``.
-
-Return a ``logged_in`` flag to the renderer
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Open ``tutorial/tutorial/views.py`` again. Add a ``logged_in`` parameter to
-the return value of ``view_page()``, ``edit_page()``, and ``add_page()`` as
-follows:
-
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 57-58
- :emphasize-lines: 1-2
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :lines: 5-7
+ :lineno-match:
+ :emphasize-lines: 1
:language: python
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 72-73
- :emphasize-lines: 1-2
- :language: python
+Edit the ``view_page`` view to declare the ``view`` permission, and remove the
+explicit checks within the view:
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 85-89
- :emphasize-lines: 3-4
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :lines: 18-23
+ :lineno-match:
+ :emphasize-lines: 1-2,4
:language: python
-Only the highlighted lines need to be added or edited.
-
-The :meth:`pyramid.request.Request.authenticated_userid` will be ``None`` if
-the user is not authenticated, or a userid if the user is authenticated.
-
-Add a "Logout" link when logged in
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+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.
-Open ``tutorial/tutorial/templates/edit.pt`` and
-``tutorial/tutorial/templates/view.pt`` and add the following code as
-indicated by the highlighted lines.
+Edit the ``edit_page`` view to declare the ``edit`` permission:
-.. literalinclude:: src/authorization/tutorial/templates/edit.pt
- :lines: 34-38
- :emphasize-lines: 3-5
- :language: html
-
-The attribute ``tal:condition="logged_in"`` will make the element be included
-when ``logged_in`` is any user id. The link will invoke the logout view. The
-above element will not be included if ``logged_in`` is ``None``, such as when
-a user is not authenticated.
-
-Reviewing our changes
----------------------
-
-Our ``tutorial/tutorial/__init__.py`` will look like this when we're done:
-
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :linenos:
- :emphasize-lines: 2-3,7,21-23,25-27,31-32
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :lines: 38-42
+ :lineno-match:
+ :emphasize-lines: 1-2,4
:language: python
-Only the highlighted lines need to be added or edited.
+Edit the ``add_page`` view to declare the ``create`` permission:
-Our ``tutorial/tutorial/models.py`` will look like this when we're done:
-
-.. literalinclude:: src/authorization/tutorial/models.py
- :linenos:
- :emphasize-lines: 1-4,33-37
+.. literalinclude:: src/authorization/tutorial/views/default.py
+ :lines: 52-56
+ :lineno-match:
+ :emphasize-lines: 1-2,4
:language: python
-Only the highlighted lines need to be added or edited.
-
-Our ``tutorial/tutorial/views.py`` will look 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/views.py
- :linenos:
- :emphasize-lines: 9-11,14-19,25,31,37,58,61,73,76,88,91-117,119-123
- :language: python
+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.
-Only the highlighted lines need to be added or edited.
+The final ``tutorial/views/default.py`` should look like the following:
-Our ``tutorial/tutorial/templates/edit.pt`` template will look like this when
-we're done:
-
-.. literalinclude:: src/authorization/tutorial/templates/edit.pt
- :linenos:
- :emphasize-lines: 36-38
- :language: html
-
-Only the highlighted lines need to be added or edited.
-
-Our ``tutorial/tutorial/templates/view.pt`` template will look like this when
-we're done:
-
-.. literalinclude:: src/authorization/tutorial/templates/view.pt
+.. literalinclude:: src/authorization/tutorial/views/default.py
:linenos:
- :emphasize-lines: 36-38
- :language: html
-
-Only the highlighted lines need to be added or edited.
+ :language: python
Viewing the application in a browser
------------------------------------
@@ -386,21 +232,32 @@ following URLs, checking that the result is as expected:
is executable by any user.
- http://localhost:6543/FrontPage invokes the ``view_page`` view of the
- ``FrontPage`` page object.
-
-- http://localhost:6543/FrontPage/edit_page 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.
-
-- http://localhost:6543/add_page/SomePageName 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.
+ ``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, and
- redirected back to the front page.
+ 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.