Skip to content

Commit 2916e05

Browse files
committed
feature(edit): optimistic locking of edit forms to avoid overwrites
Closes #37 Signed-off-by: anthraxx <levente@leventepolyak.net>
1 parent 40be7f9 commit 2916e05

18 files changed

+236
-31
lines changed

test/conftest.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,15 @@ def wrapper(db, *args, **kwargs):
161161

162162
def default_issue_dict(overrides=dict()):
163163
data = dict(cve=DEFAULT_ISSUE_ID, issue_type=issue_types[0], remote=Remote.unknown.name,
164-
severity=Severity.unknown.name, description='', notes='', reference='')
164+
severity=Severity.unknown.name, description='', notes='', reference='',
165+
changed=str(datetime.utcfromtimestamp(0)))
165166
data.update(overrides)
166167
return data
167168

168169

169170
def create_issue(func=None, id=DEFAULT_ISSUE_ID, issue_type=issue_types[0], remote=Remote.unknown,
170-
severity=Severity.unknown, description='', notes='', reference='', count=1):
171+
severity=Severity.unknown, description='', notes='', reference='',
172+
changed=datetime.utcfromtimestamp(0), count=1):
171173
def decorator(func):
172174
@wraps(func)
173175
def wrapper(db, *args, **kwargs):
@@ -180,6 +182,7 @@ def wrapper(db, *args, **kwargs):
180182
issue.description = description
181183
issue.notes = notes
182184
issue.reference = reference
185+
issue.changed = changed
183186
db.session.add(issue)
184187
db.session.commit()
185188
func(db=db, *args, **kwargs)
@@ -226,14 +229,15 @@ def wrapper(db, *args, **kwargs):
226229
def default_group_dict(overrides=dict()):
227230
data = dict(cve=DEFAULT_ISSUE_ID, pkgnames='foopkg', affected='1.0-1', fixed=None,
228231
status=Affected.unknown.name, bug_ticket='', reference='', notes='',
229-
advisory_qualified=True)
232+
advisory_qualified=True, changed=str(datetime.utcfromtimestamp(0)))
230233
data.update(overrides)
231234
return data
232235

233236

234237
def create_group(func=None, id=None, status=None, severity=None,
235238
affected='1.0-1', fixed=None, bug_ticket='', reference='', notes='',
236-
created=datetime.utcnow(), advisory_qualified=True, issues=[DEFAULT_ISSUE_ID], packages=['foo'], count=1):
239+
created=datetime.utcnow(), advisory_qualified=True, issues=[DEFAULT_ISSUE_ID], packages=['foo'],
240+
changed=datetime.utcfromtimestamp(0), count=1):
237241
def decorator(func):
238242
@wraps(func)
239243
def wrapper(db, *args, **kwargs):
@@ -255,6 +259,7 @@ def wrapper(db, *args, **kwargs):
255259
group.notes = notes
256260
group.created = created
257261
group.advisory_qualified = advisory_qualified
262+
group.changed = changed
258263

259264
db.session.add(group)
260265
db.session.commit()
@@ -323,9 +328,15 @@ def create_advisory_content(id=DEFAULT_ADVISORY_ID, group=DEFAULT_GROUP_NAME, pk
323328
DEFAULT_ADVISORY_CONTENT = create_advisory_content()
324329

325330

331+
def default_advisory_dict(overrides=dict()):
332+
data = dict(changed=str(datetime.utcfromtimestamp(0)))
333+
data.update(overrides)
334+
return data
335+
336+
326337
def create_advisory(func=None, id=DEFAULT_ADVISORY_ID, group_package_id=DEFAULT_GROUP_ID, advisory_type=None,
327-
publication=Publication.scheduled, workaround=None, impact=None, content=None, created=datetime.utcnow(),
328-
reference=None, count=1):
338+
publication=Publication.scheduled, workaround=None, impact=None, content=None, reference=None,
339+
created=datetime.utcnow(), changed=datetime.utcfromtimestamp(0), count=1):
329340
def decorator(func):
330341
@wraps(func)
331342
def wrapper(db, *args, **kwargs):
@@ -344,6 +355,7 @@ def wrapper(db, *args, **kwargs):
344355
advisory.impact = impact
345356
advisory.content = content
346357
advisory.created = created
358+
advisory.changed = changed
347359
advisory.reference = reference
348360

349361
db.session.add(advisory)

