Thursday, March 31, 2016

Learning Python 12 - Summing Up

-- Implementing the Django interface

The setup

$ django-admin startproject pwdweb

$ tree -A pwdweb

The model layer

records/models.py

from cryptography.fernet import Fernet
from django.conf import settings
from django.db import models

class Record(models.Model):
    DEFAULT_ENCODING = 'utf-8'

    title = models.CharField(max_length=64, unique=True)
    username = models.CharField(max_length=64)
    email = models.EmailField(null=True, blank=True)
    url = models.URLField(max_length=255, null=True, blank=True)
    password = models.CharField(max_length=2048)
    notes = models.TextField(null=True, blank=True)
    created = models.DateTimeField(auto_now_add=True)
    last_modified = models.DateTimeField(auto_now=True)

    def encrypt_password(self):
        self.password = self.encrypt(self.password)

    def decrypt_password(self):
        self.password = self.decrypt(self.password)

    def encrypt(self, plaintext):
        return self.cypher('encrypt', plaintext)

    def decrypt(self, cyphertext):
        return self.cypher('decrypt', cyphertext)

    def cypher(self, cypher_func, text):
        fernet = Fernet(settings.ENCRYPTION_KEY)
        result = getattr(fernet, cypher_func)(
            self._to_bytes(text))
        return self._to_str(result)

    def _to_str(self, bytes_str):
        return bytes_str.decode(self.DEFAULT_ENCODING)

    def _to_bytes(self, s):
        return s.encode(self.DEFAULT_ENCODING)

def cypher_encrypt(self, text):      
fernet = Fernet(settings.ENCRYPTION_KEY)      
result = fernet.encrypt(          
self._to_bytes(text))      
return self._to_str(result)

$ python manage.py makemigrations
$ python manage.py migrate

>>> from cryptography.fernet import
Fernet>>> Fernet.generate_key()

A simple form

records/forms.py

from django.forms import ModelForm, Textarea
from .models import Record

class RecordForm(ModelForm):  
class Meta:      
model = Record      
fields = ['title', 'username', 'email', 'url',                
'password', 'notes']      
widgets = {'notes': Textarea(          
attrs={'cols': 40, 'rows': 4})}

The view layer

Imports and home view

from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.core.urlresolvers import reverse_lazy
from django.views.generic import TemplateView
from django.views.generic.edit import (  
CreateView, UpdateView, DeleteView)
from .forms import RecordForm
from .models import Record

class HomeView(TemplateView):  
template_name = 'records/home.html'

Listing all records

class RecordListView(TemplateView):  
template_name = 'records/list.html'  

def get(self, request, *args, **kwargs):      
context = self.get_context_data(**kwargs)      
records = Record.objects.all().order_by('title')  #1      
for record in records:          
record.plaintext = record.decrypt(record.password) #2      
context['records'] = records      
return self.render_to_response(context)

Creating records

class EncryptionMixin:  
def form_valid(self, form):      
self.encrypt_password(form)      
return super(EncryptionMixin, self).form_valid(form)  

def encrypt_password(self, form):      
self.object = form.save(commit=False)      
self.object.encrypt_password()      
self.object.save()

class RecordCreateView(      
EncryptionMixin, SuccessMessageMixin, CreateView):  
template_name = 'records/record_add_edit.html'  
form_class = RecordForm  
success_url = reverse_lazy('records:add')  
success_message = 'Record was created successfully'

self.object = form.save()

Updating records

class RecordUpdateView(      
EncryptionMixin, SuccessMessageMixin, UpdateView):  
template_name = 'records/record_add_edit.html'  
form_class = RecordForm  
model = Record  
success_message = 'Record was updated successfully'  

def get_context_data(self, **kwargs):      
kwargs['update'] = True      
return super(          
RecordUpdateView, self).get_context_data(**kwargs)

def form_valid(self, form):      
self.success_url = reverse_lazy(          
'records:edit',          
kwargs={'pk': self.object.pk}        )      
return super(RecordUpdateView, self).form_valid(form)  

def get_form_kwargs(self):      
kwargs = super(RecordUpdateView, self).get_form_kwargs()      
kwargs['instance'].decrypt_password()      
return kwargs

