summaryrefslogtreecommitdiff
path: root/docs/tutorials/wiki/src/authorization
diff options
context:
space:
mode:
Diffstat (limited to 'docs/tutorials/wiki/src/authorization')
-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
17 files changed, 297 insertions, 115 deletions
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)