Steve Piercy
2017-06-11 909ae055f2f7391036736ff41a0564becb60478f
commit | author | age
5f375c 1 .. _wiki_adding_authorization:
SP 2
e53e13 3 ====================
5f375c 4 Adding authorization
e53e13 5 ====================
CM 6
a435db 7 :app:`Pyramid` provides facilities for :term:`authentication` and
a38b84 8 :term:`authorization`. We'll make use of both features to provide security to
SP 9 our application. Our application currently allows anyone with access to the
10 server to view, edit, and add pages to our wiki. We'll change that to allow
11 only people who are members of a *group* named ``group:editors`` to add and
12 edit wiki pages, but we'll continue allowing anyone with access to the server
13 to view pages.
f5eba4 14
5f375c 15 We will also add a login page and a logout link on all the pages.  The login
SP 16 page will be shown when a user is denied access to any of the views that
17 require permission, instead of a default "403 Forbidden" page.
adee7f 18
a435db 19 We will implement the access control with the following steps:
adee7f 20
3adaf3 21 * Add password hashing dependencies.
a435db 22 * Add users and groups (``security.py``, a new module).
6c3dd2 23 * Add an :term:`ACL` (``models.py``).
a435db 24 * Add an :term:`authentication policy` and an :term:`authorization policy`
PP 25   (``__init__.py``).
26 * Add :term:`permission` declarations to the ``edit_page`` and ``add_page``
27   views (``views.py``).
28
beb4f1 29 Then we will add the login and logout features:
a435db 30
PP 31 * Add ``login`` and ``logout`` views (``views.py``).
32 * Add a login template (``login.pt``).
5f375c 33 * Make the existing views return a ``logged_in`` flag to the renderer
SP 34   (``views.py``).
a435db 35 * Add a "Logout" link to be shown when logged in and viewing or editing a page
PP 36   (``view.pt``, ``edit.pt``).
e53e13 37
a435db 38
5f375c 39 Access control
a435db 40 --------------
b01a02 41
3adaf3 42
b01a02 43 Add dependencies
M 44 ~~~~~~~~~~~~~~~~
45
3adaf3 46 Just like in :ref:`wiki_defining_views`, we need a new dependency. We need to add the `bcrypt <https://pypi.python.org/pypi/bcrypt>`_ package, to our tutorial package's ``setup.py`` file by assigning this dependency to the ``requires`` parameter in the ``setup()`` function.
b01a02 47
M 48 Open ``setup.py`` and edit it to look like the following:
49
50 .. literalinclude:: src/authorization/setup.py
51    :linenos:
909ae0 52    :emphasize-lines: 23
b01a02 53    :language: python
M 54
55 Only the highlighted line needs to be added.
56
57 Do not forget to run ``pip install -e .`` just like in :ref:`wiki-running-pip-install`.
ded6e0 58
3adaf3 59 .. note::
SP 60
61    We are using the ``bcrypt`` package from PyPI to hash our passwords securely. There are other one-way hash algorithms for passwords if bcrypt is an issue on your system. Just make sure that it's an algorithm approved for storing passwords versus a generic one-way hash.
62
63
a435db 64 Add users and groups
PP 65 ~~~~~~~~~~~~~~~~~~~~
e53e13 66
3adaf3 67 Create a new ``tutorial/security.py`` module with the following content:
e53e13 68
CM 69 .. literalinclude:: src/authorization/tutorial/security.py
70    :linenos:
71    :language: python
72
6c3dd2 73 The ``groupfinder`` function accepts a userid and a request and
PP 74 returns one of these values:
75
beb4f1 76 - If ``userid`` exists in the system, it will return a sequence of group
5f375c 77   identifiers (or an empty sequence if the user isn't a member of any groups).
SP 78 - If the userid *does not* exist in the system, it will return ``None``.
6c3dd2 79
5477d8 80 For example, ``groupfinder('editor', request )`` returns ``['group:editor']``,
696e0e 81 ``groupfinder('viewer', request)`` returns ``[]``, and ``groupfinder('admin',
CM 82 request)`` returns ``None``.  We will use ``groupfinder()`` as an
83 :term:`authentication policy` "callback" that will provide the
84 :term:`principal` or principals for a user.
6c3dd2 85
b4abcd 86 There are two helper methods that will help us later to authenticate users.
b01a02 87 The first is ``hash_password`` which takes a raw password and transforms it using
3adaf3 88 bcrypt into an irreversible representation, a process known as "hashing". The
b01a02 89 second method, ``check_password``, will allow us to compare the hashed value of the
M 90 submitted password against the hashed value of the password stored in the user's
91 record. If the two hashed values match, then the submitted
92 password is valid, and we can authenticate the user.
93
b4abcd 94 We hash passwords so that it is impossible to decrypt and use them to
b01a02 95 authenticate in the application. If we stored passwords foolishly in clear text,
M 96 then anyone with access to the database could retrieve any password to authenticate
97 as any user.
98
99 In a production system, user and group data will most often be saved and come from a
5f375c 100 database, but here we use "dummy" data to represent user and groups sources.
adee7f 101
a435db 102 Add an ACL
PP 103 ~~~~~~~~~~
adee7f 104
a38b84 105 Open ``tutorial/models.py`` and add the following import
beb4f1 106 statement near the top:
adee7f 107
6c3dd2 108 .. literalinclude:: src/authorization/tutorial/models.py
beb4f1 109    :lines: 4-8
SP 110    :lineno-match:
6c3dd2 111    :language: python
adee7f 112
6d46a7 113 Add the following lines to the ``Wiki`` class:
adee7f 114
6c3dd2 115 .. literalinclude:: src/authorization/tutorial/models.py
6d46a7 116    :lines: 9-13
beb4f1 117    :lineno-match:
6c3dd2 118    :emphasize-lines: 4-5
PP 119    :language: python
adee7f 120
5f375c 121 We import :data:`~pyramid.security.Allow`, an action that means that
SP 122 permission is allowed, and :data:`~pyramid.security.Everyone`, a special
123 :term:`principal` that is associated to all requests.  Both are used in the
6c3dd2 124 :term:`ACE` entries that make up the ACL.
PP 125
beb4f1 126 The ACL is a list that needs to be named ``__acl__`` and be an attribute of a
5f375c 127 class.  We define an :term:`ACL` with two :term:`ACE` entries: the first entry
beb4f1 128 allows any user the ``view`` permission.  The second entry allows the
SP 129 ``group:editors`` principal the ``edit`` permission.
6c3dd2 130
5f375c 131 The ``Wiki`` class that contains the ACL is the :term:`resource` constructor
SP 132 for the :term:`root` resource, which is a ``Wiki`` instance.  The ACL is
133 provided to each view in the :term:`context` of the request as the ``context``
134 attribute.
adee7f 135
PP 136 It's only happenstance that we're assigning this ACL at class scope.  An ACL
137 can be attached to an object *instance* too; this is how "row level security"
5477d8 138 can be achieved in :app:`Pyramid` applications.  We actually need only *one*
adee7f 139 ACL for the entire system, however, because our security requirements are
5f375c 140 simple, so this feature is not demonstrated.  See :ref:`assigning_acls` for
SP 141 more information about what an :term:`ACL` represents.
e53e13 142
5f375c 143 Add authentication and authorization policies
9168ec 144 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
PP 145
a38b84 146 Open ``tutorial/__init__.py`` and add the highlighted import
5f375c 147 statements:
9168ec 148
PP 149 .. literalinclude:: src/authorization/tutorial/__init__.py
5f375c 150    :lines: 1-8
9168ec 151    :linenos:
beb4f1 152    :emphasize-lines: 3-6,8
9168ec 153    :language: python
PP 154
c226b1 155 Now add those policies to the configuration:
9168ec 156
PP 157 .. literalinclude:: src/authorization/tutorial/__init__.py
909ae0 158    :lines: 18-25
beb4f1 159    :lineno-match:
909ae0 160    :emphasize-lines: 1-3,7-9
9168ec 161    :language: python
PP 162
5f375c 163 Only the highlighted lines need to be added.
9168ec 164
5f375c 165 We are enabling an ``AuthTktAuthenticationPolicy``, which is based in an auth
SP 166 ticket that may be included in the request. We are also enabling an
167 ``ACLAuthorizationPolicy``, which uses an ACL to determine the *allow* or
168 *deny* outcome for a view.
9168ec 169
19b820 170 Note that the :class:`pyramid.authentication.AuthTktAuthenticationPolicy`
048754 171 constructor accepts two arguments: ``secret`` and ``callback``.  ``secret`` is
CM 172 a string representing an encryption key used by the "authentication ticket"
173 machinery represented by this policy: it is required.  The ``callback`` is the
c226b1 174 ``groupfinder()`` function that we created before.
9168ec 175
PP 176 Add permission declarations
177 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
a38b84 178 Open ``tutorial/views.py`` and add a ``permission='edit'`` parameter
5f375c 179 to the ``@view_config`` decorators for ``add_page()`` and ``edit_page()``:
9168ec 180
5f375c 181 .. literalinclude:: src/authorization/tutorial/views.py
beb4f1 182    :lines: 49-51
5f375c 183    :emphasize-lines: 2-3
SP 184    :language: python
9168ec 185
5f375c 186 .. literalinclude:: src/authorization/tutorial/views.py
beb4f1 187    :lines: 68-70
5f375c 188    :emphasize-lines: 2-3
SP 189    :language: python
c226b1 190
5f375c 191 Only the highlighted lines, along with their preceding commas, need to be
SP 192 edited and added.
c226b1 193
5f375c 194 The result is that only users who possess the ``edit`` permission at the time
SP 195 of the request may invoke those two views.
c226b1 196
5f375c 197 Add a ``permission='view'`` parameter to the ``@view_config`` decorator for
SP 198 ``view_wiki()`` and ``view_page()`` as follows:
c226b1 199
5f375c 200 .. literalinclude:: src/authorization/tutorial/views.py
SP 201    :lines: 23-24
202    :emphasize-lines: 1-2
203    :language: python
c226b1 204
5f375c 205 .. literalinclude:: src/authorization/tutorial/views.py
SP 206    :lines: 28-29
207    :emphasize-lines: 1-2
208    :language: python
c226b1 209
5f375c 210 Only the highlighted lines, along with their preceding commas, need to be
SP 211 edited and added.
c226b1 212
PP 213 This allows anyone to invoke these two views.
214
5f375c 215 We are done with the changes needed to control access.  The changes that
SP 216 follow will add the login and logout feature.
9168ec 217
5f375c 218 Login, logout
9168ec 219 -------------
PP 220
5f375c 221 Add login and logout views
9b215d 222 ~~~~~~~~~~~~~~~~~~~~~~~~~~
e53e13 223
5f375c 224 We'll add a ``login`` view which renders a login form and processes the post
SP 225 from the login form, checking credentials.
e53e13 226
5f375c 227 We'll also add a ``logout`` view callable to our application and provide a
SP 228 link to it.  This view will clear the credentials of the logged in user and
229 redirect back to the front page.
e53e13 230
5f375c 231 Add the following import statements to the head of
a38b84 232 ``tutorial/views.py``:
e53e13 233
ed252b 234 .. literalinclude:: src/authorization/tutorial/views.py
b960f6 235    :lines: 6-17
ad6b57 236    :emphasize-lines: 1-12
fad500 237    :language: python
PP 238
5f375c 239 All the highlighted lines need to be added or edited.
fad500 240
5f375c 241 :meth:`~pyramid.view.forbidden_view_config` will be used to customize the
SP 242 default 403 Forbidden page. :meth:`~pyramid.security.remember` and
243 :meth:`~pyramid.security.forget` help to create and expire an auth ticket
244 cookie.
fad500 245
5f375c 246 Now add the ``login`` and ``logout`` views at the end of the file:
fad500 247
PP 248 .. literalinclude:: src/authorization/tutorial/views.py
beb4f1 249    :lines: 80-
SP 250    :lineno-match:
e53e13 251    :language: python
b743bb 252
36e9bf 253 ``login()`` has two decorators:
ed252b 254
5f375c 255 - a ``@view_config`` decorator which associates it with the ``login`` route
SP 256   and makes it visible when we visit ``/login``,
257 - a ``@forbidden_view_config`` decorator which turns it into a
258   :term:`forbidden view`. ``login()`` will be invoked when a user tries to
259   execute a view callable for which they lack authorization.  For example, if
260   a user has not logged in and tries to add or edit a Wiki page, they will be
261   shown the login form before being allowed to continue.
ed252b 262
5f375c 263 The order of these two :term:`view configuration` decorators is unimportant.
c57b06 264
5f375c 265 ``logout()`` is decorated with a ``@view_config`` decorator which associates
SP 266 it with the ``logout`` route.  It will be invoked when we visit ``/logout``.
ed252b 267
9168ec 268 Add the ``login.pt`` Template
PP 269 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
270
a38b84 271 Create ``tutorial/templates/login.pt`` with the following content:
9168ec 272
PP 273 .. literalinclude:: src/authorization/tutorial/templates/login.pt
5f375c 274    :language: html
9168ec 275
5f375c 276 The above template is referenced in the login view that we just added in
SP 277 ``views.py``.
fad500 278
5f375c 279 Return a ``logged_in`` flag to the renderer
SP 280 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
e53e13 281
a38b84 282 Open ``tutorial/views.py`` again. Add a ``logged_in`` parameter to
ec82bb 283 the return value of ``view_page()``, ``add_page()``, and ``edit_page()`` as
5f375c 284 follows:
e53e13 285
5f375c 286 .. literalinclude:: src/authorization/tutorial/views.py
beb4f1 287    :lines: 46-47
5f375c 288    :emphasize-lines: 1-2
SP 289    :language: python
e53e13 290
5f375c 291 .. literalinclude:: src/authorization/tutorial/views.py
beb4f1 292    :lines: 65-66
5f375c 293    :emphasize-lines: 1-2
SP 294    :language: python
fad500 295
5f375c 296 .. literalinclude:: src/authorization/tutorial/views.py
beb4f1 297    :lines: 76-78
5f375c 298    :emphasize-lines: 2-3
SP 299    :language: python
300
301 Only the highlighted lines need to be added or edited.
fad500 302
0dcd56 303 The :meth:`pyramid.request.Request.authenticated_userid` will be ``None`` if
5f375c 304 the user is not authenticated, or a userid if the user is authenticated.
adee7f 305
a435db 306 Add a "Logout" link when logged in
adee7f 307 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
PP 308
a38b84 309 Open ``tutorial/templates/edit.pt`` and
SP 310 ``tutorial/templates/view.pt`` and add the following code as
5f375c 311 indicated by the highlighted lines.
adee7f 312
5f375c 313 .. literalinclude:: src/authorization/tutorial/templates/edit.pt
b960f6 314    :lines: 35-39
5f375c 315    :emphasize-lines: 3-5
SP 316    :language: html
adee7f 317
5f375c 318 The attribute ``tal:condition="logged_in"`` will make the element be included
SP 319 when ``logged_in`` is any user id. The link will invoke the logout view.  The
320 above element will not be included if ``logged_in`` is ``None``, such as when
321 a user is not authenticated.
fad500 322
5f375c 323 Reviewing our changes
SP 324 ---------------------
adee7f 325
a38b84 326 Our ``tutorial/__init__.py`` will look like this when we're done:
c226b1 327
PP 328 .. literalinclude:: src/authorization/tutorial/__init__.py
329    :linenos:
909ae0 330    :emphasize-lines: 4-5,8,18-20,24-25
c226b1 331    :language: python
PP 332
5f375c 333 Only the highlighted lines need to be added or edited.
6d46a7 334
a38b84 335 Our ``tutorial/models.py`` will look like this when we're done:
6c3dd2 336
PP 337 .. literalinclude:: src/authorization/tutorial/models.py
338    :linenos:
6d46a7 339    :emphasize-lines: 4-7,12-13
6c3dd2 340    :language: python
PP 341
5f375c 342 Only the highlighted lines need to be added or edited.
6d46a7 343
a38b84 344 Our ``tutorial/views.py`` will look like this when we're done:
adee7f 345
PP 346 .. literalinclude:: src/authorization/tutorial/views.py
347    :linenos:
beb4f1 348    :emphasize-lines: 8,11-15,17,24,29,47,51,66,70,78,80-
adee7f 349    :language: python
PP 350
5f375c 351 Only the highlighted lines need to be added or edited.
6d46a7 352
a38b84 353 Our ``tutorial/templates/edit.pt`` template will look like this when
5f375c 354 we're done:
adee7f 355
PP 356 .. literalinclude:: src/authorization/tutorial/templates/edit.pt
357    :linenos:
beb4f1 358    :emphasize-lines: 37-39
5f375c 359    :language: html
adee7f 360
5f375c 361 Only the highlighted lines need to be added or edited.
6d46a7 362
a38b84 363 Our ``tutorial/templates/view.pt`` template will look like this when
5f375c 364 we're done:
adee7f 365
PP 366 .. literalinclude:: src/authorization/tutorial/templates/view.pt
367    :linenos:
beb4f1 368    :emphasize-lines: 37-39
5f375c 369    :language: html
adee7f 370
5f375c 371 Only the highlighted lines need to be added or edited.
e53e13 372
6901d7 373 Viewing the application in a browser
a435db 374 ------------------------------------
e53e13 375
a435db 376 We can finally examine our application in a browser (See
5f375c 377 :ref:`wiki-start-the-application`).  Launch a browser and visit each of the
SP 378 following URLs, checking that the result is as expected:
e53e13 379
5f375c 380 - http://localhost:6543/ invokes the ``view_wiki`` view.  This always
SP 381   redirects to the ``view_page`` view of the ``FrontPage`` Page resource.  It
382   is executable by any user.
a435db 383
5f375c 384 - http://localhost:6543/FrontPage invokes the ``view_page`` view of the
SP 385   ``FrontPage`` Page resource. This is because it's the :term:`default view`
386   (a view without a ``name``) for ``Page`` resources.  It is executable by any
387   user.
e53e13 388
5f375c 389 - http://localhost:6543/FrontPage/edit_page invokes the edit view for the
SP 390   FrontPage object.  It is executable by only the ``editor`` user.  If a
391   different user (or the anonymous user) invokes it, a login form will be
392   displayed.  Supplying the credentials with the username ``editor``, password
393   ``editor`` will display the edit page form.
e53e13 394
5f375c 395 - http://localhost:6543/add_page/SomePageName invokes the add view for a page.
SP 396   It is executable by only the ``editor`` user.  If a different user (or the
397   anonymous user) invokes it, a login form will be displayed. Supplying the
398   credentials with the username ``editor``, password ``editor`` will display
399   the edit page form.
e53e13 400
5f375c 401 - After logging in (as a result of hitting an edit or add page and submitting
SP 402   the login form with the ``editor`` credentials), we'll see a Logout link in
403   the upper right hand corner.  When we click it, we're logged out, and
404   redirected back to the front page.