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

#1503: Initial work on generic secret assignments (WIP)

Jeremy Stretch 5 лет назад
Родитель
Сommit
ec095e58b7

+ 6 - 0
netbox/dcim/models/devices.py

@@ -582,6 +582,12 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
     images = GenericRelation(
         to='extras.ImageAttachment'
     )
+    secrets = GenericRelation(
+        to='secrets.Secret',
+        content_type_field='assigned_object_type',
+        object_id_field='assigned_object_id',
+        related_query_name='device'
+    )
     tags = TaggableManager(through=TaggedItem)
 
     objects = RestrictedQuerySet.as_manager()

+ 0 - 10
netbox/secrets/filters.py

@@ -35,16 +35,6 @@ class SecretFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterS
         to_field_name='slug',
         label='Role (slug)',
     )
-    device_id = django_filters.ModelMultipleChoiceFilter(
-        queryset=Device.objects.all(),
-        label='Device (ID)',
-    )
-    device = django_filters.ModelMultipleChoiceFilter(
-        field_name='device__name',
-        queryset=Device.objects.all(),
-        to_field_name='name',
-        label='Device (name)',
-    )
     tag = TagFilter()
 
     class Meta:

+ 49 - 0
netbox/secrets/migrations/0011_secret_generic_assignments.py

@@ -0,0 +1,49 @@
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+def device_to_generic_assignment(apps, schema_editor):
+    ContentType = apps.get_model('contenttypes', 'ContentType')
+    Secret = apps.get_model('secrets', 'Secret')
+
+    device_ct = ContentType.objects.get(app_label='dcim', model='device')
+    Secret.objects.update(assigned_object_type=device_ct, assigned_object_id=models.F('device_id'))
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contenttypes', '0002_remove_content_type_name'),
+        ('secrets', '0010_custom_field_data'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='secret',
+            options={'ordering': ('role', 'name', 'pk')},
+        ),
+        migrations.AddField(
+            model_name='secret',
+            name='assigned_object_id',
+            field=models.PositiveIntegerField(blank=True, null=True),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='secret',
+            name='assigned_object_type',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype'),
+            preserve_default=False,
+        ),
+        migrations.AlterUniqueTogether(
+            name='secret',
+            unique_together={('assigned_object_type', 'assigned_object_id', 'role', 'name')},
+        ),
+        migrations.RunPython(
+            code=device_to_generic_assignment,
+            reverse_code=migrations.RunPython.noop
+        ),
+        migrations.RemoveField(
+            model_name='secret',
+            name='device',
+        ),
+    ]

+ 24 - 22
netbox/secrets/models.py

@@ -6,13 +6,14 @@ from Crypto.Util import strxor
 from django.conf import settings
 from django.contrib.auth.hashers import make_password, check_password
 from django.contrib.auth.models import Group, User
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
 from django.db import models
 from django.urls import reverse
 from django.utils.encoding import force_bytes
 from taggit.managers import TaggableManager
 
-from dcim.models import Device
 from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
 from extras.utils import extras_features
 from utilities.querysets import RestrictedQuerySet
@@ -276,17 +277,26 @@ class SecretRole(ChangeLoggedModel):
 class Secret(ChangeLoggedModel, CustomFieldModel):
     """
     A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible
-    SHA-256 hash is stored along with the ciphertext for validation upon decryption. Each Secret is assigned to a
-    Device; Devices may have multiple Secrets associated with them. A name can optionally be defined along with the
-    ciphertext; this string is stored as plain text in the database.
+    SHA-256 hash is stored along with the ciphertext for validation upon decryption. Each Secret is assigned to exactly
+    one NetBox object, and objects may have multiple Secrets associated with them. A name can optionally be defined
+    along with the ciphertext; this string is stored as plain text in the database.
 
     A Secret can be up to 65,535 bytes (64KB - 1B) in length. Each secret string will be padded with random data to
     a minimum of 64 bytes during encryption in order to protect short strings from ciphertext analysis.
     """
