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 |
|
62ab0f
|
46 |
Just like in :ref:`wiki_defining_views`, we need a new dependency. We need to add the `bcrypt <https://pypi.org/project/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: |
|
52 |
:emphasize-lines: 21 |
|
53 |
:language: python |
|
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 |
b0b299
|
158 |
:lines: 18-23 |
beb4f1
|
159 |
:lineno-match: |
c226b1
|
160 |
:emphasize-lines: 1-3,5-6 |
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 |
674487
|
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: |
b0b299
|
330 |
:emphasize-lines: 4-5,8,18-20,22-23 |
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. |