Integrate authentication into voting booth

Also fix a number (several too many) bugs that found their way into the system following the previous few commits
This commit is contained in:
RunasSudo 2017-11-25 23:16:29 +11:00
parent f92c7640d5
commit 20abbdae78
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
12 changed files with 69 additions and 40 deletions

View File

@ -149,7 +149,7 @@ class DateTimeField(Field):
if is_python: if is_python:
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ') return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
else: else:
return Date.parse(value) return __pragma__('js', '{}', 'new Date(value)')
@staticmethod @staticmethod
def now(): def now():

View File

@ -96,6 +96,7 @@ def setup_test_election():
election.voters.append(Voter(name='Alice')) election.voters.append(Voter(name='Alice'))
election.voters.append(Voter(name='Bob')) election.voters.append(Voter(name='Bob'))
election.voters.append(Voter(name='Charlie')) election.voters.append(Voter(name='Charlie'))
election.voters.append(Voter(name='RunasSudo'))
election.mixing_trustees.append(InternalMixingTrustee(name='Eos Voting')) election.mixing_trustees.append(InternalMixingTrustee(name='Eos Voting'))
election.mixing_trustees.append(InternalMixingTrustee(name='Eos Voting')) election.mixing_trustees.append(InternalMixingTrustee(name='Eos Voting'))
@ -172,8 +173,9 @@ def election_view(election):
@using_election @using_election
def election_booth(election): def election_booth(election):
selection_model_view_map = EosObject.to_json({key._name: val for key, val in model_view_map.items()}) # ewww selection_model_view_map = EosObject.to_json({key._name: val for key, val in model_view_map.items()}) # ewww
auth_methods = EosObject.to_json(app.config['AUTH_METHODS'])
return flask.render_template('election/booth.html', election=election, selection_model_view_map=selection_model_view_map) return flask.render_template('election/booth.html', election=election, selection_model_view_map=selection_model_view_map, auth_methods=auth_methods)
@app.route('/election/<election_id>/view/questions') @app.route('/election/<election_id>/view/questions')
@using_election @using_election
@ -193,20 +195,24 @@ def election_view_trustees(election):
@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):
if election.workflow.get_task('eos.base.workflow.TaskOpenVoting').status >= WorkflowTask.Status.EXITED or election.workflow.get_task('eos.base.workflow.TaskCloseVoting').status <= WorkflowTask.Status.READY: if election.workflow.get_task('eos.base.workflow.TaskOpenVoting').status < WorkflowTask.Status.EXITED or election.workflow.get_task('eos.base.workflow.TaskCloseVoting').status > WorkflowTask.Status.READY:
# Voting is not yet open or has closed # Voting is not yet open or has closed
return flask.Response('Voting is not yet open or has closed', 405) return flask.Response('Voting is not yet open or has closed', 409)
data = json.loads(flask.request.data) data = json.loads(flask.request.data)
if 'user' not in flask.session:
# User is not authenticated
return flask.Response('Not authenticated', 403)
voter = None voter = None
for election_voter in election.voters: for election_voter in election.voters:
if election_voter.name == data['auth']['username']: if election_voter.name == flask.session['user'].username:
voter = election_voter voter = election_voter
break break
if voter is None: if voter is None:
# User is not authenticated # Invalid user
return flask.Response('Invalid credentials', 403) return flask.Response('Invalid credentials', 403)
# Cast the vote # Cast the vote

View File

