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 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.

Configuring a repoze.bfg Authentication Policy

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.

Changing configure.zcml

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>

Adding security.py

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, [])

Adding Login and Logout Views

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)
    

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 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)

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.

 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>

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 class="main_content"> div:

1
<span tal:condition="logged_in"><a href="${request.application_url}/logout">Logout</a></span>

Giving Our Root Model Object an ACL

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']

Adding permission Declarations to our bfg_view Decorators

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:

  • We add permission='view' to the bfg_view decorator attached to the static_view view function. This makes the assertion that only users who possess the effective view permission at the time of the request may invoke this view. We’ve granted Everyone the view permission at the root model via its ACL, so everyone will be able to invoke the static_view view.
  • We add permission='view' to the bfg_view decorator attached to the view_wiki view function. This makes the assertion that only users who possess the effective view permission at the time of the request may invoke this view. We’ve granted Everyone the view permission at the root model via its ACL, so everyone will be able to invoke the view_wiki view.
  • We add permission='view' to the bfg_view decorator attached to the view_page view function. This makes the assertion that only users who possess the effective view permission at the time of the request may invoke this view. We’ve granted Everyone the view permission at the root model via its ACL, so everyone will be able to invoke the view_page view.
  • We add permission='edit' to the bfg_view decorator attached to the add_page view function. This makes the assertion that only users who possess the effective view permission at the time of the request may invoke this view. We’ve granted editor the view permission at the root model via its ACL, so only the user named editor will able to invoke the add_page view.
  • We add permission='edit' to the bfg_view decorator attached to the edit_page view function. This makes the assertion that only users who possess the effective view permission at the time of the request may invoke this view. We’ve granted editor the view permission at the root model via its ACL, so only the user named editor will able to invoke the edit_page view.

Viewing the Application in a Browser

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:

  • Visiting http://localhost:6543/ in a browser invokes the view_wiki view. This always redirects to the view_page view of the FrontPage page object. It is executable by any user.
  • Visiting http://localhost:6543/FrontPage/ in a browser invokes the view_page view of the front page page object. This is because it’s the default view (a view without a name) for Page objects. It is executable by any user.
  • Visiting http://localhost:6543/FrontPage/edit_page in a browser invokes the edit view for the front page object. It is executable by only the editor user. If a different user (or the anonymous user) invokes it, a login form will be displayed. Supplying the credentials with the username editor, password editor will show the edit page form being displayed.
  • Visiting http://localhost:6543/add_page/SomePageName in a browser invokes the add view for a page. It is executable by only the editor user. If a different user (or the anonymous user) invokes it, a login form will be displayed. Supplying the credentials with the username editor, password editor will show the edit page form being displayed.

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
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>

Revisiting the Application

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.