Posted on

Pyramid 06: Authentication and Authorization

At this moment everybody can edit and add items to our database.
Now we are going to add a user for it, then the views to update, create and delete items will be restricted to that user.
The base to this tutorial can be found here.

You can find a complete guide to authorization and authentication on Pyramid at the official documentation.

At the root of the project create the file security.py and add the following code to it:

from pyramid.security import Allow, Everyone, Authenticated


class TaskFactory(object):
    __acl__ = [(Allow, Everyone, 'view'),
               (Allow, Authenticated, 'create'),
               (Allow, Authenticated, 'edit'),
               (Allow, Authenticated, 'delete'), ]

    def __init__(self, request):
        pass

As you can read at the references above mentioned, this is a context factory and it is not tied to any specific entity or item in our database.
The TaskFactory returns an ACL (Access Control List) and this ACL has four ACE (Access Control Entry). Each ACE states what can be done and by whom. In this case, “Everybody” and view all the items, and only “Authenticated” users can create, edit or delete items.

Run the command below to see how the files tree is:

tree task_manager -L 2

As a result you should see:

task_manager
├── forms.py
├── __init__.py
├── routes.py
├── security.py
├── static
│   ├── bootstrap
│   ├── js
│   ├── pyramid-16x16.png
│   └── theme.css
├── templates
│   ├── add.jinja2
│   ├── edit.jinja2
│   ├── home.jinja2
│   └── layout.jinja2
├── tests.py
└── views.py

Open the __init__.py and let’s tell Pyramid about our security policies:

from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.config import Configurator
from pymongo import MongoClient
from urllib.parse import urlparse


def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    authentication_policy = AuthTktAuthenticationPolicy('somesecret')
    authorization_policy = ACLAuthorizationPolicy()

    with Configurator(settings=settings, authentication_policy=authentication_policy,
                      authorization_policy=authorization_policy) as config:
        db_url = urlparse(settings['mongo_uri'])
        config.registry.db = MongoClient(
            host=db_url.hostname,
            port=db_url.port,
        )

        def add_db(request):
            db = config.registry.db[db_url.path[1:]]
            if db_url.username and db_url.password:
                db.authenticate(db_url.username, db_url.password)
            return db

        config.add_request_method(add_db, 'db', reify=True)

        config.include('pyramid_jinja2')
        config.include('.routes')
        config.scan()
    return config.make_wsgi_app()

Above we imported the necessary files to implement our authentication policy and set the Configurator to use it.

Next step is at file routes.py. There we are going to tell the routes to create, edit and delete to use the TaskFactory:

def includeme(config):
    config.add_static_view('static', 'static', cache_max_age=3600)
    config.add_route('home', '/')
    config.add_route('tadd', '/add', factory='task_manager.security.TaskFactory')
    config.add_route('tedit', '/edit/{id}', factory='task_manager.security.TaskFactory')
    config.add_route('tdelete', '/delete/{id}', factory='task_manager.security.TaskFactory')

Finally at the views.py we set the views to use the policies by setting the ‘permission’:

from bson.objectid import ObjectId
from pyramid.httpexceptions import HTTPFound
from pyramid.url import route_url
from pyramid.view import view_config

from .forms import TaskForm, TaskUpdateForm


@view_config(route_name='home', renderer='templates/home.jinja2')
def task_list(request):
    tasks = request.db['tasks'].find()
    return {
        'tasks': tasks,
        'project': 'task_manager',
    }


@view_config(route_name='tadd', renderer='templates/add.jinja2', permission='create')
def task_add(request):
    form = TaskForm(request.POST, None)

    if request.POST and form.validate():
        entry = form.data
        request.db['tasks'].save(entry)
        return HTTPFound(route_url('home', request))

    return {'form': form}


@view_config(route_name='tedit', renderer='templates/edit.jinja2', permission='edit')
def task_edit(request):

    id_task = request.matchdict.get('id', None)
    item = request.db['tasks'].find_one({'_id': ObjectId(id_task)})
    form = TaskUpdateForm(request.POST,
                          id=id_task, name=item['name'],
                          active=item['active'])

    if request.method == 'POST' and form.validate():
        entry = form.data
        entry['_id'] = ObjectId(entry.pop('id'))
        request.db['tasks'].save(entry)
        return HTTPFound(route_url('home', request))

    return {'form': form}


@view_config(route_name='tdelete', permission='delete')
def task_delete(request):
    id_task = request.matchdict.get('id', None)
    if id_task:
        request.db['tasks'].remove({'_id': ObjectId(id_task)})
    return HTTPFound(route_url('home', request))

Run the project and try to edit, create or delete an item. You are going to see a “403 Forbidden” page.

env/bin/pserve development.ini --reload

Next step is to setup a login system.

Login

The reference for this part of the tutorial can be found here.

At our home.jinja2 template we are going to add a form to login or logout.
The final result is:

{% extends "layout.jinja2" %}

