Просмотр исходного кода

Changed Secret parent from a GenericForeignKey to ForeignKey(Device)

Jeremy Stretch 10 лет назад
Родитель
Сommit
a6108f2fa8

+ 1 - 1
docs/data-model/secrets.md

@@ -2,7 +2,7 @@
 
 
 # Secret
 # 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.
 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 collections import OrderedDict
 
 
-from django.contrib.contenttypes.fields import GenericRelation
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.core.validators import MinValueValidator
 from django.core.validators import MinValueValidator
@@ -8,7 +7,6 @@ from django.db import models
 from django.db.models import Q, ObjectDoesNotExist
 from django.db.models import Q, ObjectDoesNotExist
 
 
 from extras.rpc import RPC_CLIENTS
 from extras.rpc import RPC_CLIENTS
-from secrets.models import Secret
 from utilities.fields import NullableCharField
 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')
     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)')
     ro_snmp = models.CharField(max_length=50, blank=True, verbose_name='SNMP (RO)')
     comments = models.TextField(blank=True)
     comments = models.TextField(blank=True)
-    secrets = GenericRelation(Secret)
 
 
     class Meta:
     class Meta:
         ordering = ['name']
         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+)/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+)/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<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
     # Console ports
     url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.consoleport_add, name='consoleport_add'),
     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)
 @admin.register(Secret)
 class SecretAdmin(admin.ModelAdmin):
 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 rest_framework import serializers
 
 
+from dcim.api.serializers import DeviceNestedSerializer
 from secrets.models import Secret, SecretRole
 from secrets.models import Secret, SecretRole
 
 
 
 
@@ -24,13 +25,13 @@ class SecretRoleNestedSerializer(SecretRoleSerializer):
 # Secrets
 # Secrets
 #
 #
 
 
-# TODO: Serialize parent info
 class SecretSerializer(serializers.ModelSerializer):
 class SecretSerializer(serializers.ModelSerializer):
+    device = DeviceNestedSerializer()
     role = SecretRoleNestedSerializer()
     role = SecretRoleNestedSerializer()
 
 
     class Meta:
     class Meta:
         model = Secret
         model = Secret
-        fields = ['id', 'role', 'name', 'hash', 'created', 'last_modified']
+        fields = ['id', 'device', 'role', 'name', 'hash', 'created', 'last_modified']
 
 
 
 
 class SecretNestedSerializer(SecretSerializer):
 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 Crypto.PublicKey import RSA
 
 
 from django import forms
 from django import forms
-from django.apps import apps
 from django.db.models import Count
 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
 from .models import Secret, SecretRole, UserKey
 
 
 
 
@@ -53,51 +53,26 @@ class SecretForm(forms.ModelForm, BootstrapMixin):
 
 
 
 
 class SecretFromCSVForm(forms.ModelForm):
 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',
     role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), to_field_name='name',
                                   error_messages={'invalid_choice': 'Invalid secret role.'})
                                   error_messages={'invalid_choice': 'Invalid secret role.'})
     plaintext = forms.CharField()
     plaintext = forms.CharField()
 
 
     class Meta:
     class Meta:
         model = Secret
         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())
     private_key = forms.CharField(widget=forms.HiddenInput())
-    parent_type = forms.ChoiceField(label='Parent Type', choices=(
-        ('dcim.Device', 'Device'),
-    ))
     csv = CSVDataField(csv_form=SecretFromCSVForm)
     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):
 class SecretBulkEditForm(forms.Form, BootstrapMixin):
     pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
     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.conf import settings
 from django.contrib.auth.hashers import make_password, check_password
 from django.contrib.auth.hashers import make_password, check_password
 from django.contrib.auth.models import User
 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.exceptions import ValidationError
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.db import models
 from django.db import models
 from django.utils.encoding import force_bytes
 from django.utils.encoding import force_bytes
 
 
+from dcim.models import Device
+
 
 
 def generate_master_key():
 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
     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).
     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)
     role = models.ForeignKey('SecretRole', related_name='secrets', on_delete=models.PROTECT)
     name = models.CharField(max_length=100, blank=True)
     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
     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
     plaintext = None
 
 
     class Meta:
     class Meta:
-        ordering = ['role', 'name']
+        ordering = ['device', 'role', 'name']
         permissions = (
         permissions = (
             ('view_secret', "Can view secrets"),
             ('view_secret', "Can view secrets"),
         )
         )