@ -21,17 +21,24 @@
{% block content %} {% block content %}
<div id="cast_prompt"> <div id="cast_prompt">
<p>Your vote has <b>not yet been cast</b>. If you have not already done so, please make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p> <p>Your vote has <b>not yet been cast</b>. If you have not already done so, please make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p>
<p>This election requires you to log in to vote. Please enter your name below, then click ‘Cast ballot’ to cast your ballot.</p> <p>This election requires you to log in to vote. If you disconnected your internet connection earlier, you must now reconnect it before proceeding.</p>
<div class="ui form">
<div class="inline field"> {% if username %}
<label for="booth_login_name">Name</label> <p>You are currently logged in as {{ username }}. Please select an option from the list below if you would like to switch accounts. Otherwise, click ‘Cast ballot’ to continue.</p>
<input type="text" id="booth_login_name"> {% else %}
</div> <p>You are not currently logged in. Please select an option from the list below to log in. Your ballot will be automatically cast once you have logged in.</p>
<div class="ui error message" id="error_invalid_id"> {% endif %}
<i class="close icon"></i>
<div class="header">Error</div> <ul class="ui list">
<p>The log in details you entered are not valid for this election. Please check your username and password and try again. If the issue persists, contact your election administrator.</p> {% for auth_method in auth_methods %}
</div> <li><a href="/auth/{{ auth_method[0] }}/login" target="_blank" onclick="login(this);return false;">{{ auth_method[1] }}</a></li>
{% endfor %}
</ul>
<div class="ui hidden error message" id="error_invalid_id">
<i class="close icon"></i>
<div class="header">Error</div>
<p>The log in details you entered are not valid for this election. Please check your username and password and try again. If the issue persists, contact your election administrator.</p>
</div> </div>
<div class="ui hidden error message" id="error_unknown"> <div class="ui hidden error message" id="error_unknown">
@ -48,7 +55,7 @@
{% block buttons %} {% block buttons %}
<button class="ui left floated button" onclick="prevTemplate(2);">Back</a> <button class="ui left floated button" onclick="prevTemplate(2);">Back</a>
<button class="ui right floated primary button" onclick="castBallot();">Cast ballot</button> <button class="ui right floated primary{% if not username %} hidden{% endif %} button" id="cast_button" onclick="castBallot();">Cast ballot</button>
{% endblock %} {% endblock %}
{% block after %} {% block after %}
@ -57,6 +64,14 @@
$(this).closest(".message").addClass("hidden"); $(this).closest(".message").addClass("hidden");
}); });
function login(el) {
window.open(el.getAttribute("href"), "eos_login_window", "width=400,height=600");
}
function callback_complete() {
$("#cast_button").removeClass("hidden");
castBallot();
}
function castBallot() { function castBallot() {
$("#cast_prompt").hide(); $("#cast_prompt").hide();
$("#casting").show(); $("#casting").show();
@ -65,8 +80,7 @@
url: "{{ election_base_url }}cast_ballot", url: "{{ election_base_url }}cast_ballot",
type: "POST", type: "POST",
data: eosjs.eos.core.objects.__all__.EosObject.to_json({ data: eosjs.eos.core.objects.__all__.EosObject.to_json({
"ballot": eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(booth.ballot, null), "ballot": eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(booth.ballot, null)
"auth": { "username": $("#booth_login_name").val() }
}), }),
contentType: "application/json", contentType: "application/json",
dataType: "text" dataType: "text"
@ -83,14 +97,14 @@
}) })
.fail(function(xhr, status, err) { .fail(function(xhr, status, err) {
if (xhr.status === 403) { // Forbidden if (xhr.status === 403) { // Forbidden
$("#error_invalid_id").addClass("visible"); $("#error_invalid_id").removeClass("hidden");
$("#error_unknown").addClass("hidden"); $("#error_unknown").addClass("hidden");
} else { } else {
$("#error_unknown_tech").text("Technical details: " + err); $("#error_unknown_tech").text("Technical details: " + err + " – " + xhr.responseText);
$("#error_unknown").removeClass("hidden"); $("#error_unknown").removeClass("hidden");
$("#error_invalid_id").removeClass("visible"); $("#error_invalid_id").addClass("hidden");
} }
$("#casting").hide(); $("#casting").hide();

View File

@ -29,9 +29,9 @@
<p>Please check that the ballot fingerprint above matches the ballot fingerprint you recorded earlier.</p> <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 }}ballots">‘Voters and ballots’ page</a> for the election 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 or click ‘Finish’, and confirm that the above ballot fingerprint appears next to your name.</p>
{% endblock %} {% endblock %}
{% block buttons %} {% block buttons %}
<a href="{{ election_base_url }}ballots" class="ui right floated primary button">Finish</a> <a href="{{ election_base_url }}view/ballots" class="ui right floated primary button">Finish</a>
{% endblock %} {% endblock %}

View File

@ -27,6 +27,8 @@
} }
booth.ballot = eosjs.eos.base.election.__all__.Ballot(); booth.ballot = eosjs.eos.base.election.__all__.Ballot();
booth.ballot.encrypted_answers = encryptedAnswers; booth.ballot.encrypted_answers = encryptedAnswers;
booth.ballot.election_id = election._id;
booth.ballot.election_hash = eosjs.eos.core.hashing.__all__.SHA256().update_obj(election).hash_as_b64();
nextTemplate(); nextTemplate();
} catch (err) { } catch (err) {

View File

@ -21,9 +21,9 @@
{% block content %} {% block content %}
<p>Your vote has <b>not yet been cast</b>. Your selections are shown below. Please review your selections and ensure you are happy with them before continuing.</p> <p>Your vote has <b>not yet been cast</b>. Your selections are shown below. Please review your selections and ensure you are happy with them before continuing.</p>
{% for question in election.questions %} {% for question in election.questions.impl %}
<h2>{{ loop.index }}. {{ question.prompt }}</h2> <h2>{{ loop.index }}. {{ question.prompt }}</h2>
{% include templates[selection_model_view_map[election.questions[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() }}</span>.</p> <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() }}</span>.</p>

View File

@ -33,7 +33,7 @@
boothError("Question template unable to save selections"); boothError("Question template unable to save selections");
} }
showTemplate(selection_model_view_map[election.questions[booth.questionNum]._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box"); showTemplate(selection_model_view_map[election.questions.__getitem__(booth.questionNum)._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box");
function previousQuestion() { function previousQuestion() {
saveSelections(); saveSelections();
@ -41,17 +41,17 @@
prevTemplate(); prevTemplate();
} else { } else {
booth.questionNum--; booth.questionNum--;
showTemplate(selection_model_view_map[election.questions[booth.questionNum]._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box"); showTemplate(selection_model_view_map[election.questions.__getitem__(booth.questionNum)._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box");
} }
} }
function nextQuestion() { function nextQuestion() {
saveSelections(); saveSelections();
if (booth.questionNum == election.questions.length - 1) { if (booth.questionNum == election.questions.__len__() - 1) {
nextTemplate(); nextTemplate();
} else { } else {
booth.questionNum++; booth.questionNum++;
showTemplate(selection_model_view_map[election.questions[booth.questionNum]._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box"); showTemplate(selection_model_view_map[election.questions.__getitem__(booth.questionNum)._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box");
} }
} }
</script> </script>

View File

@ -16,13 +16,13 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
#} #}
<h2>{{ questionNum + 1 }}. {{ election.questions[questionNum].prompt }}</h2> <h2>{{ questionNum + 1 }}. {{ election.questions.__getitem__(questionNum).prompt }}</h2>
<p><small>Vote for between {{ election.questions[questionNum].min_choices }} and {{ election.questions[questionNum].max_choices }} candidates. Click the check-boxes to the left of the candidates' names to make your selection, then click the ‘Continue’ button. If you make a mistake, click the check-boxes again to clear your selection.</small></p> <p><small>Vote for between {{ election.questions.__getitem__(questionNum).min_choices }} and {{ election.questions.__getitem__(questionNum).max_choices }} candidates. Click the check-boxes to the left of the candidates' names to make your selection, then click the ‘Continue’ button. If you make a mistake, click the check-boxes again to clear your selection.</small></p>
<div id="question-choices" class="ui form" style="margin-bottom: 1em;"> <div id="question-choices" class="ui form" style="margin-bottom: 1em;">
<div class="grouped fields"> <div class="grouped fields">
{% for choice in election.questions[questionNum].choices %} {% for choice in election.questions.__getitem__(questionNum).choices.impl %}
<div class="field"> <div class="field">
<div class="ui checkbox"> <div class="ui checkbox">
<input type="checkbox" id="question-choice-{{ loop.index0 }}" onchange="choicesChanged();"> <input type="checkbox" id="question-choice-{{ loop.index0 }}" onchange="choicesChanged();">
@ -40,7 +40,7 @@
<script> <script>
function choicesChanged() { function choicesChanged() {
var numChoices = $("#question-choices input:checked").length; var numChoices = $("#question-choices input:checked").length;
if (numChoices >= election.questions[booth.questionNum].max_choices) { if (numChoices >= election.questions.__getitem__(booth.questionNum).max_choices) {
// Prevent making any more selections // Prevent making any more selections
$("#question-choices input:not(:checked)").prop("disabled", true); $("#question-choices input:not(:checked)").prop("disabled", true);
$("#message-max-choices").removeClass("hidden"); $("#message-max-choices").removeClass("hidden");
@ -52,7 +52,7 @@
// Fill in ballot with previous selections // Fill in ballot with previous selections
if (booth.answers[booth.questionNum]) { if (booth.answers[booth.questionNum]) {
for (var selection of booth.answers[booth.questionNum].value.choices) { // Answer already serialised for (var selection of booth.answers.__getitem__(booth.questionNum).value.choices) { // Answer already serialised
$("#question-choice-" + selection).prop("checked", true); $("#question-choice-" + selection).prop("checked", true);
} }
choicesChanged(); choicesChanged();

View File

@ -20,7 +20,7 @@
{% for choice in booth.answers[loop.index0].value.choices %} {% for choice in booth.answers[loop.index0].value.choices %}
<div class="item"> <div class="item">
<i class="checkmark icon"></i> <i class="checkmark icon"></i>
<div class="content">{{ question.choices[choice] }}</div> <div class="content">{{ question.choices.__getitem__(choice) }}</div>
</div> </div>
{% else %} {% else %}
<div class="item"> <div class="item">

View File

@ -26,7 +26,7 @@
{% else %} {% else %}
<p>You are not currently logged in. Please select an option from the list below to log in.</p> <p>You are not currently logged in. Please select an option from the list below to log in.</p>
{% endif %} {% endif %}
<ul> <ul class="ui list">
{% for auth_method in eosweb.app.config['AUTH_METHODS'] %} {% for auth_method in eosweb.app.config['AUTH_METHODS'] %}
<li><a href="/auth/{{ auth_method[0] }}/login" target="_blank" onclick="login(this);return false;">{{ auth_method[1] }}</a></li> <li><a href="/auth/{{ auth_method[0] }}/login" target="_blank" onclick="login(this);return false;">{{ auth_method[1] }}</a></li>
{% endfor %} {% endfor %}

View File

@ -39,6 +39,9 @@
var currentBoothTask = 0; var currentBoothTask = 0;
var selection_model_view_map = {{ selection_model_view_map|safe }}; {# :rooWut: #} var selection_model_view_map = {{ selection_model_view_map|safe }}; {# :rooWut: #}
var username = {% if session.user %}"{{ session.user.username }}"{% else %}null{% endif %};
var auth_methods = {{ auth_methods|safe }};
function resetBooth() { function resetBooth() {
booth = { booth = {
"questionNum": 0, "questionNum": 0,
@ -143,7 +146,9 @@
"election": election, "election": election,
"booth": booth, "booth": booth,
"eosjs": eosjs, "eosjs": eosjs,
"selection_model_view_map": selection_model_view_map "selection_model_view_map": selection_model_view_map,
"username": username,
"auth_methods": auth_methods
}, opts); }, opts);
$(destination).html(templates[template].render(opts)); $(destination).html(templates[template].render(opts));
} catch (err) { } catch (err) {

View File

@ -62,7 +62,9 @@ def main(app):
user.oauth_token = resp['access_token'] user.oauth_token = resp['access_token']
flask.session['user'] = user flask.session['user'] = user
me = reddit.get('https://oauth.reddit.com/api/v1/me') me = reddit.get('https://oauth.reddit.com/api/v1/me', headers={
'User-Agent': app.config['REDDIT_USER_AGENT']
})
user.username = me.data['name'] user.username = me.data['name']
return flask.redirect(flask.url_for('login_complete')) return flask.redirect(flask.url_for('login_complete'))