Skip to content

Commit 0a0df99

Browse files
committed
Merged in [18250] from jennifer@painless-security.com:
Reject or require manual processing for submissions when inconsistent SubmissionDocEvent revs exist. Fixes ietf-tools#2909. - Legacy-Id: 18277 Note: SVN reference [18250] has been migrated to Git commit b61bdc2
2 parents 02eefc3 + b61bdc2 commit 0a0df99

4 files changed

Lines changed: 139 additions & 18 deletions

File tree

ietf/submit/tests.py

Lines changed: 103 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222

2323
from ietf.submit.utils import expirable_submissions, expire_submission
2424
from ietf.doc.factories import DocumentFactory, WgDraftFactory, IndividualDraftFactory
25-
from ietf.doc.models import Document, DocAlias, DocEvent, State, BallotPositionDocEvent, DocumentAuthor
25+
from ietf.doc.models import ( Document, DocAlias, DocEvent, State,
26+
BallotPositionDocEvent, DocumentAuthor, SubmissionDocEvent )
2627
from ietf.doc.utils import create_ballot_if_not_open
2728
from ietf.group.factories import GroupFactory, RoleFactory
2829
from ietf.group.models import Group
@@ -76,6 +77,40 @@ def submission_file(name, rev, group, format, templatename, author=None, email=N
7677
file.name = "%s-%s.%s" % (name, rev, format)
7778
return file, author
7879

80+
def create_draft_submission_with_rev_mismatch(rev='01'):
81+
"""Create a draft and submission with mismatched version
82+
83+
Creates a rev '00' draft and Submission / SubmissionDocEvent in the 'posted'
84+
state with the requested rev.
85+
"""
86+
draft_name = 'draft-authorname-testing-tests'
87+
author = PersonFactory()
88+
89+
# draft with rev 00
90+
draft = IndividualDraftFactory(
91+
name=draft_name,
92+
authors=[author],
93+
rev='00',
94+
)
95+
96+
# submission with rev mismatched to the draft
97+
sub = Submission.objects.create(
98+
name=draft_name,
99+
group=None,
100+
submission_date=datetime.date.today() - datetime.timedelta(days=1),
101+
rev=rev,
102+
state_id='posted',
103+
)
104+
SubmissionDocEvent.objects.create(
105+
doc=draft,
106+
submission=sub,
107+
by=author,
108+
desc='Existing SubmissionDocEvent with mismatched revision',
109+
rev=sub.rev,
110+
)
111+
return draft, sub
112+
113+
79114
class SubmitTests(TestCase):
80115
def setUp(self):
81116
self.saved_idsubmit_staging_path = settings.IDSUBMIT_STAGING_PATH
@@ -128,6 +163,26 @@ def tearDown(self):
128163
settings.SUBMIT_YANG_CATALOG_MODEL_DIR = self.saved_yang_catalog_model_dir
129164

130165

166+
def create_and_post_submission(self, name, rev, author, group=None, formats=("txt",)):
167+
"""Helper to create and post a submission"""
168+
url = urlreverse('ietf.submit.views.upload_submission')
169+
files = dict()
170+
for format in formats:
171+
files[format], __ = submission_file(name, rev, group, format, "test_submission.%s" % format, author=author)
172+
173+
r = self.client.post(url, files)
174+
if r.status_code != 302:
175+
q = PyQuery(r.content)
176+
print(q('div.has-error div.alert').text())
177+
178+
self.assertNoFormPostErrors(r, ".has-error,.alert-danger")
179+
180+
for format in formats:
181+
self.assertTrue(os.path.exists(os.path.join(self.staging_dir, "%s-%s.%s" % (name, rev, format))))
182+
if format == 'xml':
183+
self.assertTrue(os.path.exists(os.path.join(self.staging_dir, "%s-%s.%s" % (name, rev, 'html'))))
184+
return r
185+
131186
def do_submission(self, name, rev, group=None, formats=["txt",], author=None):
132187
# break early in case of missing configuration
133188
self.assertTrue(os.path.exists(settings.IDSUBMIT_IDNITS_BINARY))
@@ -141,24 +196,11 @@ def do_submission(self, name, rev, group=None, formats=["txt",], author=None):
141196
self.assertEqual(len(q('input[type=file][name=xml]')), 1)
142197

143198
# submit
144-
files = {}
145199
if author is None:
146200
author = PersonFactory()
147-
for format in formats:
148-
files[format], __ = submission_file(name, rev, group, format, "test_submission.%s" % format, author=author)
149-
150-
r = self.client.post(url, files)
151-
if r.status_code != 302:
152-
q = PyQuery(r.content)
153-
print(q('div.has-error div.alert').text())
154-
155-
self.assertNoFormPostErrors(r, ".has-error,.alert-danger")
156-
201+
r = self.create_and_post_submission(name, rev, author, group, formats)
157202
status_url = r["Location"]
158-
for format in formats:
159-
self.assertTrue(os.path.exists(os.path.join(self.staging_dir, "%s-%s.%s" % (name, rev, format))))
160-
if format == 'xml':
161-
self.assertTrue(os.path.exists(os.path.join(self.staging_dir, "%s-%s.%s" % (name, rev, 'html'))))
203+
162204
self.assertEqual(Submission.objects.filter(name=name).count(), 1)
163205
submission = Submission.objects.get(name=name)
164206
if len(submission.authors) != 1:
@@ -1256,6 +1298,39 @@ def test_submit_invalid_yang(self):
12561298
if settings.SUBMIT_YANGLINT_COMMAND and os.path.exists(settings.YANGLINT_BINARY):
12571299
self.assertIn("No validation errors", m)
12581300

1301+
def submit_conflicting_submissiondocevent_rev(self, new_rev='01', existing_rev='01'):
1302+
"""Test submitting a rev when an equal or later SubmissionDocEvent rev exists
1303+
1304+
The situation tested here "should" never come up. However, it may occur due to data
1305+
corruption or other unexpected situations.
1306+
"""
1307+
draft, existing_sub = create_draft_submission_with_rev_mismatch(existing_rev)
1308+
mailbox_before = len(outbox)
1309+
1310+
# Submit a "real" rev
1311+
self.create_and_post_submission(draft.name, new_rev, PersonFactory())
1312+
1313+
# Submission should have gone into the manual state
1314+
self.assertEqual(Submission.objects.filter(name=draft.name).count(), 2)
1315+
sub = Submission.objects.exclude(pk=existing_sub.pk).get(name=draft.name, rev=new_rev)
1316+
self.assertIsNotNone(sub)
1317+
self.assertEqual(sub.state_id, 'manual')
1318+
1319+
# Ensure that an email notification was sent
1320+
self.assertEqual(len(outbox), mailbox_before + 1)
1321+
self.assertTrue("Manual Post Requested" in outbox[-1]["Subject"])
1322+
self.assertTrue(draft.name in outbox[-1]["Subject"])
1323+
expected_error = "Rev %s conflicts with existing submission (%s)"%(new_rev, existing_rev)
1324+
self.assertTrue(expected_error in get_payload_text(outbox[-1]))
1325+
1326+
def test_submit_update_existing_submissiondocevent_rev(self):
1327+
"""An existing SubmissionDocEvent with same rev should trigger manual processing"""
1328+
self.submit_conflicting_submissiondocevent_rev('01', '01')
1329+
1330+
def test_submit_update_later_submissiondocevent_rev(self):
1331+
"""An existing SubmissionDocEvent with later rev should trigger manual processing"""
1332+
self.submit_conflicting_submissiondocevent_rev('01', '02')
1333+
12591334

12601335
class ApprovalsTestCase(TestCase):
12611336
def test_approvals(self):
@@ -1877,6 +1952,18 @@ def test_api_submit_wrong_revision(self):
18771952
expected = "Invalid revision (revision 00 is expected)"
18781953
self.assertContains(r, expected, status_code=400)
18791954

1955+
def test_api_submit_update_existing_submissiondocevent_rev(self):
1956+
draft, _ = create_draft_submission_with_rev_mismatch(rev='01')
1957+
r, _, __ = self.do_post_submission(rev='01', name=draft.name)
1958+
expected = "Submission failed"
1959+
self.assertContains(r, expected, status_code=409)
1960+
1961+
def test_api_submit_update_later_submissiondocevent_rev(self):
1962+
draft, _ = create_draft_submission_with_rev_mismatch(rev='02')
1963+
r, _, __ = self.do_post_submission(rev='01', name=draft.name)
1964+
expected = "Submission failed"
1965+
self.assertContains(r, expected, status_code=409)
1966+
18801967
def test_api_submit_pending_submission(self):
18811968
r, author, name = self.do_post_submission('00')
18821969
expected = "Upload of"

ietf/submit/utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,24 @@ def validate_submission_document_date(submission_date, document_date):
165165

166166
return None
167167

168+
def check_submission_revision_consistency(submission):
169+
"""Test submission for data consistency
170+
171+
Returns None if revision is consistent or an error message describing the problem.
172+
"""
173+
unexpected_events = SubmissionDocEvent.objects.filter(
174+
submission__name=submission.name, rev__gte=submission.rev
175+
)
176+
if len(unexpected_events) != 0:
177+
conflicts = [evt.rev for evt in unexpected_events]
178+
return "Rev %s conflicts with existing %s (%s). This indicates a database inconsistency that requires investigation." %(
179+
submission.rev,
180+
"submission" if len(conflicts) == 1 else "submissions",
181+
", ".join(conflicts)
182+
)
183+
return None
184+
185+
168186
def create_submission_event(request, submission, desc):
169187
by = None
170188
if request and request.user.is_authenticated:

ietf/submit/views.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
recently_approved_by_user, validate_submission, create_submission_event, docevent_from_submission,
3939
post_submission, cancel_submission, rename_submission_files, remove_submission_files, get_draft_meta,
4040
get_submission, fill_in_submission, apply_checkers, send_confirmation_emails, save_files,
41-
get_person_from_name_email )
41+
get_person_from_name_email, check_submission_revision_consistency )
4242
from ietf.stats.utils import clean_country_name
4343
from ietf.utils.accesstoken import generate_access_token
4444
from ietf.utils.log import log
@@ -63,7 +63,17 @@ def upload_submission(request):
6363

