Skip to content

Commit

Permalink
Merge "(no-ticket) Submission receipt confirmations"
Browse files Browse the repository at this point in the history
  • Loading branch information
matrach authored and Gerrit Code Review committed Oct 16, 2013
2 parents 39cfed1 + d838035 commit c45a21d
Show file tree
Hide file tree
Showing 15 changed files with 294 additions and 2 deletions.
Empty file.
17 changes: 17 additions & 0 deletions oioioi/confirmations/controllers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from oioioi.confirmations.utils import send_submission_receipt_confirmation
from oioioi.programs.controllers import ProgrammingContestController


class ConfirmationContestControllerMixin(object):
def should_confirm_submission_receipt(self, request, submission):
return False

def create_submission(self, request, *args, **kwargs):
submission = super(ConfirmationContestControllerMixin, self) \
.create_submission(request, *args, **kwargs)

if self.should_confirm_submission_receipt(request, submission):
send_submission_receipt_confirmation(request, submission)
return submission
ProgrammingContestController.mix_in(ConfirmationContestControllerMixin)

Empty file.
Empty file.
41 changes: 41 additions & 0 deletions oioioi/confirmations/management/commands/verify_receipt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import re
import sys
import os
from pprint import pprint

from django.core.management.base import BaseCommand, CommandError
from django.utils.translation import ugettext as _

from oioioi.confirmations.utils import verify_submission_receipt_proof, \
ProofCorrupted


class Command(BaseCommand):
args = _("source_file")
help = _("Verifies the cryptographic confirmation of submission receipt "
"given to the users. Pass the source file as the first argument "
"and paste the email with the '--- BEGIN PROOF DATA ---' "
"to the standard input.")

def handle(self, *args, **options):
if len(args) != 1:
raise CommandError(_("Expected exactly one argument"))

filename = args[0]
if not os.path.exists(filename):
raise CommandError(_("File not found: ") + filename)
source = open(filename, 'r').read()

match = re.search(r'--- BEGIN PROOF DATA ---(.*)--- END PROOF DATA ---',
sys.stdin.read(), re.DOTALL)
if not match:
raise CommandError(_("Proof not found in the pasted text."))
proof = match.group(1)

try:
proof_data = verify_submission_receipt_proof(proof, source)
except ProofCorrupted as e:
raise CommandError(str(e))

sys.stdout.write(_("Confirmation is valid\n"))
pprint(proof_data, sys.stdout)
4 changes: 4 additions & 0 deletions oioioi/confirmations/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from oioioi.base.utils.deps import check_django_app_dependencies


check_django_app_dependencies(__name__, ['oioioi.programs'])
21 changes: 21 additions & 0 deletions oioioi/confirmations/templates/confirmations/email_body.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% load i18n %}{% blocktrans %}Hello {{ full_name }}!

This is a confirmation that we have received your submission
to the {{ contest }} contest:

Contest id: {{ contest_id }}
Problem: {{ problem_shortname }}
Submission id: {{ submission_id }}
Submissions to this task: {{ submission_no }}{% endblocktrans %}
Submission date: {{ submission_date }}
{% blocktrans count size=size %}Source code size: {{ size }} byte{% plural %}Source code size: {{ size }} bytes{% endblocktrans %}

{% blocktrans %}
Please save this message together with the submittied source code. There is
a cryptographic confirmation code below, which, together with the code,
is a proof that the system properly registered your submission.

The Organizers
{% endblocktrans %}

{{ proof }}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% load i18n %}{% blocktrans %}Submission {{ submission_id }} receipt confirmation{% endblocktrans %}
92 changes: 92 additions & 0 deletions oioioi/confirmations/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import re

from django.contrib.auth.models import User
from django.core import mail
from django.test import TestCase

from oioioi.confirmations.utils import submission_receipt_proof, \
verify_submission_receipt_proof, ProofCorrupted
from oioioi.contests.models import Contest, ProblemInstance
from oioioi.contests.tests import SubmitFileMixin
from oioioi.participants.models import Participant
from oioioi.programs.models import ProgramSubmission


class TestMetadataProving(TestCase):
fixtures = ['test_users', 'test_contest', 'test_full_package',
'test_submission', 'test_another_submission']