-    device = models.ForeignKey(
-        to='dcim.Device',
-        on_delete=models.CASCADE,
-        related_name='secrets'
+    assigned_object_type = models.ForeignKey(
+        to=ContentType,
+        on_delete=models.PROTECT,
+        blank=True,
+        null=True
+    )
+    assigned_object_id = models.PositiveIntegerField(
+        blank=True,
+        null=True
+    )
+    assigned_object = GenericForeignKey(
+        ct_field='assigned_object_type',
+        fk_field='assigned_object_id'
     )
     role = models.ForeignKey(
         to='secrets.SecretRole',
@@ -310,34 +320,26 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
     objects = RestrictedQuerySet.as_manager()
 
     plaintext = None
-    csv_headers = ['device', 'role', 'name', 'plaintext']
+    csv_headers = ['assigned_object_type', 'assigned_object_id', 'role', 'name', 'plaintext']
 
     class Meta:
-        ordering = ['device', 'role', 'name']
-        unique_together = ['device', 'role', 'name']
+        ordering = ('role', 'name', 'pk')
+        unique_together = ('assigned_object_type', 'assigned_object_id', 'role', 'name')
 
     def __init__(self, *args, **kwargs):
         self.plaintext = kwargs.pop('plaintext', None)
         super().__init__(*args, **kwargs)
 
     def __str__(self):
-        try:
-            device = self.device
-        except Device.DoesNotExist:
-            device = None
-        if self.role and device and self.name:
-            return '{} for {} ({})'.format(self.role, self.device, self.name)
-        # Return role and device if no name is set
-        if self.role and device:
-            return '{} for {}'.format(self.role, self.device)
-        return 'Secret'
+        return self.name or 'Secret'
 
     def get_absolute_url(self):
         return reverse('secrets:secret', args=[self.pk])
 
     def to_csv(self):
         return (
-            self.device,
+            f'{self.assigned_object_type.app_label}.{self.assigned_object_type.model}',
+            self.assigned_object_id,
             self.role,
             self.name,
             self.plaintext or '',

+ 6 - 3
netbox/secrets/tables.py

@@ -28,12 +28,15 @@ class SecretRoleTable(BaseTable):
 
 class SecretTable(BaseTable):
     pk = ToggleColumn()
-    device = tables.LinkColumn()
+    assigned_object = tables.Column(
+        linkify=True,
+        verbose_name='Assigned object'
+    )
     tags = TagColumn(
         url_name='secrets:secret_list'
     )
 
     class Meta(BaseTable.Meta):
         model = Secret
-        fields = ('pk', 'device', 'role', 'name', 'last_updated', 'hash', 'tags')
-        default_columns = ('pk', 'device', 'role', 'name', 'last_updated')
+        fields = ('pk', 'assigned_object', 'role', 'name', 'last_updated', 'hash', 'tags')
+        default_columns = ('pk', 'assigned_object', 'role', 'name', 'last_updated')

+ 7 - 0
netbox/secrets/views.py

@@ -82,6 +82,13 @@ class SecretEditView(ObjectEditView):
     model_form = forms.SecretForm
     template_name = 'secrets/secret_edit.html'
 
+    def alter_obj(self, secret, request, args, kwargs):
+        if not secret.pk:
+            # Set assigned_object based on URL kwargs
+            model = kwargs.get('model')
+            secret.assigned_object = get_object_or_404(model, pk=kwargs['object_id'])
+        return secret
+
     def dispatch(self, request, *args, **kwargs):
 
         # Check that the user has a valid UserKey

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

@@ -11,7 +11,8 @@
             <ol class="breadcrumb">
                 <li><a href="{% url 'secrets:secret_list' %}">Secrets</a></li>
                 <li><a href="{% url 'secrets:secret_list' %}?role={{ secret.role.slug }}">{{ secret.role }}</a></li>
-                <li>{{ secret.device }}{% if secret.name %} ({{ secret.name }}){% endif %}</li>
+                <li><a href="{{ secret.assigned_object.get_absolute_url }}">{{ secret.assigned_object }}</a></li>
+                <li>{{ secret }}</li>
             </ol>
         </div>
     </div>
@@ -50,9 +51,9 @@
             </div>
             <table class="table table-hover panel-body">
                 <tr>
-                    <td>Device</td>
+                    <td>Assigned object</td>
                     <td>
-                        <a href="{% url 'dcim:device' pk=secret.device.pk %}">{{ secret.device }}</a>
+                        <a href="{{ secret.assigned_object.get_absolute_url }}">{{ secret.assigned_object }}</a>
                     </td>
                 </tr>
                 <tr>