test/test_advisory.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from .conftest import create_group
3434
from .conftest import create_issue
3535
from .conftest import create_package
36+
from .conftest import default_advisory_dict
3637
from .conftest import default_group_dict
3738
from .conftest import default_issue_dict
3839
from .conftest import get_advisory
@@ -206,7 +207,7 @@ def test_edit_advisory(db, client):
206207
workaround = 'the cake is a lie'
207208
impact = 'Big shit and deep trouble!'
208209
resp = client.post(url_for('tracker.edit_advisory', advisory_id=DEFAULT_ADVISORY_ID), follow_redirects=True,
209-
data={'workaround': workaround, 'impact': impact})
210+
data=default_advisory_dict({'workaround': workaround, 'impact': impact}))
210211
assert 200 == resp.status_code
211212
assert 'text/html; charset=utf-8' == resp.content_type
212213
assert_advisory_data(DEFAULT_ADVISORY_ID, workaround=workaround, impact=impact)
@@ -216,7 +217,7 @@ def test_edit_advisory(db, client):
216217
@logged_in
217218
def test_edit_advisory_not_found(db, client):
218219
resp = client.post(url_for('tracker.edit_advisory', advisory_id=DEFAULT_ADVISORY_ID), follow_redirects=True,
219-
data={'workaround': 'nothing', 'impact': 'nothing'})
220+
data=default_advisory_dict({'workaround': 'nothing', 'impact': 'nothing'}))
220221
assert resp.status_code == NotFound.code
221222
assert 'text/html; charset=utf-8' == resp.content_type
222223

@@ -666,7 +667,7 @@ def test_advisory_published_content_not_over_escaped(db, client, patch_get):
666667
def test_edit_advisory_non_relational_field_updates_changed_date(db, client):
667668
advisory_changed_old = Advisory.query.get(DEFAULT_ADVISORY_ID).changed
668669

669-
data = dict(impact='everything beyond repair', workaround='set computer on fire')
670+
data = default_advisory_dict(dict(impact='everything beyond repair', workaround='set computer on fire'))
670671
resp = client.post(url_for('tracker.edit_advisory', advisory_id=DEFAULT_ADVISORY_ID), follow_redirects=True, data=data)
671672
assert 200 == resp.status_code
672673
assert f'Edited {DEFAULT_ADVISORY_ID}' in resp.data.decode()
@@ -682,7 +683,7 @@ def test_edit_advisory_non_relational_field_updates_changed_date(db, client):
682683
def test_edit_advisory_does_nothing_when_data_is_same(db, client):
683684
advisory_changed_old = Advisory.query.get(DEFAULT_ADVISORY_ID).changed
684685

685-
data = dict(impact='everything beyond repair', workaround='set computer on fire')
686+
data = default_advisory_dict(dict(impact='everything beyond repair', workaround='set computer on fire'))
686687
resp = client.post(url_for('tracker.edit_advisory', advisory_id=DEFAULT_ADVISORY_ID), follow_redirects=True, data=data)
687688
assert 200 == resp.status_code
688689
assert f'Edited {DEFAULT_ADVISORY_ID}' not in resp.data.decode()

test/test_cve.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11

2+
from datetime import datetime
3+
24
from flask import url_for
35
from werkzeug.exceptions import Forbidden
46
from werkzeug.exceptions import NotFound
@@ -44,7 +46,8 @@ def set_and_assert_cve_data(db, client, cve_id, route):
4446
severity=severity.name,
4547
description=description,
4648
notes=notes,
47-
reference=reference))
49+
reference=reference,
50+
changed=str(datetime.utcfromtimestamp(0))))
4851
assert 200 == resp.status_code
4952

5053
cve = CVE.query.get(cve_id)

tracker/form/advisory.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from wtforms import BooleanField
2+
from wtforms import HiddenField
13
from wtforms import SelectField
24
from wtforms import SubmitField
35
from wtforms import TextAreaField
@@ -34,6 +36,9 @@ class AdvisoryEditForm(BaseForm):
3436
reference = URLField(u'Reference', validators=[Optional(), URL(), Length(max=Advisory.REFERENCE_LENGTH), ValidAdvisoryReference()])
3537
workaround = TextAreaField(u'Workaround', validators=[Optional(), Length(max=Advisory.WORKAROUND_LENGTH)])
3638
impact = TextAreaField(u'Impact', validators=[Optional(), Length(max=Advisory.IMPACT_LENGTH)])
39+
changed = HiddenField(u'Changed', validators=[Optional()])
40+
changed_latest = HiddenField(u'Latest Changed', validators=[Optional()])
41+
force_submit = BooleanField(u'Force update', default=False, validators=[Optional()])
3742
edit = SubmitField(u'edit')
3843

3944
def __init__(self, advisory_id):

tracker/form/cve.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from wtforms import BooleanField
2+
from wtforms import HiddenField
13
from wtforms import SelectField
24
from wtforms import StringField
35
from wtforms import SubmitField
@@ -24,6 +26,9 @@ class CVEForm(BaseForm):
2426
remote = SelectField(u'Remote', choices=[(e.name, e.label) for e in [*Remote]], validators=[DataRequired()])
2527
reference = TextAreaField(u'References', validators=[Optional(), Length(max=CVE.REFERENCES_LENGTH), ValidURLs()])
2628
notes = TextAreaField(u'Notes', validators=[Optional(), Length(max=CVE.NOTES_LENGTH)])
29+
changed = HiddenField(u'Changed', validators=[Optional()])
30+
changed_latest = HiddenField(u'Latest Changed', validators=[Optional()])
31+
force_submit = BooleanField(u'Force update', default=False, validators=[Optional()])
2732
submit = SubmitField(u'submit')
2833

2934
def __init__(self, edit=False):

