Basic functionality of the admin pane

This commit is contained in:
RunasSudo 2017-12-07 15:34:24 +10:30
parent 95e6a56f81
commit be4127639b
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
9 changed files with 66 additions and 82 deletions

View File

@ -126,20 +126,6 @@ You should now be able to see the election in the web interface.
Exit the Flask shell by pressing Ctrl+D. Exit the Flask shell by pressing Ctrl+D.
## Closing and counting an election ## Administrating an election
To close an election, helper commands are available, so use of the Flask shell is not required. Locally, run: Provided that you are logged in using an administrator account (defined using the `ADMINS` option in *local_settings.py*), you will be able to further administer the election from the ‘Administrate this election’ tab in the web interface.
FLASK_APP=eosweb EOSWEB_SETTINGS=$PWD/local_settings.py python -m flask close_election
On Heroku, run:
heroku run FLASK_APP=eosweb python -m flask close_election
This will close the first election found. If there are multiple elections, you can select which one to close by passing the `--electionid` flag, for example:
FLASK_APP=eosweb EOSWEB_SETTINGS=$PWD/local_settings.py python -m flask close_election --electionid 01234567-89ab-cdef-ghij-klmnopqrstuv
Repeat this process, but substitute `count_election` for `close_election` to count the ballots and release the results.
You should now be able to see the results in the web interface.

View File

@ -109,24 +109,24 @@ class Question(EmbeddedObject):
class Result(EmbeddedObject): class Result(EmbeddedObject):
pass pass
class ApprovalQuestion(Question): class ListChoiceQuestion(Question):
choices = ListField(StringField()) choices = ListField(StringField())
min_choices = IntField() min_choices = IntField()
max_choices = IntField() max_choices = IntField()
def pretty_answer(self, answer): def pretty_answer(self, answer):
if len(answer.choices) == 0:
return '(blank votes)'
return ', '.join([self.choices[choice] for choice in answer.choices]) return ', '.join([self.choices[choice] for choice in answer.choices])
class ApprovalQuestion(ListChoiceQuestion):
pass
class ApprovalAnswer(Answer): class ApprovalAnswer(Answer):
choices = ListField(IntField()) choices = ListField(IntField())
class PreferentialQuestion(Question): class PreferentialQuestion(ListChoiceQuestion):
choices = ListField(StringField()) pass
min_choices = IntField()
max_choices = IntField()
def pretty_answer(self, answer):
return ', '.join([self.choices[choice] for choice in answer.choices])
class PreferentialAnswer(Answer): class PreferentialAnswer(Answer):
choices = ListField(IntField()) choices = ListField(IntField())

View File

@ -117,7 +117,7 @@ class Workflow(EmbeddedObject):
# ============== # ==============
class TaskConfigureElection(WorkflowTask): class TaskConfigureElection(WorkflowTask):
label = 'Configure the election and freeze the election' label = 'Freeze the election'
#def on_enter(self): #def on_enter(self):
# self.status = WorkflowTask.Status.COMPLETE # self.status = WorkflowTask.Status.COMPLETE

View File

@ -175,9 +175,7 @@ class MixingTrustee(Trustee):
return False return False
class InternalMixingTrustee(MixingTrustee): class InternalMixingTrustee(MixingTrustee):
def __init__(self, **kwargs): mixnets = EmbeddedObjectListField(is_protected=True)
super().__init__(**kwargs)
self.mixnets = []
def mix_votes(self, question=0): def mix_votes(self, question=0):
__pragma__('skip') __pragma__('skip')
@ -187,7 +185,7 @@ class InternalMixingTrustee(MixingTrustee):
election = self.recurse_parents('eos.base.election.Election') election = self.recurse_parents('eos.base.election.Election')
index = self._instance[1] index = self._instance[1]
self.mixnets.append(RPCMixnet(index)) self.mixnets.append(RPCMixnet(mix_order=index))
if index > 0: if index > 0:
orig_answers = election.mixing_trustees[index - 1].mixed_questions[question] orig_answers = election.mixing_trustees[index - 1].mixed_questions[question]
else: else:

View File

