Compare commits

..

24 Commits

Author SHA1 Message Date
RunasSudo 8333dd7c2d
Fix BLT export format 2021-10-16 22:12:22 +11:00
RunasSudo 913aac26ca
Fix unit tests 2021-10-16 20:44:56 +11:00
RunasSudo fc4366c028
Allow hiding voters and/or votes 2021-10-16 20:44:31 +11:00
RunasSudo d44c21cbd7
Support null-encrypted election in web UI 2021-10-16 20:13:28 +11:00
RunasSudo adbdb21e0d
Fix typo in voting booth when failing to load fingerprintjs2 2021-10-16 01:48:20 +11:00
RunasSudo c948615647
Update to Transcrypt/Python 3.9 2021-10-16 01:47:50 +11:00
RunasSudo 2c15f443f8
Update libraries
Update for Python 3.8
Migrate from Flask-OAuthlib to Authlib
2020-07-11 17:59:32 +10:00
RunasSudo da24cc4f63
Add threading task strategy 2019-03-17 12:06:46 +11:00
RunasSudo 1bb0197b46
Implement batch operations on elections #11 2019-03-06 13:43:54 +11:00
RunasSudo fd6f6bc4b1
Implement task timeout 2019-03-06 13:06:01 +11:00
RunasSudo 3097092ae5
Add run task CLI option 2019-02-17 15:04:59 +11:00
RunasSudo 993ec142ac
Add NationStates authentication 2019-02-13 09:46:11 +11:00
RunasSudo 1bc558fc97
Open voting booth description links in new tab 2019-01-14 20:22:49 +11:00
RunasSudo 706fd132cd
Document new npm dependencies 2019-01-14 18:20:25 +11:00
RunasSudo 5e06ffe577
Explicitly specify oauthlib 2 2019-01-14 18:20:19 +11:00
RunasSudo 9c2c0cf108
Add description field to questions 2019-01-14 17:55:24 +11:00
RunasSudo 2011749836
Prevent saving different version objects 2019-01-14 17:55:14 +11:00
RunasSudo cb3623fda7
Update for Transcrypt/Python 3.7 2019-01-14 17:54:31 +11:00
RunasSudo 86f01abfdd
Tidy docs 2018-12-30 13:44:59 +11:00
RunasSudo bb061e18cd
Documentation for Reddit OAuth callback URI 2018-12-29 19:23:48 +11:00
RunasSudo 4b74072063
Fix errors trying to pickle users / log in 2018-08-31 10:37:06 +10:00
RunasSudo 4bc3fcf30c
Allow administration through email 2018-08-23 14:31:58 +10:00
RunasSudo 05d5650f2f
Fix home page error on attempting to categorise unscheduled elections 2018-04-17 21:42:30 +10:00
RunasSudo f0950effca
Fix PostgreSQL 2018-04-15 19:41:16 +10:00
48 changed files with 7578 additions and 265 deletions

3
.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env"]
}

3
.gitignore vendored
View File

@ -2,9 +2,10 @@
/.python-version /.python-version
/htmlcov /htmlcov
/venv /venv
__javascript__ __target__
__pycache__ __pycache__
refs refs
node_modules
\#* \#*
.#* .#*

View File

