Eos/eosweb/core/main.py

337 lines
11 KiB
Python
Raw Normal View History

2017-11-22 23:23:24 +11:00
# Eos - Verifiable elections
# Copyright © 2017 RunasSudo (Yingtong Li)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import click
import flask
from eos.core.objects import *
2017-11-22 23:23:24 +11:00
from eos.base.election import *
from eos.psr.crypto import *
from eos.psr.election import *
from eos.psr.mixnet import *
from eos.psr.workflow import *
import eos.core.hashing
2017-11-23 18:18:01 +11:00
import eosweb
2017-11-22 23:23:24 +11:00
from datetime import datetime
import functools
2017-11-25 22:37:20 +11:00
import importlib
import json
2017-11-25 22:37:20 +11:00
import os
2017-12-07 15:33:11 +11:00
import subprocess
app = flask.Flask(__name__, static_folder=None)
2017-11-22 23:23:24 +11:00
2017-11-25 22:37:20 +11:00
# Load config
app.config.from_object('eosweb.core.settings')
if 'EOSWEB_SETTINGS' in os.environ:
app.config.from_envvar('EOSWEB_SETTINGS')
# Load app config
for app_name in app.config['APPS']:
app.config.from_object(app_name + '.settings')
if 'EOSWEB_SETTINGS' in os.environ:
app.config.from_envvar('EOSWEB_SETTINGS')
# Connect to database
2017-11-27 19:40:01 +11:00
db_connect(app.config['DB_NAME'], app.config['DB_URI'], app.config['DB_TYPE'])
2017-11-25 22:37:20 +11:00
2017-12-07 15:33:11 +11:00
# Set configs
User.admins = app.config['ADMINS']
2017-11-25 22:37:54 +11:00
# Make Flask's serialisation, e.g. for sessions, EosObject aware
class EosObjectJSONEncoder(flask.json.JSONEncoder):
def default(self, obj):
if isinstance(obj, EosObject):
return EosObject.serialise_and_wrap(obj)
return super().default(obj)
class EosObjectJSONDecoder(flask.json.JSONDecoder):
def __init__(self, *args, **kwargs):
self.super_object_hook = kwargs.get('object_hook', None)
kwargs['object_hook'] = self.my_object_hook
super().__init__(*args, **kwargs)
def my_object_hook(self, val):
if 'type' in val:
if val['type'] in EosObject.objects:
return EosObject.deserialise_and_unwrap(val)
if self.super_object_hook:
return self.super_object_hook(val)
return val
app.json_encoder = EosObjectJSONEncoder
app.json_decoder = EosObjectJSONDecoder
# Patch Flask's static file sending to add no-cache
# Allow "caching", but require revalidation via 304 Not Modified
@app.route('/static/<path:filename>')
def static(filename):
cache_timeout = app.get_send_file_max_age(filename)
val = flask.send_from_directory('static', filename, cache_timeout=cache_timeout)
val.headers['Cache-Control'] = val.headers['Cache-Control'].replace('public', 'no-cache')
#import pdb; pdb.set_trace()
return val
2017-11-22 23:23:24 +11:00
@app.cli.command('test')
@click.option('--prefix', default=None)
@click.option('--lang', default=None)
def run_tests(prefix, lang):
import eos.tests
eos.tests.run_tests(prefix, lang)
# TODO: Will remove this once we have a web UI
@app.cli.command('drop_db_and_setup')
def setup_test_election():
# DANGER!
2017-11-27 19:40:01 +11:00
dbinfo.provider.reset_db()
2017-11-22 23:23:24 +11:00
# Set up election
election = PSRElection()
election.workflow = PSRWorkflow()
# Set election details
election.name = 'Test Election'
2017-11-26 11:17:28 +11:00
from eos.redditauth.election import RedditUser
2017-11-26 20:48:15 +11:00
election.voters.append(UserVoter(user=EmailUser(name='Alice', email='alice@localhost')))
election.voters.append(UserVoter(user=EmailUser(name='Bob', email='bob@localhost')))
election.voters.append(UserVoter(user=EmailUser(name='Carol', email='carol@localhost')))
2017-11-26 11:17:28 +11:00
election.voters.append(UserVoter(user=RedditUser(username='RunasSudo')))
2017-11-22 23:23:24 +11:00
2017-11-26 20:48:15 +11:00
for voter in election.voters:
if isinstance(voter, UserVoter):
if isinstance(voter.user, EmailUser):
voter.user.email_password(app.config['SMTP_HOST'], app.config['SMTP_PORT'], app.config['SMTP_USER'], app.config['SMTP_PASS'], app.config['SMTP_FROM'])
2017-11-24 19:37:48 +11:00
election.mixing_trustees.append(InternalMixingTrustee(name='Eos Voting'))
election.mixing_trustees.append(InternalMixingTrustee(name='Eos Voting'))
2017-11-22 23:23:24 +11:00
election.sk = EGPrivateKey.generate()
election.public_key = election.sk.public_key
2017-12-11 11:25:01 +11:00
question = PreferentialQuestion(prompt='President', choices=[
Ticket(name='ACME Party', choices=[
Choice(name='John Smith'),
Choice(name='Joe Bloggs', party='Independent ACME')
]),
Choice(name='John Q. Public')
2017-12-11 13:00:41 +11:00
], min_choices=0, max_choices=3, randomise_choices=True)
2017-11-22 23:23:24 +11:00
election.questions.append(question)
2017-12-11 11:25:01 +11:00
question = ApprovalQuestion(prompt='Chairman', choices=[Choice(name='John Doe'), Choice(name='Andrew Citizen')], min_choices=0, max_choices=1)
2017-11-22 23:23:24 +11:00
election.questions.append(question)
election.save()
@app.cli.command('verify_election')
@click.option('--electionid', default=None)
def verify_election(electionid):
if electionid is None:
election = Election.get_all()[0]
else:
election = Election.get_by_id(electionid)
election.verify()
print('The election has passed validation')
2017-11-22 23:23:24 +11:00
@app.context_processor
def inject_globals():
2017-11-23 18:18:01 +11:00
return {'eos': eos, 'eosweb': eosweb, 'SHA256': eos.core.hashing.SHA256}
2017-11-22 23:23:24 +11:00
2017-11-25 22:37:54 +11:00
# === Views ===
2017-11-22 23:23:24 +11:00
@app.route('/')
def index():
return flask.render_template('index.html')
def using_election(func):
@functools.wraps(func)
2017-12-07 16:04:24 +11:00
def wrapped(election_id, **kwargs):
2017-11-22 23:23:24 +11:00
election = Election.get_by_id(election_id)
2017-12-07 16:04:24 +11:00
return func(election, **kwargs)
2017-11-22 23:23:24 +11:00
return wrapped
2017-12-07 15:33:11 +11:00
def election_admin(func):
@functools.wraps(func)
2017-12-07 16:04:24 +11:00
def wrapped(election, **kwargs):
2017-12-07 15:33:11 +11:00
if 'user' in flask.session and flask.session['user'].is_admin():
2017-12-07 16:04:24 +11:00
return func(election, **kwargs)
2017-12-07 15:33:11 +11:00
else:
return flask.Response('Administrator credentials required', 403)
return wrapped
2017-11-23 21:07:16 +11:00
@app.route('/election/<election_id>/')
@using_election
def election_api_json(election):
return flask.Response(EosObject.to_json(EosObject.serialise_and_wrap(election, should_protect=True, for_hash=('full' not in flask.request.args))), mimetype='application/json')
2017-11-23 21:07:16 +11:00
2017-11-22 23:23:24 +11:00
@app.route('/election/<election_id>/view')
@using_election
def election_view(election):
2017-12-07 15:33:11 +11:00
return flask.render_template('election/view/view.html', election=election)
2017-11-23 18:18:01 +11:00
@app.route('/election/<election_id>/booth')
@using_election
def election_booth(election):
2017-11-23 21:07:16 +11:00
selection_model_view_map = EosObject.to_json({key._name: val for key, val in model_view_map.items()}) # ewww
auth_methods = EosObject.to_json(app.config['AUTH_METHODS'])
2017-11-23 21:07:16 +11:00
2017-12-07 15:33:11 +11:00
return flask.render_template('election/view/booth.html', election=election, selection_model_view_map=selection_model_view_map, auth_methods=auth_methods)
2017-11-23 18:18:01 +11:00
@app.route('/election/<election_id>/view/questions')
@using_election
def election_view_questions(election):
2017-12-07 15:33:11 +11:00
return flask.render_template('election/view/questions.html', election=election)
2017-11-23 18:18:01 +11:00
@app.route('/election/<election_id>/view/ballots')
@using_election
def election_view_ballots(election):
2017-12-07 15:33:11 +11:00
return flask.render_template('election/view/ballots.html', election=election)
2017-11-23 18:18:01 +11:00
@app.route('/election/<election_id>/view/trustees')
@using_election
def election_view_trustees(election):
2017-12-07 15:33:11 +11:00
return flask.render_template('election/view/trustees.html', election=election)
@app.route('/election/<election_id>/admin')
@using_election
@election_admin
def election_admin_summary(election):
return flask.render_template('election/admin/admin.html', election=election)
2017-11-23 18:18:01 +11:00
2017-12-07 16:04:24 +11:00
@app.route('/election/<election_id>/admin/enter_task')
@using_election
@election_admin
def election_admin_enter_task(election):
task = election.workflow.get_task(flask.request.args['task_name'])
if task.status != WorkflowTask.Status.READY:
return flask.Response('Task is not yet ready or has already exited', 409)
task.enter()
election.save()
return flask.redirect(flask.url_for('election_admin_summary', election_id=election._id))
@app.route('/election/<election_id>/cast_ballot', methods=['POST'])
@using_election
def election_api_cast_vote(election):
if election.workflow.get_task('eos.base.workflow.TaskOpenVoting').status < WorkflowTask.Status.EXITED or election.workflow.get_task('eos.base.workflow.TaskCloseVoting').status > WorkflowTask.Status.READY:
2017-11-24 19:37:48 +11:00
# Voting is not yet open or has closed
return flask.Response('Voting is not yet open or has closed', 409)
2017-11-24 19:37:48 +11:00
data = json.loads(flask.request.data)
if 'user' not in flask.session:
# User is not authenticated
return flask.Response('Not authenticated', 403)
voter = None
for election_voter in election.voters:
2017-11-26 11:17:28 +11:00
if election_voter.user.matched_by(flask.session['user']):
voter = election_voter
break
if voter is None:
# Invalid user
return flask.Response('Invalid credentials', 403)
# Cast the vote
ballot = EosObject.deserialise_and_unwrap(data['ballot'])
vote = Vote(ballot=ballot, cast_at=DateTimeField.now())
2017-12-11 13:53:25 +11:00
# Store data
if app.config['CAST_FINGERPRINT']:
vote.cast_fingerprint = data['fingerprint']
if app.config['CAST_IP']:
if os.path.exists('/app/.heroku'):
vote.cast_ip = flask.request.headers['X-Forwarded-For'].split(',')[-1]
else:
vote.cast_ip = flask.request.remote_addr
voter.votes.append(vote)
election.save()
return flask.Response(json.dumps({
2017-11-26 11:17:28 +11:00
'voter': EosObject.serialise_and_wrap(voter, should_protect=True),
'vote': EosObject.serialise_and_wrap(vote, should_protect=True)
}), mimetype='application/json')
2017-11-23 21:07:16 +11:00
2017-11-28 22:43:32 +11:00
@app.route('/auditor')
def auditor():
return flask.render_template('election/auditor.html')
2017-11-25 22:37:54 +11:00
@app.route('/debug')
def debug():
assert False
@app.route('/auth/login')
def login():
return flask.render_template('auth/login.html')
@app.route('/auth/logout')
def logout():
flask.session['user'] = None
#return flask.redirect(flask.request.args['next'] if 'next' in flask.request.args else '/')
# I feel like there's some kind of exploit here, so we'll leave this for now
return flask.redirect('/')
@app.route('/auth/login_complete')
def login_complete():
return flask.render_template('auth/login_complete.html')
@app.route('/auth/login_cancelled')
def login_cancelled():
return flask.render_template('auth/login_cancelled.html')
2017-11-26 20:48:15 +11:00
@app.route('/auth/email/login')
def email_login():
return flask.render_template('auth/email/login.html')
@app.route('/auth/email/authenticate', methods=['POST'])
def email_authenticate():
user = None
for election in Election.get_all():
for voter in election.voters:
if isinstance(voter.user, EmailUser):
2017-11-28 00:26:13 +11:00
if voter.user.email.lower() == flask.request.form['email'].lower():
2017-11-26 20:48:15 +11:00
if voter.user.password == flask.request.form['password']:
user = voter.user
break
if user is None:
return flask.render_template('auth/email/login.html', error='The email or password you entered was invalid. Please check your details and try again. If the issue persists, contact the election administrator.')
flask.session['user'] = user
return flask.redirect(flask.url_for('login_complete'))
2017-11-25 22:37:54 +11:00
# === Apps ===
for app_name in app.config['APPS']:
app_main = importlib.import_module(app_name + '.main')
app_main.main(app)
2017-11-23 18:18:01 +11:00
# === Model-Views ===
model_view_map = {}
# TODO: Make more modular
from . import modelview
model_view_map.update(modelview.model_view_map)