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.
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 a authorization policy.
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. 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 | <configure xmlns="http://namespaces.repoze.org/bfg">
<!-- this must be included for the view declarations to work -->
<include package="repoze.bfg.includes" />
<scan package="."/>
<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 be 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 43 44 45 46 47 | from webob.exc import HTTPFound
from repoze.bfg.chameleon_zpt import render_template_to_response
from repoze.bfg.view import bfg_view
from repoze.bfg.url import model_url
from repoze.bfg.security import remember
from repoze.bfg.security import forget
from tutorial.models import Wiki
from tutorial.security import USERS
@bfg_view(for_=Wiki, name='login')
def login(context, request):
login_url = model_url(context, request, '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 render_template_to_response(
'templates/login.pt',
message = message,
url = request.application_url + '/login',
came_from = came_from,
login = login,
password = password,
request =request,
)
@bfg_view(for_=Wiki, name='logout')
def logout(context, request):
headers = forget(request)
return HTTPFound(location = model_url(context, 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 within each view 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 = context,
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>
|
We need to give our root model object an ACL. This ACL will be sufficient to provide enough information to the repoze.bfg security machinery to challenge a user who doesn’t have appropriate credentials when he attempts to invoke the add_page or edit_page views.
We need to perform some imports at module scope in our models.py file:
1 2 | from repoze.bfg.security import Allow
from repoze.bfg.security import Everyone
|
Our root model is a Wiki object. We’ll add the following line at class scope to our Wiki class:
1 | __acl__ = [ (Allow, Everyone, 'view'), (Allow, 'editor', 'edit') ]
|
It’s only happenstance that we’re assigning this ACL at class scope. An ACL can be attached to an object instance too; this is how “row level security” can be achieved in repoze.bfg applications. We actually only need one ACL for the entire system, however, because our security requirements are simple, so this feature is not demonstrated.
Our resulting models.py file will now 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 | from persistent import Persistent
from persistent.mapping import PersistentMapping
from repoze.bfg.security import Allow
from repoze.bfg.security import Everyone
class Wiki(PersistentMapping):
__name__ = None
__parent__ = None
__acl__ = [ (Allow, Everyone, 'view'), (Allow, 'editor', 'edit') ]
class Page(Persistent):
def __init__(self, data):
self.data = data
def appmaker(zodb_root):
if not 'app_root' in zodb_root:
app_root = Wiki()
frontpage = Page('This is the front page')
app_root['FrontPage'] = frontpage
frontpage.__name__ = 'FrontPage'
frontpage.__parent__ = app_root
zodb_root['app_root'] = app_root
import transaction
transaction.commit()
return zodb_root['app_root']
|
To protect each of our views with a particular permission, we need to pass a permission argument to each of our bfg_view decorators. To do so, within views.py:
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 87 88 89 90 91 92 93 | from docutils.core import publish_parts
import re
from webob.exc import HTTPFound
from repoze.bfg.url import model_url
from repoze.bfg.chameleon_zpt import render_template_to_response
from repoze.bfg.security import authenticated_userid
from repoze.bfg.view import static
from repoze.bfg.view import bfg_view
from tutorial.models import Page
from tutorial.models import Wiki
# regular expression used to find WikiWords
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
static_app = static('templates/static')
@bfg_view(for_=Wiki, name='static', permission='view')
def static_view(context, request):
return static_app(context, request)
@bfg_view(for_=Wiki, permission='view')
def view_wiki(context, request):
return HTTPFound(location = model_url(context, request, 'FrontPage'))
@bfg_view(for_=Page, permission='view')
def view_page(context, request):
wiki = context.__parent__
def check(match):
word = match.group(1)
if word in wiki:
page = wiki[word]
view_url = model_url(page, request)
return '<a href="%s">%s</a>' % (view_url, word)
else:
add_url = request.application_url + '/add_page/' + word
return '<a href="%s">%s</a>' % (add_url, word)
content = publish_parts(context.data, writer_name='html')['html_body']
content = wikiwords.sub(check, content)
edit_url = model_url(context, request, 'edit_page')
logged_in = authenticated_userid(request)
return render_template_to_response('templates/view.pt',
request = request,
page = context,
content = content,
logged_in = logged_in,
edit_url = edit_url)
@bfg_view(for_=Wiki, name='add_page', permission='edit')
def add_page(context, request):
name = request.subpath[0]
if 'form.submitted' in request.params:
body = request.params['body']
page = Page(body)
page.__name__ = name
page.__parent__ = context
context[name] = page
return HTTPFound(location = model_url(page, request))
save_url = model_url(context, request, 'add_page', name)
page = Page('')
page.__name__ = name
page.__parent__ = context
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)
@bfg_view(for_=Page, name='edit_page', permission='edit')
def edit_page(context, request):
if 'form.submitted' in request.params:
context.data = request.params['body']
return HTTPFound(location = model_url(context, request))
logged_in = authenticated_userid(request)
return render_template_to_response('templates/edit.pt',
request = request,
page = context,
logged_in = logged_in,
save_url = model_url(context, request,
'edit_page')
)
|
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 | <!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.