diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 871bae6e1..56a135d3f 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -13,8 +13,7 @@ from django.views.generic import View from dcim.models import Device from utilities.paginator import EnhancedPaginator from utilities.views import ( - BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, BulkImportView2, ObjectDeleteView, ObjectEditView, - ObjectListView, + BulkAddView, BulkDeleteView, BulkEditView, BulkImportView2, ObjectDeleteView, ObjectEditView, ObjectListView, ) from . import filters, forms, tables from .models import ( diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index b8e165804..806c9d50c 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -7,7 +7,7 @@ from django import forms from django.db.models import Count from dcim.models import Device -from utilities.forms import BootstrapMixin, BulkEditForm, BulkImportForm, CSVDataField, FilterChoiceField, SlugField +from utilities.forms import BootstrapMixin, BulkEditForm, CSVDataField2, FilterChoiceField, SlugField from .models import Secret, SecretRole, UserKey @@ -65,27 +65,37 @@ class SecretForm(BootstrapMixin, forms.ModelForm): }) -class SecretFromCSVForm(forms.ModelForm): - device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name', - error_messages={'invalid_choice': 'Device not found.'}) - role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), to_field_name='name', - error_messages={'invalid_choice': 'Invalid secret role.'}) - plaintext = forms.CharField() +class SecretCSVForm(forms.ModelForm): + device = forms.ModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Device name', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + role = forms.ModelChoiceField( + queryset=SecretRole.objects.all(), + to_field_name='name', + help_text='Name of assigned role', + error_messages={ + 'invalid_choice': 'Invalid secret role.', + } + ) + plaintext = forms.CharField( + help_text='Plaintext secret data' + ) class Meta: model = Secret fields = ['device', 'role', 'name', 'plaintext'] def save(self, *args, **kwargs): - s = super(SecretFromCSVForm, self).save(*args, **kwargs) + s = super(SecretCSVForm, self).save(*args, **kwargs) s.plaintext = str(self.cleaned_data['plaintext']) return s -class SecretImportForm(BootstrapMixin, BulkImportForm): - csv = CSVDataField(csv_form=SecretFromCSVForm, widget=forms.Textarea(attrs={'class': 'requires-session-key'})) - - class SecretBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput) role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), required=False) diff --git a/netbox/secrets/urls.py b/netbox/secrets/urls.py index 4961b2c82..b28198a2f 100644 --- a/netbox/secrets/urls.py +++ b/netbox/secrets/urls.py @@ -16,7 +16,7 @@ urlpatterns = [ # Secrets url(r'^secrets/$', views.SecretListView.as_view(), name='secret_list'), - url(r'^secrets/import/$', views.secret_import, name='secret_import'), + url(r'^secrets/import/$', views.SecretBulkImportView.as_view(), name='secret_import'), url(r'^secrets/edit/$', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'), url(r'^secrets/delete/$', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'), url(r'^secrets/(?P\d+)/$', views.SecretView.as_view(), name='secret'), diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index d2427dd73..c6fe939aa 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -12,7 +12,9 @@ from django.utils.decorators import method_decorator from django.views.generic import View from dcim.models import Device -from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView +from utilities.views import ( + BulkDeleteView, BulkEditView, BulkImportView2, ObjectDeleteView, ObjectEditView, ObjectListView, +) from . import filters, forms, tables from .decorators import userkey_required from .models import SecretRole, Secret, SessionKey @@ -185,58 +187,50 @@ class SecretDeleteView(PermissionRequiredMixin, ObjectDeleteView): default_return_url = 'secrets:secret_list' -@permission_required('secrets.add_secret') -@userkey_required() -def secret_import(request): +class SecretBulkImportView(BulkImportView2): + permission_required = 'ipam.add_vlan' + model_form = forms.SecretCSVForm + table = tables.SecretTable + default_return_url = 'secrets:secret_list' - session_key = request.COOKIES.get('session_key', None) + master_key = None - if request.method == 'POST': - form = forms.SecretImportForm(request.POST) + def _save_obj(self, obj_form): + """ + Encrypt each object before saving it to the database. + """ + obj = obj_form.save(commit=False) + obj.encrypt(self.master_key) + obj.save() + return obj - if session_key is None: - form.add_error(None, "No session key was provided with the request. Unable to encrypt secret data.") + def post(self, request): - if form.is_valid(): + # Grab the session key from cookies. + session_key = request.COOKIES.get('session_key') + if session_key: - new_secrets = [] - - session_key = base64.b64decode(session_key) - master_key = None + # Attempt to derive the master key using the provided session key. try: sk = SessionKey.objects.get(userkey__user=request.user) - master_key = sk.get_master_key(session_key) + self.master_key = sk.get_master_key(base64.b64decode(session_key)) except SessionKey.DoesNotExist: - form.add_error(None, "No session key found for this user.") + messages.error(request, "No session key found for this user.") - if master_key is None: - form.add_error(None, "Invalid private key! Unable to encrypt secret data.") + if self.master_key is not None: + return super(SecretBulkImportView, self).post(request) else: - try: - with transaction.atomic(): - for secret in form.cleaned_data['csv']: - secret.encrypt(master_key) - secret.save() - new_secrets.append(secret) + messages.error(request, "Invalid private key! Unable to encrypt secret data.") - table = tables.SecretTable(new_secrets) - messages.success(request, "Imported {} new secrets.".format(len(new_secrets))) + else: + messages.error(request, "No session key was provided with the request. Unable to encrypt secret data.") - return render(request, 'import_success.html', { - 'table': table, - 'return_url': 'secrets:secret_list', - }) - - except IntegrityError as e: - form.add_error('csv', "Record {}: {}".format(len(new_secrets) + 1, e.__cause__)) - - else: - form = forms.SecretImportForm() - - return render(request, 'secrets/secret_import.html', { - 'form': form, - 'return_url': 'secrets:secret_list', - }) + return render(request, self.template_name, { + 'form': self._import_form(request.POST), + 'fields': self.model_form().fields, + 'obj_type': self.model_form._meta.model._meta.verbose_name, + 'return_url': self.default_return_url, + }) class SecretBulkEditView(PermissionRequiredMixin, BulkEditView): diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index feaec38be..a292f68cd 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -10,7 +10,7 @@ from circuits.models import Circuit from dcim.models import Site, Rack, Device from ipam.models import IPAddress, Prefix, VLAN, VRF from utilities.views import ( - BulkDeleteView, BulkEditView, BulkImportView, BulkImportView2, ObjectDeleteView, ObjectEditView, ObjectListView, + BulkDeleteView, BulkEditView, BulkImportView2, ObjectDeleteView, ObjectEditView, ObjectListView, ) from .models import Tenant, TenantGroup from . import filters, forms, tables diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index e06b900c0..766b0676d 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -448,6 +448,12 @@ class BulkImportView2(View): return ImportForm(*args, **kwargs) + def _save_obj(self, obj_form): + """ + Provide a hook to modify the object immediately before saving it (e.g. to encrypt secret data). + """ + return obj_form.save() + def get(self, request): return render(request, self.template_name, { @@ -471,7 +477,7 @@ class BulkImportView2(View): for row, data in enumerate(form.cleaned_data['csv'], start=1): obj_form = self.model_form(data) if obj_form.is_valid(): - obj = obj_form.save() + obj = self._save_obj(obj_form) new_objs.append(obj) else: for field, err in obj_form.errors.items(): @@ -501,9 +507,6 @@ class BulkImportView2(View): 'return_url': self.default_return_url, }) - def save_obj(self, obj): - obj.save() - class BulkEditView(View): """