summaryrefslogtreecommitdiff
path: root/docs/tutorials/wiki2
diff options
context:
space:
mode:
authorMichael Merickel <michael@merickel.org>2020-01-06 23:20:37 -0600
committerMichael Merickel <michael@merickel.org>2020-01-06 23:32:05 -0600
commit9629dcfa579e5c78a285e26e42dcff2b1b2df8b7 (patch)
tree5ea3858bf4618ee0cb96098e62d4b6b929530f37 /docs/tutorials/wiki2
parentc4626765913de97fb6410f0fdb50a4c93a38bd5b (diff)
downloadpyramid-9629dcfa579e5c78a285e26e42dcff2b1b2df8b7.tar.gz
pyramid-9629dcfa579e5c78a285e26e42dcff2b1b2df8b7.tar.bz2
pyramid-9629dcfa579e5c78a285e26e42dcff2b1b2df8b7.zip
update authorization docs with new security policy
Diffstat (limited to 'docs/tutorials/wiki2')
-rw-r--r--docs/tutorials/wiki2/authorization.rst55
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/default.py2
-rw-r--r--docs/tutorials/wiki2/src/authorization/.gitignore1
-rw-r--r--docs/tutorials/wiki2/src/authorization/setup.py2
-rw-r--r--docs/tutorials/wiki2/src/authorization/testing.ini81
-rw-r--r--docs/tutorials/wiki2/src/authorization/tests/conftest.py125
-rw-r--r--docs/tutorials/wiki2/src/authorization/tests/test_functional.py13
-rw-r--r--docs/tutorials/wiki2/src/authorization/tests/test_it.py66
-rw-r--r--docs/tutorials/wiki2/src/authorization/tests/test_views.py23
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/__init__.py4
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py22
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/routes.py7
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/scripts/initialize_db.py4
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/security.py56
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/403.jinja26
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja26
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja23
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja219
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja23
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py41
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views/default.py20
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/views/default.py2
22 files changed, 391 insertions, 170 deletions
diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst
index 234f40e3b..e8f95f8cf 100644
--- a/docs/tutorials/wiki2/authorization.rst
+++ b/docs/tutorials/wiki2/authorization.rst
@@ -12,10 +12,8 @@ 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``).
+* Update the :term:`security policy` to break down the :term:`identity` into a list of :term:`principals <principal>` (``security.py``).
+* Utilize the :class:`pyramid.authorization.ACLHelper` to support a per-context mapping of principals to 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``).
@@ -23,8 +21,8 @@ We will implement access control with the following steps:
(``views/default.py``).
-Add user principals
--------------------
+Add ACL support
+---------------
A :term:`principal` is a level of abstraction on top of the raw :term:`userid`
that describes the user in terms of its capabilities, roles, or other
@@ -42,7 +40,7 @@ Open the file ``tutorial/security.py`` and edit it as follows:
.. literalinclude:: src/authorization/tutorial/security.py
:linenos:
- :emphasize-lines: 3-6,17-24
+ :emphasize-lines: 2,4-7,15,37-48
:language: python
Only the highlighted lines need to be added.
@@ -51,33 +49,16 @@ Note that the role comes from the ``User`` object. We also add the ``user.id``
as a principal for when we want to allow that exact user to edit pages which
they have created.
+We're using the :class:`pyramid.authorization.ACLHelper`, 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__`` method or attribute.
-Add the authorization policy
-----------------------------
-
-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, so we'll
-mention it now.
-
-In the file ``tutorial/security.py``, notice the following lines:
-
-.. literalinclude:: src/authorization/tutorial/security.py
- :lines: 38-40
- :lineno-match:
- :emphasize-lines: 2
- :language: python
-
-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 ``permits`` method completes our implementation of the :class:`pyramid.interfaces.ISecurityPolicy` interface and enables our application to use :attr:`pyramid.request.Request.has_permission` and the ``permission=`` constraint on views.
Add resources and ACLs
----------------------
-Resources are the hidden gem of :app:`Pyramid`. You've made it!
+Resources and context are the hidden gems of :app:`Pyramid`. You've made it!
Every URL in a web application represents a :term:`resource` (the "R" in
Uniform Resource Locator). Often the resource is something in your data model,
@@ -108,7 +89,7 @@ Open the file ``tutorial/routes.py`` and edit the following lines:
.. literalinclude:: src/authorization/tutorial/routes.py
:linenos:
- :emphasize-lines: 1-11,17-
+ :emphasize-lines: 1-11,18-
:language: python
The highlighted lines need to be edited or added.
@@ -120,7 +101,7 @@ the principals of either ``role:editor`` or ``role:basic`` to have the
``create`` permission:
.. literalinclude:: src/authorization/tutorial/routes.py
- :lines: 30-38
+ :lines: 31-39
:lineno-match:
:emphasize-lines: 5-9
:language: python
@@ -129,7 +110,7 @@ The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` route by
declaring a ``factory`` on the route:
.. literalinclude:: src/authorization/tutorial/routes.py
- :lines: 18-19
+ :lines: 19-20
:lineno-match:
:emphasize-lines: 1-2
:language: python
@@ -138,7 +119,7 @@ The ``PageResource`` class defines the :term:`ACL` for a ``Page``. It uses an
actual ``Page`` object to determine *who* can do *what* to the page.
.. literalinclude:: src/authorization/tutorial/routes.py
- :lines: 47-
+ :lines: 48-
:lineno-match:
:emphasize-lines: 5-10
:language: python
@@ -147,7 +128,7 @@ The ``PageResource`` is loaded as the :term:`context` of the ``view_page`` and
``edit_page`` routes by declaring a ``factory`` on the routes:
.. literalinclude:: src/authorization/tutorial/routes.py
- :lines: 17-21
+ :lines: 18-22
:lineno-match:
:emphasize-lines: 1,4-5
:language: python
@@ -167,7 +148,7 @@ Open the file ``tutorial/views/default.py``.
First, you can drop a few imports that are no longer necessary:
.. literalinclude:: src/authorization/tutorial/views/default.py
- :lines: 5-7
+ :lines: 3-5
:lineno-match:
:emphasize-lines: 1
:language: python
@@ -207,7 +188,7 @@ 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.
-The ACLs defined on each :term:`resource` are used by the :term:`authorization
+The ACLs defined on each :term:`resource` are used by the :term:`security
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 an ``HTTPForbidden`` exception will be raised automatically. Thus
@@ -238,14 +219,14 @@ following URLs, checking that the result is as expected:
- http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for
the ``FrontPage`` page object. It is executable by only the ``editor`` user.
- If a different user (or the anonymous user) invokes it, then a login form
+ If an anonymous user invokes it, then a login form
will be displayed. Supplying the credentials with the username ``editor`` and
password ``editor`` will display the edit page form.
- http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for
a page. If the page already exists, then it redirects the user to the
``edit_page`` view for the page object. It is executable by either the
- ``editor`` or ``basic`` user. If a different user (or the anonymous user)
+ ``editor`` or ``basic`` user. If an anonymous user
invokes it, then a login form will be displayed. Supplying the credentials
with either the username ``editor`` and password ``editor``, or username
``basic`` and password ``basic``, will display the edit page form.
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py
index ebb49ef49..378ce0ae9 100644
--- a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py
@@ -1,5 +1,5 @@
-from html import escape
from docutils.core import publish_parts
+from html import escape
from pyramid.httpexceptions import (
HTTPForbidden,
HTTPNotFound,
diff --git a/docs/tutorials/wiki2/src/authorization/.gitignore b/docs/tutorials/wiki2/src/authorization/.gitignore
index 1853d983c..c612e59f2 100644
--- a/docs/tutorials/wiki2/src/authorization/.gitignore
+++ b/docs/tutorials/wiki2/src/authorization/.gitignore
@@ -19,3 +19,4 @@ Data.fs*
.DS_Store
coverage
test
+*.sqlite
diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py
index 500c5e599..12eabaff2 100644
--- a/docs/tutorials/wiki2/src/authorization/setup.py
+++ b/docs/tutorials/wiki2/src/authorization/setup.py
@@ -20,8 +20,8 @@ requires = [
'pyramid_tm',
'SQLAlchemy',
'transaction',
- 'zope.sqlalchemy',
'waitress',
+ 'zope.sqlalchemy',
]
tests_require = [
diff --git a/docs/tutorials/wiki2/src/authorization/testing.ini b/docs/tutorials/wiki2/src/authorization/testing.ini
new file mode 100644
index 000000000..07ec6550e
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/testing.ini
@@ -0,0 +1,81 @@
+###
+# 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
+
+sqlalchemy.url = sqlite:///%(here)s/testing.sqlite
+
+retry.attempts = 3
+
+auth.secret = test-seekrit
+
+[pshell]
+setup = tutorial.pshell.setup
+
+###
+# wsgi server configuration
+###
+
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
+[server:main]
+use = egg:waitress#main
+listen = *:6543
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, tutorial, sqlalchemy, alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_tutorial]
+level = WARN
+handlers =
+qualname = tutorial
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither. (Recommended for production systems.)
+
+[logger_alembic]
+level = WARN
+handlers =
+qualname = alembic
+
+[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/wiki2/src/authorization/tests/conftest.py b/docs/tutorials/wiki2/src/authorization/tests/conftest.py
new file mode 100644
index 000000000..2db65f887
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tests/conftest.py
@@ -0,0 +1,125 @@
+import alembic
+import alembic.config
+import alembic.command
+import os
+from pyramid.paster import get_appsettings
+from pyramid.scripting import prepare
+from pyramid.testing import DummyRequest
+import pytest
+import transaction
+from webob.cookies import Cookie
+import webtest
+
+from tutorial import main
+from tutorial import models
+from tutorial.models.meta import Base
+
+
+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 dbengine(app_settings, ini_file):
+ engine = models.get_engine(app_settings)
+
+ alembic_cfg = alembic.config.Config(ini_file)
+ Base.metadata.drop_all(bind=engine)
+ alembic.command.stamp(alembic_cfg, None, purge=True)
+
+ # run migrations to initialize the database
+ # depending on how we want to initialize the database from scratch
+ # we could alternatively call:
+ # Base.metadata.create_all(bind=engine)
+ # alembic.command.stamp(alembic_cfg, "head")
+ alembic.command.upgrade(alembic_cfg, "head")
+
+ yield engine
+
+ Base.metadata.drop_all(bind=engine)
+ alembic.command.stamp(alembic_cfg, None, purge=True)
+
+@pytest.fixture(scope='session')
+def app(app_settings, dbengine):
+ return main({}, dbengine=dbengine, **app_settings)
+
+@pytest.fixture
+def tm():
+ tm = transaction.TransactionManager(explicit=True)
+ tm.begin()
+ tm.doom()
+
+ yield tm
+
+ tm.abort()
+
+@pytest.fixture
+def dbsession(app, tm):
+ session_factory = app.registry['dbsession_factory']
+ return models.get_tm_session(session_factory, tm)
+
+@pytest.fixture
+def testapp(app, tm, dbsession):
+ # override request.dbsession and request.tm with our own
+ # externally-controlled values that are shared across requests but aborted
+ # at the end
+ testapp = webtest.TestApp(app, extra_environ={
+ 'HTTP_HOST': 'example.com',
+ 'tm.active': True,
+ 'tm.manager': tm,
+ 'app.dbsession': dbsession,
+ })
+
+ return testapp
+
+@pytest.fixture
+def app_request(app, tm, dbsession):
+ """
+ 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'
+
+ # without this, request.dbsession will be joined to the same transaction
+ # manager but it will be using a different sqlalchemy.orm.Session using
+ # a separate database transaction
+ request.dbsession = dbsession
+ request.tm = tm
+
+ yield request
+ env['closer']()
+
+@pytest.fixture
+def dummy_request(app, tm, dbsession):
+ """
+ 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'
+ request.dbsession = dbsession
+ request.tm = tm
+
+ return request
diff --git a/docs/tutorials/wiki2/src/authorization/tests/test_functional.py b/docs/tutorials/wiki2/src/authorization/tests/test_functional.py
new file mode 100644
index 000000000..dbcd8aec7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tests/test_functional.py
@@ -0,0 +1,13 @@
+from tutorial import models
+
+def test_my_view_success(testapp, dbsession):
+ model = models.MyModel(name='one', value=55)
+ dbsession.add(model)
+ dbsession.flush()
+
+ res = testapp.get('/', status=200)
+ assert res.body
+
+def test_notfound(testapp):
+ res = testapp.get('/badurl', status=404)
+ assert res.status_code == 404
diff --git a/docs/tutorials/wiki2/src/authorization/tests/test_it.py b/docs/tutorials/wiki2/src/authorization/tests/test_it.py
deleted file mode 100644
index ea16534fc..000000000
--- a/docs/tutorials/wiki2/src/authorization/tests/test_it.py
+++ /dev/null
@@ -1,66 +0,0 @@
-import unittest
-
-from pyramid import testing
-
-import transaction
-
-
-def dummy_request(dbsession):
- return testing.DummyRequest(dbsession=dbsession)
-
-
-class BaseTest(unittest.TestCase):
- def setUp(self):
- self.config = testing.setUp(settings={
- 'sqlalchemy.url': 'sqlite:///:memory:'
- })
- self.config.include('tutorial.models')
- settings = self.config.get_settings()
-
- from tutorial.models import (
- get_engine,
- get_session_factory,
- get_tm_session,
- )
-
- self.engine = get_engine(settings)
- session_factory = get_session_factory(self.engine)
-
- self.session = get_tm_session(session_factory, transaction.manager)
-
- def init_database(self):
- from tutorial.models.meta import Base
- Base.metadata.create_all(self.engine)
-
- def tearDown(self):
- from tutorial.models.meta import Base
-
- testing.tearDown()
- transaction.abort()
- Base.metadata.drop_all(self.engine)
-
-
-class TestMyViewSuccessCondition(BaseTest):
-
- def setUp(self):
- super(TestMyViewSuccessCondition, self).setUp()
- self.init_database()
-
- from tutorial.models import MyModel
-
- model = MyModel(name='one', value=55)
- self.session.add(model)
-
- def test_passing_view(self):
- from tutorial.views.default import my_view
- info = my_view(dummy_request(self.session))
- self.assertEqual(info['one'].name, 'one')
- self.assertEqual(info['project'], 'myproj')
-
-
-class TestMyViewFailureCondition(BaseTest):
-
- def test_failing_view(self):
- from tutorial.views.default import my_view
- info = my_view(dummy_request(self.session))
- self.assertEqual(info.status_int, 500)
diff --git a/docs/tutorials/wiki2/src/authorization/tests/test_views.py b/docs/tutorials/wiki2/src/authorization/tests/test_views.py
new file mode 100644
index 000000000..8ae464d03
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tests/test_views.py
@@ -0,0 +1,23 @@
+from tutorial import models
+from tutorial.views.default import my_view
+from tutorial.views.notfound import notfound_view
+
+
+def test_my_view_failure(app_request):
+ info = my_view(app_request)
+ assert info.status_int == 500
+
+def test_my_view_success(app_request, dbsession):
+ model = models.MyModel(name='one', value=55)
+ dbsession.add(model)
+ dbsession.flush()
+
+ info = my_view(app_request)
+ assert app_request.response.status_int == 200
+ assert info['one'].name == 'one'
+ 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/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py
index ce2e9f12a..81a22c68c 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py
@@ -5,9 +5,9 @@ def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
with Configurator(settings=settings) as config:
- config.include('.models')
config.include('pyramid_jinja2')
- config.include('.routes')
config.include('.security')
+ config.include('.routes')
+ config.include('.models')
config.scan()
return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py
index a4209a6e9..47d77ef01 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py
@@ -66,13 +66,21 @@ def includeme(config):
# use pyramid_retry to retry a request when transient exceptions occur
config.include('pyramid_retry')
- session_factory = get_session_factory(get_engine(settings))
+ # hook to share the dbengine fixture in testing
+ dbengine = settings.get('dbengine')
+ if not dbengine:
+ dbengine = get_engine(settings)
+
+ session_factory = get_session_factory(dbengine)
config.registry['dbsession_factory'] = session_factory
# make request.dbsession available for use in Pyramid
- config.add_request_method(
- # r.tm is the transaction manager used by pyramid_tm
- lambda r: get_tm_session(session_factory, r.tm),
- 'dbsession',
- reify=True
- )
+ def dbsession(request):
+ # hook to share the dbsession fixture in testing
+ dbsession = request.environ.get('app.dbsession')
+ if dbsession is None:
+ # request.tm is the transaction manager used by pyramid_tm
+ dbsession = get_tm_session(session_factory, request.tm)
+ return dbsession
+
+ config.add_request_method(dbsession, reify=True)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/routes.py b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py
index 1fd45a994..f016d7541 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/routes.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py
@@ -1,6 +1,6 @@
from pyramid.httpexceptions import (
HTTPNotFound,
- HTTPFound,
+ HTTPSeeOther,
)
from pyramid.security import (
Allow,
@@ -9,6 +9,7 @@ from pyramid.security import (
from . import models
+
def includeme(config):
config.add_static_view('static', 'static', cache_max_age=3600)
config.add_route('view_wiki', '/')
@@ -24,7 +25,7 @@ def new_page_factory(request):
pagename = request.matchdict['pagename']
if request.dbsession.query(models.Page).filter_by(name=pagename).count() > 0:
next_url = request.route_url('edit_page', pagename=pagename)
- raise HTTPFound(location=next_url)
+ raise HTTPSeeOther(location=next_url)
return NewPage(pagename)
class NewPage(object):
@@ -52,5 +53,5 @@ class PageResource(object):
return [
(Allow, Everyone, 'view'),
(Allow, 'role:editor', 'edit'),
- (Allow, str(self.page.creator_id), 'edit'),
+ (Allow, 'u:' + str(self.page.creator_id), 'edit'),
]
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initialize_db.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initialize_db.py
index e6350fb36..c8034e5a5 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initialize_db.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initialize_db.py
@@ -8,6 +8,10 @@ from .. import models
def setup_models(dbsession):
+ """
+ Add or update models / fixtures in the database.
+
+ """
editor = models.User(name='editor', role='editor')
editor.set_password('editor')
dbsession.add(editor)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security.py b/docs/tutorials/wiki2/src/authorization/tutorial/security.py
index 1ce1c8753..448183c95 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/security.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/security.py
@@ -1,5 +1,6 @@
-from pyramid.authentication import AuthTktAuthenticationPolicy
-from pyramid.authorization import ACLAuthorizationPolicy
+from pyramid.authentication import AuthTktCookieHelper
+from pyramid.authorization import ACLHelper
+from pyramid.csrf import CookieCSRFStoragePolicy
from pyramid.security import (
Authenticated,
Everyone,
@@ -8,33 +9,50 @@ from pyramid.security import (
from . import models
-class MyAuthenticationPolicy(AuthTktAuthenticationPolicy):
+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 None:
+ return None
+
+ userid = identity['userid']
+ user = request.dbsession.query(models.User).get(userid)
+ return user
+
def authenticated_userid(self, request):
- user = request.user
+ user = self.authenticated_identity(request)
if user is not None:
return user.id
+ 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]
- user = request.user
+ user = self.authenticated_identity(request)
if user is not None:
principals.append(Authenticated)
- principals.append(str(user.id))
+ principals.append('u:' + str(user.id))
principals.append('role:' + user.role)
return principals
-def get_user(request):
- user_id = request.unauthenticated_userid
- if user_id is not None:
- user = request.dbsession.query(models.User).get(user_id)
- return user
-
def includeme(config):
settings = config.get_settings()
- authn_policy = MyAuthenticationPolicy(
- settings['auth.secret'],
- hashalg='sha512',
- )
- config.set_authentication_policy(authn_policy)
- config.set_authorization_policy(ACLAuthorizationPolicy())
- config.add_request_method(get_user, 'user', reify=True)
+
+ config.set_csrf_storage_policy(CookieCSRFStoragePolicy())
+ config.set_default_csrf_options(require_csrf=True)
+
+ config.set_security_policy(MySecurityPolicy(settings['auth.secret']))
+ config.add_request_method(
+ lambda request: request.authenticated_identity, 'user', property=True)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/403.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/403.jinja2
new file mode 100644
index 000000000..7a6f523bc
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/403.jinja2
@@ -0,0 +1,6 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter project</span></h1>
+<p class="lead"><span class="font-semi-bold">403</span> Forbidden</p>
+{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2
index aaf12413f..5edb15285 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2
@@ -1,8 +1,6 @@
{% extends "layout.jinja2" %}
{% block content %}
-<div class="content">
- <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter project</span></h1>
- <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
-</div>
+<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter project</span></h1>
+<p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2
index 7db25c674..27b545054 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2
@@ -10,11 +10,12 @@ Editing <strong>{{pagename}}</strong>
<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
</p>
<form action="{{ save_url }}" method="post">
+<input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">
<div class="form-group">
<textarea class="form-control" name="body" rows="10" cols="60">{{ pagedata }}</textarea>
</div>
<div class="form-group">
- <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button>
+ <button type="submit" class="btn btn-default">Save</button>
</div>
</form>
{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2
index 4016b26c9..64a1db0c5 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2
@@ -35,18 +35,29 @@
<div class="content">
{% if request.user is none %}
<p class="pull-right">
- <a href="{{ request.route_url('login') }}">Login</a>
+ <a href="{{ request.route_url('login') }}">Login</a>
</p>
{% else %}
- <p class="pull-right">
- {{request.user.name}} <a href="{{request.route_url('logout')}}">Logout</a>
- </p>
+ <form class="pull-right" action="{{ request.route_url('logout') }}" method="post">
+ {{request.user.name}}
+ <input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">
+ <button class="btn btn-link" type="submit">Logout</button>
+ </form>
{% endif %}
{% block content %}{% endblock %}
</div>
</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/wiki2/src/authorization/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2
index 1806de0ff..058b7254b 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2
@@ -10,6 +10,7 @@
{{ message }}
</p>
<form action="{{ url }}" method="post">
+<input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">
<input type="hidden" name="next" value="{{ next_url }}">
<div class="form-group">
<label for="login">Username</label>
@@ -20,7 +21,7 @@
<input type="password" name="password">
</div>
<div class="form-group">
- <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button>
+ <button type="submit" class="btn btn-default">Log In</button>
</div>
</form>
{% endblock content %}
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py
index 16fa616e5..e1a564415 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py
@@ -1,14 +1,15 @@
-from pyramid.httpexceptions import HTTPFound
+from pyramid.csrf import new_csrf_token
+from pyramid.httpexceptions import HTTPSeeOther
from pyramid.security import (
remember,
forget,
- )
+)
from pyramid.view import (
forbidden_view_config,
view_config,
)
-from ..models import User
+from .. import models
@view_config(route_name='login', renderer='tutorial:templates/login.jinja2')
@@ -18,29 +19,43 @@ def login(request):
next_url = request.route_url('view_wiki')
message = ''
login = ''
- if 'form.submitted' in request.params:
+ if request.method == 'POST':
login = request.params['login']
password = request.params['password']
- user = request.dbsession.query(User).filter_by(name=login).first()
+ user = (
+ request.dbsession.query(models.User)
+ .filter_by(name=login)
+ .first()
+ )
if user is not None and user.check_password(password):
+ new_csrf_token(request)
headers = remember(request, user.id)
- return HTTPFound(location=next_url, headers=headers)
+ return HTTPSeeOther(location=next_url, headers=headers)
message = 'Failed login'
+ request.response.status = 400
return dict(
message=message,
url=request.route_url('login'),
next_url=next_url,
login=login,
- )
+ )
@view_config(route_name='logout')
def logout(request):
- headers = forget(request)
next_url = request.route_url('view_wiki')
- return HTTPFound(location=next_url, headers=headers)
+ if request.method == 'POST':
+ new_csrf_token(request)
+ headers = forget(request)
+ return HTTPSeeOther(location=next_url, headers=headers)
+
+ return HTTPSeeOther(location=next_url)
+
+@forbidden_view_config(renderer='tutorial:templates/403.jinja2')
+def forbidden_view(exc, request):
+ if request.user is None:
+ next_url = request.route_url('login', _query={'next': request.url})
+ return HTTPSeeOther(location=next_url)
-@forbidden_view_config()
-def forbidden_view(request):
- next_url = request.route_url('login', _query={'next': request.url})
- return HTTPFound(location=next_url)
+ request.response.status = 403
+ return {}
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py
index de0bcd816..214788357 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py
@@ -1,19 +1,19 @@
-from html import escape
-import re
from docutils.core import publish_parts
-
-from pyramid.httpexceptions import HTTPFound
+from html import escape
+from pyramid.httpexceptions import HTTPSeeOther
from pyramid.view import view_config
+import re
from .. import models
+
# regular expression used to find WikiWords
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
@view_config(route_name='view_wiki')
def view_wiki(request):
next_url = request.route_url('view_page', pagename='FrontPage')
- return HTTPFound(location=next_url)
+ return HTTPSeeOther(location=next_url)
@view_config(route_name='view_page', renderer='tutorial:templates/view.jinja2',
permission='view')
@@ -39,26 +39,26 @@ def view_page(request):
permission='edit')
def edit_page(request):
page = request.context.page
- if 'form.submitted' in request.params:
+ if request.method == 'POST':
page.data = request.params['body']
next_url = request.route_url('view_page', pagename=page.name)
- return HTTPFound(location=next_url)
+ return HTTPSeeOther(location=next_url)
return dict(
pagename=page.name,
pagedata=page.data,
save_url=request.route_url('edit_page', pagename=page.name),
- )
+ )
@view_config(route_name='add_page', renderer='tutorial:templates/edit.jinja2',
permission='create')
def add_page(request):
pagename = request.context.pagename
- if 'form.submitted' in request.params:
+ if request.method == 'POST':
body = request.params['body']
page = models.Page(name=pagename, data=body)
page.creator = request.user
request.dbsession.add(page)
next_url = request.route_url('view_page', pagename=pagename)
- return HTTPFound(location=next_url)
+ return HTTPSeeOther(location=next_url)
save_url = request.route_url('add_page', pagename=pagename)
return dict(pagename=pagename, pagedata='', save_url=save_url)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py
index ab6f571ca..df0e4cb9e 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py
@@ -1,5 +1,5 @@
-from html import escape
from docutils.core import publish_parts
+from html import escape
from pyramid.httpexceptions import (
HTTPNotFound,
HTTPSeeOther,