@ -12,6 +12,10 @@ Install the Python dependencies. (If doing this in a virtualenv, add the virtual
cd /path/to/Eos cd /path/to/Eos
pip install -r requirements.txt pip install -r requirements.txt
Install the node dependencies to build the JavaScript code.
npm install @babel/core @babel/cli @babel/preset-env babelify browserify
Build the JavaScript code. Build the JavaScript code.
./build_js.sh ./build_js.sh

View File

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017 RunasSudo (Yingtong Li) # Copyright © 2017-2019 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -20,21 +20,14 @@ FLAGS="-k -mc -o"
#for f in eos.js eos.js_tests; do #for f in eos.js eos.js_tests; do
for f in eos.js_tests; do for f in eos.js_tests; do
transcrypt -b -n $FLAGS $f.py || exit 1 transcrypt -b -n $FLAGS $f.py || exit 1
# Javascript identifiers cannot contain dots
perl -0777 -pi -e 's/eos.js/eosjs/g' eos/__javascript__/$f.js
# __pragma__ sometimes stops working???
perl -0777 -pi -e "s/__pragma__ \('.*?'\)//gs" eos/__javascript__/$f.js
# Transcrypt by default suppresses stack traces for some reason??
perl -0777 -pi -e 's/__except0__.__cause__ = null;//g' eos/__javascript__/$f.js
# Fix handling of properties, Transcrypt bug #407
perl -0777 -pi -e 's/var __get__ = function \(self, func, quotedFuncName\) \{/var __get__ = function (self, func, quotedFuncName) { if(typeof(func) != "function"){return func;}/g' eos/__javascript__/$f.js
perl -0777 -pi -e 's/property.call \((.*?), \g1.\g1.__impl__(.*?)\)/property.call ($1, $1.__impl__$2)/g' eos/__javascript__/$f.js
perl -0777 -pi -e 's/property.call \((.*?), \g1.\g1.__implpy_(.*?)\)/property.call ($1, $1.__impl__$2)/g' eos/__javascript__/$f.js
done done
cp eos/__javascript__/eos.js_tests.js eosweb/core/static/js/eosjs.js # Transcrypt syntax errors
perl -0777 -pi -e 's/eosjs_tests/eosjs/g' eosweb/core/static/js/eosjs.js perl -0777 -pi -e 's/import \{, /import \{/g' __target__/eos*.js
# Add export
echo >> __target__/eos.js_tests.js
echo 'export {eos, __kwargtrans__};' >> __target__/eos.js_tests.js
# Convert to ES5
./node_modules/.bin/browserify -t babelify -r ./__target__/eos.js_tests.js:eosjs > eosweb/core/static/js/eosjs.js

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017 RunasSudo (Yingtong Li) # Copyright © 2017-2021 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -30,6 +30,9 @@ class NullEncryptedAnswer(EncryptedAnswer):
def decrypt(self): def decrypt(self):
return None, self.answer return None, self.answer
def deaudit(self):
return self
class Ballot(EmbeddedObject): class Ballot(EmbeddedObject):
#_id = UUIDField() #_id = UUIDField()
encrypted_answers = EmbeddedObjectListField() encrypted_answers = EmbeddedObjectListField()
@ -77,6 +80,9 @@ class User(EmbeddedObject):
return True return True
return False return False
def __getstate__(self):
return {k: v for k, v in self.__dict__.items() if k != '_instance'}
def generate_password(): def generate_password():
if is_python: if is_python:
#__pragma__('skip') #__pragma__('skip')
@ -104,14 +110,15 @@ class UserVoter(Voter):
return self.user.name return self.user.name
class Question(EmbeddedObject): class Question(EmbeddedObject):
_ver = StringField(default='0.7')
prompt = StringField() prompt = StringField()
description = StringField()
class Result(EmbeddedObject): class Result(EmbeddedObject):
pass pass
class ListChoiceQuestion(Question): class ListChoiceQuestion(Question):
_ver = StringField(default='0.5')
choices = EmbeddedObjectListField() choices = EmbeddedObjectListField()
min_choices = IntField() min_choices = IntField()
max_choices = IntField() max_choices = IntField()
@ -207,6 +214,8 @@ class STVResult(Result):
random = BlobField() random = BlobField()
class Election(TopLevelObject): class Election(TopLevelObject):
_ver = StringField(default='0.9')
_id = UUIDField() _id = UUIDField()
workflow = EmbeddedObjectField(Workflow) # Once saved, we don't care what kind of workflow it is workflow = EmbeddedObjectField(Workflow) # Once saved, we don't care what kind of workflow it is
name = StringField() name = StringField()
@ -215,6 +224,13 @@ class Election(TopLevelObject):
questions = EmbeddedObjectListField() questions = EmbeddedObjectListField()
results = EmbeddedObjectListField(is_hashed=False) results = EmbeddedObjectListField(is_hashed=False)
is_voters_public = BooleanField(is_hashed=False, default=False)
is_votes_public = BooleanField(is_hashed=False, default=False)
def can_audit(self):
"""Can prepared votes be audited?"""
return False
def verify(self): def verify(self):
#__pragma__('skip') #__pragma__('skip')
from eos.core.hashing import SHA256 from eos.core.hashing import SHA256

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-18 RunasSudo (Yingtong Li) # Copyright © 2017-2021 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -38,7 +38,7 @@ class ElectionTestCase(EosTestCase):
def test_run_election(self): def test_run_election(self):
# Set up election # Set up election
election = Election() election = Election()
election.workflow = WorkflowBase() election.workflow = BaseWorkflow()
# Check _instance # Check _instance
self.assertEqual(election.workflow._instance, (election, 'workflow')) self.assertEqual(election.workflow._instance, (election, 'workflow'))

View File

@ -1,6 +1,6 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# pyRCV - Preferential voting counting # pyRCV - Preferential voting counting
# Copyright © 2016–2017 RunasSudo (Yingtong Li) # Copyright © 2016-2021 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -41,10 +41,10 @@ def writeBLT(election, q_num, seats, withdrawn=[]):
for candidate in flat_choices: for candidate in flat_choices:
if candidate.party: if candidate.party:
electionLines.append("'{}{}'".format(candidate.name, candidate.party)) electionLines.append('"{}{}"'.format(candidate.name, candidate.party))
else: else:
electionLines.append("'{}'".format(candidate.name)) electionLines.append('"{}"'.format(candidate.name))
electionLines.append("'{}{}'".format(election.name, question.prompt)) electionLines.append('"{}{}"'.format(election.name, question.prompt))
return electionLines return electionLines

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-18 RunasSudo (Yingtong Li) # Copyright © 2017-2021 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -58,7 +58,12 @@ class WorkflowTask(EmbeddedObject):
def are_dependencies_met(self): def are_dependencies_met(self):
for depends_on_desc in self.depends_on: for depends_on_desc in self.depends_on:
for depends_on_task in self.workflow.get_tasks(depends_on_desc): depends_on_tasks = list(self.workflow.get_tasks(depends_on_desc))
if len(depends_on_tasks) == 0:
return False
for depends_on_task in depends_on_tasks:
if depends_on_task.status is not WorkflowTaskStatus.EXITED: if depends_on_task.status is not WorkflowTaskStatus.EXITED:
return False return False
return True return True
@ -184,7 +189,9 @@ class TaskReleaseResults(WorkflowTask):
# Concrete workflows # Concrete workflows
# ================== # ==================
class WorkflowBase(Workflow): class BaseWorkflow(Workflow):
"""Base workflow, with no encryption"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017 RunasSudo (Yingtong Li) # Copyright © 2017-18 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -35,17 +35,21 @@ class PostgreSQLDBProvider(eos.core.db.DBProvider):
return [x[0] for x in self.cur.fetchall()] return [x[0] for x in self.cur.fetchall()]
def get_all_by_fields(self, table, fields): def get_all_by_fields(self, table, fields):
def does_match(val):
if '_id' in fields and val['_id'] != fields.pop('_id'):
return False
if 'type' in fields and val['type'] != fields.pop('type'):
return False
for field in fields:
if val['value'][field] != fields[field]:
return False
return True
# TODO: Make this much better # TODO: Make this much better
result = [] result = []
for val in self.get_all(table): for val in self.get_all(table):
if '_id' in fields and val['_id'] != fields.pop('_id'): if does_match(val):
continue result.append(val)
if 'type' in fields and val['type'] != fields.pop('type'):
continue
for field in fields:
if val['value'][field] != fields[field]:
continue
result.append(val)
return result return result
def get_by_id(self, table, _id): def get_by_id(self, table, _id):

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017 RunasSudo (Yingtong Li) # Copyright © 2017-2019 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -184,18 +184,21 @@ class RelatedObjectListField(Field):
return None return None
return EosList([EosObject.deserialise_and_unwrap(x, self.object_type) for x in value]) return EosList([EosObject.deserialise_and_unwrap(x, self.object_type) for x in value])
if is_python: class UUIDField(Field):
class UUIDField(Field): def __init__(self, *args, **kwargs):
def __init__(self, *args, **kwargs): if is_python:
super().__init__(default=uuid.uuid4, *args, **kwargs) super().__init__(default=uuid.uuid4, *args, **kwargs)
else:
super().__init__(*args, **kwargs)
def serialise(self, value, options=SerialiseOptions.DEFAULT): def serialise(self, value, options=SerialiseOptions.DEFAULT):
return str(value) return str(value)
def deserialise(self, value): def deserialise(self, value):
if is_python:
return uuid.UUID(value) return uuid.UUID(value)
else: else:
UUIDField = PrimitiveField return value
class DateTimeField(Field): class DateTimeField(Field):
def pad(self, number): def pad(self, number):
@ -359,7 +362,16 @@ class DocumentObjectType(EosObjectType):
fields = {} fields = {}
if hasattr(cls, '_fields'): if hasattr(cls, '_fields'):
fields = cls._fields.copy() if is_python else Object.create(cls._fields) fields = cls._fields.copy() if is_python else Object.create(cls._fields)
for attr in list(dir(cls)):
if is_python:
attrs = list(dir(cls))
else:
# We want the raw Javascript name for getOwnPropertyDescriptor
__pragma__('jsiter')
attrs = [x for x in cls]
__pragma__('nojsiter')
for attr in attrs:
if not is_python: if not is_python:
# We must skip things with getters or else they will be called here (too soon) # We must skip things with getters or else they will be called here (too soon)
if Object.getOwnPropertyDescriptor(cls, attr).js_get: if Object.getOwnPropertyDescriptor(cls, attr).js_get:
@ -407,6 +419,8 @@ class DocumentObject(EosObject, metaclass=DocumentObjectType):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__() super().__init__()
self._json = None
self._field_values = {} self._field_values = {}
# Different to Python # Different to Python
@ -444,6 +458,10 @@ class DocumentObject(EosObject, metaclass=DocumentObjectType):
val.object_init(self, default) val.object_init(self, default)
def serialise(self, options=SerialiseOptions.DEFAULT): def serialise(self, options=SerialiseOptions.DEFAULT):
if self._ver != self._fields['_ver'].default:
# Different version, use stored JSON
return self._json
return {val.real_name: val.serialise(getattr(self, val.real_name), options) for attr, val in self._fields.items() if ((val.is_hashed or not options.for_hash) and (not options.should_protect or not val.is_protected))} return {val.real_name: val.serialise(getattr(self, val.real_name), options) for attr, val in self._fields.items() if ((val.is_hashed or not options.for_hash) and (not options.should_protect or not val.is_protected))}
@classmethod @classmethod
@ -455,7 +473,11 @@ class DocumentObject(EosObject, metaclass=DocumentObjectType):
for attr, val in cls._fields.items(): for attr, val in cls._fields.items():
if attr in value: if attr in value:
attrs[val.internal_name] = val.deserialise(value[val.real_name]) attrs[val.internal_name] = val.deserialise(value[val.real_name])
return cls(**attrs) inst = cls(**attrs)
inst._json = value
return inst
class TopLevelObjectType(DocumentObjectType): class TopLevelObjectType(DocumentObjectType):
def __new__(meta, name, bases, attrs): def __new__(meta, name, bases, attrs):
@ -477,8 +499,10 @@ class TopLevelObjectType(DocumentObjectType):
class TopLevelObject(DocumentObject, metaclass=TopLevelObjectType): class TopLevelObject(DocumentObject, metaclass=TopLevelObjectType):
def save(self): def save(self):
#res = db[self._name].replace_one({'_id': self.serialise()['_id']}, self.serialise(), upsert=True) if self._ver != self._fields['_ver'].default:
#res = dbinfo.db[self._db_name].replace_one({'_id': self._fields['_id'].serialise(self._id)}, EosObject.serialise_and_wrap(self), upsert=True) # Different version, unable to save
raise Exception('Attempted to save older vesion object')
dbinfo.provider.update_by_id(self._db_name, self._fields['_id'].serialise(self._id), EosObject.serialise_and_wrap(self)) dbinfo.provider.update_by_id(self._db_name, self._fields['_id'].serialise(self._id), EosObject.serialise_and_wrap(self))
def delete(self): def delete(self):

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-18 RunasSudo (Yingtong Li) # Copyright © 2017-2019 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -31,12 +31,15 @@ class TaskStatus(EosEnum):
class Task(TopLevelObject): class Task(TopLevelObject):
label = 'Unknown task' label = 'Unknown task'
_ver = StringField(default='0.8')
_id = UUIDField() _id = UUIDField()
run_strategy = EmbeddedObjectField() run_strategy = EmbeddedObjectField()
run_at = DateTimeField() run_at = DateTimeField()
timeout = IntField(default=3600) # seconds
started_at = DateTimeField() started_at = DateTimeField()
completed_at = DateTimeField() completed_at = DateTimeField()
@ -113,6 +116,16 @@ class TaskScheduler:
@staticmethod @staticmethod
def tick(): def tick():
now = DateTimeField.now()
for task in TaskScheduler.pending_tasks(): for task in TaskScheduler.pending_tasks():
if task.run_at and task.run_at < DateTimeField.now(): if task.run_at and task.run_at < now:
task.run() task.run()
for task in TaskScheduler.active_tasks():
if task.timeout and (now - task.started_at).total_seconds() > task.timeout:
task.status = TaskStatus.TIMEOUT
task.completed_at = DateTimeField.now()
task.messages.append('Elapsed time exceeded timeout')
task.save()
task.error()

View File

@ -0,0 +1,51 @@
# Eos - Verifiable elections
# Copyright © 2017-2019 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/>.
from eos.core.tasks import *
from eos.core.objects import *
import threading
class ThreadingRunStrategy(RunStrategy):
def run(self, task):
def _run():
task.status = TaskStatus.PROCESSING
task.started_at = DateTimeField.now()
task.save()
try:
task._run()
task.status = TaskStatus.COMPLETE
task.completed_at = DateTimeField.now()
task.save()
task.complete()
except Exception as e:
task.status = TaskStatus.FAILED
task.completed_at = DateTimeField.now()
if is_python:
#__pragma__('skip')
import traceback
#__pragma__('noskip')
task.messages.append(traceback.format_exc())
else:
task.messages.append(repr(e))
task.save()
task.error()
thread = threading.Thread(target=_run, args=())
thread.start()

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-18 RunasSudo (Yingtong Li) # Copyright © 2017-2021 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -133,6 +133,7 @@ class TaskTestCase(EosTestCase):
def setUpClass(cls): def setUpClass(cls):
cls.db_connect_and_reset() cls.db_connect_and_reset()
@py_only
def test_normal(self): def test_normal(self):
class TaskNormal(Task): class TaskNormal(Task):
result = StringField() result = StringField()
@ -149,6 +150,7 @@ class TaskTestCase(EosTestCase):
self.assertEqual(task.messages[0], 'Hello World') self.assertEqual(task.messages[0], 'Hello World')
self.assertEqual(task.result, 'Success') self.assertEqual(task.result, 'Success')
@py_only
def test_error(self): def test_error(self):
class TaskError(Task): class TaskError(Task):
def _run(self): def _run(self):

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017 RunasSudo (Yingtong Li) # Copyright © 2017-2019 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -14,8 +14,23 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import eos.js import eos.core.objects
import eos.core.bigint
import eos.core.hashing
import eos.core.tests import eos.core.tests
import eos.core.tasks
import eos.core.tasks.direct
import eos.base.election
import eos.base.workflow
import eos.psr.bitstream
import eos.psr.crypto
import eos.psr.election
import eos.psr.mixnet
import eos.psr.workflow
import eos.redditauth.election
import eos.base.tests import eos.base.tests
import eos.psr.tests import eos.psr.tests

30
eos/nsauth/election.py Normal file
View File

@ -0,0 +1,30 @@
# Eos - Verifiable elections
# Copyright © 2017-2019 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/>.
from eos.base.election import *
from eos.core.objects import *
class NationStatesUser(User):
username = StringField()
@property
def name(self):
return self.username
def matched_by(self, other):
if not isinstance(other, NationStatesUser):
return False
return other.username.lower().strip().replace(' ', '_') == self.username.lower().strip().replace(' ', '_')

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017 RunasSudo (Yingtong Li) # Copyright © 2017-2021 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -259,7 +259,8 @@ class PedersenVSSPrivateKey(EmbeddedObject):
def get_modified_secret(self): def get_modified_secret(self):
mod_s = self.x mod_s = self.x
for j in range(1, threshold + 1): # 1 to threshold for j in range(1, threshold + 1): # 1 to threshold
... # TODO
pass
def decrypt(self, ciphertext): def decrypt(self, ciphertext):
if ( if (

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017 RunasSudo (Yingtong Li) # Copyright © 2017-2021 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -225,12 +225,20 @@ class InternalMixingTrustee(MixingTrustee):
return True return True
class PSRElection(Election): class PSRElection(Election):
is_voters_public = BooleanField(is_hashed=False, default=True)
is_votes_public = BooleanField(is_hashed=False, default=True)
sk = EmbeddedObjectField(SEGPrivateKey, is_protected=True) # TODO: Threshold sk = EmbeddedObjectField(SEGPrivateKey, is_protected=True) # TODO: Threshold
public_key = EmbeddedObjectField(SEGPublicKey) public_key = EmbeddedObjectField(SEGPublicKey)
mixing_trustees = EmbeddedObjectListField() mixing_trustees = EmbeddedObjectListField()
def can_audit(self):
"""Overrides Election.can_audit"""
return True
def verify(self): def verify(self):
"""Overrides Election.verify"""
# Verify ballots # Verify ballots
super().verify() super().verify()

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017 RunasSudo (Yingtong Li) # Copyright © 2017-2021 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -43,9 +43,9 @@ class BasePyTestCase(TestCase):
class BaseJSTestCase(TestCase): class BaseJSTestCase(TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
with open('eos/__javascript__/eos.js_tests.js', 'r') as f: with open('eosweb/core/static/js/eosjs.js', 'r') as f:
code = f.read() code = f.read()
cls.ctx = execjs.get().compile('var window={},navigator={};' + code + 'var test=window.eosjs_tests.' + cls.module + '.__all__.' + cls.name + '();test.setUpClass();') cls.ctx = execjs.get().compile('var window={},navigator={};' + code + 'var eosjs=require("eosjs");var test=eosjs.' + cls.module + '.' + cls.name + '();test.setUpClass();')
@classmethod @classmethod
def add_method(cls, method): def add_method(cls, method):

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-18 RunasSudo (Yingtong Li) # Copyright © 2017-2021 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -120,50 +120,6 @@ def run_tests(prefix, lang):
def sessdb(): def sessdb():
app.session_interface.db.create_all() app.session_interface.db.create_all()
# TODO: Will remove this once we have a web UI
@app.cli.command('drop_db_and_setup')
def setup_test_election():
# DANGER!
dbinfo.provider.reset_db()
# Set up election
election = PSRElection()
election.workflow = PSRWorkflow()
# Set election details
election.name = 'Test Election'
from eos.redditauth.election import RedditUser
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')))
election.voters.append(UserVoter(user=RedditUser(username='RunasSudo')))
for voter in election.voters:
if isinstance(voter, UserVoter):
if isinstance(voter.user, EmailUser):
emails.voter_email_password(election, voter)
election.mixing_trustees.append(InternalMixingTrustee(name='Eos Voting'))
election.mixing_trustees.append(InternalMixingTrustee(name='Eos Voting'))
election.sk = EGPrivateKey.generate()
election.public_key = election.sk.public_key
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')
], min_choices=0, max_choices=3, randomise_choices=True)
election.questions.append(question)
question = ApprovalQuestion(prompt='Chairman', choices=[Choice(name='John Doe'), Choice(name='Andrew Citizen')], min_choices=0, max_choices=1)
election.questions.append(question)
election.save()
@app.cli.command('verify_election') @app.cli.command('verify_election')
@click.option('--electionid', default=None) @click.option('--electionid', default=None)
def verify_election(electionid): def verify_election(electionid):
@ -196,6 +152,20 @@ def tally_stv_election(electionid, qnum, randfile, numseats):
task.save() task.save()
task.run() task.run()
@app.cli.command('run_task')
@click.option('--electionid', default=None)
@click.option('--task_name', default=None)
def tally_stv_election(electionid, task_name):
election = Election.get_by_id(electionid)
task = WorkflowTaskEntryWebTask(
election_id=election._id,
workflow_task=task_name,
status=TaskStatus.READY,
run_strategy=EosObject.lookup(app.config['TASK_RUN_STRATEGY'])()
)
task.save()
task.run()
@app.context_processor @app.context_processor
def inject_globals(): def inject_globals():
return {'eos': eos, 'eosweb': eosweb, 'SHA256': eos.core.hashing.SHA256} return {'eos': eos, 'eosweb': eosweb, 'SHA256': eos.core.hashing.SHA256}
@ -213,29 +183,6 @@ def tick_scheduler():
# === Views === # === Views ===
@app.route('/')
def index():
elections = Election.get_all()
elections.sort(key=lambda e: e.name)
elections_open = [e for e in elections if e.workflow.get_task('eos.base.workflow.TaskCloseVoting').status == WorkflowTaskStatus.READY]
elections_soon = [e for e in elections if e.workflow.get_task('eos.base.workflow.TaskOpenVoting').status != WorkflowTaskStatus.EXITED and e.workflow.get_task('eos.base.workflow.TaskOpenVoting').get_entry_task().run_at]
elections_soon.sort(key=lambda e: e.workflow.get_task('eos.base.workflow.TaskOpenVoting').get_entry_task().run_at)
elections_closed = [e for e in elections if e.workflow.get_task('eos.base.workflow.TaskCloseVoting').status == WorkflowTaskStatus.EXITED]
elections_closed.sort(key=lambda e: e.workflow.get_task('eos.base.workflow.TaskCloseVoting').exited_at, reverse=True)
elections_closed = elections_closed[:5]
return flask.render_template('index.html', elections_open=elections_open, elections_soon=elections_soon, elections_closed=elections_closed)
@app.route('/elections')
def elections():
elections = Election.get_all()
elections.sort(key=lambda e: e.name)
return flask.render_template('elections.html', elections=elections)
def using_election(func): def using_election(func):
@functools.wraps(func) @functools.wraps(func)
def wrapped(election_id, **kwargs): def wrapped(election_id, **kwargs):
@ -252,11 +199,76 @@ def election_admin(func):
return flask.Response('Administrator credentials required', 403) return flask.Response('Administrator credentials required', 403)
return wrapped return wrapped
@app.route('/')
def index():
elections = Election.get_all()
elections.sort(key=lambda e: e.name)
elections_open = [e for e in elections if e.workflow.get_task('eos.base.workflow.TaskCloseVoting').status == WorkflowTaskStatus.READY]
elections_soon = [e for e in elections if e.workflow.get_task('eos.base.workflow.TaskOpenVoting').status != WorkflowTaskStatus.EXITED and e.workflow.get_task('eos.base.workflow.TaskOpenVoting').get_entry_task()]
elections_soon.sort(key=lambda e: e.workflow.get_task('eos.base.workflow.TaskOpenVoting').get_entry_task().run_at)
elections_closed = [e for e in elections if e.workflow.get_task('eos.base.workflow.TaskCloseVoting').status == WorkflowTaskStatus.EXITED]
elections_closed.sort(key=lambda e: e.workflow.get_task('eos.base.workflow.TaskCloseVoting').exited_at, reverse=True)
elections_closed = elections_closed[:5]
return flask.render_template('index.html', elections_open=elections_open, elections_soon=elections_soon, elections_closed=elections_closed)
@app.route('/elections')
def elections():
elections = Election.get_all()
elections.sort(key=lambda e: e.name)
return flask.render_template('elections.html', elections=elections)
@app.route('/elections/batch', methods=['GET', 'POST'])
@election_admin
def elections_batch():
if flask.request.method == 'POST':
# Execute
for k, v in flask.request.form.items():
if k.startswith('election_') and v:
election_id = k[9:]
election = Election.get_by_id(election_id)
for workflow_task in election.workflow.tasks:
if workflow_task.status == eos.base.workflow.WorkflowTaskStatus.READY:
task = WorkflowTaskEntryWebTask(
election_id=election._id,
workflow_task=workflow_task._name,
status=TaskStatus.READY,
run_strategy=EosObject.lookup(app.config['TASK_RUN_STRATEGY'])()
)
task.run()
break
elections = []
for election in Election.get_all():
if any(workflow_task.status == eos.base.workflow.WorkflowTaskStatus.READY for workflow_task in election.workflow.tasks):
elections.append(election)
elections.sort(key=lambda e: e.name)
return flask.render_template('elections_batch.html', elections=elections)
@app.route('/election/<election_id>/') @app.route('/election/<election_id>/')
@using_election @using_election
def election_api_json(election): def election_api_json(election):
is_full = 'full' in flask.request.args is_full = 'full' in flask.request.args
return flask.Response(EosObject.to_json(EosObject.serialise_and_wrap(election, None, SerialiseOptions(should_protect=True, for_hash=(not is_full), combine_related=True))), mimetype='application/json')
serialised = EosObject.serialise_and_wrap(election, None, SerialiseOptions(should_protect=True, for_hash=(not is_full), combine_related=True))
# Protect voters, votes if required
if not election.is_voters_public:
if 'voters' in serialised['value']:
del serialised['value']['voters']
if not election.is_votes_public:
if 'voters' in serialised['value']:
for voter in serialised['value']['voters']:
if 'votes' in voter['value']:
del voter['value']['votes']
return flask.Response(EosObject.to_json(serialised), mimetype='application/json')
@app.route('/election/<election_id>/view') @app.route('/election/<election_id>/view')
@using_election @using_election
@ -279,14 +291,20 @@ def election_view_questions(election):
@app.route('/election/<election_id>/view/ballots') @app.route('/election/<election_id>/view/ballots')
@using_election @using_election
def election_view_ballots(election): def election_view_ballots(election):
return flask.render_template('election/view/ballots.html', election=election) if election.is_voters_public or ('user' in flask.session and flask.session['user'].is_admin()):
return flask.render_template('election/view/ballots.html', election=election)
return flask.Response('Voters not public', 403)
@app.route('/election/<election_id>/voter/<voter_id>') @app.route('/election/<election_id>/voter/<voter_id>')
@using_election @using_election
def election_voter_view(election, voter_id): def election_voter_view(election, voter_id):
voter_id = uuid.UUID(voter_id) if (election.is_voters_public and election.is_votes_public) or ('user' in flask.session and flask.session['user'].is_admin()):
voter = next(voter for voter in election.voters if voter._id == voter_id) voter_id = uuid.UUID(voter_id)
return flask.render_template('election/voter/view.html', election=election, voter=voter) voter = next(voter for voter in election.voters if voter._id == voter_id)
return flask.render_template('election/voter/view.html', election=election, voter=voter)
return flask.Response('Voters not public', 403)
@app.route('/election/<election_id>/view/trustees') @app.route('/election/<election_id>/view/trustees')
@using_election @using_election
@ -448,13 +466,21 @@ def email_login():
def email_authenticate(): def email_authenticate():
user = None user = None
for election in Election.get_all(): for u in app.config['ADMINS']:
for voter in election.voters: if isinstance(u, EmailUser):
if isinstance(voter.user, EmailUser): if u.email.lower() == flask.request.form['email'].lower():
if voter.user.email.lower() == flask.request.form['email'].lower(): if u.password == flask.request.form['password']:
if voter.user.password == flask.request.form['password']: user = u
user = voter.user break
break
if user is None:
for election in Election.get_all():
for voter in election.voters:
if isinstance(voter.user, EmailUser):
if voter.user.email.lower() == flask.request.form['email'].lower():
if voter.user.password == flask.request.form['password']:
user = voter.user
break
if user is None: 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.') 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.')

View File

@ -1,6 +1,6 @@
/* /*
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017 RunasSudo (Yingtong Li) Copyright © 2017-2021 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -19,18 +19,37 @@
window = self; // Workaround for libraries window = self; // Workaround for libraries
isLibrariesLoaded = false; isLibrariesLoaded = false;
function generateEncryptedVote(election, answers, should_do_fingerprint) { eosjs = null;
encrypted_answers = [];
for (var q_num = 0; q_num < answers.length; q_num++) {
answer_json = answers[q_num];
answer = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(answer_json, null);
encrypted_answer = eosjs.eos.psr.election.__all__.BlockEncryptedAnswer.encrypt(election.public_key, answer, election.questions.__getitem__(q_num).max_bits() + 32); // +32 bits for the length
encrypted_answers.push(eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(encrypted_answer, null));
}
postMessage({ function generateEncryptedVote(election, answers, should_do_fingerprint) {
encrypted_answers: encrypted_answers if (election._name === 'eos.psr.election.PSRElection') {
}); encrypted_answers = [];
for (var q_num = 0; q_num < answers.length; q_num++) {
answer_json = answers[q_num];
answer = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(answer_json, null);
encrypted_answer = eosjs.eos.psr.election.BlockEncryptedAnswer.encrypt(election.public_key, answer, election.questions.__getitem__(q_num).max_bits() + 32); // +32 bits for the length
encrypted_answers.push(eosjs.eos.core.objects.EosObject.serialise_and_wrap(encrypted_answer, null));
}
postMessage({
encrypted_answers: encrypted_answers
});
} else if (election._name === 'eos.base.election.Election') {
encrypted_answers = [];
for (var q_num = 0; q_num < answers.length; q_num++) {
answer_json = answers[q_num];
answer = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(answer_json, null);
encrypted_answer = eosjs.eos.base.election.NullEncryptedAnswer();
encrypted_answer.answer = answer;
encrypted_answers.push(eosjs.eos.core.objects.EosObject.serialise_and_wrap(encrypted_answer, null));
}
postMessage({
encrypted_answers: encrypted_answers
});
} else {
throw "Don't know how to encrypt ballots in election of type " + election._name;
}
} }
onmessage = function(msg) { onmessage = function(msg) {
@ -40,10 +59,11 @@ onmessage = function(msg) {
msg.data.static_base_url + "js/eosjs.js" msg.data.static_base_url + "js/eosjs.js"
); );
isLibrariesLoaded = true; isLibrariesLoaded = true;
eosjs = require("eosjs");
} }
if (msg.data.action === "generateEncryptedVote") { if (msg.data.action === "generateEncryptedVote") {
msg.data.election = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(msg.data.election, null); msg.data.election = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(msg.data.election, null);
generateEncryptedVote(msg.data.election, msg.data.answers); generateEncryptedVote(msg.data.election, msg.data.answers);
} else { } else {

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017-2019 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -25,7 +25,7 @@
<p>Your vote has <span class="superem">not</span> yet been cast. Please follow the instructions to continue.</p> <p>Your vote has <span class="superem">not</span> yet been cast. Please follow the instructions to continue.</p>
</div> </div>
<p>The following is your ballot with fingerprint <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64() }}</span>, decrypted and ready for auditing.</p> <p>The following is your ballot with fingerprint <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(ballot).hash_as_b64() }}</span>, decrypted and ready for auditing.</p>
<div class="ui form"> <div class="ui form">
{# For some reason nunjucks doesn't like calling this the normal way #} {# For some reason nunjucks doesn't like calling this the normal way #}

View File

@ -1,6 +1,6 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017-2021 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -18,7 +18,7 @@
<h1>{{ election.name }}</h1> <h1>{{ election.name }}</h1>
<p><small><b>{{ election.kind|title }} fingerprint:</b> <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(election).hash_as_b64() }}</span></small></p> <p><small><b>{{ election.kind|title }} fingerprint:</b> <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(election).hash_as_b64() }}</span></small></p>
{# Convert the template name to a numerical index for comparison #} {# Convert the template name to a numerical index for comparison #}
{% if template == 'booth/welcome.html' %} {% if template == 'booth/welcome.html' %}
@ -32,9 +32,17 @@
{% elif template == 'booth/audit.html' %} {% elif template == 'booth/audit.html' %}
{% set menuindex = 4 %} {% set menuindex = 4 %}
{% elif template == 'booth/cast.html' %} {% elif template == 'booth/cast.html' %}
{% set menuindex = 5 %} {% if election.can_audit() %}
{% set menuindex = 5 %}
{% else %}
{% set menuindex = 4 %}
{% endif %}
{% elif template == 'booth/complete.html' %} {% elif template == 'booth/complete.html' %}
{% set menuindex = 6 %} {% if election.can_audit() %}
{% set menuindex = 6 %}
{% else %}
{% set menuindex = 5 %}
{% endif %}
{% endif %} {% endif %}
{% macro menuitem(index, text) %} {% macro menuitem(index, text) %}
@ -50,9 +58,14 @@
{{ menuitem(1, "Welcome") }} {{ menuitem(1, "Welcome") }}
{{ menuitem(2, "Select") }} {{ menuitem(2, "Select") }}
{{ menuitem(3, "Review") }} {{ menuitem(3, "Review") }}
{{ menuitem(4, "Audit") }} {% if election.can_audit() %}
{{ menuitem(5, "Cast") }} {{ menuitem(4, "Audit") }}
{{ menuitem(6, "Finish") }} {{ menuitem(5, "Cast") }}
{{ menuitem(6, "Finish") }}
{% else %}
{{ menuitem(4, "Cast") }}
{{ menuitem(5, "Finish") }}
{% endif %}
</ul> </ul>
<div class="ui container"> <div class="ui container">

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017-2021 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -20,7 +20,7 @@
{% block content %} {% block content %}
<div id="cast_prompt"> <div id="cast_prompt">
<p>Your vote has <span class="superem">not</span> yet been cast. Please make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64(true) }}</span>.</p> <p>Your vote has <span class="superem">not</span> yet been cast.{% if election.can_audit() %} Please make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(ballot).hash_as_b64(true) }}</span>.{% endif %}</p>
<div class="ui negative message"> <div class="ui negative message">
<p>Your vote has <span class="superem">not</span> yet been cast. Please follow the instructions to continue.</p> <p>Your vote has <span class="superem">not</span> yet been cast. Please follow the instructions to continue.</p>
@ -69,10 +69,12 @@
{% endblock %} {% endblock %}
{% block after %} {% block after %}
<div class="ui tiny message" style="margin-top: 3em;"> {% if election.can_audit() %}
<div class="header">Information for advanced users</div> <div class="ui tiny message" style="margin-top: 3em;">
<p>Your full ballot fingerprint is <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p> <div class="header">Information for advanced users</div>
</div> <p>Your full ballot fingerprint is <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p>
</div>
{% endif %}
<script> <script>
$(".message .close").on("click", function() { $(".message .close").on("click", function() {
@ -104,8 +106,8 @@
$.ajax({ $.ajax({
url: "{{ election_base_url }}stage_ballot", url: "{{ election_base_url }}stage_ballot",
type: "POST", type: "POST",
data: eosjs.eos.core.objects.__all__.EosObject.to_json({ data: eosjs.eos.core.objects.EosObject.to_json({
"ballot": eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(deauditedBallot, null), "ballot": eosjs.eos.core.objects.EosObject.serialise_and_wrap(deauditedBallot, null),
"fingerprint": booth.fingerprint || null "fingerprint": booth.fingerprint || null
}), }),
contentType: "application/json", contentType: "application/json",
@ -167,9 +169,9 @@
dataType: "text" dataType: "text"
}) })
.done(function(data) { .done(function(data) {
response = eosjs.eos.core.objects.__all__.EosObject.from_json(data); response = eosjs.eos.core.objects.EosObject.from_json(data);
booth.voter = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(response.voter); booth.voter = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(response.voter);
booth.vote = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(response.vote); booth.vote = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(response.vote);
// Clear plaintexts // Clear plaintexts
booth.answers = null; booth.answers = null;

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017-2021 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -23,7 +23,9 @@
<p>Your vote has <span class="superem">not</span> yet been cast. Please follow the instructions to continue.</p> <p>Your vote has <span class="superem">not</span> yet been cast. Please follow the instructions to continue.</p>
</div> </div>
<p>Please make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64(true) }}</span>. Please retain a copy of your ballot fingerprint – you can use it to verify that your vote has been counted correctly. You may <a href="#" onclick="window.print();return false;">print this page</a> as a receipt if you wish.</p> {% if election.can_audit() %}
<p>Please make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(ballot).hash_as_b64(true) }}</span>. Please retain a copy of your ballot fingerprint – you can use it to verify that your vote has been counted correctly. You may <a href="#" onclick="window.print();return false;">print this page</a> as a receipt if you wish.</p>
{% endif %}
<p>To continue, copy and paste the ballot below and provide it to the election administrator.</p> <p>To continue, copy and paste the ballot below and provide it to the election administrator.</p>
@ -39,10 +41,12 @@
{% endblock %} {% endblock %}
{% block after %} {% block after %}
<div class="ui tiny message" style="margin-top: 3em;"> {% if election.can_audit() %}
<div class="header">Information for advanced users</div> <div class="ui tiny message" style="margin-top: 3em;">
<p>Your full ballot fingerprint is <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p> <div class="header">Information for advanced users</div>
</div> <p>Your full ballot fingerprint is <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p>
</div>
{% endif %}
{% endblock %} {% endblock %}
{% block help %} {% block help %}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017-2021 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -26,24 +26,33 @@
<div class="content"> <div class="content">
<div class="header">Smart ballot tracker</div> <div class="header">Smart ballot tracker</div>
<p>This smart ballot tracker confirms that {{ voter.py_name }} cast a vote in the election {{ election.py_name }} at {{ vote.cast_at }}.</p> <p>This smart ballot tracker confirms that {{ voter.py_name }} cast a vote in the election {{ election.py_name }} at {{ vote.cast_at }}.</p>
<p>Ballot fingerprint: <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(vote.ballot).hash_as_b64(true) }}</span></p> {% if election.can_audit() %}
<p>Ballot fingerprint: <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(vote.ballot).hash_as_b64(true) }}</span></p>
{% endif %}
</div> </div>
</div> </div>
<p>Please check that the ballot fingerprint above matches the ballot fingerprint you recorded earlier.</p> {% if election.can_audit() %}
<p>Please check that the ballot fingerprint above matches the ballot fingerprint you recorded earlier.</p>
<p>To confirm that your ballot was cast correctly, please go to the <a href="{{ election_base_url }}view/ballots">‘Voters and ballots’ page</a> for the {{ election.kind }} or click ‘Finish’, and confirm that the above ballot fingerprint appears next to your name.</p> <p>To confirm that your ballot was cast correctly, please go to the <a href="{{ election_base_url }}view/ballots">‘Voters and ballots’ page</a> for the {{ election.kind }} or click ‘Finish’, and confirm that the above ballot fingerprint appears next to your name.</p>
{% endif %}
{% endblock %} {% endblock %}
{% block buttons %} {% block buttons %}
<a href="{{ election_base_url }}view/ballots" class="ui right floated primary button">Finish</a> {% if election.is_votes_public %}
<a href="{{ election_base_url }}view/ballots" class="ui right floated primary button">Finish</a>
{% else %}
<a href="{{ election_base_url }}view" class="ui right floated primary button">Finish</a>
{% endif %}
{% endblock %} {% endblock %}
{% block after %} {% block after %}
<div class="ui tiny message" style="margin-top: 3em;"> {% if election.can_audit() %}
<div class="header">Information for advanced users</div> <div class="ui tiny message" style="margin-top: 3em;">
<p>Your full ballot fingerprint is <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(vote.ballot).hash_as_b64() }}</span>.</p> <div class="header">Information for advanced users</div>
</div> <p>Your full ballot fingerprint is <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(vote.ballot).hash_as_b64() }}</span>.</p>
</div>
{% endif %}
{% endblock %} {% endblock %}
{% block help %} {% block help %}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017-2019 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -30,19 +30,19 @@
try { try {
rawAnswers = []; rawAnswers = [];
for (var answer_json of booth.answers) { for (var answer_json of booth.answers) {
rawAnswers.push(eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(answer_json, null)); rawAnswers.push(eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(answer_json, null));
} }
encryptedAnswers = []; encryptedAnswers = [];
for (var encrypted_answer_json of msg.data.encrypted_answers) { for (var encrypted_answer_json of msg.data.encrypted_answers) {
encryptedAnswers.push(eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(encrypted_answer_json, null)); encryptedAnswers.push(eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(encrypted_answer_json, null));
} }
booth.ballot = eosjs.eos.base.election.__all__.Ballot(); booth.ballot = eosjs.eos.base.election.Ballot();
booth.ballot.answers = rawAnswers; booth.ballot.answers = rawAnswers;
booth.ballot.encrypted_answers = encryptedAnswers; booth.ballot.encrypted_answers = encryptedAnswers;
booth.ballot.election_id = election._id; booth.ballot.election_id = election._id;
booth.ballot.election_hash = eosjs.eos.core.hashing.__all__.SHA256().update_obj(election).hash_as_b64(); booth.ballot.election_hash = eosjs.eos.core.hashing.SHA256().update_obj(election).hash_as_b64();
if (should_do_fingerprint) { if (should_do_fingerprint) {
// String.prototype.join confuses fingerprintjs2 // String.prototype.join confuses fingerprintjs2
@ -69,7 +69,7 @@
boothWorker.postMessage({ boothWorker.postMessage({
"action": "generateEncryptedVote", "action": "generateEncryptedVote",
"static_base_url": "{{ static_base_url }}", "static_base_url": "{{ static_base_url }}",
"election": eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(election, null), "election": eosjs.eos.core.objects.EosObject.serialise_and_wrap(election, null),
"answers": booth.answers "answers": booth.answers
}); });
} catch (err) { } catch (err) {

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017-2021 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -30,8 +30,12 @@
{% include templates[selection_model_view_map[election.questions.__getitem__(loop.index0)._name]["selections_review"]] %} {% include templates[selection_model_view_map[election.questions.__getitem__(loop.index0)._name]["selections_review"]] %}
{% endfor %} {% endfor %}
<p>If you are happy with your selections, then make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64(true) }}</span>.</p> {% if election.can_audit() %}
<p>Click ‘Continue’, and you will be able to log in to cast your vote.</p> <p>If you are happy with your selections, then make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(ballot).hash_as_b64(true) }}</span>.</p>
<p>Click ‘Continue’, and you will be able to log in to cast your vote.</p>
{% else %}
<p>If you are happy with your selections, then click ‘Continue’, and you will be able to log in to cast your vote.</p>
{% endif %}
{% endblock %} {% endblock %}
{% block buttons %} {% block buttons %}
@ -40,11 +44,13 @@
{% endblock %} {% endblock %}
{% block after %} {% block after %}
<div class="ui tiny message" style="margin-top: 3em;"> {% if election.can_audit() %}
<div class="header">Information for advanced users</div> <div class="ui tiny message" style="margin-top: 3em;">
<p>Your ballot fingerprint is <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p> <div class="header">Information for advanced users</div>
<p>If you would like to audit your ballot, <a href="#" onclick="nextTemplate(1);">click here</a></p> <p>Your ballot fingerprint is <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p>
</div> <p>If you would like to audit your ballot, <a href="#" onclick="nextTemplate(1);">click here</a></p>
</div>
{% endif %}
{% endblock %} {% endblock %}
{% block help %} {% block help %}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017-2021 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -30,8 +30,12 @@
{% include templates[selection_model_view_map[election.questions.__getitem__(loop.index0)._name]["selections_review"]] %} {% include templates[selection_model_view_map[election.questions.__getitem__(loop.index0)._name]["selections_review"]] %}
{% endfor %} {% endfor %}
<p>If you are happy with your selections, then make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64(true) }}</span>.</p> {% if election.can_audit() %}
<p>Click ‘Continue’, and you will be able to copy your pre-poll ballot to provide to the election administrator.</p> <p>If you are happy with your selections, then make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(ballot).hash_as_b64(true) }}</span>.</p>
<p>Click ‘Continue’, and you will be able to copy your pre-poll ballot to provide to the election administrator.</p>
{% else %}
<p>If you are happy with your selections, then click ‘Continue’, and you will be able to copy your pre-poll ballot to provide to the election administrator.</p>
{% endif %}
{% endblock %} {% endblock %}
{% block buttons %} {% block buttons %}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017 RunasSudo (Yingtong Li) Copyright © 2017-2019 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017-2019 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by

View File

@ -1,6 +1,6 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017-2019 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -18,6 +18,10 @@
<h2>{{ questionNum + 1 }}. {{ election.questions.__getitem__(questionNum).prompt }}</h2> <h2>{{ questionNum + 1 }}. {{ election.questions.__getitem__(questionNum).prompt }}</h2>
{% if election.questions.__getitem__(questionNum).description %}
<p>{{ election.questions.__getitem__(questionNum).description | urlize | replace('<a ', '<a target="_blank" ') | safe }}</p>
{% endif %}
<p><small> <p><small>
Vote for Vote for
{% if election.questions.__getitem__(questionNum).min_choices == election.questions.__getitem__(questionNum).max_choices %} {% if election.questions.__getitem__(questionNum).min_choices == election.questions.__getitem__(questionNum).max_choices %}
@ -107,8 +111,8 @@
} }
} }
answer = eosjs.eos.base.election.__all__.ApprovalAnswer(eosjs.__kwargtrans__({choices: selections})); answer = eosjs.eos.base.election.ApprovalAnswer(eosjs.__kwargtrans__({choices: selections}));
booth.answers[booth.questionNum] = eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(answer); booth.answers[booth.questionNum] = eosjs.eos.core.objects.EosObject.serialise_and_wrap(answer);
return true; return true;
} }

View File

@ -1,6 +1,6 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017-2019 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -18,6 +18,10 @@
<h2>{{ questionNum + 1 }}. {{ election.questions.__getitem__(questionNum).prompt }}</h2> <h2>{{ questionNum + 1 }}. {{ election.questions.__getitem__(questionNum).prompt }}</h2>
{% if election.questions.__getitem__(questionNum).description %}
<p>{{ election.questions.__getitem__(questionNum).description | urlize | replace('<a ', '<a target="_blank" ') | safe }}</p>
{% endif %}
<p><small> <p><small>
Vote for Vote for
{% if election.questions.__getitem__(questionNum).min_choices == election.questions.__getitem__(questionNum).max_choices %} {% if election.questions.__getitem__(questionNum).min_choices == election.questions.__getitem__(questionNum).max_choices %}
@ -249,8 +253,8 @@
} }
} }
answer = eosjs.eos.base.election.__all__.PreferentialAnswer(eosjs.__kwargtrans__({choices: selections})); answer = eosjs.eos.base.election.PreferentialAnswer(eosjs.__kwargtrans__({choices: selections}));
booth.answers[booth.questionNum] = eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(answer); booth.answers[booth.questionNum] = eosjs.eos.core.objects.EosObject.serialise_and_wrap(answer);
booth.q_state[booth.questionNum] = [$("#question-choices-selected .dragarea").html(), $("#question-choices-remaining .dragarea").html()]; // wew lad booth.q_state[booth.questionNum] = [$("#question-choices-selected .dragarea").html(), $("#question-choices-remaining .dragarea").html()]; // wew lad

View File

@ -1,6 +1,6 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017 RunasSudo (Yingtong Li) Copyright © 2017-2021 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -19,5 +19,11 @@
{% block tabs %} {% block tabs %}
{{ tab('Overview', 'election_view') }} {{ tab('Overview', 'election_view') }}
{{ tab('Questions', 'election_view_questions') }} {{ tab('Questions', 'election_view_questions') }}
{{ tab('Voters and ballots', 'election_view_ballots') }} {% if election.is_voters_public or (session.user and session.user.is_admin()) %}
{% if election.is_votes_public or (session.user and session.user.is_admin()) %}
{{ tab('Voters and ballots', 'election_view_ballots') }}
{% else %}
{{ tab('Voters', 'election_view_ballots') }}
{% endif %}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017-2021 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -23,21 +23,27 @@
<thead> <thead>
<tr> <tr>
<th>Voter</th> <th>Voter</th>
<th>Ballot fingerprint</th> {% if election.is_votes_public or (session.user and session.user.is_admin()) %}
<th>Ballot fingerprint</th>
{% endif %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for voter in election.voters %} {% for voter in election.voters %}
<tr> <tr>
<td class="selectable"><a href="{{ url_for('election_voter_view', election_id=election._id, voter_id=voter._id) }}">{{ voter.name }}</a></td> {% if election.is_votes_public or (session.user and session.user.is_admin()) %}
{% set votes = voter.votes.get_all() %} <td class="selectable"><a href="{{ url_for('election_voter_view', election_id=election._id, voter_id=voter._id) }}">{{ voter.name }}</a></td>
<td class="selectable"><a href="{{ url_for('election_voter_view', election_id=election._id, voter_id=voter._id) }}"> {% set votes = voter.votes.get_all() %}
{% if votes|length > 0 %} <td class="selectable"><a href="{{ url_for('election_voter_view', election_id=election._id, voter_id=voter._id) }}">
<span class="hash">{{ SHA256().update_obj(votes[-1].ballot).hash_as_b64(True) }}</span> {% if votes|length > 0 %}
{% else %} <span class="hash">{{ SHA256().update_obj(votes[-1].ballot).hash_as_b64(True) }}</span>
&nbsp; {% else %}
{% endif %} &nbsp;
</a></td> {% endif %}
</a></td>
{% else %}
<td>{{ voter.name }}</td>
{% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017-2021 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -44,6 +44,8 @@
{% endif %} {% endif %}
<script> <script>
var eosjs = require("eosjs");
var templates = {}; var templates = {};
var election = null; var election = null;
var booth = null; var booth = null;
@ -70,7 +72,7 @@
// Verify booth // Verify booth
if (should_do_fingerprint) { if (should_do_fingerprint) {
if (typeof Fingerprint2 === 'undefined') { if (typeof Fingerprint2 === 'undefined') {
boothError('Your browser did not load fingerprintj2 correctly. Please try again after disabling your ad blockers and similar software. If the issue persists, try using a different browser.'); boothError('Your browser did not load fingerprintjs2 correctly. Please try again after disabling your ad blockers and similar software. If the issue persists, try using a different browser.');
return; return;
} }
} }
@ -78,7 +80,7 @@
$.ajax({ url: "{{ url_for('election_api_json', election_id=election._id) }}", dataType: "text" }) $.ajax({ url: "{{ url_for('election_api_json', election_id=election._id) }}", dataType: "text" })
.done(function(data) { .done(function(data) {
try { try {
election = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(eosjs.eos.core.objects.__all__.EosObject.from_json(data), null); election = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(eosjs.eos.core.objects.EosObject.from_json(data), null);
boothWorker = new Worker("{{ url_for('static', filename='js/booth_worker.js') }}"); boothWorker = new Worker("{{ url_for('static', filename='js/booth_worker.js') }}");
@ -117,7 +119,7 @@
}) })
.done(function(data) { .done(function(data) {
try { try {
templates[templateUrl] = nunjucks.compile(data); templates[templateUrl] = nunjucks.compile(data, null, templateUrl);
numTemplatesLoaded += 1; numTemplatesLoaded += 1;
if (numTemplatesLoaded == templateUrls.length) { if (numTemplatesLoaded == templateUrls.length) {
// All templates loaded. Show voting booth // All templates loaded. Show voting booth
@ -210,12 +212,14 @@
} }
}); });
templates['booth/welcome.html'] = null; templates['booth/welcome.html'] = null;
boothTasks.append({ boothTasks.append({
activate: function(fromLeft) { activate: function(fromLeft) {
showTemplate('booth/selections.html'); showTemplate('booth/selections.html');
} }
}); });
templates['booth/selections.html'] = null; templates['booth/selections.html'] = null;
boothTasks.append({ boothTasks.append({
activate: function(fromLeft) { activate: function(fromLeft) {
if (fromLeft) { if (fromLeft) {
@ -235,12 +239,14 @@
} }
}); });
templates['booth/review_prepoll.html'] = null; templates['booth/review_prepoll.html'] = null;
boothTasks.append({ boothTasks.append({
activate: function(fromLeft) { activate: function(fromLeft) {
showTemplate('booth/audit.html', {ballot: booth.ballot}); showTemplate('booth/audit.html', {ballot: booth.ballot});
} }
}); });
templates['booth/audit.html'] = null; templates['booth/audit.html'] = null;
boothTasks.append({ boothTasks.append({
activate: function(fromLeft) { activate: function(fromLeft) {
showTemplate('booth/cast_prepoll.html', {ballot: booth.ballot}); showTemplate('booth/cast_prepoll.html', {ballot: booth.ballot});
@ -255,18 +261,21 @@
} }
}); });
templates['booth/review.html'] = null; templates['booth/review.html'] = null;
boothTasks.append({ boothTasks.append({
activate: function(fromLeft) { activate: function(fromLeft) {
showTemplate('booth/audit.html', {ballot: booth.ballot}); showTemplate('booth/audit.html', {ballot: booth.ballot});
} }
}); });
templates['booth/audit.html'] = null; templates['booth/audit.html'] = null;
boothTasks.append({ boothTasks.append({
activate: function(fromLeft) { activate: function(fromLeft) {
showTemplate('booth/cast.html', {ballot: booth.ballot, is_cast: false}); showTemplate('booth/cast.html', {ballot: booth.ballot, is_cast: false});
} }
}); });
templates['booth/cast.html'] = null; templates['booth/cast.html'] = null;
boothTasks.append({ boothTasks.append({
activate: function(fromLeft) { activate: function(fromLeft) {
showTemplate('booth/complete.html', {voter: booth.voter, vote: booth.vote}); showTemplate('booth/complete.html', {voter: booth.voter, vote: booth.vote});
@ -277,7 +286,7 @@
} else { } else {
// Cast immediately // Cast immediately
{% if session.staged_ballot %} {% if session.staged_ballot %}
booth.ballot = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(eosjs.eos.core.objects.__all__.EosObject.from_json('{{ eos.core.objects.EosObject.to_json(session.staged_ballot.ballot)|safe }}'), null); booth.ballot = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(eosjs.eos.core.objects.EosObject.from_json('{{ eos.core.objects.EosObject.to_json(session.staged_ballot.ballot)|safe }}'), null);
{% endif %} {% endif %}
boothTasks.append({ boothTasks.append({
activate: function(fromLeft) { activate: function(fromLeft) {
@ -285,6 +294,7 @@
} }
}); });
templates['booth/cast.html'] = null; templates['booth/cast.html'] = null;
boothTasks.append({ boothTasks.append({
activate: function(fromLeft) { activate: function(fromLeft) {
showTemplate('booth/complete.html', {voter: booth.voter, vote: booth.vote}); showTemplate('booth/complete.html', {voter: booth.voter, vote: booth.vote});

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017 RunasSudo (Yingtong Li) Copyright © 2017-2019 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -21,6 +21,9 @@
{% block electioncontent %} {% block electioncontent %}
{% for question in election.questions %} {% for question in election.questions %}
<h2>{{ loop.index }}. {{ question.prompt }}</h2> <h2>{{ loop.index }}. {{ question.prompt }}</h2>
{% if question.description %}
<p>{{ question.description | urlize }}</p>
{% endif %}
{% include eosweb.core.main.model_view_map[question.__class__]['view'] %} {% include eosweb.core.main.model_view_map[question.__class__]['view'] %}
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017-2019 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -23,6 +23,10 @@
{% block content %} {% block content %}
<h1>All elections: Eos Voting for {{ eosweb.app.config['ORG_NAME'] }}</h1> <h1>All elections: Eos Voting for {{ eosweb.app.config['ORG_NAME'] }}</h1>
{% if session.user and session.user.is_admin() %}
<div style="text-align: right; font-size: small;"><a href="{{ url_for('elections_batch') }}">Batch operations</a></div>
{% endif %}
<p>Please choose an election from the list below:</p> <p>Please choose an election from the list below:</p>
<ul> <ul>

View File

@ -0,0 +1,56 @@
{% extends 'base.html' %}
{#
Eos - Verifiable elections
Copyright © 2017-2019 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/>.
#}
{% block title %}Perform batch operations{% endblock %}
{% block content %}
<h1>Perform batch operations</h1>
<form method="POST">
<table class="ui selectable celled table">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Next stage</th>
</tr>
</thead>
<tbody>
{% for election in elections %}
<tr>
<td><input type="checkbox" name="election_{{ election._id }}" id="election_{{ election._id }}"></td>
<td>{{ election.name }}</td>
<td>
<ul style="padding-left: 1em; margin: 0;">
{% for task in election.workflow.tasks %}
{% if task.status == eos.base.workflow.WorkflowTaskStatus.READY %}
<li>{{ task.label }}</li>
{% endif %}
{% endfor %}
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<input class="ui primary button" type="submit" value="Execute">
</form>
{% endblock %}

46
eosweb/nsauth/main.py Normal file
View File

@ -0,0 +1,46 @@
# Eos - Verifiable elections
# Copyright © 2017-2019 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 flask
from eos.nsauth.election import *
import urllib.request, urllib.parse
blueprint = flask.Blueprint('eosweb.nsauth', __name__, template_folder='templates')
app = None
@blueprint.record
def reddit_register(setup_state):
global app
app = setup_state.app
@blueprint.route('/auth/nationstates/login')
def nationstates_login():
return flask.render_template('auth/nationstates/login.html')
@blueprint.route('/auth/nationstates/authenticate', methods=['POST'])
def nationstates_authenticate():
username = flask.request.form['username'].lower().strip().replace(' ', '_')
with urllib.request.urlopen(urllib.request.Request('https://www.nationstates.net/cgi-bin/api.cgi?a=verify&' + urllib.parse.urlencode({'nation': username, 'checksum': flask.request.form['checksum']}), headers={'User-Agent': app.config['NATIONSTATES_USER_AGENT']})) as resp:
if resp.read().decode('utf-8').strip() != '1':
return flask.render_template('auth/nationstates/login.html', error='The nation name or verification code you entered was invalid. Please check your details and try again. If the issue persists, contact the election administrator.')
flask.session['user'] = NationStatesUser(username=username)
return flask.redirect(flask.url_for('login_complete'))

17
eosweb/nsauth/settings.py Normal file
View File

@ -0,0 +1,17 @@
# Eos - Verifiable elections
# Copyright © 2017-2019 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/>.
NATIONSTATES_USER_AGENT = 'FIXME'

View File

@ -0,0 +1,51 @@
{% extends 'semantic_base.html' %}
{#
Eos - Verifiable elections
Copyright © 2017-2019 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/>.
#}
{% block title %}Log in{% endblock %}
{% block basecontent %}
<div class="ui middle aligned center aligned grid" style="height: 100%;">
<div class="column" style="max-width: 400px;">
<p>1. Log in to NationStates below if necessary, and copy your <i>Login Verification Code</i>.</p>
<iframe src="https://m.nationstates.net/page=verify_login" style="width: 100%; height: 10em;"></iframe>
<p>2. Type your nation name and paste your Login Verification Code into the form below.</p>
<form class="ui large form" action="{{ url_for('eosweb.nsauth.nationstates_authenticate') }}" method="post">
{% if error %}
<div class="ui visible error message">{{ error }}</div>
{% endif %}
<div class="ui stacked segment">
<div class="field">
<div class="ui left icon input">
<i class="user icon"></i>
<input type="text" name="username" placeholder="Nation name">
</div>
</div>
<div class="field">
<div class="ui left icon input">
<i class="linkify icon"></i>
<input type="text" name="checksum" placeholder="Login verification code">
</div>
</div>
<input type="submit" class="ui fluid large teal submit button" value="Log in">
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -14,7 +14,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask_oauthlib.client import OAuth from authlib.integrations.flask_client import OAuth
import flask import flask
@ -27,51 +27,48 @@ blueprint = flask.Blueprint('eosweb.redditauth', __name__)
app = None app = None
oauth = None oauth = None
reddit = None
@blueprint.record @blueprint.record
def reddit_register(setup_state): def reddit_register(setup_state):
global app, oauth, reddit global app, oauth
app = setup_state.app app = setup_state.app
oauth = OAuth() oauth = OAuth(app)
reddit = oauth.remote_app('Reddit', oauth.register('reddit',
request_token_url=None, #request_token_url=None,
authorize_url='https://www.reddit.com/api/v1/authorize.compact', authorize_url='https://www.reddit.com/api/v1/authorize.compact',
request_token_params={'duration': 'temporary', 'scope': 'identity'}, authorize_params={'duration': 'temporary', 'scope': 'identity'},
access_token_url='https://www.reddit.com/api/v1/access_token', access_token_url='https://www.reddit.com/api/v1/access_token',
access_token_method='POST', access_token_method='POST',
access_token_headers={ access_token_headers={
'Authorization': 'Basic ' + base64.b64encode('{}:{}'.format(app.config['REDDIT_OAUTH_CLIENT_ID'], app.config['REDDIT_OAUTH_CLIENT_SECRET']).encode('ascii')).decode('ascii'), 'Authorization': 'Basic ' + base64.b64encode('{}:{}'.format(app.config['REDDIT_OAUTH_CLIENT_ID'], app.config['REDDIT_OAUTH_CLIENT_SECRET']).encode('ascii')).decode('ascii'),
'User-Agent': app.config['REDDIT_USER_AGENT'] 'User-Agent': app.config['REDDIT_USER_AGENT']
}, },
consumer_key=app.config['REDDIT_OAUTH_CLIENT_ID'], client_id=app.config['REDDIT_OAUTH_CLIENT_ID'],
consumer_secret=app.config['REDDIT_OAUTH_CLIENT_SECRET'] client_secret=app.config['REDDIT_OAUTH_CLIENT_SECRET'],
fetch_token=lambda: flask.session.get('user').oauth_token
) )
@reddit.tokengetter
def get_reddit_oauth_token():
return (flask.session.get('user').oauth_token, '')
@blueprint.route('/auth/reddit/login') @blueprint.route('/auth/reddit/login')
def reddit_login(): def reddit_login():
return reddit.authorize(callback=app.config['BASE_URI'] + flask.url_for('eosweb.redditauth.reddit_oauth_authorized'), state=uuid.uuid4()) return oauth.reddit.authorize_redirect(redirect_uri=app.config['BASE_URI'] + flask.url_for('eosweb.redditauth.reddit_oauth_authorized'), state=str(uuid.uuid4()))
@blueprint.route('/auth/reddit/oauth_callback') @blueprint.route('/auth/reddit/oauth_callback')
def reddit_oauth_authorized(): def reddit_oauth_authorized():
resp = reddit.authorized_response() try:
if resp is None: token = oauth.reddit.authorize_access_token()
except:
# Request denied # Request denied
return flask.redirect(flask.url_for('login_cancelled')) return flask.redirect(flask.url_for('login_cancelled'))
user = RedditUser() user = RedditUser()
user.oauth_token = resp['access_token'] user.oauth_token = token
flask.session['user'] = user flask.session['user'] = user
me = reddit.get('https://oauth.reddit.com/api/v1/me', headers={ me = oauth.reddit.get('https://oauth.reddit.com/api/v1/me', headers={
'User-Agent': app.config['REDDIT_USER_AGENT'] 'User-Agent': app.config['REDDIT_USER_AGENT']
}) })
user.username = me.data['name'] user.username = me.json()['name']
return flask.redirect(flask.url_for('login_complete')) return flask.redirect(flask.url_for('login_complete'))

View File

@ -1 +1 @@
<script src="eos/__javascript__/eos.js_tests.js"></script> <script src="eosweb/core/static/js/eosjs.js"></script>

View File

@ -9,9 +9,11 @@ AUTH_METHODS = [
('reddit', 'Reddit') ('reddit', 'Reddit')
] ]
import eos.base.election
import eos.redditauth.election import eos.redditauth.election
ADMINS = [ ADMINS = [
#eos.redditauth.election.RedditUser(username='xxxxxxxx') #eos.redditauth.election.RedditUser(username='xxxxxxxx'),
#eos.base.election.EmailUser(email='xxxxx@example.com', password='abc123'),
] ]
TASK_RUN_STRATEGY = 'eos.core.tasks.direct.DirectRunStrategy' TASK_RUN_STRATEGY = 'eos.core.tasks.direct.DirectRunStrategy'
@ -37,6 +39,8 @@ MAIL_USERNAME, MAIL_PASSWORD = None, None
MAIL_DEFAULT_SENDER = 'eos@localhost' MAIL_DEFAULT_SENDER = 'eos@localhost'
# Reddit # Reddit
# Register a web app at https://www.reddit.com/prefs/apps
# The redirect URI will be http(s)://hostname(:port)/auth/reddit/oauth_callback
REDDIT_OAUTH_CLIENT_ID = 'xxxxxxxxxxxxxx' REDDIT_OAUTH_CLIENT_ID = 'xxxxxxxxxxxxxx'
REDDIT_OAUTH_CLIENT_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxx' REDDIT_OAUTH_CLIENT_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxx'

6839
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

9
package.json Normal file
View File

@ -0,0 +1,9 @@
{
"dependencies": {
"@babel/cli": "^7.15.7",
"@babel/core": "^7.15.8",
"@babel/preset-env": "^7.15.8",
"babelify": "^10.0.0",
"browserify": "^17.0.0"
}
}

View File

@ -1,16 +1,17 @@
Authlib==0.14.3
coverage==4.4.1 coverage==4.4.1
Flask==0.12.2 Flask==0.12.2
Flask-Mail==0.9.1 Flask-Mail==0.9.1
Flask-OAuthlib==0.9.4 flask-paginate==0.7.0
Flask-Session==0.3.1 Flask-Session==0.3.1
Flask-SQLAlchemy==2.3.2 Flask-SQLAlchemy==2.3.2
gunicorn==19.7.1 gunicorn==19.7.1
libsass==0.13.4 libsass==0.13.4
premailer==3.1.1 premailer==3.1.1
psycopg2==2.7.3.2 psycopg2==2.8.5
PyExecJS==1.4.1 PyExecJS==1.4.1
pymongo==3.5.1 pymongo[srv]==3.10.1
pyRCV==0.3 pyRCV==0.3
pytz==2017.3 pytz==2017.3
timeago==1.0.8 timeago==1.0.8
Transcrypt==3.6.60 Transcrypt==3.9.0

View File

@ -1 +1 @@
python-3.6.3 python-3.7.2