diff options
Diffstat (limited to 'docs/tutorials/wiki2/authorization.rst')
| -rw-r--r-- | docs/tutorials/wiki2/authorization.rst | 501 |
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. |