@ -19,13 +19,19 @@ from eos.core.objects import *
from eos.core.hashing import * from eos.core.hashing import *
from eos.psr.election import * from eos.psr.election import *
class RPCMixnet: class RPCMixnetParam(EmbeddedObject):
def __init__(self, mix_order): permutation = IntField()
self.mix_order = mix_order reencryption = EmbeddedObjectListField(BigInt)
rand_a = EmbeddedObjectField(BigInt)
self.is_left = (self.mix_order % 2 == 0) rand_b = EmbeddedObjectField(BigInt)
self.params = [] class RPCMixnet(EmbeddedObject):
mix_order = IntField()
params = EmbeddedObjectListField(RPCMixnetParam)
@property
def is_left(self):
return (self.mix_order % 2 == 0)
def random_permutation(self, n): def random_permutation(self, n):
permutation = list(range(n)) permutation = list(range(n))
@ -57,21 +63,21 @@ class RPCMixnet:
# And shuffle it to the new position # And shuffle it to the new position
shuffled_answers[permutations[i]] = BlockEncryptedAnswer(blocks=shuffled_blocks) shuffled_answers[permutations[i]] = BlockEncryptedAnswer(blocks=shuffled_blocks)
# Record the parameters # Record the parameters
permutations_and_reenc.append([permutations[i], block_reencryptions, block.public_key.group.random_Zq_element(), block.public_key.group.random_Zq_element()]) permutations_and_reenc.append(RPCMixnetParam(permutation=permutations[i], reencryption=block_reencryptions, rand_a=block.public_key.group.random_Zq_element(), rand_b=block.public_key.group.random_Zq_element()))
commitments = [] commitments = []
if self.is_left: if self.is_left:
for i in range(len(permutations_and_reenc)): for i in range(len(permutations_and_reenc)):
val = permutations_and_reenc[i] val = permutations_and_reenc[i]
val_obj = MixChallengeResponse(challenge_index=i, response_index=val[0], reenc=val[1], rand=val[2]) val_obj = MixChallengeResponse(challenge_index=i, response_index=val.permutation, reenc=val.reencryption, rand=val.rand_a)
commitments.append(SHA256().update_obj(val_obj).hash_as_bigint()) commitments.append(SHA256().update_obj(val_obj).hash_as_bigint())
else: else:
for i in range(len(permutations_and_reenc)): for i in range(len(permutations_and_reenc)):
# Find the answer that went to 'i' # Find the answer that went to 'i'
idx = next(idx for idx in range(len(permutations_and_reenc)) if permutations_and_reenc[idx][0] == i) idx = next(idx for idx in range(len(permutations_and_reenc)) if permutations_and_reenc[idx].permutation == i)
val = permutations_and_reenc[idx] val = permutations_and_reenc[idx]
val_obj = MixChallengeResponse(challenge_index=i, response_index=idx, reenc=val[1], rand=val[3]) val_obj = MixChallengeResponse(challenge_index=i, response_index=idx, reenc=val.reencryption, rand=val.rand_b)
commitments.append(SHA256().update_obj(val_obj).hash_as_bigint()) commitments.append(SHA256().update_obj(val_obj).hash_as_bigint())
self.params = permutations_and_reenc self.params = permutations_and_reenc
@ -80,8 +86,8 @@ class RPCMixnet:
def challenge(self, i): def challenge(self, i):
if self.is_left: if self.is_left:
val = self.params[i] val = self.params[i]
return MixChallengeResponse(challenge_index=i, response_index=val[0], reenc=val[1], rand=val[2]) return MixChallengeResponse(challenge_index=i, response_index=val.permutation, reenc=val.reencryption, rand=val.rand_a)
else: else:
idx = next(idx for idx in range(len(self.params)) if self.params[idx][0] == i) idx = next(idx for idx in range(len(self.params)) if self.params[idx].permutation == i)
val = self.params[idx] val = self.params[idx]
return MixChallengeResponse(challenge_index=i, response_index=idx, reenc=val[1], rand=val[3]) return MixChallengeResponse(challenge_index=i, response_index=idx, reenc=val.reencryption, rand=val.rand_b)

View File

