diff --git a/README.txt b/README.txt index c89c4faf1bbeafab088de5b82c5ab957103a0cf0..caf0756898a088c9c0296c0a0393a10c107efad2 100644 --- a/README.txt +++ b/README.txt @@ -21,6 +21,10 @@ NEEDS: - South for schema migration -- easy_install South +- django-webtest for testing +-- http://pypi.python.org/pypi/django-webtest +-- easy_install webtest +-- easy_install django-webtest GETTING SOUTH WORKING ON EXISTING INSTALL - as of Helios v3.0.4, we're using South to migrate data models diff --git a/helios/security.py b/helios/security.py index 50e4a03d50e94230b7864c27d35253bf1b97b950..05e99b4e24c87d93cb3bdff211aa180cc3ae8549 100644 --- a/helios/security.py +++ b/helios/security.py @@ -19,6 +19,23 @@ import urllib import helios +# current voter +def get_voter(request, user, election): + """ + return the current voter + """ + voter = None + if request.session.has_key('CURRENT_VOTER'): + voter = request.session['CURRENT_VOTER'] + if voter.election != election: + voter = None + + if not voter: + if user: + voter = Voter.get_by_election_and_user(election, user) + + return voter + # a function to check if the current user is a trustee HELIOS_TRUSTEE_UUID = 'helios_trustee_uuid' def get_logged_in_trustee(request): @@ -80,9 +97,8 @@ def election_view(**checks): # if private election, only logged in voters if election.private_p and not checks.get('allow_logins',False): - from views import get_voter, get_user, password_voter_login - user = get_user(request) - if not user_can_admin_election(user, election) and not get_voter(request, user, election): + from views import password_voter_login + if not user_can_see_election(request, election): return_url = request.get_full_path() return HttpResponseRedirect("%s?%s" % (reverse(password_voter_login, args=[election.uuid]), urllib.urlencode({ 'return_url' : return_url @@ -101,6 +117,21 @@ def user_can_admin_election(user, election): # election or site administrator return election.admin == user or user.admin_p +def user_can_see_election(request, election): + user = get_user(request) + + if not election.private_p: + return True + + # election is private + + # but maybe this user is the administrator? + if user_can_admin_election(user, election): + return True + + # then this user has to be a voter + return (get_voter(request, user, election) != None) + def api_client_can_admin_election(api_client, election): return election.api_client == api_client and api_client != None diff --git a/helios/tests.py b/helios/tests.py index ad026a5ec4fec6ad2a111f36a5a8c4ee68edd2fd..91a414f29ec7b76dcff8835ce607f3e6e7dac24d 100644 --- a/helios/tests.py +++ b/helios/tests.py @@ -3,6 +3,7 @@ Unit Tests for Helios """ import unittest, datetime, re +import django_webtest import models import datatypes @@ -316,11 +317,30 @@ class LegacyElectionBlackboxTests(DataFormatBlackboxTests, TestCase): # EXPECTED_TRUSTEES_FILE = 'helios/fixtures/v3.1-trustees-expected.json' # EXPECTED_BALLOTS_FILE = 'helios/fixtures/v3.1-ballots-expected.json' +class WebTest(django_webtest.WebTest): + def assertRedirects(self, response, url): + """ + reimplement this in case it's a WebOp response + """ + if hasattr(response, 'status_code'): + return super(django_webtest.WebTest, self).assertRedirects(response, url) + + assert response.status_int == 302 + assert url in response.location, "redirected to %s instead of %s" % (response.location, url) + + def assertContains(self, response, text): + if hasattr(response, 'status_code'): + return super(django_webtest.WebTest, self).assertContains(response, text) + + assert response.status_int == 200 + assert text in response.testbody, "missing text %s" % text + + ## ## overall operation of the system ## -class ElectionBlackboxTests(TestCase): +class ElectionBlackboxTests(WebTest): fixtures = ['users.json', 'election.json'] def setUp(self): @@ -331,7 +351,10 @@ class ElectionBlackboxTests(TestCase): # set up the session session = self.client.session session['user'] = {'type': self.user.user_type, 'user_id': self.user.user_id} - session.save() + session.save() + + # set up the app, too + self.app.cookies['sessionid'] = self.client.cookies.get('sessionid').value def clear_login(self): session = self.client.session @@ -513,38 +536,45 @@ class ElectionBlackboxTests(TestCase): check_user_logged_in looks for the "you're already logged" message """ # vote by preparing a ballot via the server-side encryption - response = self.client.post("/helios/elections/%s/encrypt-ballot" % election_id, { + response = self.app.post("/helios/elections/%s/encrypt-ballot" % election_id, { 'answers_json': utils.to_json([[1]])}) self.assertContains(response, "answers") # parse it as an encrypted vote, and re-serialize it - ballot = datatypes.LDObject.fromDict(utils.from_json(response.content), type_hint='legacy/EncryptedVote') + ballot = datatypes.LDObject.fromDict(utils.from_json(response.testbody), type_hint='legacy/EncryptedVote') encrypted_vote = ballot.serialize() # cast the ballot - response = self.client.post("/helios/elections/%s/cast" % election_id, { + response = self.app.post("/helios/elections/%s/cast" % election_id, { 'encrypted_vote': encrypted_vote}) self.assertRedirects(response, "%s/helios/elections/%s/cast_confirm" % (settings.SECURE_URL_HOST, election_id)) + cast_confirm_page = response.follow() + if need_login: if check_user_logged_in: - response = self.client.get("/helios/elections/%s/cast_confirm" % election_id) - self.assertContains(response, "You are logged in as") - self.assertContains(response, "requires election-specific credentials") - - response = self.client.post("/helios/elections/%s/password_voter_login" % election_id, { - 'voter_id' : username, - 'password' : password - }) - self.assertRedirects(response, "/helios/elections/%s/cast_confirm" % election_id) - else: - response = self.client.get("/helios/elections/%s/cast_confirm" % election_id) - self.assertContains(response, "I am ") + self.assertContains(cast_confirm_page, "You are logged in as") + self.assertContains(cast_confirm_page, "requires election-specific credentials") - # confirm the vote - response = self.client.post("/helios/elections/%s/cast_confirm" % election_id, { - "csrf_token" : self.client.session['csrf_token'], - "status_update" : False}) + # set the form + login_form = cast_confirm_page.form + login_form['voter_id'] = username + login_form['password'] = password + + cast_confirm_page = login_form.submit() + + self.assertRedirects(cast_confirm_page, "/helios/elections/%s/cast_confirm" % election_id) + cast_confirm_page = cast_confirm_page.follow() + + # here we should be at the cast-confirm page and logged in + self.assertContains(cast_confirm_page, "I am ") + + # confirm the vote, now with the actual form + cast_form = cast_confirm_page.form + + if 'status_update' in cast_form.fields.keys(): + cast_form['status_update'] = False + response = cast_form.submit() self.assertRedirects(response, "%s/helios/elections/%s/cast_done" % (settings.URL_HOST, election_id)) # at this point an email should have gone out to the user @@ -559,18 +589,25 @@ class ElectionBlackboxTests(TestCase): # so if need_login is False, it was a private election, and we do need to re-login here # we need to re-login if it's a private election, because all data, including ballots # is otherwise private - response = self.client.post("/helios/elections/%s/password_voter_login" % election_id, { - 'voter_id' : username, - 'password' : password - }) + login_page = self.app.get("/helios/elections/%s/password_voter_login" % election_id) + + # if we redirected, that's because we can see the page, I think + if login_page.status_int != 302: + login_form = login_page.form + + login_form['voter_id'] = username + login_form['password'] = password + login_form.submit() - response = self.client.get(url) + response = self.app.get(url) self.assertContains(response, ballot.hash) self.assertContains(response, html_escape(encrypted_vote)) # if we request the redirect to cast_done, the voter should be logged out, but not the user - response = self.client.get("/helios/elections/%s/cast_done" % election_id) - assert not self.client.session.has_key('CURRENT_VOTER') + response = self.app.get("/helios/elections/%s/cast_done" % election_id) + + # FIXME: how to check this? We can't do it by checking session that we're doign webtes + # assert not self.client.session.has_key('CURRENT_VOTER') def _do_tally(self, election_id): # log back in as administrator @@ -615,15 +652,19 @@ class ElectionBlackboxTests(TestCase): # private election election_id, username, password = self._setup_complete_election({'private_p' : "1"}) - # log in - response = self.client.post("/helios/elections/%s/password_voter_login" % election_id, { - 'voter_id' : username, - 'password' : password, - 'return_url' : "/helios/elections/%s/view" % election_id - }) + # get the password_voter_login_form via the front page + # (which will test that redirects are doing the right thing) + response = self.app.get("/helios/elections/%s/view" % election_id) + + # ensure it redirects + self.assertRedirects(response, "/helios/elections/%s/password_voter_login" % election_id) + + login_form = response.follow().form + + login_form['voter_id'] = username + login_form['password'] = password - # FIXME: probably better to fetch password_voter_login as a get and post the form obtained - # rather than assume return_url + response = login_form.submit() self.assertRedirects(response, "/helios/elections/%s/view" % election_id) self._cast_ballot(election_id, username, password, need_login = False) diff --git a/helios/views.py b/helios/views.py index 119575baf8ff0e3e950d711af8fb22828e8363eb..33efb07fbc7a3abf25d3a95b7e4f0536cf817357 100644 --- a/helios/views.py +++ b/helios/views.py @@ -100,22 +100,6 @@ def stats(request): 'limit' : limit}) -def get_voter(request, user, election): - """ - return the current voter - """ - voter = None - if request.session.has_key('CURRENT_VOTER'): - voter = request.session['CURRENT_VOTER'] - if voter.election != election: - voter = None - - if not voter: - if user: - voter = Voter.get_by_election_and_user(election, user) - - return voter - ## ## simple admin for development ## @@ -546,10 +530,15 @@ def password_voter_login(request, election): """ This is used to log in as a voter for a particular election """ - + # the URL to send the user to after they've logged in return_url = request.REQUEST.get('return_url', reverse(one_election_cast_confirm, args=[election.uuid])) if request.method == "GET": + # if user logged in somehow in the interim, e.g. using the login link for administration, + # then go! + if user_can_see_election(request, election): + return HttpResponseRedirect(reverse(one_election_view, args = [election.uuid])) + password_login_form = forms.VoterPasswordForm() return render_template(request, 'password_voter_login', {'election': election, 'return_url' : return_url, @@ -663,6 +652,7 @@ def one_election_cast_confirm(request, election): return render_template(request, 'election_cast_confirm', { 'login_box': login_box, 'election' : election, 'vote_fingerprint': vote_fingerprint, 'past_votes': past_votes, 'issues': issues, 'voter' : voter, + 'return_url': return_url, 'status_update_label': status_update_label, 'status_update_message': status_update_message, 'show_password': show_password, 'password_only': password_only, 'password_login_form': password_login_form, 'bad_voter_login': bad_voter_login})