Adding Authorization¶
Our application currently allows anyone with access to the server to view, edit, and add pages to our wiki. For purposes of demonstration we’ll change our application to allow only people whom possess a specific username (editor) to add and edit wiki pages but we’ll continue allowing anyone with access to the server to view pages. Pyramid provides facilities for authorization and authentication. We’ll make use of both features to provide security to our application.
We will add an authentication policy and an
authorization policy to our application
registry, add a security.py
module, create a root factory
with an ACL, and add permission declarations to
the edit_page
and add_page
views.
Then we will add login
and logout
views, and modify the
existing views to make them return a logged_in
flag to the
renderer.
Finally, we will add a login.pt
template and change the existing
view.pt
and edit.pt
to show a “Logout” link when not logged in.
The source code for this tutorial stage can be browsed at http://github.com/Pylons/pyramid/tree/1.2-branch/docs/tutorials/wiki2/src/authorization/.
Changing __init__.py
For Authorization¶
We’re going to be making several changes to our __init__.py
file which
will help us configure an authorization policy.
Adding A Root Factory¶
We’re going to start to use a custom root factory within our
__init__.py
file. The objects generated by the root factory will be used
as the context of each request to our application. We do this to
allow Pyramid declarative security to work properly. The context
object generated by the root factory during a request will be decorated with
security declarations. When we begin to use a custom root factory to generate
our contexts, we can begin to make use of the declarative security features
of Pyramid.
We’ll modify our __init__.py
, passing in a root factory to our
Configurator constructor. We’ll point it at a new class we create
inside our models.py
file. Add the following statements to your
models.py
file:
1 2 3 4 5 6 7 8 | from pyramid.security import Allow
from pyramid.security import Everyone
class RootFactory(object):
__acl__ = [ (Allow, Everyone, 'view'),
(Allow, 'group:editors', 'edit') ]
def __init__(self, request):
pass
|
The RootFactory
class we’ve just added will be used by Pyramid to
construct a context
object. The context is attached to the request
object passed to our view callables as the context
attribute.
The context object generated by our root factory will possess an __acl__
attribute that allows pyramid.security.Everyone
(a special principal)
to view all pages, while allowing only a principal named
group:editors
to edit and add pages. The __acl__
attribute attached
to a context is interpreted specially by Pyramid as an access control
list during view callable execution. See Assigning ACLs to your Resource Objects for more
information about what an ACL represents.
We’ll pass the RootFactory
we created in the step above in as the
root_factory
argument to a Configurator.
Configuring an Authorization Policy¶
For any Pyramid application to perform authorization, we need to add a
security.py
module (we’ll do that shortly) and we’ll need to change our
__init__.py
file to add an authentication policy and an
authorization policy which uses the security.py
file for a
callback.
We’ll change our __init__.py
file to enable an
AuthTktAuthenticationPolicy
and an ACLAuthorizationPolicy
to enable
declarative security checking. We need to import the new policies:
1 2 3 | from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from tutorial.security import groupfinder
|
Then, we’ll add those policies to the configuration:
1 2 3 4 5 6 7 | authn_policy = AuthTktAuthenticationPolicy(
'sosecret', callback=groupfinder)
authz_policy = ACLAuthorizationPolicy()
config = Configurator(settings=settings,
root_factory='tutorial.models.RootFactory',
authentication_policy=authn_policy,
authorization_policy=authz_policy)
|
Note that that the
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 a
groupfinder
function in the current directory’s security.py
file. We
haven’t added that module yet, but we’re about to.
We’ll also change __init__.py
, adding a call to
pyramid.config.Configurator.add_view()
that points at our login
view callable. This is also known as a forbidden view:
1 2 3 4 | config.add_route('login', '/login')
config.add_view('tutorial.login.login',
context='pyramid.httpexceptions.HTTPForbidden',
renderer='tutorial:templates/login.pt')
|
A forbidden view configures our newly created login view to show up when Pyramid detects that a view invocation can not be authorized.
A logout
view callable will allow users to log out later:
1 2 | config.add_route('logout', '/logout')
config.add_view('tutorial.login.logout', route_name='logout')
|
We’ll also add permission
arguments with the value edit
to the
edit_page
and add_page
views. This indicates that the view
callables which these views reference cannot be invoked without the
authenticated user possessing the edit
permission with respect to the
current context.
1 2 3 4 | config.add_view('tutorial.views.add_page', route_name='add_page',
renderer='tutorial:templates/edit.pt', permission='edit')
config.add_view('tutorial.views.edit_page', route_name='edit_page',
renderer='tutorial:templates/edit.pt', permission='edit')
|
Adding these permission
arguments causes Pyramid to make the
assertion that only users who possess the effective edit
permission at
the time of the request may invoke those two views. We’ve granted the
group:editors
principal the edit
permission at the root model via its
ACL, so only the a user whom is a member of the group named group:editors
will able to invoke the views associated with the add_page
or
edit_page
routes.
Viewing Your Changes¶
When we’re done configuring a root factory, adding an authorization policy,
and adding views, your application’s __init__.py
will look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | from pyramid.config import Configurator
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from sqlalchemy import engine_from_config
from tutorial.models import initialize_sql
from tutorial.security import groupfinder
def main(global_config, **settings):
""" This function returns a WSGI application.
"""
engine = engine_from_config(settings, 'sqlalchemy.')
initialize_sql(engine)
authn_policy = AuthTktAuthenticationPolicy(
'sosecret', callback=groupfinder)
authz_policy = ACLAuthorizationPolicy()
config = Configurator(settings=settings,
root_factory='tutorial.models.RootFactory',
authentication_policy=authn_policy,
authorization_policy=authz_policy)
config.add_static_view('static', 'tutorial:static', cache_max_age=3600)
config.add_route('view_wiki', '/')
config.add_route('login', '/login')
config.add_route('logout', '/logout')
config.add_route('view_page', '/{pagename}')
config.add_route('add_page', '/add_page/{pagename}')
config.add_route('edit_page', '/{pagename}/edit_page')
config.add_view('tutorial.views.view_wiki', route_name='view_wiki')
config.add_view('tutorial.login.login', route_name='login',
renderer='tutorial:templates/login.pt')
config.add_view('tutorial.login.logout', route_name='logout')
config.add_view('tutorial.views.view_page', route_name='view_page',
renderer='tutorial:templates/view.pt')
config.add_view('tutorial.views.add_page', route_name='add_page',
renderer='tutorial:templates/edit.pt', permission='edit')
config.add_view('tutorial.views.edit_page', route_name='edit_page',
renderer='tutorial:templates/edit.pt', permission='edit')
config.add_view('tutorial.login.login',
context='pyramid.httpexceptions.HTTPForbidden',
renderer='tutorial:templates/login.pt')
return config.make_wsgi_app()
|
Adding security.py
¶
Add a security.py
module within your package (in the same directory as
__init__.py
, views.py
, etc.) with the following content:
1 2 3 4 5 6 7 8 | USERS = {'editor':'editor',
'viewer':'viewer'}
GROUPS = {'editor':['group:editors']}
def groupfinder(userid, request):
if userid in USERS:
return GROUPS.get(userid, [])
|
The groupfinder
function defined here is an authentication policy
“callback”; it is a callable that accepts a userid and a request. If
the userid exists in the system, the callback will return a sequence
of group identifiers (or an empty sequence if the user isn’t a member
of any groups). If the userid does not exist in the system, the
callback will return None
. In a production system, user and group
data will most often come from a database, but here we use “dummy”
data to represent user and groups sources. Note that the editor
user is a member of the group:editors
group in our dummy group
data (the GROUPS
data structure).
We’ve given the editor
user membership to the group:editors
by
mapping him to this group in the GROUPS
data structure (GROUPS =
{'editor':['group:editors']}
). Since the groupfinder
function
consults the GROUPS
data structure, this will mean that, as a
result of the ACL attached to the root returned by the root factory,
and the permission associated with the add_page
and edit_page
views, the editor
user should be able to add and edit pages.
Adding Login and Logout Views¶
We’ll add a login
view callable which renders a login form and
processes the post from the login form, checking credentials.
We’ll 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.
We’ll add a different file (for presentation convenience) to add login
and the logout view callables. Add a file named login.py
to your
application (in the same directory as views.py
) with the following
content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | from pyramid.httpexceptions import HTTPFound
from pyramid.security import remember
from pyramid.security import forget
from tutorial.security import USERS
def login(request):
login_url = request.route_url('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 USERS.get(login) == password:
headers = remember(request, login)
return HTTPFound(location = came_from,
headers = headers)
message = 'Failed login'
return dict(
message = message,
url = request.application_url + '/login',
came_from = came_from,
login = login,
password = password,
)
def logout(request):
headers = forget(request)
return HTTPFound(location = request.route_url('view_wiki'),
headers = headers)
|
Changing Existing Views¶
Then we need to change each of our view_page
, edit_page
and
add_page
views in views.py
to pass a “logged in” parameter to its
template. We’ll add something like this to each view body:
1 2 | from pyramid.security import authenticated_userid
logged_in = authenticated_userid(request)
|
We’ll then change the return value of these views to pass the resulting `logged_in` value to the template, e.g.:
1 2 3 4 | return dict(page = page,
content = content,
logged_in = logged_in,
edit_url = edit_url)
|
Adding the login.pt
Template¶
Add a login.pt
template to your templates directory. It’s
referred to within the login view we just added to login.py
.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
xmlns:tal="http://xml.zope.org/namespaces/tal">
<head>
<title>Login - Pyramid tutorial wiki (based on TurboGears
20-Minute Wiki)</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<meta name="keywords" content="python web application" />
<meta name="description" content="pyramid web application" />
<link rel="shortcut icon"
href="${request.static_url('tutorial:static/favicon.ico')}" />
<link rel="stylesheet"
href="${request.static_url('tutorial:static/pylons.css')}"
type="text/css" media="screen" charset="utf-8" />
<!--[if lte IE 6]>
<link rel="stylesheet"
href="${request.static_url('tutorial:static/ie6.css')}"
type="text/css" media="screen" charset="utf-8" />
<![endif]-->
</head>
<body>
<div id="wrap">
<div id="top-small">
<div class="top-small align-center">
<div>
<img width="220" height="50" alt="pyramid"
src="${request.static_url('tutorial:static/pyramid-small.png')}" />
</div>
</div>
</div>
<div id="middle">
<div class="middle align-right">
<div id="left" class="app-welcome align-left">
<b>Login</b><br/>
<span tal:replace="message"/>
</div>
<div id="right" class="app-welcome align-right"></div>
</div>
</div>
<div id="bottom">
<div class="bottom">
<form action="${url}" method="post">
<input type="hidden" name="came_from" value="${came_from}"/>
<input type="text" name="login" value="${login}"/><br/>
<input type="password" name="password"
value="${password}"/><br/>
<input type="submit" name="form.submitted" value="Log In"/>
</form>
</div>
</div>
</div>
<div id="footer">
<div class="footer"
>© Copyright 2008-2011, Agendaless Consulting.</div>
</div>
</body>
</html>
Change view.pt
and edit.pt
¶
We’ll also need to change our edit.pt
and view.pt
templates to
display a “Logout” link if someone is logged in. This link will
invoke the logout view.
To do so we’ll add this to both templates within the <div id="right"
class="app-welcome align-right">
div:
<span tal:condition="logged_in">
<a href="${request.application_url}/logout">Logout</a>
</span>
Seeing Our Changes To views.py
and our Templates¶
Our views.py
module will look something like this when we’re done:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | import re
from docutils.core import publish_parts
from pyramid.httpexceptions import HTTPFound, HTTPNotFound
from pyramid.security import authenticated_userid
from tutorial.models import DBSession
from tutorial.models import Page
# regular expression used to find WikiWords
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
def view_wiki(request):
return HTTPFound(location = request.route_url('view_page',
pagename='FrontPage'))
def view_page(request):
pagename = request.matchdict['pagename']
session = DBSession()
page = session.query(Page).filter_by(name=pagename).first()
if page is None:
return HTTPNotFound('No such page')
def check(match):
word = match.group(1)
exists = session.query(Page).filter_by(name=word).all()
if exists:
view_url = request.route_url('view_page', pagename=word)
return '<a href="%s">%s</a>' % (view_url, word)
else:
add_url = request.route_url('add_page', pagename=word)
return '<a href="%s">%s</a>' % (add_url, word)
content = publish_parts(page.data, writer_name='html')['html_body']
content = wikiwords.sub(check, content)
edit_url = request.route_url('edit_page', pagename=pagename)
logged_in = authenticated_userid(request)
return dict(page=page, content=content, edit_url=edit_url,
logged_in=logged_in)
def add_page(request):
name = request.matchdict['pagename']
if 'form.submitted' in request.params:
session = DBSession()
body = request.params['body']
page = Page(name, body)
session.add(page)
return HTTPFound(location = request.route_url('view_page',
pagename=name))
save_url = request.route_url('add_page', pagename=name)
page = Page('', '')
logged_in = authenticated_userid(request)
return dict(page=page, save_url=save_url, logged_in=logged_in)
def edit_page(request):
name = request.matchdict['pagename']
session = DBSession()
page = session.query(Page).filter_by(name=name).one()
if 'form.submitted' in request.params:
page.data = request.params['body']
session.add(page)
return HTTPFound(location = request.route_url('view_page',
pagename=name))
logged_in = authenticated_userid(request)
return dict(
page=page,
save_url = request.route_url('edit_page', pagename=name),
logged_in = logged_in,
)
|
Our edit.pt
template will look something like this when we’re done:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
xmlns:tal="http://xml.zope.org/namespaces/tal">
<head>
<title>${page.name} - Pyramid tutorial wiki (based on
TurboGears 20-Minute Wiki)</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<meta name="keywords" content="python web application" />
<meta name="description" content="pyramid web application" />
<link rel="shortcut icon"
href="${request.static_url('tutorial:static/favicon.ico')}" />
<link rel="stylesheet"
href="${request.static_url('tutorial:static/pylons.css')}"
type="text/css" media="screen" charset="utf-8" />
<!--[if lte IE 6]>
<link rel="stylesheet"
href="${request.static_url('tutorial:static/ie6.css')}"
type="text/css" media="screen" charset="utf-8" />
<![endif]-->
</head>
<body>
<div id="wrap">
<div id="top-small">
<div class="top-small align-center">
<div>
<img width="220" height="50" alt="pyramid"
src="${request.static_url('tutorial:static/pyramid-small.png')}" />
</div>
</div>
</div>
<div id="middle">
<div class="middle align-right">
<div id="left" class="app-welcome align-left">
Editing <b><span tal:replace="page.name">Page Name
Goes Here</span></b><br/>
You can return to the
<a href="${request.application_url}">FrontPage</a>.<br/>
</div>
<div id="right" class="app-welcome align-right">
<span tal:condition="logged_in">
<a href="${request.application_url}/logout">Logout</a>
</span>
</div>
</div>
</div>
<div id="bottom">
<div class="bottom">
<form action="${save_url}" method="post">
<textarea name="body" tal:content="page.data" rows="10"
cols="60"/><br/>
<input type="submit" name="form.submitted" value="Save"/>
</form>
</div>
</div>
</div>
<div id="footer">
<div class="footer"
>© Copyright 2008-2011, Agendaless Consulting.</div>
</div>
</body>
</html>
Our view.pt
template will look something like this when we’re done:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
xmlns:tal="http://xml.zope.org/namespaces/tal">
<head>
<title>${page.name} - Pyramid tutorial wiki (based on
TurboGears 20-Minute Wiki)</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<meta name="keywords" content="python web application" />
<meta name="description" content="pyramid web application" />
<link rel="shortcut icon"
href="${request.static_url('tutorial:static/favicon.ico')}" />
<link rel="stylesheet"
href="${request.static_url('tutorial:static/pylons.css')}"
type="text/css" media="screen" charset="utf-8" />
<!--[if lte IE 6]>
<link rel="stylesheet"
href="${request.static_url('tutorial:static/ie6.css')}"
type="text/css" media="screen" charset="utf-8" />
<![endif]-->
</head>
<body>
<div id="wrap">
<div id="top-small">
<div class="top-small align-center">
<div>
<img width="220" height="50" alt="pyramid"
src="${request.static_url('tutorial:static/pyramid-small.png')}" />
</div>
</div>
</div>
<div id="middle">
<div class="middle align-right">
<div id="left" class="app-welcome align-left">
Viewing <b><span tal:replace="page.name">Page Name
Goes Here</span></b><br/>
You can return to the
<a href="${request.application_url}">FrontPage</a>.<br/>
</div>
<div id="right" class="app-welcome align-right">
<span tal:condition="logged_in">
<a href="${request.application_url}/logout">Logout</a>
</span>
</div>
</div>
</div>
<div id="bottom">
<div class="bottom">
<div tal:replace="structure content">
Page text goes here.
</div>
<p>
<a tal:attributes="href edit_url" href="">
Edit this page
</a>
</p>
</div>
</div>
</div>
<div id="footer">
<div class="footer"
>© Copyright 2008-2011, Agendaless Consulting.</div>
</div>
</body>
</html>
Viewing the Application in a Browser¶
We can finally examine our application in a browser. The views we’ll try are as follows:
- Visiting
http://localhost:6543/
in a browser invokes theview_wiki
view. This always redirects to theview_page
view of the FrontPage page object. It is executable by any user. - Visiting
http://localhost:6543/FrontPage
in a browser invokes theview_page
view of the FrontPage page object. - Visiting
http://localhost:6543/FrontPage/edit_page
in a browser invokes the edit view for the FrontPage object. It is executable by only theeditor
user. If a different user (or the anonymous user) invokes it, a login form will be displayed. Supplying the credentials with the usernameeditor
, passwordeditor
will display the edit page form. - Visiting
http://localhost:6543/add_page/SomePageName
in a browser invokes the add view for a page. It is executable by only theeditor
user. If a different user (or the anonymous user) invokes it, a login form will be displayed. Supplying the credentials with the usernameeditor
, passwordeditor
will display the edit page form. - After logging in (as a result of hitting an edit or add page
and submitting the login form with the
editor
credentials), we’ll see a Logout link in the upper right hand corner. When we click it, we’re logged out, and redirected back to the front page.