Ver código fonte

Support assignment of secrets to virtual machines

Jeremy Stretch 5 anos atrás
pai
commit
43f3e682c5

+ 29 - 10
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):

+ 0 - 7
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

+ 2 - 28
netbox/templates/dcim/device.html

@@ -395,34 +395,8 @@
                                     </table>
                                 </div>
                             {% endif %}
-                            {% if request.user.is_authenticated %}
-                                <div class="panel panel-default">
-                                    <div class="panel-heading">
-                                        <strong>Secrets</strong>
-                                    </div>
-                                    {% if secrets %}
-                                        <table class="table table-hover panel-body">
-                                            {% for secret in secrets %}
-                                                {% include 'secrets/inc/secret_tr.html' %}
-                                            {% endfor %}
-                                        </table>
-                                    {% else %}
-                                        <div class="panel-body text-muted">
-                                            None found
-                                        </div>
-                                    {% endif %}
-                                    {% if perms.secrets.add_secret %}
-                                        <form id="secret_form">
-                                            {% csrf_token %}
-                                        </form>
-                                        <div class="panel-footer text-right noprint">
-                                            <a href="{% url 'secrets:secret_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
-                                                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
-                                                Add secret
-                                            </a>
-                                        </div>
-                                    {% endif %}
-                                </div>
+                            {% if perms.secrets.view_secret %}
+                                {% include 'secrets/inc/assigned_secrets.html' %}
                             {% endif %}
                             <div class="panel panel-default">
                                 <div class="panel-heading">

+ 41 - 0
netbox/templates/secrets/inc/assigned_secrets.html

@@ -0,0 +1,41 @@
+<div class="panel panel-default">
+    <div class="panel-heading">
+        <strong>Secrets</strong>
+    </div>
+    {% if secrets %}
+        <table class="table table-hover panel-body">
+            {% for secret in secrets %}
+                <tr>
+                    <td><a href="{% url 'secrets:secret' pk=secret.pk %}">{{ secret.role }}</a></td>
+                    <td>{{ secret.name }}</td>
+                    <td id="secret_{{ secret.pk }}">********</td>
+                    <td class="text-right noprint">
+                        <button class="btn btn-xs btn-success unlock-secret" secret-id="{{ secret.pk }}">
+                            <i class="fa fa-lock"></i> Unlock
+                        </button>
+                        <button class="btn btn-xs btn-default copy-secret collapse" secret-id="{{ secret.pk }}" data-clipboard-target="#secret_{{ secret.pk }}">
+                            <i class="fa fa-copy"></i> Copy
+                        </button>
+                        <button class="btn btn-xs btn-danger lock-secret collapse" secret-id="{{ secret.pk }}">
+                            <i class="fa fa-unlock-alt"></i> Lock
+                        </button>
+                    </td>
+                </tr>
+            {% endfor %}
+        </table>
+    {% else %}
+        <div class="panel-body text-muted">
+            None found
+        </div>
+    {% endif %}
+    {% if perms.secrets.add_secret %}
+        <form id="secret_form">
+            {% csrf_token %}
+        </form>
+        <div class="panel-footer text-right noprint">
+            <a href="{% url 'secrets:secret_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
+                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add secret
+            </a>
+        </div>
+    {% endif %}
+</div>

+ 0 - 16
netbox/templates/secrets/inc/secret_tr.html

@@ -1,16 +0,0 @@
-<tr>
-    <td><a href="{% url 'secrets:secret' pk=secret.pk %}">{{ secret.role }}</a></td>
-    <td>{{ secret.name }}</td>
-    <td id="secret_{{ secret.pk }}">********</td>
-    <td class="text-right noprint">
-        <button class="btn btn-xs btn-success unlock-secret" secret-id="{{ secret.pk }}">
-            <i class="fa fa-lock"></i> Unlock
-        </button>
-        <button class="btn btn-xs btn-default copy-secret collapse" secret-id="{{ secret.pk }}" data-clipboard-target="#secret_{{ secret.pk }}">
-            <i class="fa fa-copy"></i> Copy
-        </button>
-        <button class="btn btn-xs btn-danger lock-secret collapse" secret-id="{{ secret.pk }}">
-            <i class="fa fa-unlock-alt"></i> Lock
-        </button>
-    </td>
-</tr>

+ 17 - 2
netbox/templates/secrets/secret_edit.html

@@ -18,9 +18,24 @@
                 </div>
             {% endif %}
             <div class="panel panel-default">
-                <div class="panel-heading"><strong>Secret Attributes</strong></div>
+                <div class="panel-heading">
+                    <strong>Secret Assignment</strong>
+                </div>
                 <div class="panel-body">
-                    {% render_field form.device %}
+                    {% with vm_tab_active=form.initial.virtual_machine %}
+                        <ul class="nav nav-tabs" role="tablist">
+                            <li role="presentation"{% if not vm_tab_active %} class="active"{% endif %}><a href="#device" role="tab" data-toggle="tab">Device</a></li>
+                            <li role="presentation"{% if vm_tab_active %} class="active"{% endif %}><a href="#virtualmachine" role="tab" data-toggle="tab">Virtual Machine</a></li>
+                        </ul>
+                        <div class="tab-content">
+                            <div class="tab-pane{% if not vm_tab_active %} active{% endif %}" id="device">
+                                {% render_field form.device %}
+                            </div>
+                            <div class="tab-pane{% if vm_tab_active %} active{% endif %}" id="virtualmachine">
+                                {% render_field form.virtual_machine %}
+                            </div>
+                        </div>
+                    {% endwith %}
                     {% render_field form.role %}
                     {% render_field form.name %}
                     {% render_field form.userkeys %}

+ 5 - 0
netbox/templates/virtualization/virtualmachine.html

@@ -220,6 +220,9 @@
                 </tr>
             </table>
         </div>
+        {% if perms.secrets.view_secret %}
+            {% include 'secrets/inc/assigned_secrets.html' %}
+        {% endif %}
         <div class="panel panel-default">
             <div class="panel-heading">
                 <strong>Services</strong>
@@ -325,8 +328,10 @@
         {% endif %}
 	</div>
 </div>
+{% include 'secrets/inc/private_key_modal.html' %}
 {% endblock %}
 
 {% block javascript %}
 <script src="{% static 'js/interface_toggles.js' %}?v{{ settings.VERSION }}"></script>
+<script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script>
 {% endblock %}

+ 6 - 0
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()

+ 9 - 1
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,
         })