diff options
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 © 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: @@ -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 |