@ -131,37 +131,6 @@ def setup_test_election():
election.save() election.save()
@app.cli.command('close_election')
@click.option('--electionid', default=None)
def close_election(electionid):
if electionid is None:
election = Election.get_all()[0]
else:
election = Election.get_by_id(electionid)
election.workflow.get_task('eos.base.workflow.TaskCloseVoting').enter()
election.save()
@app.cli.command('count_election')
@click.option('--electionid', default=None)
def count_election(electionid):
if electionid is None:
election = Election.get_all()[0]
else:
election = Election.get_by_id(electionid)
# Mix votes
election.workflow.get_task('eos.psr.workflow.TaskMixVotes').enter()
# Prove mixes
election.workflow.get_task('eos.psr.workflow.TaskProveMixes').enter()
# Decrypt votes, for realsies
election.workflow.get_task('eos.psr.workflow.TaskDecryptVotes').enter()
# Release result
election.workflow.get_task('eos.base.workflow.TaskReleaseResults').enter()
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):
@ -185,16 +154,16 @@ def index():
def using_election(func): def using_election(func):
@functools.wraps(func) @functools.wraps(func)
def wrapped(election_id): def wrapped(election_id, **kwargs):
election = Election.get_by_id(election_id) election = Election.get_by_id(election_id)
return func(election) return func(election, **kwargs)
return wrapped return wrapped
def election_admin(func): def election_admin(func):
@functools.wraps(func) @functools.wraps(func)
def wrapped(election): def wrapped(election, **kwargs):
if 'user' in flask.session and flask.session['user'].is_admin(): if 'user' in flask.session and flask.session['user'].is_admin():
return func(election) return func(election, **kwargs)
else: else:
return flask.Response('Administrator credentials required', 403) return flask.Response('Administrator credentials required', 403)
return wrapped return wrapped
@ -238,6 +207,19 @@ def election_view_trustees(election):
def election_admin_summary(election): def election_admin_summary(election):
return flask.render_template('election/admin/admin.html', election=election) return flask.render_template('election/admin/admin.html', election=election)
@app.route('/election/<election_id>/admin/enter_task')
@using_election
@election_admin
def election_admin_enter_task(election):
task = election.workflow.get_task(flask.request.args['task_name'])
if task.status != WorkflowTask.Status.READY:
return flask.Response('Task is not yet ready or has already exited', 409)
task.enter()
election.save()
return flask.redirect(flask.url_for('election_admin_summary', election_id=election._id))
@app.route('/election/<election_id>/cast_ballot', methods=['POST']) @app.route('/election/<election_id>/cast_ballot', methods=['POST'])
@using_election @using_election
def election_api_cast_vote(election): def election_api_cast_vote(election):

View File

@ -24,8 +24,14 @@
<ul> <ul>
{% for task in election.workflow.tasks %} {% for task in election.workflow.tasks %}
{% if task.status == eos.base.workflow.WorkflowTask.Status.READY %} {% if task.status == eos.base.workflow.WorkflowTask.Status.READY %}
<li>{{ task.label }}</li> <li><a href="{{ url_for('election_admin_enter_task', election_id=election._id, task_name=task._name) }}" onclick="return confirmTask(this);">{{ task.label }}</a></li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</ul> </ul>
<script>
function confirmTask(taskLink) {
return window.confirm("Are you sure you want to execute the task \"" + taskLink.innerText + "\"? This action is irreversible.");
}
</script>
{% endblock %} {% endblock %}

View File

@ -32,7 +32,7 @@
<div class="ui secondary pointing menu" id="election-tab-menu"> <div class="ui secondary pointing menu" id="election-tab-menu">
{% include eosweb.core.main.model_view_map[election.__class__]['tabs'] %} {% include eosweb.core.main.model_view_map[election.__class__]['tabs'] %}
{% if session.user and session.user.is_admin() %} {% if session.user and session.user.is_admin() %}
<a href="{{ url_for('election_admin_summary', election_id=election._id) }}" class="election-tab-ajax item{% if request.endpoint == 'election_admin' %} active{% endif %} right"><i class="configure icon"></i> Administrate this election</a> <a href="{{ url_for('election_admin_summary', election_id=election._id) }}" class="election-tab-ajax item{% if request.endpoint == 'election_admin_summary' %} active{% endif %} right"><i class="configure icon"></i> Administrate this election</a>
{% endif %} {% endif %}
</div> </div>
<div class="ui container" id="election-tab-content"> <div class="ui container" id="election-tab-content">

View File

@ -55,10 +55,16 @@
<p>The administrator of this {{ election.kind }} has not yet finished setting the election parameters. The details of the {{ election.kind }} may change at any time.</p> <p>The administrator of this {{ election.kind }} has not yet finished setting the election parameters. The details of the {{ election.kind }} may change at any time.</p>
{% endif %} {% endif %}
{% if election.workflow.get_task('eos.base.workflow.TaskReleaseResults').status == Status.EXITED %} {% if (session.user and session.user.is_admin()) or election.workflow.get_task('eos.base.workflow.TaskReleaseResults').status == Status.EXITED %}
<h2>Results</h2> <h2>Results</h2>
<p>Results were released at {{ election.workflow.get_task('eos.base.workflow.TaskReleaseResults').exited_at.strftime('%Y-%m-%d %H:%M:%S') }} UTC.</p> {% if election.workflow.get_task('eos.base.workflow.TaskReleaseResults').status == Status.EXITED %}
<p>Results were released at {{ election.workflow.get_task('eos.base.workflow.TaskReleaseResults').exited_at.strftime('%Y-%m-%d %H:%M:%S') }} UTC.</p>
{% else %}
<div class="ui warning message">
This is a preview of the election results, shown only to you, the election administrator. To publicly release the results, you must do so from the <a href="{{ url_for('election_admin_summary', election_id=election._id) }}">‘Administrate this election’</a> tab.
</div>
{% endif %}
{% for question in election.questions %} {% for question in election.questions %}
<h3>{{ loop.index }}. {{ question.prompt }}</h2> <h3>{{ loop.index }}. {{ question.prompt }}</h2>