{% block content %}
<div class="content">
    <h1><span class="font-semi-bold">Task Manager</span> <span class="smaller">A simple CRUD</span></h1>
    <p class="lead">Welcome to <span class="font-normal">{{project}}</span>, a&nbsp;Pyramid application that&nbsp;intends
        to help you with MongoDB and WTForms.</p>
    <hr>
    {% if request.authenticated_userid %}
    Welcome <strong>{{request.authenticated_userid}}</strong> ::
    <a href="{{request.route_url('auth',action='out')}}">Sign Out</a>
    {% else %}
    <form action="{{request.route_url('auth',action='in')}}" method="post" class="form-inline">
        <div class="form-group">
            <input type="text" name="username" class="form-control" placeholder="Username">
        </div>
        <div class="form-group">
            <input type="password" name="password" class="form-control" placeholder="Password">
        </div>
        <div class="form-group">
            <input type="submit" value="Sign in" class="btn btn-default">
        </div>
    </form>
    {% endif %}
    <hr>
    <h1> Tasks</h1> <br>
    <ul>
        {% if tasks %}

        {% else %}
        <li>No tasks</li>
        {% endif %}
        {% for task in tasks %}
        <li>
            {{task.name}} | <a href="{{ request.route_url('tedit', id=task['_id'])}}">Edit</a> | <a
                href="{{ request.route_url('tdelete', id=task['_id'])}}">Delete</a>
        </li>

        {% endfor %}
    </ul>
    <h2>Actions</h2>
    <ul>
        <li><a href="{{request.route_url('tadd')}}">Add new Task</a></li>
    </ul>
</div>

{% endblock content %}

So, at this moment we have to create a collection for users on our database.
Go ahead and use Robo 3T for that.
Inside the users collection insert the document:


Now create a view for authentication at our views.py. The complete file will be:

from bson.objectid import ObjectId
from pyramid.httpexceptions import HTTPFound
from pyramid.security import remember, forget
from pyramid.url import route_url
from pyramid.view import view_config

from .forms import TaskForm, TaskUpdateForm


@view_config(route_name='home', renderer='templates/home.jinja2')
def task_list(request):
    tasks = request.db['tasks'].find()
    return {
        'tasks': tasks,
        'project': 'task_manager',
    }


@view_config(route_name='tadd', renderer='templates/add.jinja2', permission='create')
def task_add(request):
    form = TaskForm(request.POST, None)

    if request.POST and form.validate():
        entry = form.data
        request.db['tasks'].save(entry)
        return HTTPFound(route_url('home', request))

    return {'form': form}


@view_config(route_name='tedit', renderer='templates/edit.jinja2', permission='edit')
def task_edit(request):

    id_task = request.matchdict.get('id', None)
    item = request.db['tasks'].find_one({'_id': ObjectId(id_task)})
    form = TaskUpdateForm(request.POST,
                          id=id_task, name=item['name'],
                          active=item['active'])

    if request.method == 'POST' and form.validate():
        entry = form.data
        entry['_id'] = ObjectId(entry.pop('id'))
        request.db['tasks'].save(entry)
        return HTTPFound(route_url('home', request))

    return {'form': form}


@view_config(route_name='tdelete', permission='delete')
def task_delete(request):
    id_task = request.matchdict.get('id', None)
    if id_task:
        request.db['tasks'].remove({'_id': ObjectId(id_task)})
    return HTTPFound(route_url('home', request))


@view_config(route_name='auth', match_param='action=in', renderer='string', request_method='POST')
@view_config(route_name='auth', match_param='action=out', renderer='string')
def sign_in_out(request):
    username = request.POST.get('username')
    if username:
        user = request.db['users'].find_one({'name': username})
        if user and user['password'] == request.POST.get('password'):
            headers = remember(request, user['name'])
        else:
            headers = forget(request)
    else:
        headers = forget(request)
    return HTTPFound(location=request.route_url('home'), headers=headers)

Configure the route for that view at routes.py:

def includeme(config):
    config.add_static_view('static', 'static', cache_max_age=3600)
    config.add_route('home', '/')
    config.add_route('tadd', '/add', factory='task_manager.security.TaskFactory')
    config.add_route('tedit', '/edit/{id}', factory='task_manager.security.TaskFactory')
    config.add_route('tdelete', '/delete/{id}', factory='task_manager.security.TaskFactory')
    config.add_route('auth', '/sign/{action}')

Now you can create, edit and delete items from your mongodb database only after you login.

There are some important things that must be implemented before using this on production.
For example: you must to encrypt your password. But this will be explored in a future tutorial, probably in a more complex project.

You can find the code for this part of the tutorial at the part06 branch of the project repository.
To clone and run the specific branch use:

git clone -b part06 https://github.com/albertosdneto/tutorial_pyramid_mongo.git
cd tutorial_pyramid_mongo/
python3 -m venv env
env/bin/pip install --upgrade pip setuptools
env/bin/pip install -e .
env/bin/pserve development.ini --reload

I hope you enjoy.