diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index 8f04edc5b..b391b59e6 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -11,6 +11,7 @@ from utilities.forms import ( BootstrapMixin, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, TagFilterField, ) +from virtualization.models import VirtualMachine from .constants import * from .models import Secret, SecretRole, UserKey @@ -64,8 +65,13 @@ class SecretRoleCSVForm(CSVModelForm): class SecretForm(BootstrapMixin, CustomFieldModelForm): device = DynamicModelChoiceField( queryset=Device.objects.all(), + required=False, display_field='display_name' ) + virtual_machine = DynamicModelChoiceField( + queryset=VirtualMachine.objects.all(), + required=False + ) plaintext = forms.CharField( max_length=SECRET_PLAINTEXT_MAX_LENGTH, required=False, @@ -93,10 +99,21 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm): class Meta: model = Secret fields = [ - 'device', 'role', 'name', 'plaintext', 'plaintext2', 'tags', + 'device', 'virtual_machine', 'role', 'name', 'plaintext', 'plaintext2', 'tags', ] def __init__(self, *args, **kwargs): + + # Initialize helper selectors + instance = kwargs.get('instance') + initial = kwargs.get('initial', {}).copy() + if instance: + if type(instance.assigned_object) is Device: + initial['device'] = instance.assigned_object + elif type(instance.assigned_object) is VirtualMachine: + initial['virtual_machine'] = instance.assigned_object + kwargs['initial'] = initial + super().__init__(*args, **kwargs) # A plaintext value is required when creating a new Secret @@ -105,21 +122,23 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm): def clean(self): + if not self.cleaned_data['device'] and not self.cleaned_data['virtual_machine']: + raise forms.ValidationError("Secrets must be assigned to a device or virtual machine.") + + if self.cleaned_data['device'] and self.cleaned_data['virtual_machine']: + raise forms.ValidationError("Cannot select both a device and virtual machine for secret assignment.") + # Verify that the provided plaintext values match if self.cleaned_data['plaintext'] != self.cleaned_data['plaintext2']: raise forms.ValidationError({ 'plaintext2': "The two given plaintext values do not match. Please check your input." }) - # Validate uniqueness - if Secret.objects.filter( - device=self.cleaned_data['device'], - role=self.cleaned_data['role'], - name=self.cleaned_data['name'] - ).exclude(pk=self.instance.pk).exists(): - raise forms.ValidationError( - "Each secret assigned to a device must have a unique combination of role and name" - ) + def save(self, *args, **kwargs): + # Set assigned object + self.instance.assigned_object = self.cleaned_data.get('device') or self.cleaned_data.get('virtual_machine') + + return super().save(*args, **kwargs) class SecretCSVForm(CustomFieldModelCSVForm): diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index e3d607755..2872616b8 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -82,13 +82,6 @@ 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/dcim/device.html b/netbox/templates/dcim/device.html index ff82a49e2..6f8ae69c6 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -395,34 +395,8 @@ {% endif %} - {% if request.user.is_authenticated %} -
-
- Secrets -
- {% if secrets %} - - {% for secret in secrets %} - {% include 'secrets/inc/secret_tr.html' %} - {% endfor %} -
- {% else %} -
- None found -
- {% endif %} - {% if perms.secrets.add_secret %} -
- {% csrf_token %} -
- - {% endif %} -
+ {% if perms.secrets.view_secret %} + {% include 'secrets/inc/assigned_secrets.html' %} {% endif %}
diff --git a/netbox/templates/secrets/inc/assigned_secrets.html b/netbox/templates/secrets/inc/assigned_secrets.html new file mode 100644 index 000000000..11ad5e75d --- /dev/null +++ b/netbox/templates/secrets/inc/assigned_secrets.html @@ -0,0 +1,41 @@ +
+
+ Secrets +
+ {% if secrets %} + + {% for secret in secrets %} + + + + + + + {% endfor %} +
{{ secret.role }}{{ secret.name }}******** + + + +
+ {% else %} +
+ None found +
+ {% endif %} + {% if perms.secrets.add_secret %} +
+ {% csrf_token %} +
+ + {% endif %} +
diff --git a/netbox/templates/secrets/inc/secret_tr.html b/netbox/templates/secrets/inc/secret_tr.html deleted file mode 100644 index 2af609289..000000000 --- a/netbox/templates/secrets/inc/secret_tr.html +++ /dev/null @@ -1,16 +0,0 @@ - - {{ secret.role }} - {{ secret.name }} - ******** - - - - - - diff --git a/netbox/templates/secrets/secret_edit.html b/netbox/templates/secrets/secret_edit.html index 0cb1eefef..d3c2f88dc 100644 --- a/netbox/templates/secrets/secret_edit.html +++ b/netbox/templates/secrets/secret_edit.html @@ -18,9 +18,24 @@
{% endif %}
-
Secret Attributes
+
+ Secret Assignment +
- {% render_field form.device %} + {% with vm_tab_active=form.initial.virtual_machine %} + +
+
+ {% render_field form.device %} +
+
+ {% render_field form.virtual_machine %} +
+
+ {% endwith %} {% render_field form.role %} {% render_field form.name %} {% render_field form.userkeys %} diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index ea33aa460..3f0a37b88 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -220,6 +220,9 @@
+ {% if perms.secrets.view_secret %} + {% include 'secrets/inc/assigned_secrets.html' %} + {% endif %}
Services @@ -325,8 +328,10 @@ {% endif %}
+{% include 'secrets/inc/private_key_modal.html' %} {% endblock %} {% block javascript %} + {% endblock %} diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index a492370ef..e81ee1e49 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -270,6 +270,12 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): comments = models.TextField( blank=True ) + secrets = GenericRelation( + to='secrets.Secret', + content_type_field='assigned_object_type', + object_id_field='assigned_object_id', + related_query_name='virtual_machine' + ) tags = TaggableManager(through=TaggedItem) objects = RestrictedQuerySet.as_manager() diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index a06a2e5ff..5e4b99553 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -9,6 +9,7 @@ from dcim.tables import DeviceTable from extras.views import ObjectConfigContextView from ipam.models import IPAddress, Service from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable +from secrets.models import Secret from utilities.utils import get_subquery from utilities.views import ( BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, BulkRenameView, ComponentCreateView, @@ -240,23 +241,30 @@ class VirtualMachineView(ObjectView): queryset = VirtualMachine.objects.prefetch_related('tenant__group') def get(self, request, pk): - virtualmachine = get_object_or_404(self.queryset, pk=pk) + + # Interfaces interfaces = VMInterface.objects.restrict(request.user, 'view').filter( virtual_machine=virtualmachine ).prefetch_related( Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)) ) + + # Services services = Service.objects.restrict(request.user, 'view').filter( virtual_machine=virtualmachine ).prefetch_related( Prefetch('ipaddresses', queryset=IPAddress.objects.restrict(request.user)) ) + # Secrets + secrets = Secret.objects.restrict(request.user, 'view').filter(virtual_machine=virtualmachine) + return render(request, 'virtualization/virtualmachine.html', { 'virtualmachine': virtualmachine, 'interfaces': interfaces, 'services': services, + 'secrets': secrets, })