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:
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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">
|
||||||
|
41
netbox/templates/secrets/inc/assigned_secrets.html
Normal file
41
netbox/templates/secrets/inc/assigned_secrets.html
Normal 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>
|
@ -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>
|
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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,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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user