Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes #11732: Protect against errant overwriting of data via web UI forms #13332

Merged
merged 1 commit into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions netbox/netbox/forms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices
from extras.forms.mixins import CustomFieldsMixin, SavedFiltersMixin
from extras.models import CustomField, Tag
from utilities.forms import BootstrapMixin, CSVModelForm
from utilities.forms import BootstrapMixin, CSVModelForm, CheckLastUpdatedMixin
from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField

__all__ = (
Expand All @@ -17,7 +17,7 @@
)


class NetBoxModelForm(BootstrapMixin, CustomFieldsMixin, forms.ModelForm):
class NetBoxModelForm(BootstrapMixin, CheckLastUpdatedMixin, CustomFieldsMixin, forms.ModelForm):
"""
Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields.

Expand Down
43 changes: 42 additions & 1 deletion netbox/utilities/forms/mixins.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import time

from django import forms
from django.utils.translation import gettext_lazy as _

from .widgets import APISelect, APISelectMultiple, ClearableFileInput

__all__ = (
'BootstrapMixin',
'CheckLastUpdatedMixin',
)


class BootstrapMixin:
"""
Add the base Bootstrap CSS classes to form elements.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

Expand Down Expand Up @@ -60,3 +63,41 @@ def is_valid(self):
field.widget.attrs['class'] = f'{css} is-invalid'

return is_valid


class CheckLastUpdatedMixin(forms.Form):
"""
Checks whether the object being saved has been updated since the form was initialized. If so, validation fails.
This prevents a user from inadvertently overwriting any changes made to the object between when the form was
initialized and when it was submitted.

This validation does not apply to newly created objects, or if the `_init_time` field is not present in the form
data.
"""
_init_time = forms.DecimalField(
initial=time.time,
required=False,
widget=forms.HiddenInput()
)

def clean(self):
super().clean()

# Skip for absent or newly created instances
if not self.instance or not self.instance.pk:
return

# Skip if a form init time has not been specified
if not (form_init_time := self.cleaned_data.get('_init_time')):
return

# Skip if the object does not have a last_updated value
if not (last_updated := getattr(self.instance, 'last_updated', None)):
return

# Check that the submitted initialization time is not earlier than the object's modification time
if form_init_time < last_updated.timestamp():
raise forms.ValidationError(_(
"This object has been modified since the form was rendered. Please consult the object's change "
"log for details."
))
Loading