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.
## 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:
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.
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.

View File

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

View File

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

View File

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

View File

@ -19,13 +19,19 @@ from eos.core.objects import *
from eos.core.hashing import *
from eos.psr.election import *
class RPCMixnet:
def __init__(self, mix_order):
self.mix_order = mix_order
self.is_left = (self.mix_order % 2 == 0)
self.params = []
class RPCMixnetParam(EmbeddedObject):
permutation = IntField()
reencryption = EmbeddedObjectListField(BigInt)
rand_a = EmbeddedObjectField(BigInt)
rand_b = EmbeddedObjectField(BigInt)
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):
permutation = list(range(n))
@ -57,21 +63,21 @@ class RPCMixnet:
# And shuffle it to the new position
shuffled_answers[permutations[i]] = BlockEncryptedAnswer(blocks=shuffled_blocks)
# 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 = []
if self.is_left:
for i in range(len(permutations_and_reenc)):
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())
else:
for i in range(len(permutations_and_reenc)):
# 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_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())
self.params = permutations_and_reenc
@ -80,8 +86,8 @@ class RPCMixnet:
def challenge(self, i):
if self.is_left:
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:
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]
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()
@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')
@click.option('--electionid', default=None)
def verify_election(electionid):
@ -185,16 +154,16 @@ def index():
def using_election(func):
@functools.wraps(func)
def wrapped(election_id):
def wrapped(election_id, **kwargs):
election = Election.get_by_id(election_id)
return func(election)
return func(election, **kwargs)
return wrapped
def election_admin(func):
@functools.wraps(func)
def wrapped(election):
def wrapped(election, **kwargs):
if 'user' in flask.session and flask.session['user'].is_admin():
return func(election)
return func(election, **kwargs)
else:
return flask.Response('Administrator credentials required', 403)
return wrapped
@ -238,6 +207,19 @@ def election_view_trustees(election):
def election_admin_summary(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'])
@using_election
def election_api_cast_vote(election):

View File

@ -24,8 +24,14 @@
<ul>
{% for task in election.workflow.tasks %}
{% 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 %}
{% endfor %}
</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 %}

View File

@ -32,7 +32,7 @@
<div class="ui secondary pointing menu" id="election-tab-menu">
{% include eosweb.core.main.model_view_map[election.__class__]['tabs'] %}
{% 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 %}
</div>
<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>
{% 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>
<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 %}
<h3>{{ loop.index }}. {{ question.prompt }}</h2>