def test_valid_proof(self):
submission = ProgramSubmission.objects.get(pk=1)
proof_data_orig, proof = submission_receipt_proof(submission)
proof_data = verify_submission_receipt_proof(proof,
submission.source_file.read())

self.assertEquals(proof_data['id'], submission.id)
self.assertEquals(proof_data['date'], submission.date)

def test_invalid_proof(self):
submission = ProgramSubmission.objects.get(pk=1)
proof_data_orig, proof = submission_receipt_proof(submission)

with self.assertRaises(ProofCorrupted):
verify_submission_receipt_proof(proof, 'spam')

submission2 = ProgramSubmission.objects.get(pk=2)
proof_data_orig2, proof2 = submission_receipt_proof(submission2)

proof_tokens = proof.split(':')
proof2_tokens = proof2.split(':')
proof_tokens[0] = proof2_tokens[0]
corrupted_proof = ':'.join(proof_tokens)
with self.assertRaises(ProofCorrupted):
verify_submission_receipt_proof(corrupted_proof,
submission.source_file.read())

class TestEmailReceipt(TestCase, SubmitFileMixin):
fixtures = ['test_users', 'test_contest', 'test_full_package',
'test_submission']

def setUp(self):
contest = Contest.objects.get()
contest.controller_name = 'oioioi.oi.controllers.OIContestController'
contest.save()
Participant(contest=contest,
user=User.objects.get(username='test_user')).save()

def test_sending_receipt(self):
contest = Contest.objects.get()
problem_instance = ProblemInstance.objects.get()
self.client.login(username='test_user')
response = self.submit_file(contest, problem_instance, file_size=1337)
self._assertSubmitted(contest, response)


email = mail.outbox[0].message().as_string()
del mail.outbox[0]
self.assertIn("Submissions to this task: 2", email)
self.assertIn("1337 bytes", email)
proof = re.search(r'--- BEGIN PROOF DATA ---(.*)--- END PROOF DATA ---',
email, re.DOTALL)
self.assertTrue(proof)
verify_submission_receipt_proof(proof.group(1), 'a'*1337)

self.client.login(username='test_admin')
response = self.submit_file(contest, problem_instance,
user='test_admin', kind='NORMAL')
self._assertSubmitted(contest, response)
self.assertEquals(len(mail.outbox), 1)

def test_not_sending_receipt(self):
contest = Contest.objects.get()
problem_instance = ProblemInstance.objects.get()
self.client.login(username='test_admin')

response = self.submit_file(contest, problem_instance, user='test_user')
self._assertSubmitted(contest, response)
self.assertEquals(len(mail.outbox), 0)

response = self.submit_file(contest, problem_instance,
user='test_admin', kind='IGNORED')
self._assertSubmitted(contest, response)
self.assertEquals(len(mail.outbox), 0)
103 changes: 103 additions & 0 deletions oioioi/confirmations/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import hashlib
import sys
import dateutil.parser
from django.conf import settings
from django.template.loader import render_to_string

from django.core import signing
from oioioi.dashboard.views import grouper

from oioioi.programs.models import ProgramSubmission


SUBMISSION_RECEIVED_SALT = 'submission_reveived'
class ProofCorrupted(Exception):
pass


def sign_submission_metadata(data):
return signing.dumps(data, salt=SUBMISSION_RECEIVED_SALT, compress=True)


def unsign_submission_metadata(data):
return signing.loads(data, salt=SUBMISSION_RECEIVED_SALT)


def submission_receipt_proof(submission):
"""Returns pair of data and its signed version which may be used by
the user to prove that we received his submission someday.
The returned data are not encrypted, just signed.
"""
submission_no = ProgramSubmission.objects.filter(
user=submission.user, kind=submission.kind,
problem_instance=submission.problem_instance,
date__lt=submission.date).count() + 1
source_hash = hashlib.sha256()
for chunk in submission.source_file.chunks():
source_hash.update(chunk)
submission.source_file.seek(0)

