Przeglądaj źródła

Support assignment of secrets to virtual machines

Jeremy Stretch 5 lat temu
rodzic
commit
43f3e682c5

+ 29 - 10
netbox/secrets/forms.py

@@ -11,6 +11,7 @@ from utilities.forms import (
     BootstrapMixin, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
     BootstrapMixin, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
     SlugField, TagFilterField,
     SlugField, TagFilterField,
 )
 )
+from virtualization.models import VirtualMachine
 from .constants import *
 from .constants import *
 from .models import Secret, SecretRole, UserKey
 from .models import Secret, SecretRole, UserKey
 
 
@@ -64,8 +65,13 @@ class SecretRoleCSVForm(CSVModelForm):
 class SecretForm(BootstrapMixin, CustomFieldModelForm):
 class SecretForm(BootstrapMixin, CustomFieldModelForm):
     device = DynamicModelChoiceField(
     device = DynamicModelChoiceField(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
+        required=False,
         display_field='display_name'
         display_field='display_name'
     )
     )
+    virtual_machine = DynamicModelChoiceField(
+        queryset=VirtualMachine.objects.all(),
+        required=False
+    )
     plaintext = forms.CharField(
     plaintext = forms.CharField(
         max_length=SECRET_PLAINTEXT_MAX_LENGTH,
         max_length=SECRET_PLAINTEXT_MAX_LENGTH,
         required=False,
         required=False,
@@ -93,10 +99,21 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm):
     class Meta:
     class Meta:
         model = Secret
         model = Secret
         fields = [
         fields = [
-            'device', 'role', 'name', 'plaintext', 'plaintext2', 'tags',
+            'device', 'virtual_machine', 'role', 'name', 'plaintext', 'plaintext2', 'tags',
         ]
         ]
 
 
     def __init__(self, *args, **kwargs):
     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)
         super().__init__(*args, **kwargs)
 
 
         # A plaintext value is required when creating a new Secret
         # A plaintext value is required when creating a new Secret
@@ -105,21 +122,23 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm):
 
 
     def clean(self):
     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
         # Verify that the provided plaintext values match
         if self.cleaned_data['plaintext'] != self.cleaned_data['plaintext2']:
         if self.cleaned_data['plaintext'] != self.cleaned_data['plaintext2']:
             raise forms.ValidationError({
             raise forms.ValidationError({
                 'plaintext2': "The two given plaintext values do not match. Please check your input."
                 '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):
 class SecretCSVForm(CustomFieldModelCSVForm):

+ 0 - 7
netbox/secrets/views.py

@@ -82,13 +82,6 @@ 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

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

@@ -395,34 +395,8 @@
                                     </table>
                                     </table>
                                 </div>
                                 </div>
                             {% endif %}
                             {% 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 %}
                             {% endif %}
                             <div class="panel panel-default">
                             <div class="panel panel-default">
                                 <div class="panel-heading">
                                 <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>
                 </div>
             {% endif %}
             {% endif %}
             <div class="panel panel-default">
             <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">
                 <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.role %}
                     {% render_field form.name %}
                     {% render_field form.name %}
                     {% render_field form.userkeys %}
                     {% render_field form.userkeys %}

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

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

+ 6 - 0
netbox/virtualization/models.py

@@ -270,6 +270,12 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
     comments = models.TextField(
     comments = models.TextField(
         blank=True
         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)
     tags = TaggableManager(through=TaggedItem)
 
 
     objects = RestrictedQuerySet.as_manager()
     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 extras.views import ObjectConfigContextView
 from ipam.models import IPAddress, Service
 from ipam.models import IPAddress, Service
 from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
 from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
+from secrets.models import Secret
 from utilities.utils import get_subquery
 from utilities.utils import get_subquery
 from utilities.views import (
 from utilities.views import (
     BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, BulkRenameView, ComponentCreateView,
     BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, BulkRenameView, ComponentCreateView,
@@ -240,23 +241,30 @@ class VirtualMachineView(ObjectView):
     queryset = VirtualMachine.objects.prefetch_related('tenant__group')
     queryset = VirtualMachine.objects.prefetch_related('tenant__group')
 
 
     def get(self, request, pk):
     def get(self, request, pk):
-
         virtualmachine = get_object_or_404(self.queryset, pk=pk)
         virtualmachine = get_object_or_404(self.queryset, pk=pk)
+
+        # Interfaces
         interfaces = VMInterface.objects.restrict(request.user, 'view').filter(
         interfaces = VMInterface.objects.restrict(request.user, 'view').filter(
             virtual_machine=virtualmachine
             virtual_machine=virtualmachine
         ).prefetch_related(
         ).prefetch_related(
             Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user))
             Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user))
         )
         )
+
+        # Services
         services = Service.objects.restrict(request.user, 'view').filter(
         services = Service.objects.restrict(request.user, 'view').filter(
             virtual_machine=virtualmachine
             virtual_machine=virtualmachine
         ).prefetch_related(
         ).prefetch_related(
             Prefetch('ipaddresses', queryset=IPAddress.objects.restrict(request.user))
             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', {
         return render(request, 'virtualization/virtualmachine.html', {
             'virtualmachine': virtualmachine,
             'virtualmachine': virtualmachine,
             'interfaces': interfaces,
             'interfaces': interfaces,
             'services': services,
             'services': services,
+            'secrets': secrets,
         })
         })