diff --git a/docs/data-model/secrets.md b/docs/data-model/secrets.md index 425e28df4..76e066ab2 100644 --- a/docs/data-model/secrets.md +++ b/docs/data-model/secrets.md @@ -2,7 +2,7 @@ # Secret -A secret represents a single credential or other string which must be stored securely. Each secret is assigned to a parent object with NetBox, such as a device. The plaintext value of a secret is encrypted to a ciphertext immediately prior to storage within the database using a 256-bit AES master key. A SHA256 hash of the plaintext is also stored along with each ciphertext to validate the decrypted plaintext. +A secret represents a single credential or other string which must be stored securely. Each secret is assigned to a device within NetBox. The plaintext value of a secret is encrypted to a ciphertext immediately prior to storage within the database using a 256-bit AES master key. A SHA256 hash of the plaintext is also stored along with each ciphertext to validate the decrypted plaintext. Each secret can also store an optional name parameter, which is not encrypted. This may be useful for storing user names. diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 14f1a13e6..39040257d 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1,6 +1,5 @@ from collections import OrderedDict -from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.core.validators import MinValueValidator @@ -8,7 +7,6 @@ from django.db import models from django.db.models import Q, ObjectDoesNotExist from extras.rpc import RPC_CLIENTS -from secrets.models import Secret from utilities.fields import NullableCharField @@ -420,7 +418,6 @@ class Device(models.Model): primary_ip = models.OneToOneField('ipam.IPAddress', related_name='primary_for', on_delete=models.SET_NULL, blank=True, null=True, verbose_name='Primary IP') ro_snmp = models.CharField(max_length=50, blank=True, verbose_name='SNMP (RO)') comments = models.TextField(blank=True) - secrets = GenericRelation(Secret) class Meta: ordering = ['name'] diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index f328237b0..c7b55ee80 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -70,8 +70,7 @@ urlpatterns = [ url(r'^devices/(?P\d+)/inventory/$', views.device_inventory, name='device_inventory'), url(r'^devices/(?P\d+)/lldp-neighbors/$', views.device_lldp_neighbors, name='device_lldp_neighbors'), url(r'^devices/(?P\d+)/ip-addresses/assign/$', views.ipaddress_assign, name='ipaddress_assign'), - url(r'^devices/(?P\d+)/add-secret/$', secret_add, {'parent_model': 'dcim.Device'}, - name='device_addsecret'), + url(r'^devices/(?P\d+)/add-secret/$', secret_add, name='device_addsecret'), # Console ports url(r'^devices/(?P\d+)/console-ports/add/$', views.consoleport_add, name='consoleport_add'), diff --git a/netbox/secrets/admin.py b/netbox/secrets/admin.py index d9a425087..87d894fa1 100644 --- a/netbox/secrets/admin.py +++ b/netbox/secrets/admin.py @@ -66,6 +66,6 @@ class SecretRoleAdmin(admin.ModelAdmin): @admin.register(Secret) class SecretAdmin(admin.ModelAdmin): - list_display = ['parent', 'role', 'name', 'created', 'last_modified'] - fields = ['parent', 'role', 'name', 'hash', 'created', 'last_modified'] - readonly_fields = ['parent', 'hash', 'created', 'last_modified'] + list_display = ['device', 'role', 'name', 'created', 'last_modified'] + fields = ['device', 'role', 'name', 'hash', 'created', 'last_modified'] + readonly_fields = ['device', 'hash', 'created', 'last_modified'] diff --git a/netbox/secrets/api/serializers.py b/netbox/secrets/api/serializers.py index 462666fad..2c016a5a7 100644 --- a/netbox/secrets/api/serializers.py +++ b/netbox/secrets/api/serializers.py @@ -1,5 +1,6 @@ from rest_framework import serializers +from dcim.api.serializers import DeviceNestedSerializer from secrets.models import Secret, SecretRole @@ -24,13 +25,13 @@ class SecretRoleNestedSerializer(SecretRoleSerializer): # Secrets # -# TODO: Serialize parent info class SecretSerializer(serializers.ModelSerializer): + device = DeviceNestedSerializer() role = SecretRoleNestedSerializer() class Meta: model = Secret - fields = ['id', 'role', 'name', 'hash', 'created', 'last_modified'] + fields = ['id', 'device', 'role', 'name', 'hash', 'created', 'last_modified'] class SecretNestedSerializer(SecretSerializer): diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index a0f4d693c..787ac7407 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -2,10 +2,10 @@ from Crypto.Cipher import PKCS1_OAEP from Crypto.PublicKey import RSA from django import forms -from django.apps import apps from django.db.models import Count -from utilities.forms import BootstrapMixin, ConfirmationForm, CSVDataField +from dcim.models import Device +from utilities.forms import BootstrapMixin, BulkImportForm, ConfirmationForm, CSVDataField from .models import Secret, SecretRole, UserKey @@ -53,51 +53,26 @@ class SecretForm(forms.ModelForm, BootstrapMixin): class SecretFromCSVForm(forms.ModelForm): - parent_name = forms.CharField() + 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 Meta: model = Secret - fields = ['parent_name', 'role', 'name', 'plaintext'] + fields = ['device', 'role', 'name', 'plaintext'] + + def save(self, *args, **kwargs): + s = super(SecretFromCSVForm, self).save(*args, **kwargs) + s.plaintext = str(self.cleaned_data['plaintext']) + return s -class SecretImportForm(forms.Form, BootstrapMixin): +class SecretImportForm(BulkImportForm, BootstrapMixin): private_key = forms.CharField(widget=forms.HiddenInput()) - parent_type = forms.ChoiceField(label='Parent Type', choices=( - ('dcim.Device', 'Device'), - )) csv = CSVDataField(csv_form=SecretFromCSVForm) - def clean(self): - parent_type = self.cleaned_data.get('parent_type') - records = self.cleaned_data.get('csv') - if not records or not parent_type: - return - - secrets = [] - parent_cls = apps.get_model(parent_type) - - for i, record in enumerate(records, start=1): - secret_form = SecretFromCSVForm(data=record) - if secret_form.is_valid(): - s = secret_form.save(commit=False) - # Set parent - try: - s.parent = parent_cls.objects.get(name=secret_form.cleaned_data['parent_name']) - except parent_cls.DoesNotExist: - self.add_error('csv', "Invalid parent object ({})".format(secret_form.cleaned_data['parent_name'])) - # Set plaintext - s.plaintext = str(secret_form.cleaned_data['plaintext']) - secrets.append(s) - else: - for field, errors in secret_form.errors.items(): - for e in errors: - self.add_error('csv', "Record {} {}: {}".format(i, field, e)) - - self.cleaned_data['csv'] = secrets - class SecretBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput) diff --git a/netbox/secrets/migrations/0002_auto_20160321_1448.py b/netbox/secrets/migrations/0002_auto_20160321_1448.py new file mode 100644 index 000000000..6087cdcc3 --- /dev/null +++ b/netbox/secrets/migrations/0002_auto_20160321_1448.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.1 on 2016-03-21 14:48 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0003_auto_20160304_1642'), + ('secrets', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='secret', + name='content_type', + ), + migrations.RemoveField( + model_name='secret', + name='object_id', + ), + migrations.AddField( + model_name='secret', + name='device', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='secrets', to='dcim.Device'), + preserve_default=False, + ), + ] diff --git a/netbox/secrets/migrations/0003_auto_20160321_1524.py b/netbox/secrets/migrations/0003_auto_20160321_1524.py new file mode 100644 index 000000000..db74af79b --- /dev/null +++ b/netbox/secrets/migrations/0003_auto_20160321_1524.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.1 on 2016-03-21 15:24 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('secrets', '0002_auto_20160321_1448'), + ] + + operations = [ + migrations.AlterModelOptions( + name='secret', + options={'ordering': ['device', 'role', 'name'], 'permissions': (('view_secret', 'Can view secrets'),)}, + ), + ] diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index 21f726357..6bcc5995a 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -5,13 +5,13 @@ from Crypto.PublicKey import RSA from django.conf import settings from django.contrib.auth.hashers import make_password, check_password from django.contrib.auth.models import User -from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.db import models from django.utils.encoding import force_bytes +from dcim.models import Device + def generate_master_key(): """ @@ -176,9 +176,7 @@ class Secret(models.Model): A secret string of up to 255 bytes in length, stored as both an AES256-encrypted ciphertext and an irreversible salted SHA256 hash (for plaintext validation). """ - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) - object_id = models.PositiveIntegerField() - parent = GenericForeignKey('content_type', 'object_id') + device = models.ForeignKey(Device, related_name='secrets') role = models.ForeignKey('SecretRole', related_name='secrets', on_delete=models.PROTECT) name = models.CharField(max_length=100, blank=True) ciphertext = models.BinaryField(editable=False, max_length=65568) # 16B IV + 2B pad length + {62-65550}B padded @@ -189,7 +187,7 @@ class Secret(models.Model): plaintext = None class Meta: - ordering = ['role', 'name'] + ordering = ['device', 'role', 'name'] permissions = ( ('view_secret', "Can view secrets"), ) @@ -199,8 +197,8 @@ class Secret(models.Model): super(Secret, self).__init__(*args, **kwargs) def __unicode__(self): - if self.role and self.parent: - return "{} for {}".format(self.role, self.parent) + if self.role and self.device: + return "{} for {}".format(self.role, self.device) return "Secret" def get_absolute_url(self): diff --git a/netbox/secrets/tables.py b/netbox/secrets/tables.py index b299ebd7d..f1d1bfd5b 100644 --- a/netbox/secrets/tables.py +++ b/netbox/secrets/tables.py @@ -9,14 +9,14 @@ from .models import Secret # class SecretTable(tables.Table): - parent = tables.LinkColumn('secrets:secret', args=[Accessor('pk')], verbose_name='Parent') + device = tables.LinkColumn('secrets:secret', args=[Accessor('pk')], verbose_name='Device') role = tables.Column(verbose_name='Role') name = tables.Column(verbose_name='Name') last_modified = tables.DateTimeColumn(verbose_name='Last modified') class Meta: model = Secret - fields = ('parent', 'role', 'name', 'last_modified') + fields = ('device', 'role', 'name', 'last_modified') empty_text = "No secrets found." attrs = { 'class': 'table table-hover', @@ -28,4 +28,4 @@ class SecretBulkEditTable(SecretTable): class Meta(SecretTable.Meta): model = None # django_tables2 bugfix - fields = ('pk', 'parent', 'role', 'name') + fields = ('pk', 'device', 'role', 'name') diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 4949e5d50..669847ec0 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -1,4 +1,3 @@ -from django.apps import apps from django.contrib import messages from django.contrib.auth.decorators import permission_required, login_required from django.contrib.auth.mixins import PermissionRequiredMixin @@ -8,6 +7,7 @@ from django.db.models import ProtectedError from django.shortcuts import get_object_or_404, redirect, render from django.utils.decorators import method_decorator +from dcim.models import Device from utilities.error_handlers import handle_protectederror from utilities.forms import ConfirmationForm from utilities.views import BulkEditView, BulkDeleteView, ObjectListView @@ -25,7 +25,7 @@ from .tables import SecretTable, SecretBulkEditTable @method_decorator(login_required, name='dispatch') class SecretListView(ObjectListView): - queryset = Secret.objects.select_related('role').prefetch_related('parent') + queryset = Secret.objects.select_related('role').prefetch_related('device') filter = SecretFilter filter_form = SecretFilterForm table = SecretTable @@ -46,13 +46,12 @@ def secret(request, pk): @permission_required('secrets.add_secret') @userkey_required() -def secret_add(request, parent_model, parent_pk): +def secret_add(request, pk): - # Retrieve parent object - parent_cls = apps.get_model(parent_model) - parent = get_object_or_404(parent_cls, pk=parent_pk) + # Retrieve device + device = get_object_or_404(Device, pk=pk) - secret = Secret(parent=parent) + secret = Secret(device=device) uk = UserKey.objects.get(user=request.user) if request.method == 'POST': @@ -83,7 +82,7 @@ def secret_add(request, parent_model, parent_pk): return render(request, 'secrets/secret_edit.html', { 'secret': secret, 'form': form, - 'cancel_url': parent.get_absolute_url(), + 'cancel_url': device.get_absolute_url(), }) diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 45a33b1bd..01e254b55 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -132,7 +132,7 @@ {% csrf_token %} - + diff --git a/netbox/templates/secrets/secret_bulk_edit.html b/netbox/templates/secrets/secret_bulk_edit.html index 5bb5c2ef1..60ae9b22b 100644 --- a/netbox/templates/secrets/secret_bulk_edit.html +++ b/netbox/templates/secrets/secret_bulk_edit.html @@ -7,7 +7,7 @@ {% for secret in selected_objects %} - + diff --git a/netbox/templates/secrets/secret_edit.html b/netbox/templates/secrets/secret_edit.html index 8a719d804..5764ab364 100644 --- a/netbox/templates/secrets/secret_edit.html +++ b/netbox/templates/secrets/secret_edit.html @@ -30,9 +30,9 @@
Secret Attributes
- +
-

{{ secret.parent }}

+

{{ secret.device }}

{% render_field form.role %} diff --git a/netbox/templates/secrets/secret_import.html b/netbox/templates/secrets/secret_import.html index 694a0f613..d0c684a92 100644 --- a/netbox/templates/secrets/secret_import.html +++ b/netbox/templates/secrets/secret_import.html @@ -37,8 +37,8 @@
- - + +
ParentDevice - {{ secret.parent }} + {{ secret.device }}
{{ secret }}{{ secret.parent }}{{ secret.device }} {{ secret.role }} {{ secret.name }}
ParentName of the parent objectDeviceName of the parent device edge-router1