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/wiki2/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/wiki2/authorization.rst')
| -rw-r--r-- | docs/tutorials/wiki2/authorization.rst | 462 |
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. |
