diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 38ed84370..3d7963dbb 100644 --- a/netbox/dcim/models/devices.py +++ b/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() diff --git a/netbox/secrets/filters.py b/netbox/secrets/filters.py index 78f25952a..9ccc86de4 100644 --- a/netbox/secrets/filters.py +++ b/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: diff --git a/netbox/secrets/migrations/0011_secret_generic_assignments.py b/netbox/secrets/migrations/0011_secret_generic_assignments.py new file mode 100644 index 000000000..4758a9084 --- /dev/null +++ b/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', + ), + ] diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index 23a883103..2a14aef59 100644 --- a/netbox/secrets/models.py +++ b/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 '', diff --git a/netbox/secrets/tables.py b/netbox/secrets/tables.py index 5e8c5a8b4..7158b0b13 100644 --- a/netbox/secrets/tables.py +++ b/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') diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 2872616b8..e3d607755 100644 --- a/netbox/secrets/views.py +++ b/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 diff --git a/netbox/templates/secrets/secret.html b/netbox/templates/secrets/secret.html index 841d9843a..ce7c37c40 100644 --- a/netbox/templates/secrets/secret.html +++ b/netbox/templates/secrets/secret.html @@ -11,7 +11,8 @@ @@ -50,9 +51,9 @@ - +
DeviceAssigned object - {{ secret.device }} + {{ secret.assigned_object }}