General housekeeping such as adding package.json for Node server, bash script for launching a celery worker and updating the readme to assist with launching the DEMOS2 app. Updated some models to include UI helper functions. Main work done is around event preparation - with a Celery worker running and the Node server, trustees are now emailed a link to prepare events. The event detail page has also had a bit of an overhaul to include additional information and to make it easier to use

This commit is contained in:
vince0656 2018-07-02 10:06:05 +01:00
parent de9eaa7881
commit f82a380fa4
21 changed files with 337 additions and 696 deletions

View file

@ -4,18 +4,21 @@ import subprocess
import json
import urllib2
#change this file name etc., temporary change to get it working for the meantime
'''
All functions in this file have been re-implemenented by Thomas Smith
File then updated by Vincent de Almeida. Changes include:
-Update filename to 'crypto_rpc' to reflect the RPC nature of the methods
'''
def param():
jsondict = json.load(urllib2.urlopen('http://localhost:8080/param'))
url = 'http://localhost:8080/param' # RPC URL
jsondict = json.load(urllib2.urlopen(url))
return json.dumps(jsondict)
def combpk(amount, pks):
url = 'http://localhost:8080/cmpkstring'
url = 'http://localhost:8080/cmpkstring' # RPC URL
querystring = '?number='+str(amount)
for pk in pks:
querystring += '&PK='+pk
@ -26,7 +29,7 @@ def combpk(amount, pks):
return json.dumps(jsondict)
def addec(amount, ciphers):
url = 'http://localhost:8080/addec'
url = 'http://localhost:8080/addec' # RPC URL
querystring = '?number='+str(amount)
c1s = ciphers['c1s']
c2s = ciphers['c2s']
@ -40,7 +43,7 @@ def addec(amount, ciphers):
return json.dumps(jsondict)
def tally(amount, param, decs, cipher):
url = 'http://localhost:8080/tally'
url = 'http://localhost:8080/tally' # RPC URL
querystring = '?number='+str(amount)
querystring += '&param='+urllib2.quote(str(param))

View file

@ -1,7 +1,8 @@
from __future__ import unicode_literals
from datetime import datetime
import json
from django.core.mail import send_mail
from django.db import models
from django.utils import timezone
@ -9,9 +10,17 @@ from allauthdemo.auth.models import DemoUser
class EmailUser(models.Model):
email = models.CharField(max_length=80, unique=True)
def send_email(self, subject, message, from_email=None):
"""
Sends an email to this User.
"""
send_mail(subject, message, from_email, [self.email])
def __unicode__(self):
return self.email
class Event(models.Model):
users_organisers = models.ManyToManyField(DemoUser, blank=True, related_name="organisers")
users_trustees = models.ManyToManyField(EmailUser, blank=True, related_name="trustees")
@ -26,11 +35,32 @@ class Event(models.Model):
c_email = models.CharField(max_length=512, blank=True)
trustees = models.CharField(max_length=4096)
def EID_hr(self):
EID_json = json.loads(self.EID)
return EID_json['hr']
def EID_crypto(self):
EID_json = json.loads(self.EID)
EID_crypto_str = EID_json['crypto']
return json.loads(EID_crypto_str)
def duration(self):
duration_str = self.start_time.strftime("%d-%m-%y %H:%M")
duration_str = duration_str + " - " + self.end_time.strftime("%d-%m-%y %H:%M %Z")
duration_str = self.start_time_formatted()
duration_str = duration_str + " - " + self.end_time_formatted_utc()
return duration_str
def start_time_formatted(self):
return self.start_time.strftime("%d-%m-%y %H:%M")
def start_time_formatted_utc(self):
return self.start_time.strftime("%d-%m-%y %H:%M %Z")
def end_time_formatted(self):
return self.end_time.strftime("%d-%m-%y %H:%M")
def end_time_formatted_utc(self):
return self.end_time.strftime("%d-%m-%y %H:%M %Z")
def status(self):
status_str = ""
@ -113,14 +143,4 @@ class Organiser(models.Model):
email = models.CharField(max_length=100, blank=False, null=False)
event = models.ForeignKey(Event, on_delete=models.CASCADE)
'''
class Organiser(models.Model):
user = models.ForeignKey(DemoUser, on_delete=models.CASCADE)
event = models.ForeignKey(Event, on_delete=models.CASCADE)
class Trustee(models.Model):
user = models.ForeignKey(DemoUser, on_delete=models.CASCADE)
event = models.ForeignKey(Event, on_delete=models.CASCADE)
'''
#class EventOrganisers():
#event = models.ForeignKey(Event, on_delete=models.CASCADE)

