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

#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(
     images = GenericRelation(
         to='extras.ImageAttachment'
         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)
     tags = TaggableManager(through=TaggedItem)
 
 
     objects = RestrictedQuerySet.as_manager()
     objects = RestrictedQuerySet.as_manager()

+ 0 - 10
netbox/secrets/filters.py

@@ -35,16 +35,6 @@ class SecretFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterS
         to_field_name='slug',
         to_field_name='slug',
         label='Role (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()
     tag = TagFilter()
 
 
     class Meta:
     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.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 Group, User
 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.core.exceptions import ValidationError
 from django.db import models
 from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.encoding import force_bytes
 from django.utils.encoding import force_bytes
 from taggit.managers import TaggableManager
 from taggit.managers import TaggableManager
 
 
-from dcim.models import Device
 from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
 from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
 from extras.utils import extras_features
 from extras.utils import extras_features
 from utilities.querysets import RestrictedQuerySet
 from utilities.querysets import RestrictedQuerySet
@@ -276,17 +277,26 @@ class SecretRole(ChangeLoggedModel):
 class Secret(ChangeLoggedModel, CustomFieldModel):
 class Secret(ChangeLoggedModel, CustomFieldModel):
     """
     """
     A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible
     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 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.
     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(
     role = models.ForeignKey(
         to='secrets.SecretRole',
         to='secrets.SecretRole',
@@ -310,34 +320,26 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
     objects = RestrictedQuerySet.as_manager()
     objects = RestrictedQuerySet.as_manager()
 
 
     plaintext = None
     plaintext = None
-    csv_headers = ['device', 'role', 'name', 'plaintext']
+    csv_headers = ['assigned_object_type', 'assigned_object_id', 'role', 'name', 'plaintext']
 
 
     class Meta:
     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):
     def __init__(self, *args, **kwargs):
         self.plaintext = kwargs.pop('plaintext', None)
         self.plaintext = kwargs.pop('plaintext', None)
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
     def __str__(self):
     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):
     def get_absolute_url(self):
         return reverse('secrets:secret', args=[self.pk])
         return reverse('secrets:secret', args=[self.pk])
 
 
     def to_csv(self):
     def to_csv(self):
         return (
         return (
-            self.device,
+            f'{self.assigned_object_type.app_label}.{self.assigned_object_type.model}',
+            self.assigned_object_id,
             self.role,
             self.role,
             self.name,
             self.name,
             self.plaintext or '',
             self.plaintext or '',

+ 6 - 3
netbox/secrets/tables.py

@@ -28,12 +28,15 @@ class SecretRoleTable(BaseTable):
 
 
 class SecretTable(BaseTable):
 class SecretTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
-    device = tables.LinkColumn()
+    assigned_object = tables.Column(
+        linkify=True,
+        verbose_name='Assigned object'
+    )
     tags = TagColumn(
     tags = TagColumn(
         url_name='secrets:secret_list'
         url_name='secrets:secret_list'
     )
     )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Secret
         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
     model_form = forms.SecretForm
     template_name = 'secrets/secret_edit.html'
     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):
     def dispatch(self, request, *args, **kwargs):
 
 
         # Check that the user has a valid UserKey
         # Check that the user has a valid UserKey

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

@@ -11,7 +11,8 @@
             <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><a href="{% url 'secrets:secret_list' %}?role={{ secret.role.slug }}">{{ secret.role }}</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>
             </ol>
         </div>
         </div>
     </div>
     </div>
@@ -50,9 +51,9 @@
             </div>
             </div>
             <table class="table table-hover panel-body">
             <table class="table table-hover panel-body">
                 <tr>
                 <tr>
-                    <td>Device</td>
+                    <td>Assigned object</td>
                     <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>
                     </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>