6464
apply_checkers(submission, file_name)
6565

66-
create_submission_event(request, submission, desc="Uploaded submission")
66+
consistency_error = check_submission_revision_consistency(submission)
67+
if consistency_error:
68+
# A data consistency problem diverted this to manual processing - send notification
69+
submission.state = DraftSubmissionStateName.objects.get(slug="manual")
70+
submission.save()
71+
create_submission_event(request, submission, desc="Uploaded submission (diverted to manual process)")
72+
send_manual_post_request(request, submission, errors=dict(consistency=consistency_error))
73+
else:
74+
# This is the usual case
75+
create_submission_event(request, submission, desc="Uploaded submission")
76+
6777
# Don't add an "Uploaded new revision doevent yet, in case of cancellation
6878

6979
return redirect("ietf.submit.views.submission_status", submission_id=submission.pk, access_token=submission.access_token())
@@ -149,6 +159,11 @@ def err(code, text):
149159
if errors:
150160
raise ValidationError(errors)
151161

162+
# must do this after validate_submission() or data needed for check may be invalid
163+
if check_submission_revision_consistency(submission):
164+
return err( 409, "Submission failed due to a document revision inconsistency error "
165+
"in the database. Please contact the secretariat for assistance.")
166+
152167
errors = [ c.message for c in submission.checks.all() if c.passed==False ]
153168
if errors:
154169
raise ValidationError(errors)

ready-for-merge

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
# --- Add entries at the top ---
99

10+
/personal/housley/7.10.1.dev0@18276
1011
/personal/valery/7.10.1.dev0@18271
1112
/personal/valery/7.10.1.dev0@18270
1213

0 commit comments

Comments
 (0)