View file

@ -1,14 +1,29 @@
from __future__ import absolute_import
import csv
from os import urandom
import base64
from io import StringIO
import json
from os import urandom
from celery import task
from django.core.exceptions import ValidationError
from django.core.validators import EmailValidator
from django.core.mail import send_mail
from allauthdemo.polls.models import Ballot, Event, EmailUser, AccessKey
from .cpp_calls import param, combpk, addec, tally
from django.conf import settings
from allauthdemo.polls.models import AccessKey
from .crypto_rpc import param, combpk, addec, tally
'''
Goal: This py file defines celery tasks that can be initiated
The following tasks were re-implemented by Thomas Smith: generate_event_param, tally_results, generate_combpk, generate_enc
This file was also updated by Vincent de Almeida
'''
# Will store the result of the initial cal to param() from .cpp_calls
group_param = None
def is_valid_email(email):
try:
@ -23,36 +38,59 @@ def create_ballots(poll):
for voter in poll.event.voters.all():
ballot = poll.ballots.create(voter=voter, poll=poll)
@task()
def create_voters(csvfile, event):
print("Creating voters for event " + event.title)
reader = csv.reader(csvfile, delimiter=',')
string = ""
for row in reader:
email = string.join(row)
print(email)
#testvoter = EmailUser.objects.get_or_create(email='notarealemail@live.com')[0]
#event.voters.add(testvoter)
if (is_valid_email(email)):
voter = EmailUser.objects.get_or_create(email=email)[0]
event.voters.add(voter)
key = base64.urlsafe_b64encode(urandom(16)).decode('utf-8')
AccessKey.objects.create(user=voter, event=event, key=key)
send_mail(
'Your Voting Key',
'Key: ' + key,
'from@example.com',
[string.join(row)],
fail_silently=False,
)
'''
Starting here: functions re-implemented by Thomas Smith
Will generate a key for accessing either the event preparation page or the voting page
'''
def gen_access_key():
return base64.urlsafe_b64encode(urandom(16)).decode('utf-8')
'''
Emails an event preparation URL containing an access key for all of the trustees for an event
'''
@task()
def generate_event_param(event):
event.EID = param()
def email_trustees_prep(trustees, event):
email_subject = "Key Generation and Preparation for Event '" + event.title + "'"
# Plain text email - this could be replaced for a HTML-based email in the future
email_body = "Please visit the following URL to prepare the event and generate your trustee secret key:\n\n"
url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/prepare/?key="
email_body = email_body + url_base
for trustee in trustees:
# Generate a key and create an AccessKey object
key = gen_access_key()
AccessKey.objects.create(user=trustee, event=event, key=key)
trustee.send_email(email_subject, email_body + key)
'''
Emails the access keys for all of the voters for an event
'''
@task()
def email_voters_a_key(voters, event):
email_subject = "Voting Access for Event '" + event.title + "'"
email_body = 'Key: '
for voter in voters:
# Generate a key and create an AccessKey object
key = gen_access_key()
AccessKey.objects.create(user=voter, event=event, key=key)
voter.send_email(email_subject, email_body + key)
'''
Updates the EID of an event to contain 2 event IDs: a human readable one (hr) and a crypto one (GP from param())
'''
@task()
def update_EID(event):
global group_param
if group_param is None:
group_param = param()
EID = {}
EID['hr'] = event.EID
EID['crypto'] = group_param
event.EID = json.dumps(EID)
event.save()
@task()
@ -98,20 +136,4 @@ def generate_enc(poll):
poll.enc = addec(amount, ciphers)
poll.save()
'''
End of re-implemented code
'''
@task()
def add(x, y):
return x + y
@task()
def mul(x, y):
return x * y
@task()
def xsum(numbers):
return sum(numbers)