proof_data = {
'id': submission.id,
'size': submission.source_file.size,
'source_hash': source_hash.hexdigest(),
'date': submission.date.isoformat(),
'contest': submission.problem_instance.contest.id,
'problem_instance_id': submission.problem_instance_id,
'problem_name': submission.problem_instance.short_name,
'user_id': submission.user_id,
'username': submission.user.username,
'submission_no': submission_no,
}
proof = sign_submission_metadata(proof_data)
return proof_data, proof


def format_proof(proof):
lines = ['--- BEGIN PROOF DATA ---']
lines.extend(''.join(line) for line in grouper(70, proof, ' '))
lines.append('--- END PROOF DATA ---')
return '\n'.join(lines)


def verify_submission_receipt_proof(proof, source):
"""Verifies a signed proof of user's submission and returns proven metadata.
:raises :class:`ProofCorrupted` upon failure of any reason.
"""
proof = ''.join(proof.split())
try:
proof_data = unsign_submission_metadata(proof)
except signing.BadSignature as e:
raise ProofCorrupted, str(e), sys.exc_info()[2]

proof_data['date'] = dateutil.parser.parse(proof_data['date'])
source_hash = hashlib.sha256(source).hexdigest()
if source_hash != proof_data['source_hash']:
raise ProofCorrupted('Source file does not match the original one.')

return proof_data


def send_submission_receipt_confirmation(request, submission):
proof_data, proof = submission_receipt_proof(submission)
context = {
'proof_data': proof_data,
'proof': format_proof(proof),
'contest': request.contest,
'contest_id': request.contest.id,
'submission_id': submission.id,
'submission_no': proof_data['submission_no'],
'submission_date': submission.date,
'problem_shortname': proof_data['problem_name'],
'size': proof_data['size'],
'full_name': submission.user.get_full_name(),
}

subject = render_to_string('confirmations/email_subject.txt', context)
subject = settings.EMAIL_SUBJECT_PREFIX + \
' '.join(subject.strip().splitlines())
body = render_to_string('confirmations/email_body.txt', context)

submission.user.email_user(subject, body)
Empty file added oioioi/confirmations/views.py
Empty file.
9 changes: 7 additions & 2 deletions oioioi/contests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,13 +642,18 @@ def test_attachments(self):

class SubmitFileMixin(object):
def submit_file(self, contest, problem_instance, file_size=1024,
file_name='submission.cpp'):
file_name='submission.cpp', kind='NORMAL', user=None):
url = reverse('submit', kwargs={'contest_id': contest.id})
file = ContentFile('a' * file_size, name=file_name)
post_data = {
'problem_instance_id': problem_instance.id,
'file': file
'file': file,
}
if user:
post_data.update({
'kind': kind,
'user': user,
})
return self.client.post(url, post_data)

def _assertSubmitted(self, contest, response):
Expand Down
1 change: 1 addition & 0 deletions oioioi/deployment/settings.py.template
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ INSTALLED_APPS = (
# 'oioioi.scoresreveal',
# 'oioioi.oireports',
# 'oioioi.complaints',
# 'oioioi.confirmations',
# 'oioioi.contestexcl',
# 'oioioi.sharingcli',
) + INSTALLED_APPS
Expand Down
6 changes: 6 additions & 0 deletions oioioi/oi/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ def can_submit(self, request, problem_instance, check_round_times=True):
return super(OIContestController, self)\
.can_submit(request, problem_instance, check_round_times)

def should_confirm_submission_receipt(self, request, submission):
return submission.kind == 'NORMAL' and request.user == submission.user

def update_user_result_for_problem(self, result):
try:
latest_submission = Submission.objects \
Expand Down Expand Up @@ -146,3 +149,6 @@ def can_see_round(self, request, round):
return rtimes.is_active(request.timestamp)
return super(OIOnsiteContestController, self) \
.can_see_round(request, round)

def should_confirm_submission_receipt(self, request, submission):
return False
1 change: 1 addition & 0 deletions oioioi/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
'oioioi.complaints',
'oioioi.contestexcl',
'oioioi.forum',
'oioioi.confirmations',
) + INSTALLED_APPS

AUTHENTICATION_BACKENDS += (
Expand Down

0 comments on commit c45a21d

Please sign in to comment.