diff options
| author | Michael Merickel <michael@merickel.org> | 2016-02-18 01:18:20 -0600 |
|---|---|---|
| committer | Michael Merickel <michael@merickel.org> | 2016-02-18 01:18:20 -0600 |
| commit | 9e85d2bf9489fff46ec7ea47b79bebcdc19d9a8e (patch) | |
| tree | f2a1b513afb09155689312b45fbcb869310f068b /docs/tutorials/wiki2 | |
| parent | 2fa90465bfdd213b6ce51ca8de6eaf9b614c283e (diff) | |
| download | pyramid-9e85d2bf9489fff46ec7ea47b79bebcdc19d9a8e.tar.gz pyramid-9e85d2bf9489fff46ec7ea47b79bebcdc19d9a8e.tar.bz2 pyramid-9e85d2bf9489fff46ec7ea47b79bebcdc19d9a8e.zip | |
update the authorization chapter
Diffstat (limited to 'docs/tutorials/wiki2')
| -rw-r--r-- | docs/tutorials/wiki2/authentication.rst | 2 | ||||
| -rw-r--r-- | docs/tutorials/wiki2/authorization.rst | 418 |
2 files changed, 147 insertions, 273 deletions
diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index 0b5e71099..1b18e5c55 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -49,7 +49,7 @@ Here we've defined: the :term:`userid` using a signed cookie. * A ``get_user`` function which can convert the ``unauthenticated_userid`` from the policy into a ``User`` object from our database. -* Finally, the ``get_user`` is registered on the request as ``request.user`` +* The ``get_user`` is registered on the request as ``request.user`` to be used throughout our application as the authenticated ``User`` object for the logged-in user. diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 1ee5cc714..eb9269dff 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -4,342 +4,216 @@ 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/default.py``, a new subpackage). -* Add an :term:`ACL` (``models/mymodel.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/default.py``). - -Then we will add the login and logout feature: - -* Add routes for /login and /logout (``__init__.py``). -* Add ``login`` and ``logout`` views (``views/default.py``). -* Add a login template (``login.jinja2``). -* Make the existing views return a ``logged_in`` flag to the renderer +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 a "Logout" link to be shown when logged in and viewing or editing a page - (``view.jinja2``, ``edit.jinja2``). +Add user principals +------------------- -Access control --------------- +A :term:`principal` is a level of abstraction on top of the raw +:term:`userid` that describes the user in terms of 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. -Add users and groups -~~~~~~~~~~~~~~~~~~~~ +: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`` and ``basic``. These will be prefixed by the ``role:`` +string to avoid clasing with any other types of principals. -Create a new ``tutorial/security/default.py`` subpackage with the -following content: +Open the file ``tutorial/security.py`` and edit the following lines: -.. literalinclude:: src/authorization/tutorial/security/default.py +.. 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: - -- 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. +Only the highlighted lines need to be added. -Add an ACL -~~~~~~~~~~ +Note that the role comes from the ``User`` object and finally we also +add the ``user.id`` as a principal for when we want to allow that exact +user to edit page's they've created. -Open ``tutorial/models/mymodel.py`` and add the following import -statement at the top: +Add the authorization policy +---------------------------- -.. literalinclude:: src/authorization/tutorial/models/mymodel.py - :lines: 1-4 - :language: python +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 and so we'll +mention it now. -Add the following class definition at the end: +Open the file ``tutorial/security.py`` and notice the following lines: -.. literalinclude:: src/authorization/tutorial/models/mymodel.py - :lines: 22-29 +.. literalinclude:: src/authorization/tutorial/security.py + :lines: 38-40 + :lineno-match: + :emphasize-lines: 2 :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. - -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 (``Everyone``) the `view` permission. The second entry allows -the ``group:editors`` principal the `edit` permission. +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__``. -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. +Add resources and ACLs +---------------------- -Open ``tutorial/__init__.py`` and define a new root factory using -:meth:`pyramid.config.Configurator.set_root_factory` using the class that we -created above: +Resources are the hidden gem of :app:`Pyramid`. You've made it! -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 14-17 - :emphasize-lines: 17 - :language: python +Every URL in a web application is representing 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. -Only the highlighted line needs to be added. +Our wiki has two resources: -We are now providing the ACL to the application. See :ref:`assigning_acls` -for more information about what an :term:`ACL` represents. +#. A ``PageResource``. Represents a ``Page`` that is to be viewed or edited. + Only ``editor`` users as well as the original creator of the ``Page`` + may edit the ``PageResource`` but anyone may view it. -.. 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. +#. A ``NewPage``. Represents a potential ``Page`` that does not exist. + Any logged-in user (roles ``basic`` or ``editor``) can create pages. -Add authentication and authorization policies -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. note:: -Open ``tutorial/__init__.py`` and add the highlighted import -statements: + The wiki data model is simple enough that the ``PageResource`` is + actually 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 it clear the + parts that :app:`Pyramid` cares about versus application-defined objects. -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 1-5 - :emphasize-lines: 2-5 - :language: python +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! -Now add those policies to the configuration: +Open the file ``tutorial/routes.py`` and edit the following lines: -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 11-19 - :emphasize-lines: 1-3,8-9 +.. literalinclude:: src/authorization/tutorial/routes.py + :linenos: + :emphasize-lines: 1-7,14-50 :language: python -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. +The highlighted lines need to be edited or added. +The ``NewPage`` 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 only +allow users with the principals ``role:editor`` and ``role:basic`` to +have the ``create`` permission: -Add permission declarations -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Open ``tutorial/views/default.py`` and add a ``permission='view'`` -parameter to the ``@view_config`` decorator for ``view_wiki()`` and -``view_page()`` as follows: - -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 24-25 - :emphasize-lines: 1 +.. literalinclude:: src/authorization/tutorial/routes.py + :lines: 20-32 + :lineno-match: + :emphasize-lines: 11,12 :language: python -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 29-31 - :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. +The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` +route by declaring a ``factory`` on the route: -Add a ``permission='edit'`` parameter to the ``@view_config`` decorators for -``add_page()`` and ``edit_page()``: - -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 52-54 - :emphasize-lines: 1-2 +.. literalinclude:: src/authorization/tutorial/routes.py + :lines: 15-16 + :lineno-match: + :emphasize-lines: 2 :language: python -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 66-68 - :emphasize-lines: 1-2 - :language: python +The ``PageResource`` defines the :term:`ACL` for a ``Page``. It uses an +actual ``Page`` object to determine **who** can do **what** to the page. -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. - -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/__init__.py`` and add these two routes as -highlighted: - -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 21-24 - :emphasize-lines: 2-3 +.. literalinclude:: src/authorization/tutorial/routes.py + :lines: 34-50 + :lineno-match: + :emphasize-lines: 14-16 :language: python -.. note:: The preceding lines must be added *before* the following - ``view_page`` route definition: - - .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 24 - :language: python +The ``PageResource`` is loaded as the :term:`context` of the ``view_page`` +and ``edit_page`` route by declaring a ``factory`` on the routes: - 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}``. +.. literalinclude:: src/authorization/tutorial/routes.py + :lines: 14-18 + :lineno-match: + :emphasize-lines: 1,4-5 + :language: python -Add login and logout views -~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add view permissions +-------------------- -We'll add a ``login`` view which renders a login form and processes the post -from the login form, checking credentials. +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. -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. +Open the file ``views/default.py``. -Add the following import statements to ``tutorial/views/default.py`` -after the import from ``pyramid.httpexceptions``: +First, you can drop a few imports that are no longer necessary: .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 9-19 - :emphasize-lines: 1-8,11 + :lines: 5-7 + :lineno-match: + :emphasize-lines: 1 :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: +Edit the ``view_page`` view to declare the ``view`` permission and remove +the explicit checks within the view: .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 81-112 - :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``, and -- 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. - -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.jinja2`` template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Create ``tutorial/templates/login.jinja2`` with the following content: - -.. literalinclude:: src/authorization/tutorial/templates/login.jinja2 - :language: html - -The above template is referenced in the login view that we just added in -``views/default.py``. - -Add a "Logout" link when logged in -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Open ``tutorial/templates/edit.jinja2`` and -``tutorial/templates/view.jinja2`` and add the following code as -indicated by the highlighted lines. - -.. literalinclude:: src/authorization/tutorial/templates/edit.jinja2 - :lines: 34-40 - :emphasize-lines: 3-7 - :language: html - -The :meth:`pyramid.request.Request.authenticated_userid` will be ``None`` if -the user is not authenticated, or a userid if the user is authenticated. This -check will make the logout link active only when the user is logged in. - -Reviewing our changes ---------------------- - -Our ``tutorial/__init__.py`` will look like this when we're done: - -.. literalinclude:: src/authorization/tutorial/__init__.py - :linenos: - :emphasize-lines: 2-3,5,11-13,17-19,22-23 + :lines: 18-23 + :lineno-match: + :emphasize-lines: 2,4 :language: python -Only the highlighted lines need to be added or edited. +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 ``HTTPNotFound`` otherwise - again simplifying the view logic. -Our ``tutorial/models/mymodel.py`` will look like this when we're done: +Edit the ``edit_page`` view to declare the ``edit`` permission: -.. literalinclude:: src/authorization/tutorial/models/mymodel.py - :linenos: - :emphasize-lines: 1-4,22-29 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 38-42 + :lineno-match: + :emphasize-lines: 2,4 :language: python -Only the highlighted lines need to be added or edited. - -Our ``tutorial/views/default.py`` will look like this when we're done: +Edit the ``add_page`` view to declare the ``create`` permission: .. literalinclude:: src/authorization/tutorial/views/default.py - :linenos: - :emphasize-lines: 9-16,19,24,29-30,52-53,66-67,81-112 + :lines: 52-56 + :lineno-match: + :emphasize-lines: 2,4 :language: python -Only the highlighted lines need to be added or edited. - -Our ``tutorial/templates/edit.jinja2`` template will look like this when -we're done: - -.. literalinclude:: src/authorization/tutorial/templates/edit.jinja2 - :linenos: - :emphasize-lines: 36-40 - :language: html +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. -Only the highlighted lines need to be added or edited. +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 a ``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 ``tutorial/templates/view.jinja2`` template will look like this when -we're done: +The final ``tutorial/views/default.py`` should look like the following: -.. literalinclude:: src/authorization/tutorial/templates/view.jinja2 +.. literalinclude:: src/authorization/tutorial/views/default.py :linenos: - :emphasize-lines: 36-40 - :language: html - -Only the highlighted lines need to be added or edited. + :language: python Viewing the application in a browser ------------------------------------ |
