-- 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" />
<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
Thursday, March 31, 2016
Subscribe to:
Post Comments (Atom)
Blog Archive
-
▼
2016
(87)
-
▼
March
(25)
- Learning Python 12 - Summing Up
- Learning Python 11 - Debugging and Troubleshooting
- Learning Python 10 - Web Development Done Right
- Learning Python 9 - Data Science
- Learning Python 8 - The GUIs and Scripts
- Learning Python 7 - Tesing, Profiling, and Dealing...
- Learning Python 6 - OOP, Decorators, and Iterators
- Learning Python 5 - Saving Time and Memory
- Learning Python 4 - Functions
- Learning Python 3 - Interating and Making Decisions
- Learning Python 2 - Build-in Data Types
- Learning Python 1 - Introduction
- Bandit algorithms 7 - Bandits in the Real World: C...
- Bandit algorithms 6 - UCB - The Upper Confidence B...
- Bandit algorithms 5 - The Softmax Algorithm
- Bandit algorithms 4 - Debugging Bandit Algorithms
- Bandit algorithms 3 - The Epsilon-Greedy Algorithm
- Bandit algorithms 2 - Multiarmed Bandit Algorithms
- Bandit algorithms 1 - Exploration and Exploitation
- Python Data Analysis 11 - Recognizing Handwritten ...
- Python Data Analysis 10 - Embedding the JavaScript...
- Python Data Analysis 9 - An Example - Meteorologic...
- Python Data Analysis 8 - Machine Learning with sci...
- Python Data Analysis 7 - Data Visualization with m...
- Python Data Analysis 6 - pandas in Depth: Data Man...
-
▼
March
(25)
No comments:
Post a Comment