Adding Authorization¶
Pyramid provides facilities for authentication and authorization. We’ll make use of both features to provide security to our application. Our application currently allows anyone with access to the server to view, edit, and add pages to our wiki. We’ll change that to allow only people who 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.
We will do the following steps:
- Add a root factory with an ACL (
models.py
). - Add an authentication policy and an authorization policy
(
__init__.py
). - Add an authentication policy callback (new
security.py
module). - Add
login
andlogout
views (views.py
). - Add permission declarations to the
edit_page
andadd_page
views (views.py
). - Make the existing views return a
logged_in
flag to the renderer (views.py
). - Add a login template (new
login.pt
). - Add a “Logout” link to be shown when logged in and viewing or editing a page
(
view.pt
,edit.pt
).
The source code for this tutorial stage can be browsed at http://github.com/Pylons/pyramid/tree/1.3-branch/docs/tutorials/wiki2/src/authorization/.
Adding A Root Factory¶
Open models.py
and add the following statements:
1 2 3 4 5 6 7 8 9 | from pyramid.security import (
Allow,
Everyone,
)
class RootFactory(object):
__acl__ = [ (Allow, Everyone, 'view'),
(Allow, 'group:editors', 'edit') ]
def __init__(self, request):
pass
|
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.
Those context objects will be decorated with security
declarations. When we 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 the new class we created
inside our models.py
file.
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.
Note
Although we don’t use the functionality here, the factory
used
to create route contexts may differ per-route as opposed to globally. See
the factory
argument to
pyramid.config.Configurator.add_route()
for more info.
We’ll pass the RootFactory
we created in the step above in as the
root_factory
argument to a Configurator.
Add an Authorization Policy and an Authentication Policy¶
We’re going to be making several changes to our __init__.py
file which
will help us configure 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 enable an AuthTktAuthenticationPolicy
and an ACLAuthorizationPolicy
to implement declarative security checking. Open tutorial/__init__.py
and
add these import statements:
1 2 3 | from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from tutorial.security import groupfinder
|
Now 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')
config.set_authentication_policy(authn_policy)
config.set_authorization_policy(authz_policy)
|
Note 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.
Viewing Your Changes¶
When we’re done configuring a root factory, adding a authentication and
authorization policies, and adding routes for /login
and /logout
,
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 | from pyramid.config import Configurator
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from sqlalchemy import engine_from_config
from tutorial.security import groupfinder
from .models import DBSession
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
engine = engine_from_config(settings, 'sqlalchemy.')
DBSession.configure(bind=engine)
authn_policy = AuthTktAuthenticationPolicy(
'sosecret', callback=groupfinder)
authz_policy = ACLAuthorizationPolicy()
config = Configurator(settings=settings,
root_factory='tutorial.models.RootFactory')
config.set_authentication_policy(authn_policy)
config.set_authorization_policy(authz_policy)
config.add_static_view('static', '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.scan()
return config.make_wsgi_app()
|
Adding an authentication policy callback¶
Add a tutorial/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 | 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 context object 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¶
To our views.py
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.
The login
view callable will look something 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 | @view_config(route_name='login', renderer='templates/login.pt')
@forbidden_view_config(renderer='templates/login.pt')
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,
)
|
The logout
view callable will look something like this:
1 2 3 4 5 | @view_config(route_name='logout')
def logout(request):
headers = forget(request)
return HTTPFound(location = request.route_url('view_wiki'),
headers = headers)
|
The login
view callable is decorated with two decorators, a
@view_config
decorator, which associates it with the login
route, and a @forbidden_view_config
decorator which turns it in to
an exception view. The one which associates it with the
login
route makes it visible when we visit /login
. The other
one makes it a forbidden view. The forbidden view is
displayed whenever Pyramid or your application raises an
pyramid.httpexceptions.HTTPForbidden
exception. In this
case, we’ll be relying on the forbidden view to show the login form
whenver someone attempts to execute an action which they’re not yet
authorized to perform.
The logout
view callable is decorated with a @view_config
decorator
which associates it with the logout
route. This makes it visible when we
visit /logout
.
We’ll need to import some stuff to service the needs of these two functions:
the pyramid.view.forbidden_view_config
class, a number of values from the
pyramid.security
module, and a value from our newly added
tutorial.security
package. Add the following import statements to the
head of the views.py
file:
1 2 3 4 5 6 7 8 9 10 11 12 | from pyramid.view import (
view_config,
forbidden_view_config,
)
from pyramid.security import (
remember,
forget,
authenticated_userid,
)
from .security import USERS
|
Changing Existing Views¶
Add permision declarations¶
Then we need to change each of our view_page
, edit_page
and
add_page
view callables in views.py
. Within each of these views,
we’ll need 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)
|
Return a logged_in flag to the renderer¶
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)
|
We’ll also need to add a permission
value to the @view_config
decorator for each of the add_page
and edit_page
view callables. For
each, we’ll add permission='edit'
, for example:
1 2 | @view_config(route_name='edit_page', renderer='templates/edit.pt',
permission='edit')
|
See the permission='edit'
we added there? 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.
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 in the root factory via its ACL, so only a user who
is a member of the group named group:editors
will be able to
invoke the views associated with the add_page
or edit_page
routes.
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 views.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>
Add a “Logout” link when logged in¶
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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | import re
from docutils.core import publish_parts
from pyramid.httpexceptions import (
HTTPFound,
HTTPNotFound,
)
from pyramid.view import (
view_config,
forbidden_view_config,
)
from pyramid.security import (
remember,
forget,
authenticated_userid,
)
from .models import (
DBSession,
Page,
)
from .security import USERS
# 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):
return HTTPFound(location = request.route_url('view_page',
pagename='FrontPage'))
@view_config(route_name='view_page', renderer='templates/view.pt')
def view_page(request):
pagename = request.matchdict['pagename']
page = DBSession.query(Page).filter_by(name=pagename).first()
if page is None:
return HTTPNotFound('No such page')
def check(match):
word = match.group(1)
exists = DBSession.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)
return dict(page=page, content=content, edit_url=edit_url,
logged_in=authenticated_userid(request))
@view_config(route_name='add_page', renderer='templates/edit.pt',
permission='edit')
def add_page(request):
name = request.matchdict['pagename']
if 'form.submitted' in request.params:
body = request.params['body']
page = Page(name, body)
DBSession.add(page)
return HTTPFound(location = request.route_url('view_page',
pagename=name))
save_url = request.route_url('add_page', pagename=name)
page = Page('', '')
return dict(page=page, save_url=save_url,
logged_in=authenticated_userid(request))
@view_config(route_name='edit_page', renderer='templates/edit.pt',
permission='edit')
def edit_page(request):
name = request.matchdict['pagename']
page = DBSession.query(Page).filter_by(name=name).one()
if 'form.submitted' in request.params:
page.data = request.params['body']
DBSession.add(page)
return HTTPFound(location = request.route_url('view_page',
pagename=name))
return dict(
page=page,
save_url = request.route_url('edit_page', pagename=name),
logged_in=authenticated_userid(request),
)
@view_config(route_name='login', renderer='templates/login.pt')
@forbidden_view_config(renderer='templates/login.pt')
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,
)
@view_config(route_name='logout')
def logout(request):
headers = forget(request)
return HTTPFound(location = request.route_url('view_wiki'),
headers = headers)
|
(Only the highlighted lines need to be added.)
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>
(Only the highlighted lines need to be added.)
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>
(Only the highlighted lines need to be added.)
Viewing the Application in a Browser¶
We can finally examine our application in a browser (See Starting the Application). Launch a browser and visit each of the following URLs, check that the result is as expected:
http://localhost:6543/
invokes theview_wiki
view. This always redirects to theview_page
view of the FrontPage page object. It is executable by any user.http://localhost:6543/FrontPage
invokes theview_page
view of the FrontPage page object.http://localhost:6543/FrontPage/edit_page
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.http://localhost:6543/add_page/SomePageName
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.