tracker/form/group.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from pyalpm import vercmp
22
from wtforms import BooleanField
3+
from wtforms import HiddenField
34
from wtforms import SelectField
45
from wtforms import StringField
56
from wtforms import SubmitField
@@ -30,7 +31,10 @@ class GroupForm(BaseForm):
3031
reference = TextAreaField(u'References', validators=[Optional(), Length(max=CVEGroup.REFERENCES_LENGTH), ValidURLs()])
3132
notes = TextAreaField(u'Notes', validators=[Optional(), Length(max=CVEGroup.NOTES_LENGTH)])
3233
advisory_qualified = BooleanField(u'Advisory qualified', default=True, validators=[Optional()])
33-
force_submit = BooleanField(u'Force creation', default=False, validators=[Optional()])
34+
changed = HiddenField(u'Changed', validators=[Optional()])
35+
changed_latest = HiddenField(u'Latest Changed', validators=[Optional()])
36+
force_update = BooleanField(u'Force update', default=False, validators=[Optional()])
37+
force_creation = BooleanField(u'Force creation', default=False, validators=[Optional()])
3438
submit = SubmitField(u'submit')
3539

3640
def __init__(self, packages=[]):

tracker/templates/_formhelpers.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@
159159
{%- endmacro -%}
160160

161161
{%- macro label_from_model(model) -%}
162-
{%- if model.__class__.__name__ == 'CVEGroupVersion' %}AVG-{% endif %}{{ model.id }}
162+
{%- if model.__class__.__name__ in ['CVEGroupVersion', 'CVEGroup'] %}AVG-{% endif %}{{ model.id }}
163163
{%- endmacro -%}
164164

165165
{%- macro link_to_model(model) -%}

tracker/templates/form/advisory.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
{% block content %}
33
<h1>{{ title }}</h1>
44
<div class="wide size">
5+
{%- if concurrent_modification %}
6+
{%- include 'log/advisory_log_table.html' %}
7+
{%- endif %}
58
<form action="" method="post" name="edit-advisory">
69
{{ form.hidden_tag() }}
710
<div class="row">
@@ -19,6 +22,13 @@ <h1>{{ title }}</h1>
1922
{{ render_field(form.impact, class='full size', maxlength=Advisory.IMPACT_LENGTH, placeholder='Overall impact...', indent=7, autofocus='') }}
2023
</div>
2124
</div>
25+
{%- if concurrent_modification %}
26+
<div class="row">
27+
<div class="one-quarter column">
28+
{{ render_checkbox(form.force_submit, indent=7) }}
29+
</div>
30+
</div>
31+
{%- endif %}
2232
{{ form.edit(class='button-primary', accesskey='s') }}
2333
</form>
2434
</div>

tracker/templates/form/cve.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
{% block content %}
33
<h1>{{ title }}</h1>
44
<div class="wide size">
5+
{%- if concurrent_modification %}
6+
{%- include 'log/cve_log_table.html' %}
7+
{%- endif %}
58
<form action="{{ action }}" method="post" name="add">
69
{{ form.hidden_tag() }}
710
<div class="row">
@@ -23,6 +26,13 @@ <h1>{{ title }}</h1>
2326
{{ render_field(form.description, class='full size', maxlength=CVE.DESCRIPTION_LENGTH, placeholder='Detailed description...', indent=5) }}
2427
{{ render_field(form.reference, class='full size', maxlength=CVE.REFERENCES_LENGTH, placeholder='Relevant external references...', indent=5) }}
2528
{{ render_field(form.notes, class='full size', maxlength=CVE.NOTES_LENGTH, placeholder='Internal side notes...', indent=5) }}
29+
{%- if concurrent_modification %}
30+
<div class="row">
31+
<div class="one-quarter column">
32+
{{ render_checkbox(form.force_submit, indent=7) }}
33+
</div>
34+
</div>
35+
{%- endif %}
2636
{{ form.submit(class='button-primary', accesskey='s') }}
2737
</form>
2838
</div>

tracker/templates/form/group.html

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
{% block content %}
33
<h1>{{ title }}</h1>
44
<div class="wide size">
5+
{%- if concurrent_modification %}
6+
{%- include 'log/group_log_table.html' %}
7+
{%- endif %}
58
<form action="{{ action }}" method="post" name="add">
69
{{ form.hidden_tag() }}
710
<div class="row">
@@ -32,9 +35,14 @@ <h1>{{ title }}</h1>
3235
<div class="one-quarter column">
3336
{{ render_checkbox(form.advisory_qualified, indent=7) }}
3437
</div>
35-
{%- if show_force %}
38+
{%- if concurrent_modification %}
3639
<div class="one-quarter column">
37-
{{ render_checkbox(form.force_submit, indent=7) }}
40+
{{ render_checkbox(form.force_update, indent=7) }}
41+
</div>
42+
{%- endif %}
43+
{%- if show_force_creation %}
44+
<div class="one-quarter column">
45+
{{ render_checkbox(form.force_creation, indent=7) }}
3846
</div>
3947
{%- endif %}
4048
</div>

0 commit comments

Comments
 (0)