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
2017-11-24 20:26:18 +11:00
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
2017-11-23 23:10:57 +11:00
from datetime import datetime
2017-11-22 23:35:46 +11:00
import functools
2017-11-25 22:37:20 +11:00
import importlib
2017-11-23 23:10:57 +11:00
import json
2017-11-25 22:37:20 +11:00
import os
2017-12-07 15:33:11 +11:00
import subprocess
2017-11-22 23:35:46 +11:00
2017-11-28 13:58:22 +11:00
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
2017-11-28 13:58:22 +11:00
# 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 ( )
2017-11-29 18:29:42 +11:00
@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 ) :
2017-11-22 23:35:46 +11:00
@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 ) :
2017-11-29 20:23:52 +11:00
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
2017-11-25 23:16:29 +11:00
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 ) )
2017-11-23 23:10:57 +11:00
@app.route ( ' /election/<election_id>/cast_ballot ' , methods = [ ' POST ' ] )
@using_election
def election_api_cast_vote ( election ) :
2017-11-25 23:16:29 +11:00
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
2017-11-25 23:16:29 +11:00
return flask . Response ( ' Voting is not yet open or has closed ' , 409 )
2017-11-24 19:37:48 +11:00
2017-11-23 23:10:57 +11:00
data = json . loads ( flask . request . data )
2017-11-25 23:16:29 +11:00
if ' user ' not in flask . session :
# User is not authenticated
return flask . Response ( ' Not authenticated ' , 403 )
2017-11-23 23:10:57 +11:00
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 ' ] ) :
2017-11-23 23:10:57 +11:00
voter = election_voter
break
if voter is None :
2017-11-25 23:16:29 +11:00
# Invalid user
2017-11-23 23:10:57 +11:00
return flask . Response ( ' Invalid credentials ' , 403 )
# Cast the vote
ballot = EosObject . deserialise_and_unwrap ( data [ ' ballot ' ] )
2017-11-24 20:26:18 +11:00
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
2017-11-23 23:10:57 +11:00
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 )
2017-11-23 23:10:57 +11:00
} ) , 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 )