@@ -199,8 +197,8 @@ class Secret(models.Model):
         super(Secret, self).__init__(*args, **kwargs)
         super(Secret, self).__init__(*args, **kwargs)
 
 
     def __unicode__(self):
     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"
         return "Secret"
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):

+ 3 - 3
netbox/secrets/tables.py

@@ -9,14 +9,14 @@ from .models import Secret
 #
 #
 
 
 class SecretTable(tables.Table):
 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')
     role = tables.Column(verbose_name='Role')
     name = tables.Column(verbose_name='Name')
     name = tables.Column(verbose_name='Name')
     last_modified = tables.DateTimeColumn(verbose_name='Last modified')
     last_modified = tables.DateTimeColumn(verbose_name='Last modified')
 
 
     class Meta:
     class Meta:
         model = Secret
         model = Secret
-        fields = ('parent', 'role', 'name', 'last_modified')
+        fields = ('device', 'role', 'name', 'last_modified')
         empty_text = "No secrets found."
         empty_text = "No secrets found."
         attrs = {
         attrs = {
             'class': 'table table-hover',
             'class': 'table table-hover',
@@ -28,4 +28,4 @@ class SecretBulkEditTable(SecretTable):
 
 
     class Meta(SecretTable.Meta):
     class Meta(SecretTable.Meta):
         model = None  # django_tables2 bugfix
         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 import messages
 from django.contrib.auth.decorators import permission_required, login_required
 from django.contrib.auth.decorators import permission_required, login_required
 from django.contrib.auth.mixins import PermissionRequiredMixin
 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.shortcuts import get_object_or_404, redirect, render
 from django.utils.decorators import method_decorator
 from django.utils.decorators import method_decorator
 
 
+from dcim.models import Device
 from utilities.error_handlers import handle_protectederror
 from utilities.error_handlers import handle_protectederror
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.views import BulkEditView, BulkDeleteView, ObjectListView
 from utilities.views import BulkEditView, BulkDeleteView, ObjectListView
@@ -25,7 +25,7 @@ from .tables import SecretTable, SecretBulkEditTable
 
 
 @method_decorator(login_required, name='dispatch')
 @method_decorator(login_required, name='dispatch')
 class SecretListView(ObjectListView):
 class SecretListView(ObjectListView):
-    queryset = Secret.objects.select_related('role').prefetch_related('parent')
+    queryset = Secret.objects.select_related('role').prefetch_related('device')
     filter = SecretFilter
     filter = SecretFilter
     filter_form = SecretFilterForm
     filter_form = SecretFilterForm
     table = SecretTable
     table = SecretTable
@@ -46,13 +46,12 @@ def secret(request, pk):
 
 
 @permission_required('secrets.add_secret')
 @permission_required('secrets.add_secret')
 @userkey_required()
 @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)
     uk = UserKey.objects.get(user=request.user)
 
 
     if request.method == 'POST':
     if request.method == 'POST':
@@ -83,7 +82,7 @@ def secret_add(request, parent_model, parent_pk):
     return render(request, 'secrets/secret_edit.html', {
     return render(request, 'secrets/secret_edit.html', {
         'secret': secret,
         'secret': secret,
         'form': form,
         '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 %}
                         {% csrf_token %}
                     </form>
                     </form>
                     <div class="panel-footer text-right">
                     <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>
                             <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
                             Add secret
                             Add secret
                         </a>
                         </a>

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

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

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

@@ -7,7 +7,7 @@
     {% for secret in selected_objects %}
     {% for secret in selected_objects %}
         <tr>
         <tr>
             <td><a href="{% url 'secrets:secret' pk=secret.pk %}">{{ secret }}</a></td>
             <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.role }}</td>
             <td>{{ secret.name }}</td>
             <td>{{ secret.name }}</td>
         </tr>
         </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-heading"><strong>Secret Attributes</strong></div>
                 <div class="panel-body">
                 <div class="panel-body">
                     <div class="form-group">
                     <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">
                         <div class="col-md-9">
-                            <p class="form-control-static">{{ secret.parent }}</p>
+                            <p class="form-control-static">{{ secret.device }}</p>
                         </div>
                         </div>
                     </div>
                     </div>
                     {% render_field form.role %}
                     {% render_field form.role %}

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

@@ -37,8 +37,8 @@
 			</thead>
 			</thead>
 			<tbody>
 			<tbody>
 				<tr>
 				<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>
 					<td>edge-router1</td>
 				</tr>
 				</tr>
 				<tr>
 				<tr>