View file

@ -1,13 +1,9 @@
import re
from datetime import datetime
from django.utils.dateparse import parse_datetime
from allauthdemo.polls.models import Event
from allauthdemo.polls.models import Poll
from allauthdemo.polls.models import PollOption
from allauthdemo.polls.models import EmailUser
from allauthdemo.polls.models import Event, Poll, PollOption, EmailUser
from allauthdemo.auth.models import DemoUser
'''
@ -382,6 +378,7 @@ class EventModelAdaptor:
errors_summary = errors_summary + str(i + 1) + " "
self.invalid_form_fields['polls_data'] = {'val': polls_json}
self.invalid_form_fields['poll_count'] = {'val': poll_count}
if not valid and len(errors_summary) > 34:
errors_summary = errors_summary + "and can be corrected by editing them."
@ -536,7 +533,7 @@ class EventModelAdaptor:
organisers_list = self.form_data.pop('organiser-email-input')
for organiser in organisers_list:
if organiser != '' and DemoUser.objects.filter(email=organiser).count() == 1:
if organiser != '' and DemoUser.objects.filter(email=organiser).exists():
self.organisers.append(DemoUser.objects.filter(email=organiser).get())
# Extract the list of trustees
@ -544,7 +541,7 @@ class EventModelAdaptor:
for trustee in trustees_list:
if trustee != '':
if EmailUser.objects.filter(email=trustee).count() == 1:
if EmailUser.objects.filter(email=trustee).exists():
self.trustees.append(EmailUser.objects.filter(email=trustee).get())
else:
self.trustees.append(EmailUser(email=trustee))
@ -555,7 +552,7 @@ class EventModelAdaptor:
for voter_email in voters_email_list:
if voter_email != '':
if EmailUser.objects.filter(email=voter_email).count() == 1:
if EmailUser.objects.filter(email=voter_email).exists():
self.voters.append(EmailUser.objects.filter(email=voter_email).get())
else:
self.voters.append(EmailUser(email=voter_email))
@ -634,14 +631,15 @@ class EventModelAdaptor:
# Add the list of trustees to the event, making sure they're instantiated
for trustee in self.trustees:
if EmailUser.objects.filter(email=trustee.email).count() == 0:
if not EmailUser.objects.filter(email=trustee.email).exists():
trustee.save()
self.event.users_trustees = self.trustees
# Add the list of voters to the event, making sure they're instantiated
# Additionally, generating the AccessKey for voters
for voter in self.voters:
if EmailUser.objects.filter(email=voter.email).count() == 0:
if not EmailUser.objects.filter(email=voter.email).exists():
voter.save()
self.event.voters = self.voters
@ -655,10 +653,10 @@ class EventModelAdaptor:
self.event.save()
# Finally perform a data clean up
self.__clear_data()
# Finally return a reference to the event
return self.event
def __clear_data(self):
def clear_data(self):
self.form_data = None
self.form_data_validation = None
self.invalid_form_fields = {}

View file

@ -17,8 +17,7 @@ from .forms import EventForm, PollForm, OptionFormset, QuestionFormset, Organise
from .models import Event, Poll, PollOption, EmailUser, Ballot, TrusteeKey, Decryption
from allauthdemo.auth.models import DemoUser
from .tasks import create_voters, create_ballots, generate_event_param, generate_combpk, generate_enc, tally_results
from .cpp_calls import param, addec, combpk, tally
from .tasks import email_trustees_prep, update_EID, generate_combpk, generate_enc, tally_results
from .utils.EventModelAdaptor import EventModelAdaptor
@ -38,6 +37,7 @@ class EventDetailView(generic.DetailView):
def get_context_data(self, **kwargs):
context = super(EventDetailView, self).get_context_data(**kwargs)
context['is_organiser'] = ((not self.request.user.is_anonymous()) and (self.object.users_organisers.filter(email=self.request.user.email).exists()))
#context['now'] = timezone.now()
return context
@ -153,28 +153,42 @@ def view_poll(request, event_id, poll_num):
})
def event_trustee_setup(request, event_id):
# Obtain the event and the event preparation access key that's been supplied
event = get_object_or_404(Event, pk=event_id)
access_key = request.GET.get('key', None)
if (access_key):
# If the a_key is present, check it's valid and related to a trustee EmailUser instance for this event
if access_key:
email_key = event.keys.filter(key=access_key)
if (email_key.exists() and event.users_trustees.filter(email=email_key[0].user.email).exists()):
if (TrusteeKey.objects.filter(event=event, user=email_key[0].user).exists()):
if email_key.exists() and event.users_trustees.filter(email=email_key[0].user.email).exists():
if TrusteeKey.objects.filter(event=event, user=email_key[0].user).exists():
messages.add_message(request, messages.WARNING, 'You have already submitted your key for this event')
return HttpResponseRedirect(reverse("user_home"))
if (request.method == "POST"):
if request.method == "POST":
form = EventSetupForm(request.POST)
if (form.is_valid()):
# If form data is valid, create a TrusteeKey object with the supplied public key
if form.is_valid():
public_key = request.POST["public_key"]
key = TrusteeKey.objects.get_or_create(event=event, user=email_key[0].user)[0]
key.key = public_key
key.save()
if (event.trustee_keys.count() == event.users_trustees.count()): # ready for combpk
# When all trustees have supplied their public key, we can combine them to create a master key
# The event will now be ready to receive votes on the various polls that have been defined -
# voters therefore need to be informed
if event.trustee_keys.count() == event.users_trustees.count():
generate_combpk.delay(event)
messages.add_message(request, messages.SUCCESS, 'You have successfully submitted your public key for this event')
# TODO: Create Celery task that generates voting URLs for voters as well as creates the ballots
success_msg = 'You have successfully submitted your public key for this event'
messages.add_message(request, messages.SUCCESS, success_msg)
# This re-direct may not be appropriate for trustees that don't have logins
return HttpResponseRedirect(reverse("user_home"))
else:
form = EventSetupForm()
return render(request, "polls/event_setup.html", {"event": event, "form": form })
return render(request, "polls/event_setup.html", {"event": event, "form": form})
#if no key or is invalid?
messages.add_message(request, messages.WARNING, 'You do not have permission to access: ' + request.path)
@ -247,11 +261,11 @@ def manage_questions(request, event_id):
poll.save()
formset = OptionFormset(request.POST, prefix="formset_organiser", instance=poll)
if formset.is_valid():
for form in formset:
formset.save()
create_ballots.delay(poll)
messages.add_message(request, messages.SUCCESS, 'Poll created successfully')
return HttpResponseRedirect(reverse('polls:view-poll', kwargs={'event_id': poll.event_id, 'poll_num': event.polls.count() }))
formset.save()
#create_ballots.delay(poll)
messages.add_message(request, messages.SUCCESS, 'Poll created successfully')
return HttpResponseRedirect(reverse('polls:event-polls', args=[poll.event_id]))
return render(request, "polls/create_poll.html", {"event": event, "question_form": form, "option_formset": formset})
elif request.method == "GET":
@ -296,17 +310,28 @@ def create_event(request):
'''Process form data based on above results'''
if result['success']:
if form_data_valid:
# Create the new event using the form data
adaptor.extractData()
adaptor.updateModel()
new_event = adaptor.updateModel()
# Update the EID to include the GP in its EID
update_EID.delay(new_event)
# Send an email to all trustees for event preparation
trustees = new_event.users_trustees.all()
email_trustees_prep.delay(trustees, new_event)
adaptor.clear_data()
return HttpResponseRedirect(reverse('polls:index'))
else:
invalid_fields = adaptor.getInvalidFormFields()
adaptor.clear_data()
return render_invalid(request, events, demo_users, invalid_fields)
else:
invalid_fields = adaptor.getInvalidFormFields()
invalid_fields['recaptcha'] = {'error': 'The reCAPTCHA server validation failed, please try again.'}
adaptor.clear_data()
return render_invalid(request, events, demo_users, invalid_fields)
elif request.method == "GET":