forked from sio2project/oioioi
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge "(no-ticket) Submission receipt confirmations"
- Loading branch information
Showing
15 changed files
with
294 additions
and
2 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
41
oioioi/confirmations/management/commands/verify_receipt.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
21
oioioi/confirmations/templates/confirmations/email_body.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{% load i18n %}{% blocktrans %}Submission {{ submission_id }} receipt confirmation{% endblocktrans %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters