Step 02: Login and Logout

Our first step introduced security ACL statements on our site root. In this next example, we add the ability to log in and log out.

Goals

  • Allow users (in groups) to access protected stuff
  • Custom login forms

Objectives

  • When the system generates forbidden error (via an ACL) you are redirected to login
  • A login leads to an authenticated user who might have some groups
  • Logout clears the “ticket” for the login

Steps

  1. $ cd ../../security; mkdir step02; cd step02

  2. Copy the following into step02/application.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
    from wsgiref.simple_server import make_server
    
    from pyramid.config import Configurator
    from pyramid.authentication import AuthTktAuthenticationPolicy
    
    from resources import bootstrap
    
    from usersdb import groupfinder
    
    
    def main():
        config = Configurator(
            root_factory=bootstrap,
            authentication_policy=AuthTktAuthenticationPolicy(
                'seekr1t',
                callback=groupfinder)
        )
        config.scan("views")
        app = config.make_wsgi_app()
        return app
    
    
    if __name__ == '__main__':
        app = main()
        server = make_server(host='0.0.0.0', port=8080, app=app)
        server.serve_forever()
    
  3. Copy the following into step02/views.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
    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
    from pyramid.view import view_config
    
    from pyramid.httpexceptions import HTTPFound
    from pyramid.httpexceptions import HTTPForbidden
    from pyramid.security import remember
    from pyramid.security import forget
    
    # Get our database that manages users
    from usersdb import USERS
    from pyramid.security import has_permission
    
    class ProjectorViews(object):
        def __init__(self, context, request):
            self.context = context
            self.request = request
    
        @view_config(renderer="templates/default_view.pt",
                     permission='view')
        def default_view(self):
            can_i_edit = has_permission("edit", self.context,
                                        self.request)
            return dict(page_title="Site View",
                        can_i_edit=can_i_edit)
    
        @view_config(renderer="templates/default_view.pt",
                     permission='edit',
                     name="edit")
        def edit_view(self):
            return dict(page_title="Edit Site")
    
        @view_config(renderer="templates/login.pt", context=HTTPForbidden)
        @view_config(renderer="templates/login.pt", name="login.html")
        def login(self):
            request = self.request
            login_url = request.resource_url(request.context, 'login.html')
            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(
                page_title="Login",
                message=message,
                url=request.application_url + '/login.html',
                came_from=came_from,
                login=login,
                password=password,
                )
    
        @view_config(name="logout.html")
        def logout(self):
            headers = forget(self.request)
            url = self.request.resource_url(self.context, 'login.html')
            return HTTPFound(location=url, headers=headers)
    
  4. Copy the following into step02/resources.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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    from pyramid.security import Allow
    from pyramid.security import Everyone
    
    class Folder(dict):
        def __init__(self, name, parent, title):
            self.__name__ = name
            self.__parent__ = parent
            self.title = title
    
    
    class SiteFolder(Folder):
        __acl__ = [
            (Allow, Everyone, 'view'),
            (Allow, 'group:editors', 'edit')
        ]
    
    
    class Document(object):
        def __init__(self, name, parent, title):
            self.__name__ = name
            self.__parent__ = parent
            self.title = title
    
    root = SiteFolder('', None, 'Projector Site')
    
    from pyramid.security import DENY_ALL
    
    def bootstrap(request):
        # Let's make:
        # /
        #   doc1
        #   doc2
        #   folder1/
        #      doc1
        doc1 = Document('doc1', root, 'Document 01')
        root['doc1'] = doc1
        doc2 = Document('doc2', root, 'Document 02')
        doc2.__acl__ = [
            (Allow, Everyone, 'view'),
            (Allow, 'group:admin', 'edit'),
            DENY_ALL
        ]
        root['doc2'] = doc2
        folder1 = Folder('folder1', root, 'Folder 01')
        root['folder1'] = folder1
    
        return root
    
  5. Copy the following into step02/usersdb.py:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    USERS = {'editor': 'editor',
             'viewer': 'viewer'}
    GROUPS = {'editor': ['group:editors']}
    
    def groupfinder(userid, request):
        # Has 3 potential returns:
        #   - None, meaning userid doesn't exist in our database
        #   - An empty list, meaning existing user but no groups
        #   - Or a list of groups for that userid
        if userid in USERS:
            return GROUPS.get(userid, [])
    
  6. Copy the following into step02/templates/default_view.pt:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    <!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"
          xmlns:tal="http://xml.zope.org/namespaces/tal">
    <head>
        <title>${page_title}</title>
    </head>
    <body>
    <h1>${page_title}</h1>
    
    <p>This is the home page, only visible to <code>group:editors</code></p>
    
    <p><a href="edit">Edit</a> | <a href="/logout.html">Logout</a></p>
    </body>
    </html>
    
  7. Copy the following into step02/templates/login.pt:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!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"
          xmlns:tal="http://xml.zope.org/namespaces/tal">
    <head>
        <title>${page_title}</title>
    </head>
    <body>
    <h1>${page_title}</h1>
    
    <p><em>${message}</em></p>
    
    <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>
    </body>
    </html>
    
  8. $ python application.py

  9. Open http://127.0.0.1:8080/ in your browser.

Extra Credit

  1. Change Document to be editable by the world.
  2. Make your doc2 document which, only for that resource instance, is editable by the world.
  3. Can you make a view that that catches some other exception besides HTTPForbidden? For example, Not Found.
  4. We show an edit link even if the person doesn’t have permission to edit. How can you make that conditional in the ZPT?

Analysis

The login view has two decorators that are “stacked”. One makes sure that the login view gets shown when your URL is “/login.html”. (Normal stuff.)

The second @view_config has a context of HTTPForbidden. This is invoked when Pyramid raises the exception.

We redirect back to where you came from when logging in, which is a common pattern.

Discussion

  • Of course this isn’t using your user database.
  • What are all the hand-wavey plug points for all the misery involved in web framework authentication?

Table Of Contents