Deleting records

class RecordDeleteView(SuccessMessageMixin, DeleteView):  
model = Record  
success_url = reverse_lazy('records:list')  

def delete(self, request, *args, **kwargs):      
messages.success(          
request, 'Record was deleted successfully')      
return super(RecordDeleteView, self).delete(          
request, *args, **kwargs)

Setting up the URLs

pwdweb/urls.py

from django.conf.urls import include, url
from django.contrib import admin
from records import urls as records_url
from records.views import HomeView

urlpatterns = [  
url(r'^admin/', include(admin.site.urls)),  
url(r'^records/', include(records_url, namespace='records')),  
url(r'^$', HomeView.as_view(), name='home'),]

records/urls.py
from django.conf.urls import include, url
from django.contrib import admin
from .views import (RecordCreateView, RecordUpdateView,
                    RecordDeleteView, RecordListView)

urlpatterns = [
    url(r'^add/$', RecordCreateView.as_view(), name='add'),
    url(r'^edit/(?P<pk>[0-9]+)/$', RecordUpdateView.as_view(),
        name='edit'),
    url(r'^delete/(?P<pk>[0-9]+)/$', RecordDeleteView.as_view(),
        name='delete'),
    url(r'^$', RecordListView.as_view(), name='list'),
]

The template layer

records/templates/records/base.html

{% load static from staticfiles %}
<!DOCTYPE html>
<html lang="en">
<head>  
<meta charset="utf-8">  
<meta name="viewport"        
content="width=device-width, initial-scale=1.0">  
<link href="{% static "records/css/main.css" %}"        
rel="stylesheet">  
<title>{% block title %}Title{% endblock title %}</title>
</head>

<body>
<div id="page-content">    
{% block page-content %}{% endblock page-content %}  
</div>  
<div id="footer">{% block footer %}{% endblock footer %}</div>  
{% block scripts %}    
<script      
src="{% static "records/js/jquery-2.1.4.min.js" %}">    
</script>  
{% endblock scripts %}
</body></html>


Home and footer templates

records/templates/records/home.html

{% extends "records/base.html" %}
{% block title %}Welcome to the Records website.{% endblock %}

{% block page-content %}
<h1>Welcome {{ user.first_name }}!</h1>
<div class="home-option">To create a record click  
<a href="{% url "records:add" %}">here.</a>
</div>
<div class="home-option">To see all records click  
<a href="{% url "records:list" %}">here.</a>
</div>{% endblock page-content %}

records/templates/records/footer.html

<div class="footer">  Go back <a href="{% url "home" %}">home</a>.</div>

Listing all records

records/templates/records/list.html

{% extends "records/base.html" %}
{% load record_extras %}
{% block title %}Records{% endblock title %}
{% block page-content %}
<h1>Records</h1><span name="top"></span>

{% include "records/messages.html" %}
{% for record in records %}
<div class="record {% cycle 'row-light-blue' 'row-white' %}"    
id="record-{{ record.pk }}">  
<div class="record-left">    
<div class="record-list">      
<span class="record-span">Title</span>{{ record.title }}    
</div>    
<div class="record-list">      
<span class="record-span">Username</span>      
{{ record.username }}    
</div>    
<div class="record-list">      
<span class="record-span">Email</span>{{ record.email }}    
</div>    
<div class="record-list">      
<span class="record-span">URL</span>        
<a href="{{ record.url }}" target="_blank">          
{{ record.url }}</a>    
</div>    
<div class="record-list">      
<span class="record-span">Password</span>      
{% hide_password record.plaintext %}    
</div>  
</div>  
<div class="record-right">    
<div class="record-list">      
<span class="record-span">Notes</span>      
<textarea rows="3" cols="40" class="record-notes"                
readonly>{{ record.notes }}</textarea>    
</div>  
<div class="record-list">      
<span class="record-span">Last modified</span>      
{{ record.last_modified }}    
</div>    
<div class="record-list">      
<span class="record-span">Created</span>    
{{ record.created }}    
</div>  
</div>  
<div class="record-list-actions">    
<a href="{% url "records:edit" pk=record.pk %}">ª edit</a>    
<a href="{% url "records:delete" pk=record.pk %}">ª delete    
</a>  
</div>
</div>
{% endfor %}
{% endblock page-content %}

