1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Support assignment of secrets to virtual machines

This commit is contained in:
Jeremy Stretch
2020-09-18 15:39:41 -04:00
parent ec095e58b7
commit 43f3e682c5
9 changed files with 109 additions and 64 deletions

View File

@ -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 def save(self, *args, **kwargs):
if Secret.objects.filter( # Set assigned object
device=self.cleaned_data['device'], self.instance.assigned_object = self.cleaned_data.get('device') or self.cleaned_data.get('virtual_machine')
role=self.cleaned_data['role'],
name=self.cleaned_data['name'] return super().save(*args, **kwargs)
).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError(
"Each secret assigned to a device must have a unique combination of role and name"
)
class SecretCSVForm(CustomFieldModelCSVForm): class SecretCSVForm(CustomFieldModelCSVForm):

View File

@ -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

View File

@ -395,34 +395,8 @@
</table> </table>
</div> </div>
{% endif %} {% endif %}
{% if request.user.is_authenticated %} {% if perms.secrets.view_secret %}
<div class="panel panel-default"> {% include 'secrets/inc/assigned_secrets.html' %}
<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>
{% endif %} {% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">

View File

@ -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>

View File

@ -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>

View File

@ -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">
{% 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 %} {% 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 %}

View File

@ -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 %}

View File

@ -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()

View File

@ -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,
}) })