diff options
| author | Michael Merickel <michael@merickel.org> | 2016-12-14 00:25:46 -0600 |
|---|---|---|
| committer | Michael Merickel <michael@merickel.org> | 2016-12-14 00:25:46 -0600 |
| commit | 12a11daa6a29c36488f083ec7323ad1d91f11b8b (patch) | |
| tree | 6967360418b3f8c1ab335598f4e5a6df3b184584 /docs/tutorials/wiki | |
| parent | 3c5731a1cac958e05093df56263d52433fc683a9 (diff) | |
| parent | 8dfa640a52d57b630843a65b3b84bc13a8a1ed87 (diff) | |
| download | pyramid-12a11daa6a29c36488f083ec7323ad1d91f11b8b.tar.gz pyramid-12a11daa6a29c36488f083ec7323ad1d91f11b8b.tar.bz2 pyramid-12a11daa6a29c36488f083ec7323ad1d91f11b8b.zip | |
Merge branch 'master' into pr/2853
Diffstat (limited to 'docs/tutorials/wiki')
| -rw-r--r-- | docs/tutorials/wiki/authorization.rst | 41 | ||||
| -rw-r--r-- | docs/tutorials/wiki/definingviews.rst | 1 | ||||
| -rw-r--r-- | docs/tutorials/wiki/src/authorization/setup.py | 1 | ||||
| -rw-r--r-- | docs/tutorials/wiki/src/authorization/tutorial/security.py | 17 | ||||
| -rw-r--r-- | docs/tutorials/wiki/src/authorization/tutorial/views.py | 4 | ||||
| -rw-r--r-- | docs/tutorials/wiki/src/tests/setup.py | 1 | ||||
| -rw-r--r-- | docs/tutorials/wiki/src/tests/tutorial/security.py | 17 | ||||
| -rw-r--r-- | docs/tutorials/wiki/src/tests/tutorial/tests.py | 11 | ||||
| -rw-r--r-- | docs/tutorials/wiki/src/tests/tutorial/views.py | 4 |
9 files changed, 86 insertions, 11 deletions
diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 44097b35b..67af83b25 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -18,6 +18,7 @@ require permission, instead of a default "403 Forbidden" page. We will implement the access control with the following steps: +* Add password hashing dependencies. * 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` @@ -38,11 +39,32 @@ Then we will add the login and logout feature: Access control -------------- + +Add dependencies +~~~~~~~~~~~~~~~~ + +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. + +Open ``setup.py`` and edit it to look like the following: + +.. literalinclude:: src/authorization/setup.py + :linenos: + :emphasize-lines: 21 + :language: python + +Only the highlighted line needs to be added. + +Do not forget to run ``pip install -e .`` just like in :ref:`wiki-running-pip-install`. + +.. note:: + + 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. + + Add users and groups ~~~~~~~~~~~~~~~~~~~~ -Create a new ``tutorial/security.py`` module with the -following content: +Create a new ``tutorial/security.py`` module with the following content: .. literalinclude:: src/authorization/tutorial/security.py :linenos: @@ -61,7 +83,20 @@ 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 +There are two helper methods that will help us later to authenticate users. +The first is ``hash_password`` which takes a raw password and transforms it using +bcrypt into an irreversible representation, a process known as "hashing". The +second method, ``check_password``, will allow us to compare the hashed value of the +submitted password against the hashed value of the password stored in the user's +record. If the two hashed values match, then the submitted +password is valid, and we can authenticate the user. + +We hash passwords so that it is impossible to decrypt and use them to +authenticate in the application. If we stored passwords foolishly in clear text, +then anyone with access to the database could retrieve any password to authenticate +as any user. + +In a production system, user and group data will most often be saved and come from a database, but here we use "dummy" data to represent user and groups sources. Add an ACL diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index ac94d8059..3859d2cad 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -52,6 +52,7 @@ Open ``setup.py`` and edit it to look like the following: Only the highlighted line needs to be added. +.. _wiki-running-pip-install: Running ``pip install -e .`` ============================ diff --git a/docs/tutorials/wiki/src/authorization/setup.py b/docs/tutorials/wiki/src/authorization/setup.py index beeed75c9..68e3c0abd 100644 --- a/docs/tutorials/wiki/src/authorization/setup.py +++ b/docs/tutorials/wiki/src/authorization/setup.py @@ -18,6 +18,7 @@ requires = [ 'ZODB3', 'waitress', 'docutils', + 'bcrypt', ] tests_require = [ diff --git a/docs/tutorials/wiki/src/authorization/tutorial/security.py b/docs/tutorials/wiki/src/authorization/tutorial/security.py index d88c9c71f..cbb3acd5d 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/security.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/security.py @@ -1,5 +1,18 @@ -USERS = {'editor':'editor', - 'viewer':'viewer'} +import bcrypt + + +def hash_password(pw): + hashed_pw = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt()) + # return unicode instead of bytes because databases handle it better + return hashed_pw.decode('utf-8') + +def check_password(expected_hash, pw): + if expected_hash is not None: + return bcrypt.checkpw(pw.encode('utf-8'), expected_hash.encode('utf-8')) + return False + +USERS = {'editor': hash_password('editor'), + 'viewer': hash_password('viewer')} GROUPS = {'editor':['group:editors']} def groupfinder(userid, request): diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py index c271d2cc1..e4560dfe1 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py @@ -14,7 +14,7 @@ from pyramid.security import ( ) -from .security import USERS +from .security import USERS, check_password from .models import Page # regular expression used to find WikiWords @@ -94,7 +94,7 @@ def login(request): if 'form.submitted' in request.params: login = request.params['login'] password = request.params['password'] - if USERS.get(login) == password: + if check_password(USERS.get(login), password): headers = remember(request, login) return HTTPFound(location=came_from, headers=headers) diff --git a/docs/tutorials/wiki/src/tests/setup.py b/docs/tutorials/wiki/src/tests/setup.py index beeed75c9..68e3c0abd 100644 --- a/docs/tutorials/wiki/src/tests/setup.py +++ b/docs/tutorials/wiki/src/tests/setup.py @@ -18,6 +18,7 @@ requires = [ 'ZODB3', 'waitress', 'docutils', + 'bcrypt', ] tests_require = [ diff --git a/docs/tutorials/wiki/src/tests/tutorial/security.py b/docs/tutorials/wiki/src/tests/tutorial/security.py index d88c9c71f..cbb3acd5d 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/security.py +++ b/docs/tutorials/wiki/src/tests/tutorial/security.py @@ -1,5 +1,18 @@ -USERS = {'editor':'editor', - 'viewer':'viewer'} +import bcrypt + + +def hash_password(pw): + hashed_pw = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt()) + # return unicode instead of bytes because databases handle it better + return hashed_pw.decode('utf-8') + +def check_password(expected_hash, pw): + if expected_hash is not None: + return bcrypt.checkpw(pw.encode('utf-8'), expected_hash.encode('utf-8')) + return False + +USERS = {'editor': hash_password('editor'), + 'viewer': hash_password('viewer')} GROUPS = {'editor':['group:editors']} def groupfinder(userid, request): diff --git a/docs/tutorials/wiki/src/tests/tutorial/tests.py b/docs/tutorials/wiki/src/tests/tutorial/tests.py index 04beaea44..098e9c1bd 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/tests.py +++ b/docs/tutorials/wiki/src/tests/tutorial/tests.py @@ -122,6 +122,17 @@ class EditPageTests(unittest.TestCase): self.assertEqual(response.location, 'http://example.com/') self.assertEqual(context.data, 'Hello yo!') +class SecurityTests(unittest.TestCase): + def test_hashing(self): + from .security import hash_password, check_password + password = 'secretpassword' + hashed_password = hash_password(password) + self.assertTrue(check_password(hashed_password, password)) + + self.assertFalse(check_password(hashed_password, 'attackerpassword')) + + self.assertFalse(check_password(None, password)) + class FunctionalTests(unittest.TestCase): viewer_login = '/login?login=viewer&password=viewer' \ diff --git a/docs/tutorials/wiki/src/tests/tutorial/views.py b/docs/tutorials/wiki/src/tests/tutorial/views.py index c271d2cc1..e4560dfe1 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki/src/tests/tutorial/views.py @@ -14,7 +14,7 @@ from pyramid.security import ( ) -from .security import USERS +from .security import USERS, check_password from .models import Page # regular expression used to find WikiWords @@ -94,7 +94,7 @@ def login(request): if 'form.submitted' in request.params: login = request.params['login'] password = request.params['password'] - if USERS.get(login) == password: + if check_password(USERS.get(login), password): headers = remember(request, login) return HTTPFound(location=came_from, headers=headers) |
