Browse Source

Changed Secret parent from a GenericForeignKey to ForeignKey(Device)

Jeremy Stretch 9 years ago
parent
commit
a6108f2fa8

+ 1 - 1
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.
 

+ 0 - 3
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']

+ 1 - 2
netbox/dcim/urls.py

@@ -70,8 +70,7 @@ urlpatterns = [
     url(r'^devices/(?P<pk>\d+)/inventory/$', views.device_inventory, name='device_inventory'),
     url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.device_lldp_neighbors, name='device_lldp_neighbors'),
     url(r'^devices/(?P<pk>\d+)/ip-addresses/assign/$', views.ipaddress_assign, name='ipaddress_assign'),
-    url(r'^devices/(?P<parent_pk>\d+)/add-secret/$', secret_add, {'parent_model': 'dcim.Device'},
-        name='device_addsecret'),
+    url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
 
     # Console ports
     url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.consoleport_add, name='consoleport_add'),

+ 3 - 3
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']

+ 3 - 2
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):

+ 11 - 36
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)

+ 31 - 0
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,
+        ),
+    ]

+ 19 - 0
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'),)},
+        ),
+    ]

+ 6 - 8
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):

+ 3 - 3
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')

+ 7 - 8
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(),
     })
 
 

+ 1 - 1
netbox/templates/dcim/device.html

@@ -132,7 +132,7 @@
                         {% csrf_token %}
                     </form>
                     <div class="panel-footer text-right">
-                        <a href="{% url 'dcim:device_addsecret' parent_pk=device.pk %}" class="btn btn-xs btn-primary">
+                        <a href="{% url 'dcim:device_addsecret' pk=device.pk %}" class="btn btn-xs btn-primary">
                             <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
                             Add secret
                         </a>

+ 3 - 3
netbox/templates/secrets/secret.html

@@ -7,7 +7,7 @@
     <div class="col-md-12">
         <ol class="breadcrumb">
             <li><a href="{% url 'secrets:secret_list' %}">Secrets</a></li>
-            <li>{{ secret.parent }}</li>
+            <li><a href="{% url 'dcim:device' pk=secret.device.pk %}">{{ secret.device }}</a></li>
             <li>{{ secret.role }}{% if secret.name %} ({{ secret.name }}){% endif %}</li>
         </ol>
     </div>
@@ -35,9 +35,9 @@
             </div>
             <table class="table table-hover panel-body">
                 <tr>
-                    <td>Parent</td>
+                    <td>Device</td>
                     <td>
-                        <a href="{{ secret.parent.get_absolute_url }}">{{ secret.parent }}</a>
+                        <a href="{% url 'dcim:device' pk=secret.device.pk %}">{{ secret.device }}</a>
                     </td>
                 </tr>
                 <tr>

+ 1 - 1
netbox/templates/secrets/secret_bulk_edit.html

@@ -7,7 +7,7 @@
     {% for secret in selected_objects %}
         <tr>
             <td><a href="{% url 'secrets:secret' pk=secret.pk %}">{{ secret }}</a></td>
-            <td>{{ secret.parent }}</td>
+            <td>{{ secret.device }}</td>
             <td>{{ secret.role }}</td>
             <td>{{ secret.name }}</td>
         </tr>

+ 2 - 2
netbox/templates/secrets/secret_edit.html

@@ -30,9 +30,9 @@
                 <div class="panel-heading"><strong>Secret Attributes</strong></div>
                 <div class="panel-body">
                     <div class="form-group">
-                        <label class="col-md-3 control-label required">Parent</label>
+                        <label class="col-md-3 control-label required">Device</label>
                         <div class="col-md-9">
-                            <p class="form-control-static">{{ secret.parent }}</p>
+                            <p class="form-control-static">{{ secret.device }}</p>
                         </div>
                     </div>
                     {% render_field form.role %}

+ 2 - 2
netbox/templates/secrets/secret_import.html

@@ -37,8 +37,8 @@
 			</thead>
 			<tbody>
 				<tr>
-					<td>Parent</td>
-					<td>Name of the parent object</td>
+					<td>Device</td>
+					<td>Name of the parent device</td>
 					<td>edge-router1</td>
 				</tr>
 				<tr>