diff options
| author | Christoph Zwerschke <cito@online.de> | 2016-04-19 20:07:12 +0200 |
|---|---|---|
| committer | Christoph Zwerschke <cito@online.de> | 2016-04-19 20:07:12 +0200 |
| commit | 3629c49e46207ff5162a82883c14937e6ef4c186 (patch) | |
| tree | 1306181202cb8313f16080789f5b9ab1eeb61d53 /docs/tutorials/wiki/authorization.rst | |
| parent | 804ba0b2f434781e77d2b5191f1cd76a490f6610 (diff) | |
| parent | 6c16fb020027fac47e4d2e335cd9e264dba8aa3b (diff) | |
| download | pyramid-3629c49e46207ff5162a82883c14937e6ef4c186.tar.gz pyramid-3629c49e46207ff5162a82883c14937e6ef4c186.tar.bz2 pyramid-3629c49e46207ff5162a82883c14937e6ef4c186.zip | |
Merge remote-tracking branch 'refs/remotes/Pylons/master'
Diffstat (limited to 'docs/tutorials/wiki/authorization.rst')
| -rw-r--r-- | docs/tutorials/wiki/authorization.rst | 523 |
1 files changed, 299 insertions, 224 deletions
diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 358c1d5eb..44097b35b 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -1,297 +1,372 @@ +.. _wiki_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 people whom 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. :app:`Pyramid` provides -facilities for :term:`authorization` and :term:`authentication`. We'll make -use of both features to provide security to our application. +: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 add an :term:`authentication policy` and an -:term:`authorization policy` to our :term:`application -registry`, add a ``security.py`` module and give our :term:`root` -resource an :term:`ACL`. +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. -Then we will add ``login`` and ``logout`` views, and modify the -existing views to make them return a ``logged_in`` flag to the -renderer and add :term:`permission` declarations to their ``view_config`` -decorators. +We will implement the access control with the following steps: -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. +* Add users and groups (``security.py``, a new module). +* Add an :term:`ACL` (``models.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``). -The source code for this tutorial stage can be browsed via -`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/ -<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/>`_. +Then we will add the login and logout feature: -Adding Authentication and Authorization Policies -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* 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``). -We'll change our package's ``__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: 4-5,8 +Access control +-------------- + +Add users and groups +~~~~~~~~~~~~~~~~~~~~ + +Create a new ``tutorial/security.py`` module with the +following content: + +.. literalinclude:: src/authorization/tutorial/security.py :linenos: :language: python -Then, we'll add those policies to the configuration: +The ``groupfinder`` function accepts a userid and a request and +returns one of these values: -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 16-18,26-28 +- 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``. + +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 an ACL +~~~~~~~~~~ + +Open ``tutorial/models.py`` and add the following import +statement at the head: + +.. literalinclude:: src/authorization/tutorial/models.py + :lines: 4-7 :linenos: :language: python -Note that the creation of an ``AuthTktAuthenticationPolicy`` requires 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 reference to a -``groupfinder`` function in the ``tutorial`` package's ``security.py`` file. -We haven't added that module yet, but we're about to. - -When you're done, your ``__init__.py`` will -look like so: +Add the following lines to the ``Wiki`` class: -.. literalinclude:: src/authorization/tutorial/__init__.py +.. literalinclude:: src/authorization/tutorial/models.py + :lines: 9-13 :linenos: + :lineno-start: 9 + :emphasize-lines: 4-5 :language: python -Adding ``security.py`` -~~~~~~~~~~~~~~~~~~~~~~ +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 a ``security.py`` module within your package (in the same -directory as ``__init__.py``, ``views.py``, etc.) with the following -content: +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. -.. literalinclude:: src/authorization/tutorial/security.py +The ``Wiki`` class that contains the ACL is the :term:`resource` constructor +for the :term:`root` resource, which is a ``Wiki`` instance. The ACL is +provided to each view in the :term:`context` of the request as the ``context`` +attribute. + +It's only happenstance that we're assigning this ACL at class scope. An ACL +can be attached to an object *instance* too; this is how "row level security" +can be achieved in :app:`Pyramid` applications. We actually need only *one* +ACL for the entire system, however, because our security requirements are +simple, so this feature is not demonstrated. See :ref:`assigning_acls` for +more information about what an :term:`ACL` represents. + +Add authentication and authorization policies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Open ``tutorial/__init__.py`` and add the highlighted import +statements: + +.. literalinclude:: src/authorization/tutorial/__init__.py + :lines: 1-8 :linenos: + :emphasize-lines: 4-5,8 :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). +Now add those policies to the configuration: -Giving Our Root Resource an ACL -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. literalinclude:: src/authorization/tutorial/__init__.py + :lines: 18-23 + :linenos: + :lineno-start: 18 + :emphasize-lines: 1-3,5-6 + :language: python -We need to give our root resource object an :term:`ACL`. This ACL will be -sufficient to provide enough information to the :app:`Pyramid` security -machinery to challenge a user who doesn't have appropriate credentials when -he attempts to invoke the ``add_page`` or ``edit_page`` views. +Only the highlighted lines need to be added. -We need to perform some imports at module scope in our ``models.py`` file: +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. -.. code-block:: python - :linenos: +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. - from pyramid.security import Allow - from pyramid.security import Everyone +Add permission declarations +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Open ``tutorial/views.py`` and add a ``permission='edit'`` parameter +to the ``@view_config`` decorators for ``add_page()`` and ``edit_page()``: -Our root resource object is a ``Wiki`` instance. We'll add the following -line at class scope to our ``Wiki`` class: +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 50-52 + :emphasize-lines: 2-3 + :language: python -.. code-block:: python - :linenos: +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 70-72 + :emphasize-lines: 2-3 + :language: python - __acl__ = [ (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit') ] +Only the highlighted lines, along with their preceding commas, need to be +edited and added. -It's only happenstance that we're assigning this ACL at class scope. An ACL -can be attached to an object *instance* too; this is how "row level security" -can be achieved in :app:`Pyramid` applications. We actually only need *one* -ACL for the entire system, however, because our security requirements are -simple, so this feature is not demonstrated. +The result is that only users who possess the ``edit`` permission at the time +of the request may invoke those two views. -Our resulting ``models.py`` file will now look like so: +Add a ``permission='view'`` parameter to the ``@view_config`` decorator for +``view_wiki()`` and ``view_page()`` as follows: -.. literalinclude:: src/authorization/tutorial/models.py - :linenos: +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 23-24 + :emphasize-lines: 1-2 :language: python -Adding Login and Logout Views -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 28-29 + :emphasize-lines: 1-2 + :language: python + +Only the highlighted lines, along with their preceding commas, need to be +edited and added. -We'll add a ``login`` view which renders a login form and processes -the post from the login form, checking credentials. +This allows anyone to invoke these two views. -We'll also add a ``logout`` view to our application and provide a link -to it. This view will clear the credentials of the logged in user and +We are done with the changes needed to control access. The changes that +follow will add the login and logout feature. + +Login, logout +------------- + +Add login and logout views +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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. -We'll add a different file (for presentation convenience) to add login -and logout views. Add a file named ``login.py`` to your application -(in the same directory as ``views.py``) with the following content: +Add the following import statements to the head of +``tutorial/views.py``: -.. literalinclude:: src/authorization/tutorial/login.py - :linenos: +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 6-17 + :emphasize-lines: 1-12 :language: python -Note that the ``login`` view callable in the ``login.py`` file has *two* view -configuration decorators. The order of these decorators is unimportant. -Each just adds a different :term:`view configuration` for the ``login`` view -callable. - -The first view configuration decorator configures the ``login`` view callable -so it will be invoked when someone visits ``/login`` (when the context is a -Wiki and the view name is ``login``). The second decorator (with context of -``pyramid.exceptions.Forbidden``) specifies a :term:`forbidden view`. This -configures our login view to be presented to the user when :app:`Pyramid` -detects that a view invocation can not be authorized. Because we've -configured a forbidden view, the ``login`` view callable will be invoked -whenever one of our users tries to execute a view callable that they are not -allowed to invoke as determined by the :term:`authorization policy` in use. -In our application, for example, this means that if a user has not logged in, -and he tries to add or edit a Wiki page, he will be shown the login form. -Before being allowed to continue on to the add or edit form, he will have to -provide credentials that give him permission to add or edit via this login -form. - -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 -into its template. We'll add something like this to each view body: - -.. ignore-next-block -.. code-block:: python - :linenos: +All the highlighted lines need to be added or edited. - from pyramid.security import authenticated_userid - logged_in = authenticated_userid(request) +: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. -We'll then change the return value of each view that has an associated -``renderer`` to pass the resulting ``logged_in`` value to the -template. For example: +Now add the ``login`` and ``logout`` views at the end of the file: -.. ignore-next-block -.. code-block:: python +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 82-116 :linenos: + :lineno-start: 82 + :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. - return dict(page = context, - content = content, - logged_in = logged_in, - edit_url = edit_url) - -Adding ``permission`` Declarations to our ``view_config`` Decorators -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To protect each of our views with a particular permission, we need to pass a -``permission`` argument to each of our :class:`pyramid.view.view_config` -decorators. To do so, within ``views.py``: - -- We add ``permission='view'`` to the decorator attached to the - ``view_wiki`` and ``view_page`` view functions. This makes the - assertion that only users who possess the ``view`` permission - against the context resource at the time of the request may - invoke these views. We've granted - :data:`pyramid.security.Everyone` the view permission at the - root model via its ACL, so everyone will be able to invoke the - ``view_wiki`` and ``view_page`` views. - -- We add ``permission='edit'`` to the decorator attached to the - ``add_page`` and ``edit_page`` view functions. This makes the - assertion that only users who possess the effective ``edit`` - permission against the context resource at the time of the - request may invoke these views. We've granted the - ``group:editors`` principal the ``edit`` permission at the - root model via its ACL, so only a user whom is a member of - the group named ``group:editors`` will able to invoke the - ``add_page`` or ``edit_page`` views. We've likewise given - the ``editor`` user membership to this group via the - ``security.py`` file by mapping him to the ``group:editors`` - group in the ``GROUPS`` data structure (``GROUPS - = {'editor':['group:editors']}``); the ``groupfinder`` - function consults the ``GROUPS`` data structure. This means - that the ``editor`` user can add and edit pages. - -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``. +The order of these two :term:`view configuration` decorators is unimportant. + +``logout()`` is decorated with a ``@view_config`` decorator which associates +it with the ``logout`` route. It will be invoked when we visit ``/logout``. + +Add the ``login.pt`` Template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create ``tutorial/templates/login.pt`` with the following content: .. literalinclude:: src/authorization/tutorial/templates/login.pt - :language: xml + :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 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Change ``view.pt`` and ``edit.pt`` +Open ``tutorial/views.py`` again. Add a ``logged_in`` parameter to +the return value of ``view_page()``, ``add_page()``, and ``edit_page()`` as +follows: + +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 47-48 + :emphasize-lines: 1-2 + :language: python + +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 67-68 + :emphasize-lines: 1-2 + :language: python + +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 78-80 + :emphasize-lines: 2-3 + :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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -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. +Open ``tutorial/templates/edit.pt`` and +``tutorial/templates/view.pt`` and add the following code as +indicated by the highlighted lines. -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/templates/edit.pt + :lines: 34-38 + :emphasize-lines: 3-5 + :language: html -.. code-block:: xml +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. - <span tal:condition="logged_in"> - <a href="${request.application_url}/logout">Logout</a> - </span> +Reviewing our changes +--------------------- -Seeing Our Changes To ``views.py`` and our Templates -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Our ``tutorial/__init__.py`` will look like this when we're done: -Our ``views.py`` module will look something like this when we're done: +.. literalinclude:: src/authorization/tutorial/__init__.py + :linenos: + :emphasize-lines: 4-5,8,18-20,22-23 + :language: python + +Only the highlighted lines need to be added or edited. + +Our ``tutorial/models.py`` will look like this when we're done: + +.. literalinclude:: src/authorization/tutorial/models.py + :linenos: + :emphasize-lines: 4-7,12-13 + :language: python + +Only the highlighted lines need to be added or edited. + +Our ``tutorial/views.py`` will look like this when we're done: .. literalinclude:: src/authorization/tutorial/views.py :linenos: + :emphasize-lines: 8,11-15,17,24,29,48,52,68,72,80,82-120 :language: python -Our ``edit.pt`` template will look something like this when we're done: +Only the highlighted lines need to be added or edited. + +Our ``tutorial/templates/edit.pt`` template will look like this when +we're done: .. literalinclude:: src/authorization/tutorial/templates/edit.pt :linenos: - :language: xml + :emphasize-lines: 36-38 + :language: html + +Only the highlighted lines need to be added or edited. -Our ``view.pt`` template will look something like this when we're done: +Our ``tutorial/templates/view.pt`` template will look like this when +we're done: .. literalinclude:: src/authorization/tutorial/templates/view.pt :linenos: - :language: xml - -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 resource. It is executable by any user. - -- Visiting ``http://localhost:6543/FrontPage/`` in a browser invokes the - ``view_page`` view of the ``FrontPage`` Page resource. This is because - it's the :term:`default view` (a view without a ``name``) for ``Page`` - resources. It is executable by any user. - -- Visiting ``http://localhost:6543/FrontPage/edit_page`` in a browser invokes - the edit view for the ``FrontPage`` Page resource. 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 show the edit page form - being displayed. - -- 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 - show the edit page form being displayed. - -- 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. + :emphasize-lines: 36-38 + :language: html + +Only the highlighted lines need to be added or edited. + +Viewing the application in a browser +------------------------------------ + +We can finally examine our application in a browser (See +:ref:`wiki-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 resource. It + is executable by any user. + +- http://localhost:6543/FrontPage invokes the ``view_page`` view of the + ``FrontPage`` Page resource. This is because it's the :term:`default view` + (a view without a ``name``) for ``Page`` resources. It is executable by any + user. + +- 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. + +- 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. |