{% block footer %}
<p><a href="#top">Go back to top</a></p>
{% include "records/footer.html" %}
{% endblock footer %}

records/templatetags/record_extras.py

from django import template
from django.utils.html import escape

register = template.Library()

@register.simple_tagdef hide_password(password):  
return '<span title="{0}">{1}</span>'.format(      
escape(password), '*' * len(password))

records/templates/records/messages.html

{% if messages %}
{% for message in messages %}  
<p class="{{ message.tags }}">{{ message }}</p>
{% endfor %}{% endif %}

records/static/records/css/main.css

html, body, * {  font-family: 'Trebuchet MS', Helvetica, sans-serif; }a { color: #333; }
.record {  clear: both; padding: 1em; border-bottom: 1px solid #666;}
.record-left { float: left; width: 300px;}
.record-list { padding: 2px 0; }
.fieldWrapper { padding: 5px; }
.footer { margin-top: 1em; color: #333; }
.home-option { padding: .6em 0; }
.record-span { font-weight: bold; padding-right: 1em; }
.record-notes { vertical-align: top; }
.record-list-actions { padding: 4px 0; clear: both; }
.record-list-actions a { padding: 0 4px; }
#pwd-info { padding: 0 6px; font-size: 1.1em; font-weight: bold;}
#id_notes { vertical-align: top; }
/* Messages */
.success, .errorlist {font-size: 1.2em; font-weight: bold; }
.success {color: #25B725; }
.errorlist {color: #B12B2B; }/* colors */
.row-light-blue { background-color: #E6F0FA; }
.row-white { background-color: #fff; }
.green { color: #060; }
.orange { color: #FF3300; }
.red { color: #900; }

Creating and editing records

records/templates/records/record_add_edit.html

{% extends "records/base.html" %}
{% load static from staticfiles %}
{% block title %}
{% if update %}Update{% else %}Create{% endif %} Record
{% endblock title %}

{% block page-content %}
<h1>{% if update %}Update a{% else %}Create a new{% endif %}  
Record
</h1>
{% include "records/messages.html" %}

<form action="." method="post">{% csrf_token %}  
{{ form.non_field_errors }}  

<div class="fieldWrapper">{{ form.title.errors }}    
{{ form.title.label_tag }} {{ form.title }}</div>  

<div class="fieldWrapper">{{ form.username.errors }}    
{{ form.username.label_tag }} {{ form.username }}</div>  

<div class="fieldWrapper">{{ form.email.errors }}    
{{ form.email.label_tag }} {{ form.email }}</div>  

<div class="fieldWrapper">{{ form.url.errors }}    
{{ form.url.label_tag }} {{ form.url }}</div>  

<div class="fieldWrapper">{{ form.password.errors }}    
{{ form.password.label_tag }} {{ form.password }}    

<span id="pwd-info"></span></div>  
<button type="button" id="validate-btn">    
Validate Password</button>  
<button type="button" id="generate-btn">    
Generate Password</button>  

<div class="fieldWrapper">{{ form.notes.errors }}    
{{ form.notes.label_tag }} {{ form.notes }}</div>  
<input type="submit"    
value="{% if update %}Update{% else %}Insert{% endif %}">
</form>{% endblock page-content %}{% block footer %}
<br>{% include "records/footer.html" %}<br>
Go to <a href="{% url "records:list" %}">the records list</a>.{% endblock footer %}{% block scripts %}

{{ block.super }}
<script src="{% static "records/js/api.js" %}"></script>{% endblock scripts %}


Talking to the API

records/static/records/js/api.js

var baseURL = 'http://127.0.0.1:5555/password';var getRandomPassword = function() {
var apiURL = '{url}/generate'.replace('{url}', baseURL);
$.ajax({  
type: 'GET',  
url: apiURL,  
success: function(data, status, request) {    
$('#id_password').val(data[1]);  
},  
error: function() { alert('Unexpected error'); }
});
}

$(function() {
$('#generate-btn').click(getRandomPassword);
});


var validatePassword = function() {
var apiURL = '{url}/validate'.replace('{url}', baseURL);
$.ajax({  
type: 'POST',  
url: apiURL,  
data: JSON.stringify({'password': $('#id_password').val()}),  
contentType: "text/plain",  // Avoid CORS preflight  
success: function(data, status, request) {    
var valid = data['valid'], infoClass, grade;    
var msg = (valid?'Valid':'Invalid') + ' password.';    
if (valid) {      
var score = data['score']['total'];      
grade = (score<10?'Poor':(score<18?'Medium':'Strong'));      
infoClass = (score<10?'red':(score<18?'orange':'green'));      
msg += ' (Score: {score}, {grade})'        
.replace('{score}', score).replace('{grade}', grade);    
}    
$('#pwd-info').html(msg);    
$('#pwd-info').removeClass().addClass(infoClass);  
},  
error: function(data) { alert('Unexpected error'); }
});
}
$(function() {  $('#validate-btn').click(validatePassword);});


# Python
error = 'critical' if error_level > 50 else 'medium'
// JavaScript equivalent
error = (error_level > 50 ? 'critical' : 'medium');

Deleting records

records/templates/records/record_confirm_delete.html

{% extends "records/base.html" %}
{% block title %}Delete record{% endblock title %}

{% block page-content %}
<h1>Confirm Record Deletion</h1>
<form action="." method="post">{% csrf_token %}  
<p>Are you sure you want to delete "{{ object }}"?</p>  
<input type="submit" value="Confirm" />&nbsp;  
<a href="{% url "records:list" %}#record-{{ object.pk }}">    
ª cancel</a>
</form>
{% endblock page-content %}

records/models.py

class Record(models.Model):  
...  

def __str__(self):      
return '{}'.format(self.title)

-- Implementing the Falcon API

$ tree -A pwdapi/

The main application

main.py

import falcon
from core.handlers import (  
PasswordValidatorHandler,  
PasswordGeneratorHandler,)

validation_handler = PasswordValidatorHandler()
generator_handler = PasswordGeneratorHandler()

app = falcon.API()
app.add_route('/password/validate/', validation_handler)
app.add_route('/password/generate/', generator_handler)


Writing the helpers

from math import ceil
from random import sample
from string import ascii_lowercase, ascii_uppercase, digits

punctuation = '!#$%&()*+-?@_|'
allchars = ''.join(  
(ascii_lowercase, ascii_uppercase, digits, punctuation))

Coding the password validator

class PasswordValidator:  
def __init__(self, password):      
self.password = password.strip()

def is_valid(self):      
return (len(self.password) > 0 and              
all(char in allchars for char in self.password))

def score(self):      
result = {          
'length': self._score_length(),          
'case': self._score_case(),          
'numbers': self._score_numbers(),          
'special': self._score_special(),          
'ratio': self._score_ratio(),      
}      
result['total'] = sum(result.values())      
return result

def _score_length(self):      
scores_list = ([0]*4) + ([1]*4) + ([3]*4) + ([5]*4)      
scores = dict(enumerate(scores_list))      
return scores.get(len(self.password), 7)

def _score_numbers(self):      
return 2 if (set(self.password) & set(digits)) else 0

def _score_special(self):      
return 4 if (          
set(self.password) & set(punctuation)) else 0

def _score_ratio(self):      
alpha_count = sum(          
1 if c.lower() in ascii_lowercase else 0          
for c in self.password)      
digits_count = sum(          
1 if c in digits else 0 for c in self.password)      
if digits_count == 0:          
return 0      
return min(ceil(alpha_count / digits_count), 7)

Coding the password generator

class PasswordGenerator:  
@classmethod  
def generate(cls, length, bestof=10):      
candidates = sorted([          
cls._generate_candidate(length)          
for k in range(max(1, bestof))      
])      
return candidates[-1]  

@classmethod  
def _generate_candidate(cls, length):      
password = cls._generate_password(length)      
score = PasswordValidator(password).score()      
return (score['total'], password)  

@classmethod  
def _generate_password(cls, length):      
chars = allchars * (ceil(length / len(allchars)))      
return ''.join(sample(chars, length))

Writing the handlers

import json
import falcon
from .passwords import PasswordValidator, PasswordGenerator

class HeaderMixin:  
def set_access_control_allow_origin(self, resp):      
resp.set_header('Access-Control-Allow-Origin', '*')

Coding the password validator handler

class PasswordValidatorHandler(HeaderMixin):  
def on_post(self, req, resp):      
self.process_request(req, resp)      
password = req.context.get('_body', {}).get('password')      
if password is None:          
resp.status = falcon.HTTP_BAD_REQUEST          
return None      

result = self.parse_password(password)      
resp.body = json.dumps(result)  

def parse_password(self, password):      
validator = PasswordValidator(password)      
return {          
'password': password,          
'valid': validator.is_valid(),          
'score': validator.score(),      
}  

def process_request(self, req, resp):      
self.set_access_control_allow_origin(resp)      

body = req.stream.read()      
if not body:          
raise falcon.HTTPBadRequest('Empty request body',              
'A valid JSON document is required.')      
try:          
req.context['_body'] = json.loads(              
body.decode('utf-8'))      
except (ValueError, UnicodeDecodeError):          
raise falcon.HTTPError(              
falcon.HTTP_753, 'Malformed JSON',              
'JSON incorrect or not utf-8 encoded.')

Coding the password generator handler

class PasswordGeneratorHandler(HeaderMixin):  
def on_get(self, req, resp):      
self.process_request(req, resp)      
length = req.context.get('_length', 16)      
resp.body = json.dumps(          
PasswordGenerator.generate(length))  

def process_request(self, req, resp):      
self.set_access_control_allow_origin(resp)      
length = req.get_param('length')      
if length is None:          
return      
try:          
length = int(length)          
assert length > 0          
req.context['_length'] = length      
except (ValueError, TypeError, AssertionError):          
raise falcon.HTTPBadRequest('Wrong query parameter',              
'`length` must be a positive integer.')

Running the API

Testing the API

Testing the helpers

tests/test_core/test_passwords.py

class PasswordGeneratorTestCase(TestCase):
 
def test__generate_password_length(self):      
for length in range(300):          
assert_equal(              
length,              
len(PasswordGenerator._generate_password(length))          
)  

def test__generate_password_validity(self):      
for length in range(1, 300):          
password = PasswordGenerator._generate_password(              
length)          
assert_true(PasswordValidator(password).is_valid())  

def test__generate_candidate(self):      
score, password = (          
PasswordGenerator._generate_candidate(42))      
expected_score = PasswordValidator(password).score()      
assert_equal(expected_score['total'], score)  

@patch.object(PasswordGenerator, '_generate_candidate')  
def test__generate(self, _generate_candidate_mock):      
# checks `generate` returns the highest score candidate      
_generate_candidate_mock.side_effect = [          
(16, '&a69Ly+0H4jZ'),          
(17, 'UXaF4stRfdlh'),          
(21, 'aB4Ge_KdTgwR'),  # the winner          
(12, 'IRLT*XEfcglm'),          
(16, '$P92-WZ5+DnG'),          
(18, 'Xi#36jcKA_qQ'),          
(19, '?p9avQzRMIK0'),          
(17, '4@sY&bQ9*H!+'),          
(12, 'Cx-QAYXG_Ejq'),          
(18, 'C)RAV(HP7j9n'),      
]
assert_equal(          
(21, 'aB4Ge_KdTgwR'),
PasswordGenerator.generate(12))

pwdapi/tests/test_core/test_passwords.py
from unittest import TestCase
from unittest.mock import patch
from nose_parameterized import parameterized, param
from nose.tools import (  
assert_equal, assert_dict_equal, assert_true)

from core.passwords import PasswordValidator, PasswordGenerator
class PasswordValidatorTestCase(TestCase):  

@parameterized.expand([      
(False, ''),      
(False, '  '),      
(True, 'abcdefghijklmnopqrstuvwxyz'),      
(True, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'),      
(True, '0123456789'),      
(True, '!#$%&()*+-?@_|'),  
])  

def test_is_valid(self, valid, password):      
validator = PasswordValidator(password)      
assert_equal(valid, validator.is_valid())

@parameterized.expand(      
param.explicit(char) for char in '>]{<`\\;,[^/"\'~:}=.'  
)  

def test_is_valid_invalid_chars(self, password):      
validator = PasswordValidator(password)      
assert_equal(False, validator.is_valid())

@parameterized.expand([      
(0, ''),  # 0-3: score 0      
(0, 'a'),  # 0-3: score 0      
(0, 'aa'),  # 0-3: score 0      
(0, 'aaa'),  # 0-3: score 0      
(1, 'aaab'),  # 4-7: score 1      
...      
(5, 'aaabbbbccccddd'),  # 12-15: score 5      
(5, 'aaabbbbccccdddd'),  # 12-15: score 5  
])  

def test__score_length(self, score, password):      
validator = PasswordValidator(password)      
assert_equal(score, validator._score_length())

def test__score_length_sixteen_plus(self):      
# all password whose length is 16+ score 7 points      
password = 'x' * 255      
for length in range(16, len(password)):          
validator = PasswordValidator(password[:length])          
assert_equal(7, validator._score_length())

@patch.object(PasswordValidator, '_score_length')  
@patch.object(PasswordValidator, '_score_case')  
@patch.object(PasswordValidator, '_score_numbers')  
@patch.object(PasswordValidator, '_score_special')  
@patch.object(PasswordValidator, '_score_ratio')  
def test_score(          
self,          
_score_ratio_mock,          
_score_special_mock,          
_score_numbers_mock,          
_score_case_mock,          
_score_length_mock):      
_score_ratio_mock.return_value = 2      
_score_special_mock.return_value = 3      
_score_numbers_mock.return_value = 5      
_score_case_mock.return_value = 7      
_score_length_mock.return_value = 11      

expected_result = {          
'length': 11,          
'case': 7,          
'numbers': 5,          
'special': 3,          
'ratio': 2,          
'total': 28,      
}      

validator = PasswordValidator('')      
assert_dict_equal(expected_result, validator.score())

Testing the handlers

pwdapi/tests/test_core/test_handlers.py

import json
from unittest.mock import patchfrom nose.tools
import assert_dict_equal, assert_equal
import falcon
import falcon.testing as testing
from core.handlers import (  
PasswordValidatorHandler,
PasswordGeneratorHandler)

class PGHTest(PasswordGeneratorHandler):  
def process_request(self, req, resp):      
self.req, self.resp = req, resp      
return super(PGHTest, self).process_request(req, resp)

class PVHTest(PasswordValidatorHandler):  
def process_request(self, req, resp):      
self.req, self.resp = req, resp      
return super(PVHTest, self).process_request(req, resp)

class TestPasswordValidatorHandler(testing.TestBase):  
def before(self):      
self.resource = PVHTest()      
self.api.add_route('/password/validate/', self.resource)

def test_post(self):      
self.simulate_request(          
'/password/validate/',          
body=json.dumps({'password': 'abcABC0123#&'}),          
method='POST')      
resp = self.resource.resp      
assert_equal('200 OK', resp.status)      
assert_dict_equal(          
{'password': 'abcABC0123#&',          
'score': {'case': 3, 'length': 5, 'numbers': 2,              
'special': 4, 'ratio': 2, 'total': 16},          
'valid': True},          
json.loads(resp.body))


class TestPasswordGeneratorHandler(testing.TestBase):  
def before(self):      
self.resource = PGHTest()      
self.api.add_route('/password/generate/', self.resource)  

@patch('core.handlers.PasswordGenerator')  
def test_get(self, PasswordGenerator):      
PasswordGenerator.generate.return_value = (7, 'abc123')      
self.simulate_request(          
'/password/generate/',          
query_string='length=7',          
method='GET')      
resp = self.resource.resp      
assert_equal('200 OK', resp.status)      
assert_equal([7, 'abc123'], json.loads(resp.body))

-- Summary

No comments:

Post a Comment

Blog Archive