summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/tutorials/wiki/authorization.rst253
-rw-r--r--docs/tutorials/wiki/definingmodels.rst6
-rw-r--r--docs/tutorials/wiki/src/authorization/.gitignore1
-rw-r--r--docs/tutorials/wiki/src/authorization/development.ini2
-rw-r--r--docs/tutorials/wiki/src/authorization/production.ini2
-rw-r--r--docs/tutorials/wiki/src/authorization/setup.py4
-rw-r--r--docs/tutorials/wiki/src/authorization/testing.ini62
-rw-r--r--docs/tutorials/wiki/src/authorization/tests/conftest.py69
-rw-r--r--docs/tutorials/wiki/src/authorization/tests/test_functional.py7
-rw-r--r--docs/tutorials/wiki/src/authorization/tests/test_it.py24
-rw-r--r--docs/tutorials/wiki/src/authorization/tests/test_views.py13
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/__init__.py15
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/models/__init__.py8
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/security.py55
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt3
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/templates/layout.pt20
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt3
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/views/auth.py50
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/views/default.py74
-rw-r--r--docs/tutorials/wiki/src/models/tutorial/models/__init__.py1
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/models/__init__.py1
-rw-r--r--docs/tutorials/wiki/src/views/tutorial/views/default.py6
-rw-r--r--docs/tutorials/wiki2/authentication.rst4
-rw-r--r--tox.ini2
24 files changed, 412 insertions, 273 deletions
diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst
index 2ff9deb31..1469fae44 100644
--- a/docs/tutorials/wiki/authorization.rst
+++ b/docs/tutorials/wiki/authorization.rst
@@ -18,8 +18,8 @@ We will implement the access control with the following steps:
- Add password hashing dependencies.
- Add users and groups (``security.py``, a new module).
+- Add a :term:`security policy` (``security.py``).
- Add an :term:`ACL` (``models.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.py``).
Then we will add the login and logout features:
@@ -43,8 +43,9 @@ We need to add the `bcrypt <https://pypi.org/project/bcrypt/>`_ package to our t
Open ``setup.py`` and edit it to look like the following:
.. literalinclude:: src/authorization/setup.py
- :linenos:
- :emphasize-lines: 23
+ :lines: 11-30
+ :lineno-match:
+ :emphasize-lines: 2
:language: python
Only the highlighted line needs to be added.
@@ -58,8 +59,8 @@ Do not forget to run ``pip install -e .`` just like in :ref:`wiki-running-pip-in
Just make sure that it is an algorithm approved for storing passwords versus a generic one-way hash.
-Add users and groups
-~~~~~~~~~~~~~~~~~~~~
+Add the security policy
+~~~~~~~~~~~~~~~~~~~~~~~
Create a new ``tutorial/security.py`` module with the following content:
@@ -67,21 +68,52 @@ Create a new ``tutorial/security.py`` module with the following content:
:linenos:
:language: python
-The ``groupfinder`` function accepts a ``userid`` and a ``request``
-It returns one of these values:
+Since we've added a new ``tutorial/security.py`` module, we need to include it.
+Open the file ``tutorial/__init__.py`` and edit the following lines:
+
+.. literalinclude:: src/authorization/tutorial/__init__.py
+ :linenos:
+ :emphasize-lines: 21
+ :language: python
+
+The security policy controls several aspects of authentication and authorization:
+
+- Identifying the current user / :term:`identity` for a ``request``.
+
+- Authorizating access to resources.
+
+- Creating payloads for remembering and forgetting users.
+
+
+Identifying logged-in users
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``MySecurityPolicy.authenticated_identity`` method inspects the ``request`` and determines if it came from an authenticated user.
+It does this by utilizing the :class:`pyramid.authentication.AuthTktCookieHelper` class which stores the :term:`identity` in a cryptographically-signed cookie.
+If a ``request`` does contain an identity then we perform a final check to determine if the user is valid in our current ``USERS`` store.
+
+
+Authorizing access to resources
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``MySecurityPolicy.permits`` method determines if the ``request`` is allowed a specific ``permission`` on the given ``context``.
+This process is done in a few steps:
-- If ``userid`` exists in the system, it will return either a sequence of group identifiers, or an empty sequence if the user is not a member of any groups.
-- If the userid *does not* exist in the system, it will return ``None``.
+- Convert the ``request`` into a list of :term:`principals <principal>` via the ``MySecurityPolicy.effective_principals`` method.
-For example:
+- Compare the list of principals to the ``context`` using the :class:`pyramid.authorization.ACLHelper`.
+ It will only allow access if it can find an :term:`ACE` that grants one of the principals the necessary permission.
-- ``groupfinder('editor', request )`` returns ``['group:editor']``.
-- ``groupfinder('viewer', request)`` returns ``[]``.
-- ``groupfinder('admin', request)`` returns ``None``.
+For our application we've defined a list of a few principals:
-We will use ``groupfinder()`` as an :term:`authentication policy` "callback" that will provide the :term:`principal` or principals for a user.
+- ``u:<userid>``
+- ``group:editor``
+- :attr:`pyramid.security.Authenticated`
+- :attr:`pyramid.security.Everyone`
-There are two helper methods that will help us later to authenticate users.
+Later, various wiki pages will grant some of these principals access to edit, or add new pages.
+
+Finally, 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
@@ -96,22 +128,52 @@ database.
Here we use "dummy" data to represent user and groups sources.
+Add new settings
+~~~~~~~~~~~~~~~~
+
+Our authentication policy is expecting a new setting, ``auth.secret``. Open
+the file ``development.ini`` and add the highlighted line below:
+
+.. literalinclude:: src/authorization/development.ini
+ :lines: 19-21
+ :emphasize-lines: 3
+ :lineno-match:
+ :language: ini
+
+Finally, best practices tell us to use a different secret in each environment, so
+open ``production.ini`` and add a different secret:
+
+.. literalinclude:: src/authorization/production.ini
+ :lines: 17-19
+ :emphasize-lines: 3
+ :lineno-match:
+ :language: ini
+
+And ``testing.ini``:
+
+.. literalinclude:: src/authorization/testing.ini
+ :lines: 17-19
+ :emphasize-lines: 3
+ :lineno-match:
+ :language: ini
+
+
Add an ACL
~~~~~~~~~~
Open ``tutorial/models/__init__.py`` and add the following import statement near the top:
.. literalinclude:: src/authorization/tutorial/models/__init__.py
- :lines: 4-8
+ :lines: 4-7
:lineno-match:
:language: python
Add the following lines to the ``Wiki`` class:
.. literalinclude:: src/authorization/tutorial/models/__init__.py
- :lines: 9-13
+ :pyobject: Wiki
:lineno-match:
- :emphasize-lines: 4-5
+ :emphasize-lines: 4-7
:language: python
We import :data:`~pyramid.security.Allow`, an action which means that
@@ -137,49 +199,20 @@ We actually need only *one* ACL for the entire system, however, because our secu
See :ref:`assigning_acls` for more information about what an :term:`ACL` represents.
-Add authentication and authorization policies
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Open ``tutorial/__init__.py`` and add the highlighted import
-statements:
-
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 1-8
- :linenos:
- :emphasize-lines: 3-6,8
- :language: python
-
-Now add those policies to the configuration:
-
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 15-25
- :lineno-match:
- :emphasize-lines: 4-6,8-9
- :language: python
-
-Only the highlighted lines need to be added.
-
-We enabled an ``AuthTktAuthenticationPolicy`` which is based in an auth ticket that may be included in the request.
-We also enabled 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 earlier.
-
-
Add permission declarations
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Open ``tutorial/views/default.py`` and add a ``permission='edit'`` parameter to the ``@view_config`` decorators for ``add_page()`` and ``edit_page()``:
.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 49-51
+ :lines: 39-41
+ :lineno-match:
:emphasize-lines: 2-3
:language: python
.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 69-71
+ :lines: 58-60
+ :lineno-match:
:emphasize-lines: 2-3
:language: python
@@ -188,16 +221,20 @@ Only the highlighted lines, along with their preceding commas, need to be edited
The result is that only users who possess the ``edit`` permission at the time of the request may invoke those two views.
Add a ``permission='view'`` parameter to the ``@view_config`` decorator for
-``view_wiki()`` and ``view_page()`` as follows:
+``view_wiki()`` as follows:
.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 21-22
- :emphasize-lines: 1-2
+ :lines: 12
+ :lineno-match:
+ :emphasize-lines: 1
:language: python
+And ``view_page()`` as follows:
+
.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 27-28
- :emphasize-lines: 1-2
+ :lines: 17-19
+ :lineno-match:
+ :emphasize-lines: 2-3
:language: python
Only the highlighted lines, along with their preceding commas, need to be edited and added.
@@ -220,25 +257,15 @@ We will add a ``login`` view which renders a login form and processes the post f
We will 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.
-Add the following import statements to the head of ``tutorial/views/default.py``:
+Add a new file ``tutorial/views/auth.py`` with the following contents:
-.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 4-15
- :emphasize-lines: 2-10,12
+.. literalinclude:: src/authorization/tutorial/views/auth.py
+ :lineno-match:
: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:
-
-.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 78-
- :lineno-match:
- :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``.
@@ -263,41 +290,15 @@ Create ``tutorial/templates/login.pt`` with the following content:
The above template is referenced in the login view that we just added in ``views.py``.
-Return a ``logged_in`` flag to the renderer
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Add a "Login" and "Logout" links
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Open ``tutorial/views/default.py`` again.
-Add a ``logged_in`` parameter to the return value of ``view_page()``, ``add_page()``, and ``edit_page()`` as follows:
+Open ``tutorial/templates/layout.pt`` and add the following code as indicated by the highlighted lines.
-.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 45-46
- :emphasize-lines: 1-2
- :language: python
-
-.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 65-66
- :emphasize-lines: 1-2
- :language: python
-
-.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 77-79
- :emphasize-lines: 2-3
- :language: python
-
-Only the highlighted lines need to be added or edited.
-
-The :meth:`pyramid.request.Request.authenticated_userid` will be ``None`` if the user is not authenticated, or a ``userid`` if the user is authenticated.
-
-
-Add a "Logout" link when logged in
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Open ``tutorial/templates/edit.pt`` and ``tutorial/templates/view.pt``.
-Add the following code as indicated by the highlighted lines.
-
-.. literalinclude:: src/authorization/tutorial/templates/edit.pt
- :lines: 4-8
- :emphasize-lines: 2-4
+.. literalinclude:: src/authorization/tutorial/templates/layout.pt
+ :lines: 34-43
+ :lineno-match:
+ :emphasize-lines: 2-9
:language: html
The attribute ``tal:condition="logged_in"`` will make the element be included when ``logged_in`` is any user id.
@@ -306,54 +307,6 @@ The above element will not be included if ``logged_in`` is ``None``, such as whe
a user is not authenticated.
-Reviewing our changes
----------------------
-
-Our ``tutorial/__init__.py`` will look like this when we are done:
-
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :linenos:
- :emphasize-lines: 3-6,8,18-20,22-23
- :language: python
-
-Only the highlighted lines need to be added or edited.
-
-Our ``tutorial/models/__init__.py`` will look like this when we are done:
-
-.. literalinclude:: src/authorization/tutorial/models/__init__.py
- :linenos:
- :emphasize-lines: 4-8,12-13
- :language: python
-
-Only the highlighted lines need to be added or edited.
-
-Our ``tutorial/views/default.py`` will look like this when we are done:
-
-.. literalinclude:: src/authorization/tutorial/views/default.py
- :linenos:
- :emphasize-lines: 5-12,15,21-22,27-28,45-46,50-51,65-66,70-71,78-
- :language: python
-
-Only the highlighted lines need to be added or edited.
-
-Our ``tutorial/templates/edit.pt`` template will look like this when we are done:
-
-.. literalinclude:: src/authorization/tutorial/templates/edit.pt
- :linenos:
- :emphasize-lines: 5-7
- :language: html
-
-Only the highlighted lines need to be added or edited.
-
-Our ``tutorial/templates/view.pt`` template will look like this when we are done:
-
-.. literalinclude:: src/authorization/tutorial/templates/view.pt
- :linenos:
- :emphasize-lines: 5-7
- :language: html
-
-Only the highlighted lines need to be added or edited.
-
Viewing the application in a browser
------------------------------------
diff --git a/docs/tutorials/wiki/definingmodels.rst b/docs/tutorials/wiki/definingmodels.rst
index 3a340e6f7..d4402915a 100644
--- a/docs/tutorials/wiki/definingmodels.rst
+++ b/docs/tutorials/wiki/definingmodels.rst
@@ -60,7 +60,7 @@ We will use this for a new ``Page`` class in a moment.
Then we add a ``Wiki`` class.
.. literalinclude:: src/models/tutorial/models/__init__.py
- :lines: 4-6
+ :pyobject: Wiki
:lineno-match:
:language: py
@@ -74,7 +74,7 @@ The ``__name__`` of the root model is also always ``None``.
Now we add a ``Page`` class.
.. literalinclude:: src/models/tutorial/models/__init__.py
- :lines: 8-10
+ :pyobject: Page
:lineno-match:
:language: py
@@ -91,7 +91,7 @@ We will create this function in the next chapter.
As a last step, edit the ``appmaker`` function.
.. literalinclude:: src/models/tutorial/models/__init__.py
- :lines: 12-20
+ :pyobject: appmaker
:lineno-match:
:emphasize-lines: 4-8
:language: py
diff --git a/docs/tutorials/wiki/src/authorization/.gitignore b/docs/tutorials/wiki/src/authorization/.gitignore
index 1853d983c..c612e59f2 100644
--- a/docs/tutorials/wiki/src/authorization/.gitignore
+++ b/docs/tutorials/wiki/src/authorization/.gitignore
@@ -19,3 +19,4 @@ Data.fs*
.DS_Store
coverage
test
+*.sqlite
diff --git a/docs/tutorials/wiki/src/authorization/development.ini b/docs/tutorials/wiki/src/authorization/development.ini
index 228f18f36..e8aef6b43 100644
--- a/docs/tutorials/wiki/src/authorization/development.ini
+++ b/docs/tutorials/wiki/src/authorization/development.ini
@@ -18,6 +18,8 @@ zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000
retry.attempts = 3
+auth.secret = seekrit
+
# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1
diff --git a/docs/tutorials/wiki/src/authorization/production.ini b/docs/tutorials/wiki/src/authorization/production.ini
index 46b1e331b..35ef6aabe 100644
--- a/docs/tutorials/wiki/src/authorization/production.ini
+++ b/docs/tutorials/wiki/src/authorization/production.ini
@@ -16,6 +16,8 @@ zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000
retry.attempts = 3
+auth.secret = real-seekrit
+
[pshell]
setup = tutorial.pshell.setup
diff --git a/docs/tutorials/wiki/src/authorization/setup.py b/docs/tutorials/wiki/src/authorization/setup.py
index f19d643e6..cdfa18e09 100644
--- a/docs/tutorials/wiki/src/authorization/setup.py
+++ b/docs/tutorials/wiki/src/authorization/setup.py
@@ -9,6 +9,8 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'bcrypt',
+ 'docutils',
'plaster_pastedeploy',
'pyramid',
'pyramid_chameleon',
@@ -19,8 +21,6 @@ requires = [
'pyramid_zodbconn',
'transaction',
'ZODB3',
- 'docutils',
- 'bcrypt',
]
tests_require = [
diff --git a/docs/tutorials/wiki/src/authorization/testing.ini b/docs/tutorials/wiki/src/authorization/testing.ini
new file mode 100644
index 000000000..81193b35a
--- /dev/null
+++ b/docs/tutorials/wiki/src/authorization/testing.ini
@@ -0,0 +1,62 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:tutorial
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+
+zodbconn.uri = file://%(here)s/Data.testing.fs?connection_cache_size=20000
+
+retry.attempts = 3
+
+auth.secret = testing-seekrit
+
+[pshell]
+setup = tutorial.pshell.setup
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+listen = localhost:6543
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, tutorial
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_tutorial]
+level = DEBUG
+handlers =
+qualname = tutorial
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/tutorials/wiki/src/authorization/tests/conftest.py b/docs/tutorials/wiki/src/authorization/tests/conftest.py
new file mode 100644
index 000000000..12e75d8e9
--- /dev/null
+++ b/docs/tutorials/wiki/src/authorization/tests/conftest.py
@@ -0,0 +1,69 @@
+import os
+from pyramid.paster import get_appsettings
+from pyramid.scripting import prepare
+from pyramid.testing import DummyRequest
+import pytest
+import webtest
+
+from tutorial import main
+
+
+def pytest_addoption(parser):
+ parser.addoption('--ini', action='store', metavar='INI_FILE')
+
+@pytest.fixture(scope='session')
+def ini_file(request):
+ # potentially grab this path from a pytest option
+ return os.path.abspath(request.config.option.ini or 'testing.ini')
+
+@pytest.fixture(scope='session')
+def app_settings(ini_file):
+ return get_appsettings(ini_file)
+
+@pytest.fixture(scope='session')
+def app(app_settings):
+ return main({}, **app_settings)
+
+@pytest.fixture
+def testapp(app):
+ testapp = webtest.TestApp(app, extra_environ={
+ 'HTTP_HOST': 'example.com',
+ })
+
+ return testapp
+
+@pytest.fixture
+def app_request(app):
+ """
+ A real request.
+
+ This request is almost identical to a real request but it has some
+ drawbacks in tests as it's harder to mock data and is heavier.
+
+ """
+ env = prepare(registry=app.registry)
+ request = env['request']
+ request.host = 'example.com'
+
+ yield request
+ env['closer']()
+
+@pytest.fixture
+def dummy_request(app):
+ """
+ A lightweight dummy request.
+
+ This request is ultra-lightweight and should be used only when the
+ request itself is not a large focus in the call-stack.
+
+ It is way easier to mock and control side-effects using this object.
+
+ - It does not have request extensions applied.
+ - Threadlocals are not properly pushed.
+
+ """
+ request = DummyRequest()
+ request.registry = app.registry
+ request.host = 'example.com'
+
+ return request
diff --git a/docs/tutorials/wiki/src/authorization/tests/test_functional.py b/docs/tutorials/wiki/src/authorization/tests/test_functional.py
new file mode 100644
index 000000000..bac5d63f4
--- /dev/null
+++ b/docs/tutorials/wiki/src/authorization/tests/test_functional.py
@@ -0,0 +1,7 @@
+def test_root(testapp):
+ res = testapp.get('/', status=200)
+ assert b'Pyramid' in res.body
+
+def test_notfound(testapp):
+ res = testapp.get('/badurl', status=404)
+ assert res.status_code == 404
diff --git a/docs/tutorials/wiki/src/authorization/tests/test_it.py b/docs/tutorials/wiki/src/authorization/tests/test_it.py
deleted file mode 100644
index 6c72bcc62..000000000
--- a/docs/tutorials/wiki/src/authorization/tests/test_it.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import unittest
-
-from pyramid import testing
-
-
-class ViewTests(unittest.TestCase):
- def setUp(self):
- self.config = testing.setUp()
-
- def tearDown(self):
- testing.tearDown()
-
- def test_my_view(self):
- from tutorial.views.default import my_view
- request = testing.DummyRequest()
- info = my_view(request)
- self.assertEqual(info['project'], 'myproj')
-
- def test_notfound_view(self):
- from tutorial.views.notfound import notfound_view
- request = testing.DummyRequest()
- info = notfound_view(request)
- self.assertEqual(info, {})
-
diff --git a/docs/tutorials/wiki/src/authorization/tests/test_views.py b/docs/tutorials/wiki/src/authorization/tests/test_views.py
new file mode 100644
index 000000000..2b4201955
--- /dev/null
+++ b/docs/tutorials/wiki/src/authorization/tests/test_views.py
@@ -0,0 +1,13 @@
+from tutorial.views.default import my_view
+from tutorial.views.notfound import notfound_view
+
+
+def test_my_view(app_request):
+ info = my_view(app_request)
+ assert app_request.response.status_int == 200
+ assert info['project'] == 'myproj'
+
+def test_notfound_view(app_request):
+ info = notfound_view(app_request)
+ assert app_request.response.status_int == 404
+ assert info == {}
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki/src/authorization/tutorial/__init__.py
index 935a5d6d2..2706cc184 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/__init__.py
+++ b/docs/tutorials/wiki/src/authorization/tutorial/__init__.py
@@ -1,11 +1,8 @@
from pyramid.config import Configurator
from pyramid_zodbconn import get_connection
-from pyramid.authentication import AuthTktAuthenticationPolicy
-from pyramid.authorization import ACLAuthorizationPolicy
-
from .models import appmaker
-from .security import groupfinder
+
def root_factory(request):
conn = get_connection(request)
@@ -15,17 +12,13 @@ def root_factory(request):
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
- authn_policy = AuthTktAuthenticationPolicy(
- 'sosecret', callback=groupfinder, hashalg='sha512')
- authz_policy = ACLAuthorizationPolicy()
with Configurator(settings=settings) as config:
- config.set_authentication_policy(authn_policy)
- config.set_authorization_policy(authz_policy)
+ config.include('pyramid_chameleon')
config.include('pyramid_tm')
config.include('pyramid_retry')
config.include('pyramid_zodbconn')
- config.set_root_factory(root_factory)
- config.include('pyramid_chameleon')
config.include('.routes')
+ config.include('.security')
+ config.set_root_factory(root_factory)
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/models/__init__.py b/docs/tutorials/wiki/src/authorization/tutorial/models/__init__.py
index ebd70e912..64ae4bf5c 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki/src/authorization/tutorial/models/__init__.py
@@ -4,13 +4,15 @@ from persistent.mapping import PersistentMapping
from pyramid.security import (
Allow,
Everyone,
- )
+)
class Wiki(PersistentMapping):
__name__ = None
__parent__ = None
- __acl__ = [ (Allow, Everyone, 'view'),
- (Allow, 'group:editors', 'edit') ]
+ __acl__ = [
+ (Allow, Everyone, 'view'),
+ (Allow, 'group:editors', 'edit'),
+ ]
class Page(Persistent):
def __init__(self, data):
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/security.py b/docs/tutorials/wiki/src/authorization/tutorial/security.py
index cbb3acd5d..9f51aa54c 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/security.py
+++ b/docs/tutorials/wiki/src/authorization/tutorial/security.py
@@ -1,4 +1,10 @@
import bcrypt
+from pyramid.authentication import AuthTktCookieHelper
+from pyramid.authorization import ACLHelper
+from pyramid.security import (
+ Authenticated,
+ Everyone,
+)
def hash_password(pw):
@@ -11,10 +17,47 @@ def check_password(expected_hash, pw):
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']}
+USERS = {
+ 'editor': hash_password('editor'),
+ 'viewer': hash_password('viewer'),
+}
+GROUPS = {'editor': ['group:editors']}
-def groupfinder(userid, request):
- if userid in USERS:
- return GROUPS.get(userid, [])
+class MySecurityPolicy:
+ def __init__(self, secret):
+ self.authtkt = AuthTktCookieHelper(secret)
+ self.acl = ACLHelper()
+
+ def authenticated_identity(self, request):
+ identity = self.authtkt.identify(request)
+ if identity is not None and identity['userid'] in USERS:
+ return identity
+
+ def authenticated_userid(self, request):
+ identity = self.authenticated_identity(request)
+ if identity is not None:
+ return identity['userid']
+
+ def remember(self, request, userid, **kw):
+ return self.authtkt.remember(request, userid, **kw)
+
+ def forget(self, request, **kw):
+ return self.authtkt.forget(request, **kw)
+
+ def permits(self, request, context, permission):
+ principals = self.effective_principals(request)
+ return self.acl.permits(context, principals, permission)
+
+ def effective_principals(self, request):
+ principals = [Everyone]
+ identity = self.authenticated_identity(request)
+ if identity is not None:
+ principals.append(Authenticated)
+ principals.append('u:' + identity['userid'])
+ principals.extend(GROUPS.get(identity['userid'], []))
+ return principals
+
+def includeme(config):
+ settings = config.get_settings()
+
+ config.set_security_policy(MySecurityPolicy(settings['auth.secret']))
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt
index 6438b1569..488e7a6af 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt
+++ b/docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt
@@ -2,9 +2,6 @@
<div metal:fill-slot="content">
<div class="content">
- <p tal:condition="logged_in" class="pull-right">
- <a href="${request.application_url}/logout">Logout</a>
- </p>
<p>
Editing <strong><span tal:replace="page.__name__">
Page Name Goes Here</span></strong>
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/layout.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/layout.pt
index 06a3c8157..61042da24 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/templates/layout.pt
+++ b/docs/tutorials/wiki/src/authorization/tutorial/templates/layout.pt
@@ -8,8 +8,7 @@
<meta name="author" content="Pylons Project">
<link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}">
- <title><span tal:replace="page.__name__ | title"></span> - Pyramid tutorial wiki (based on
- TurboGears 20-Minute Wiki)</title>
+ <title><span tal:replace="page.__name__ | title"></span> - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
@@ -33,6 +32,14 @@
<img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework">
</div>
<div class="col-md-10">
+ <div class="content">
+ <p tal:condition="request.authenticated_userid is None" class="pull-right">
+ <a href="${request.application_url}/login">Login</a>
+ </p>
+ <p tal:condition="request.authenticated_userid is not None" class="pull-right">
+ <a href="${request.application_url}/logout">Logout</a>
+ </p>
+ </div>
<div metal:define-slot="content">No content</div>
<div class="content">
<p>You can return to the
@@ -42,6 +49,15 @@
</div>
</div>
<div class="row">
+ <div class="links">
+ <ul>
+ <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
+ <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="https://webchat.freenode.net/?channels=pyramid">IRC Channel</a></li>
+ <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="https://pylonsproject.org">Pylons Project</a></li>
+ </ul>
+ </div>
+ </div>
+ <div class="row">
<div class="copyright">
Copyright &copy; Pylons Project
</div>
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt
index 911ab0c99..b8a6fbbaf 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt
+++ b/docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt
@@ -2,9 +2,6 @@
<div metal:fill-slot="content">
<div class="content">
- <p tal:condition="logged_in" class="pull-right">
- <a href="${request.application_url}/logout">Logout</a>
- </p>
<div tal:replace="structure page_text">
Page text goes here.
</div>
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views/auth.py b/docs/tutorials/wiki/src/authorization/tutorial/views/auth.py
new file mode 100644
index 000000000..cee3cc22b
--- /dev/null
+++ b/docs/tutorials/wiki/src/authorization/tutorial/views/auth.py
@@ -0,0 +1,50 @@
+from pyramid.httpexceptions import HTTPSeeOther
+from pyramid.security import (
+ forget,
+ remember,
+)
+from pyramid.view import (
+ forbidden_view_config,
+ view_config,
+)
+
+from ..security import check_password, USERS
+
+
+@view_config(context='..models.Wiki', name='login',
+ renderer='tutorial:templates/login.pt')
+@forbidden_view_config(renderer='tutorial:templates/login.pt')
+def login(request):
+ login_url = request.resource_url(request.root, 'login')
+ referrer = request.url
+ if referrer == login_url:
+ referrer = '/' # never use the login form itself as came_from
+ came_from = request.params.get('came_from', referrer)
+ message = ''
+ login = ''
+ password = ''
+ if 'form.submitted' in request.params:
+ login = request.params['login']
+ password = request.params['password']
+ if check_password(USERS.get(login), password):
+ headers = remember(request, login)
+ return HTTPSeeOther(location=came_from, headers=headers)
+ message = 'Failed login'
+
+ return dict(
+ message=message,
+ url=login_url,
+ came_from=came_from,
+ login=login,
+ password=password,
+ title='Login',
+ )
+
+
+@view_config(context='..models.Wiki', name='logout')
+def logout(request):
+ headers = forget(request)
+ return HTTPSeeOther(
+ location=request.resource_url(request.context),
+ headers=headers,
+ )
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki/src/authorization/tutorial/views/default.py
index 7ba99c65b..5bb21fbcd 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/views/default.py
+++ b/docs/tutorials/wiki/src/authorization/tutorial/views/default.py
@@ -1,30 +1,21 @@
from docutils.core import publish_parts
+from pyramid.httpexceptions import HTTPSeeOther
+from pyramid.view import view_config
import re
-from pyramid.httpexceptions import HTTPFound
-from pyramid.security import (
- forget,
- remember,
-)
-from pyramid.view import (
- forbidden_view_config,
- view_config,
- )
-
from ..models import Page
-from ..security import check_password, USERS
+
# regular expression used to find WikiWords
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
-
-@view_config(context='..models.Wiki',
- permission='view')
+@view_config(context='..models.Wiki', permission='view')
def view_wiki(context, request):
- return HTTPFound(location=request.resource_url(context, 'FrontPage'))
+ return HTTPSeeOther(location=request.resource_url(context, 'FrontPage'))
-@view_config(context='..models.Page', renderer='tutorial:templates/view.pt',
+@view_config(context='..models.Page',
+ renderer='tutorial:templates/view.pt',
permission='view')
def view_page(context, request):
wiki = context.__parent__
@@ -42,8 +33,7 @@ def view_page(context, request):
page_text = publish_parts(context.data, writer_name='html')['html_body']
page_text = wikiwords.sub(check, page_text)
edit_url = request.resource_url(context, 'edit_page')
- return dict(page=context, page_text=page_text, edit_url=edit_url,
- logged_in=request.authenticated_userid)
+ return dict(page=context, page_text=page_text, edit_url=edit_url)
@view_config(name='add_page', context='..models.Wiki',
@@ -57,13 +47,12 @@ def add_page(context, request):
page.__name__ = pagename
page.__parent__ = context
context[pagename] = page
- return HTTPFound(location=request.resource_url(page))
+ return HTTPSeeOther(location=request.resource_url(page))
save_url = request.resource_url(context, 'add_page', pagename)
page = Page('')
page.__name__ = pagename
page.__parent__ = context
- return dict(page=page, save_url=save_url,
- logged_in=request.authenticated_userid)
+ return dict(page=page, save_url=save_url)
@view_config(name='edit_page', context='..models.Page',
@@ -72,46 +61,9 @@ def add_page(context, request):
def edit_page(context, request):
if 'form.submitted' in request.params:
context.data = request.params['body']
- return HTTPFound(location=request.resource_url(context))
-
- return dict(page=context,
- save_url=request.resource_url(context, 'edit_page'),
- logged_in=request.authenticated_userid)
-
-
-@view_config(context='..models.Wiki', name='login',
- renderer='tutorial:templates/login.pt')
-@forbidden_view_config(renderer='tutorial:templates/login.pt')
-def login(request):
- login_url = request.resource_url(request.context, 'login')
- referrer = request.url
- if referrer == login_url:
- referrer = '/' # never use the login form itself as came_from
- came_from = request.params.get('came_from', referrer)
- message = ''
- login = ''
- password = ''
- if 'form.submitted' in request.params:
- login = request.params['login']
- password = request.params['password']
- if check_password(USERS.get(login), password):
- headers = remember(request, login)
- return HTTPFound(location=came_from,
- headers=headers)
- message = 'Failed login'
+ return HTTPSeeOther(location=request.resource_url(context))
return dict(
- message=message,
- url=request.application_url + '/login',
- came_from=came_from,
- login=login,
- password=password,
- title='Login',
+ page=context,
+ save_url=request.resource_url(context, 'edit_page'),
)
-
-
-@view_config(context='..models.Wiki', name='logout')
-def logout(request):
- headers = forget(request)
- return HTTPFound(location=request.resource_url(request.context),
- headers=headers)
diff --git a/docs/tutorials/wiki/src/models/tutorial/models/__init__.py b/docs/tutorials/wiki/src/models/tutorial/models/__init__.py
index 7c6597afa..53e105a8e 100644
--- a/docs/tutorials/wiki/src/models/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki/src/models/tutorial/models/__init__.py
@@ -1,6 +1,7 @@
from persistent import Persistent
from persistent.mapping import PersistentMapping
+
class Wiki(PersistentMapping):
__name__ = None
__parent__ = None
diff --git a/docs/tutorials/wiki/src/views/tutorial/models/__init__.py b/docs/tutorials/wiki/src/views/tutorial/models/__init__.py
index 7c6597afa..53e105a8e 100644
--- a/docs/tutorials/wiki/src/views/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki/src/views/tutorial/models/__init__.py
@@ -1,6 +1,7 @@
from persistent import Persistent
from persistent.mapping import PersistentMapping
+
class Wiki(PersistentMapping):
__name__ = None
__parent__ = None
diff --git a/docs/tutorials/wiki/src/views/tutorial/views/default.py b/docs/tutorials/wiki/src/views/tutorial/views/default.py
index 7ea54bf51..b3baa7e9a 100644
--- a/docs/tutorials/wiki/src/views/tutorial/views/default.py
+++ b/docs/tutorials/wiki/src/views/tutorial/views/default.py
@@ -59,5 +59,7 @@ def edit_page(context, request):
context.data = request.params['body']
return HTTPSeeOther(location=request.resource_url(context))
- return dict(page=context,
- save_url=request.resource_url(context, 'edit_page'))
+ return dict(
+ page=context,
+ save_url=request.resource_url(context, 'edit_page'),
+ )
diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst
index 46aa9f14a..c799d79bf 100644
--- a/docs/tutorials/wiki2/authentication.rst
+++ b/docs/tutorials/wiki2/authentication.rst
@@ -76,8 +76,8 @@ It is up to individual security policies and applications to determine the best
Applications with long-running requests may want to avoid caching the identity, or tracking some extra metadata to re-verify it periodically against the authentication source.
-Configure the app
-~~~~~~~~~~~~~~~~~
+Add new settings
+~~~~~~~~~~~~~~~~
Our authentication policy is expecting a new setting, ``auth.secret``. Open
the file ``development.ini`` and add the highlighted line below:
diff --git a/tox.ini b/tox.ini
index c8d715548..8aeb1f4f8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -33,7 +33,7 @@ deps =
[testenv:docs]
whitelist_externals = make
commands =
- make -C docs doctest html epub BUILDDIR={envdir} "SPHINXOPTS=-W -E"
+ make -C docs {posargs:doctest html epub} BUILDDIR={envdir} "SPHINXOPTS=-W -E"
extras =
docs