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 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. repoze.bfg provides facilities for authorization and authentication. We’ll make use of both features to provide security to our application.
We’re going to start to use a custom root factory within our run.py file. The objects generated by the root factory will be used as the context of each request to our application. In order for repoze.bfg declarative security to work properly, the context object generated during a request must 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 repoze.bfg.
Let’s modify our run.py, passing in a root factory as the first argument to repoze.bfg.router.make_app. We’ll point it at a new class we create inside our models.py file. Add the following statements to your models.py file:
from repoze.bfg.security import Allow
from repoze.bfg.security import Everyone
class RootFactory(object):
__acl__ = [ (Allow, Everyone, 'view'), (Allow, 'editor', 'edit') ]
def __init__(self, environ):
self.__dict__.update(environ['bfg.routes.matchdict'])
The RootFactory class we’ve just added will be used by repoze.bfg to construct a context object. The context is attached to our request as the context attribute.
All of our context objects will possess an __acl__ attribute that allows “Everyone” (a special principal) to view all pages, while allowing only a user named editor to edit and add pages. The __acl__ attribute attached to a context is interpreted specially by repoze.bfg as an access control list during view execution. See Assigning ACLs to your Model Objects for more information about what an ACL represents.
For any repoze.bfg application to perform authorization, we need to add a security.py module and we’ll need to change our application registry to add an authentication policy and an authorization policy.
We’ll pass the RootFactory we created in the step above in as the first argument to make_app. When we’re done, your application’s run.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 | from repoze.bfg.router import make_app
import tutorial
from tutorial.models import DBSession
from tutorial.models import initialize_sql
from tutorial.models import RootFactory
class Cleanup:
def __init__(self, cleaner):
self.cleaner = cleaner
def __del__(self):
self.cleaner()
def handle_teardown(event):
environ = event.request.environ
environ['tutorial.sasession'] = Cleanup(DBSession.remove)
def app(global_config, **kw):
""" This function returns a repoze.bfg.router.Router object.
It is usually called by the PasteDeploy framework during ``paster serve``.
"""
db_string = kw.get('db_string')
if db_string is None:
raise ValueError("No 'db_string' value in application configuration.")
initialize_sql(db_string)
return make_app(RootFactory, tutorial, options=kw)
|
We’ll change our configure.zcml file to enable an AuthTktAuthenticationPolicy and an ACLAuthorizationPolicy to enable declarative security checking. We’ll also add a forbidden stanza. This configures our login view to show up when repoze.bfg detects that a view invocation can not be authorized. We’ll also change configure.zcml to add a forbidden stanza which points at our login view. This configures our newly created login view to show up when repoze.bfg detects that a view invocation can not be authorized. Also, we’ll add permission attributes with the value edit to the edit_page and add_page routes. This indicates that the views which these routes reference cannot be invoked without the authenticated user possessing the edit permission with respect to the current context. When you’re done, your configure.zcml will look like so
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 | <configure xmlns="http://namespaces.repoze.org/bfg">
<!-- this must be included for the view declarations to work -->
<include package="repoze.bfg.includes" />
<subscriber for="repoze.bfg.interfaces.INewRequest"
handler=".run.handle_teardown"/>
<route
path="/static/*subpath"
name="static"
view=".views.static_view"
/>
<route
path="login"
name="login"
view=".login.login"
/>
<route
path="logout"
name="logout"
view=".login.logout"
/>
<route
path=""
name="view_wiki"
view=".views.view_wiki"
/>
<route
path=":pagename"
name="view_page"
view=".views.view_page"
/>
<route
path=":pagename/edit_page"
name="edit_page"
view=".views.edit_page"
permission="edit"
/>
<route
path="add_page/:pagename"
name="add_page"
view=".views.add_page"
permission="edit"
/>
<forbidden
view=".login.login"/>
<authtktauthenticationpolicy
secret="sosecret"
/>
<aclauthorizationpolicy/>
</configure>
|
Add a security.py module within your package (in the same directory as “run.py”, “views.py”, etc) with the following content: The groupfinder defined here is an authorization policy “callback”; it is a 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. We’ll use “dummy” data to represent user and groups sources. When we’re done, your application’s security.py will look like this.
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, [])
|
We’ll add a login view which renders a login form and processes the post from the login form, checking credentials.
We’ll also add a logout view 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 logout views. 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 38 39 40 41 42 | from webob.exc import HTTPFound
from repoze.bfg.chameleon_zpt import render_template_to_response
from repoze.bfg.security import remember
from repoze.bfg.security import forget
from repoze.bfg.url import route_url
from tutorial.security import USERS
def login(request):
login_url = route_url('login', request)
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 render_template_to_response(
'templates/login.pt',
message = message,
url = request.application_url + '/login',
came_from = came_from,
login = login,
password = password,
request =request,
)
def logout(request):
headers = forget(request)
return HTTPFound(location = route_url('view_wiki', request),
headers = headers)
|
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 into its template. We’ll add something like this to each view body:
1 | logged_in = authenticated_userid(request)
|
We’ll then change the return value of render_template_to_response to pass the resulting `logged_in` value to the template, e.g.:
1 2 3 4 5 6 | return render_template_to_response('templates/view.pt',
request = request,
page = page,
content = content,
logged_in = logged_in,
edit_url = edit_url)
|
Add a login.pt template to your templates directory. It’s referred to within the login view we just added to login.py.
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 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
<title>bfg tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
<link rel="stylesheet" type="text/css"
href="${request.application_url}/static/style.css" />
</head>
<body>
<h1>Log In</h1>
<div tal:replace="message"/>
<div class="main_content">
<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>
</body>
</html>
|
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 class="main_content"> div:
1 | <span tal:condition="logged_in"><a href="${request.application_url}/logout">Logout</a></span>
|
Once we’ve set up the WSGI pipeline properly, we can finally examine our application in a browser. The views we’ll try are as follows:
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 | import re
from docutils.core import publish_parts
from webob.exc import HTTPFound
from repoze.bfg.chameleon_zpt import render_template_to_response
from repoze.bfg.view import static
from repoze.bfg.security import authenticated_userid
from repoze.bfg.url import route_url
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+)")
static_view = static('templates/static')
def view_wiki(request):
return HTTPFound(location = route_url('view_page', request,
pagename='FrontPage'))
def view_page(request):
pagename = request.matchdict['pagename']
session = DBSession()
page = session.query(Page).filter_by(name=pagename).one()
def check(match):
word = match.group(1)
exists = session.query(Page).filter_by(name=word).all()
if exists:
view_url = route_url('view_page', request, pagename=word)
return '<a href="%s">%s</a>' % (view_url, word)
else:
add_url = route_url('add_page', request, 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 = route_url('edit_page', request, pagename=pagename)
logged_in = authenticated_userid(request)
return render_template_to_response('templates/view.pt',
request = request,
page = page,
content = content,
logged_in = logged_in,
edit_url = edit_url)
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 = route_url('view_page', request,
pagename=name))
save_url = route_url('add_page', request, pagename=name)
page = Page('', '')
logged_in = authenticated_userid(request)
return render_template_to_response('templates/edit.pt',
request = request,
page = page,
logged_in = logged_in,
save_url = save_url)
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 = route_url('view_page', request,
pagename=name))
logged_in = authenticated_userid(request)
return render_template_to_response('templates/edit.pt',
request = request,
page = page,
logged_in = logged_in,
save_url = route_url('edit_page',
request,
pagename=name),
)
|
Our edit.pt template 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 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
<title>bfg tutorial wiki (based on TurboGears 20-Minute Wiki) Editing: ${page.name}</title>
<link rel="stylesheet" type="text/css"
href="${request.application_url}/static/style.css" />
</head>
<body>
<div class="main_content">
<div style="float:right; width: 10em;"> Viewing
<span tal:replace="page.name">Page Name Goes Here</span> <br/>
You can return to the <a href="${request.application_url}">FrontPage</a>.
<span tal:condition="logged_in"><a href="${request.application_url}/logout">Logout</a></span>
</div>
<div>
<form action="${save_url}" method="post">
<textarea name="body" tal:content="page.data" rows="10" cols="60"/>
<input type="submit" name="form.submitted" value="Save"/>
</form>
</div>
</div>
</body>
</html>
|
Our view.pt template 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 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
<title>${page.name} - bfg tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
<link rel="stylesheet" type="text/css"
href="${request.application_url}/static/style.css" />
</head>
<body>
<div class="main_content">
<div style="float:right; width: 10em;"> Viewing
<span tal:replace="page.name">Page Name Goes Here</span> <br/>
You can return to the <a href="${request.application_url}">FrontPage</a>.
<span tal:condition="logged_in"><a href="${request.application_url}/logout">Logout</a></span>
</div>
<div tal:replace="structure content">Page text goes here.</div>
<p><a tal:attributes="href edit_url" href="">Edit this page</a></p>
</div>
</body>
</html>
|
When we revisit the application in a browser, and log 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.