mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-21 11:37:21 -06:00
Merge pull request #5151 from netbox-community/1503-secret-assignment
#1503: Extend secrets assignment to virtual machines
This commit is contained in:
commit
2b689239ae
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
||||||
|
* [#1503](https://github.com/netbox-community/netbox/issues/1503) - Allow assigment of secrets to virtual machines
|
||||||
* [#1692](https://github.com/netbox-community/netbox/issues/1692) - Allow assigment of inventory items to parent items in web UI
|
* [#1692](https://github.com/netbox-community/netbox/issues/1692) - Allow assigment of inventory items to parent items in web UI
|
||||||
* [#2179](https://github.com/netbox-community/netbox/issues/2179) - Support the assignment of multiple port numbers for services
|
* [#2179](https://github.com/netbox-community/netbox/issues/2179) - Support the assignment of multiple port numbers for services
|
||||||
* [#4956](https://github.com/netbox-community/netbox/issues/4956) - Include inventory items on primary device view
|
* [#4956](https://github.com/netbox-community/netbox/issues/4956) - Include inventory items on primary device view
|
||||||
@ -30,3 +31,4 @@
|
|||||||
* extras.ExportTemplate: The `template_language` field has been removed
|
* extras.ExportTemplate: The `template_language` field has been removed
|
||||||
* extras.Graph: This API endpoint has been removed (see #4349)
|
* extras.Graph: This API endpoint has been removed (see #4349)
|
||||||
* ipam.Service: Renamed `port` to `ports`; now holds a list of one or more port numbers
|
* ipam.Service: Renamed `port` to `ports`; now holds a list of one or more port numbers
|
||||||
|
* secrets.Secret: Removed `device` field; replaced with `assigned_object` generic foreign key. This may represent either a device or a virtual machine. Assign an object by setting `assigned_object_type` and `assigned_object_id`.
|
||||||
|
@ -582,6 +582,12 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
images = GenericRelation(
|
images = GenericRelation(
|
||||||
to='extras.ImageAttachment'
|
to='extras.ImageAttachment'
|
||||||
)
|
)
|
||||||
|
secrets = GenericRelation(
|
||||||
|
to='secrets.Secret',
|
||||||
|
content_type_field='assigned_object_type',
|
||||||
|
object_id_field='assigned_object_id',
|
||||||
|
related_query_name='device'
|
||||||
|
)
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from drf_yasg.utils import swagger_serializer_method
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from dcim.api.nested_serializers import NestedDeviceSerializer
|
|
||||||
from extras.api.customfields import CustomFieldModelSerializer
|
from extras.api.customfields import CustomFieldModelSerializer
|
||||||
from extras.api.serializers import TaggedObjectSerializer
|
from extras.api.serializers import TaggedObjectSerializer
|
||||||
|
from secrets.constants import SECRET_ASSIGNMENT_MODELS
|
||||||
from secrets.models import Secret, SecretRole
|
from secrets.models import Secret, SecretRole
|
||||||
from utilities.api import ValidatedModelSerializer
|
from utilities.api import ContentTypeField, ValidatedModelSerializer, get_serializer_for_model
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
|
|
||||||
@ -23,18 +25,27 @@ class SecretRoleSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
class SecretSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
class SecretSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secret-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secret-detail')
|
||||||
device = NestedDeviceSerializer()
|
assigned_object_type = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.filter(SECRET_ASSIGNMENT_MODELS)
|
||||||
|
)
|
||||||
|
assigned_object = serializers.SerializerMethodField(read_only=True)
|
||||||
role = NestedSecretRoleSerializer()
|
role = NestedSecretRoleSerializer()
|
||||||
plaintext = serializers.CharField()
|
plaintext = serializers.CharField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Secret
|
model = Secret
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'device', 'role', 'name', 'plaintext', 'hash', 'tags', 'custom_fields', 'created',
|
'id', 'url', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'role', 'name', 'plaintext',
|
||||||
'last_updated',
|
'hash', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
validators = []
|
validators = []
|
||||||
|
|
||||||
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
|
def get_assigned_object(self, obj):
|
||||||
|
serializer = get_serializer_for_model(obj.assigned_object, prefix='Nested')
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(obj.assigned_object, context=context).data
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
|
|
||||||
# Encrypt plaintext data using the master key provided from the view context
|
# Encrypt plaintext data using the master key provided from the view context
|
||||||
|
@ -46,9 +46,7 @@ class SecretRoleViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class SecretViewSet(ModelViewSet):
|
class SecretViewSet(ModelViewSet):
|
||||||
queryset = Secret.objects.prefetch_related(
|
queryset = Secret.objects.prefetch_related('role', 'tags')
|
||||||
'device__primary_ip4', 'device__primary_ip6', 'role', 'tags',
|
|
||||||
)
|
|
||||||
serializer_class = serializers.SecretSerializer
|
serializer_class = serializers.SecretSerializer
|
||||||
filterset_class = filters.SecretFilterSet
|
filterset_class = filters.SecretFilterSet
|
||||||
|
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Secrets
|
# Secrets
|
||||||
#
|
#
|
||||||
|
|
||||||
|
SECRET_ASSIGNMENT_MODELS = Q(
|
||||||
|
Q(app_label='dcim', model='device') |
|
||||||
|
Q(app_label='virtualization', model='virtualmachine')
|
||||||
|
)
|
||||||
|
|
||||||
SECRET_PLAINTEXT_MAX_LENGTH = 65535
|
SECRET_PLAINTEXT_MAX_LENGTH = 65535
|
||||||
|
@ -4,6 +4,7 @@ from django.db.models import Q
|
|||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
|
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||||
from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter
|
from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter
|
||||||
|
from virtualization.models import VirtualMachine
|
||||||
from .models import Secret, SecretRole
|
from .models import Secret, SecretRole
|
||||||
|
|
||||||
|
|
||||||
@ -35,16 +36,28 @@ class SecretFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterS
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Role (slug)',
|
label='Role (slug)',
|
||||||
)
|
)
|
||||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
queryset=Device.objects.all(),
|
|
||||||
label='Device (ID)',
|
|
||||||
)
|
|
||||||
device = django_filters.ModelMultipleChoiceFilter(
|
device = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='device__name',
|
field_name='device__name',
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
label='Device (name)',
|
label='Device (name)',
|
||||||
)
|
)
|
||||||
|
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='device',
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
label='Device (ID)',
|
||||||
|
)
|
||||||
|
virtual_machine = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='virtual_machine__name',
|
||||||
|
queryset=VirtualMachine.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
label='Virtual machine (name)',
|
||||||
|
)
|
||||||
|
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='virtual_machine',
|
||||||
|
queryset=VirtualMachine.objects.all(),
|
||||||
|
label='Virtual machine (ID)',
|
||||||
|
)
|
||||||
tag = TagFilter()
|
tag = TagFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from Crypto.Cipher import PKCS1_OAEP
|
from Crypto.Cipher import PKCS1_OAEP
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from extras.forms import (
|
from extras.forms import (
|
||||||
@ -11,6 +12,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 +66,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 +100,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,28 +123,31 @@ 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):
|
||||||
device = CSVModelChoiceField(
|
assigned_object_type = CSVModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
to_field_name='name',
|
limit_choices_to=SECRET_ASSIGNMENT_MODELS,
|
||||||
help_text='Assigned device'
|
to_field_name='model',
|
||||||
|
help_text='Side A type'
|
||||||
)
|
)
|
||||||
role = CSVModelChoiceField(
|
role = CSVModelChoiceField(
|
||||||
queryset=SecretRole.objects.all(),
|
queryset=SecretRole.objects.all(),
|
||||||
|
67
netbox/secrets/migrations/0011_secret_generic_assignments.py
Normal file
67
netbox/secrets/migrations/0011_secret_generic_assignments.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
def device_to_generic_assignment(apps, schema_editor):
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
Device = apps.get_model('dcim', 'Device')
|
||||||
|
Secret = apps.get_model('secrets', 'Secret')
|
||||||
|
|
||||||
|
device_ct = ContentType.objects.get_for_model(Device)
|
||||||
|
Secret.objects.update(assigned_object_type=device_ct, assigned_object_id=models.F('device_id'))
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('secrets', '0010_custom_field_data'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='secret',
|
||||||
|
options={'ordering': ('role', 'name', 'pk')},
|
||||||
|
),
|
||||||
|
|
||||||
|
# Add assigned_object type & ID fields
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='secret',
|
||||||
|
name='assigned_object_id',
|
||||||
|
field=models.PositiveIntegerField(blank=True, null=True),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='secret',
|
||||||
|
name='assigned_object_type',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='secret',
|
||||||
|
unique_together={('assigned_object_type', 'assigned_object_id', 'role', 'name')},
|
||||||
|
),
|
||||||
|
|
||||||
|
# Copy device assignments and delete device ForeignKey
|
||||||
|
migrations.RunPython(
|
||||||
|
code=device_to_generic_assignment,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='secret',
|
||||||
|
name='device',
|
||||||
|
),
|
||||||
|
|
||||||
|
# Remove blank/null from assigned_object fields
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='secret',
|
||||||
|
name='assigned_object_id',
|
||||||
|
field=models.PositiveIntegerField(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='secret',
|
||||||
|
name='assigned_object_type',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype'),
|
||||||
|
),
|
||||||
|
]
|
@ -6,13 +6,14 @@ from Crypto.Util import strxor
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import make_password, check_password
|
from django.contrib.auth.hashers import make_password, check_password
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import force_bytes
|
from django.utils.encoding import force_bytes
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from dcim.models import Device
|
|
||||||
from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
|
from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
|
||||||
from extras.utils import extras_features
|
from extras.utils import extras_features
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
@ -276,17 +277,21 @@ class SecretRole(ChangeLoggedModel):
|
|||||||
class Secret(ChangeLoggedModel, CustomFieldModel):
|
class Secret(ChangeLoggedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible
|
A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible
|
||||||
SHA-256 hash is stored along with the ciphertext for validation upon decryption. Each Secret is assigned to a
|
SHA-256 hash is stored along with the ciphertext for validation upon decryption. Each Secret is assigned to exactly
|
||||||
Device; Devices may have multiple Secrets associated with them. A name can optionally be defined along with the
|
one NetBox object, and objects may have multiple Secrets associated with them. A name can optionally be defined
|
||||||
ciphertext; this string is stored as plain text in the database.
|
along with the ciphertext; this string is stored as plain text in the database.
|
||||||
|
|
||||||
A Secret can be up to 65,535 bytes (64KB - 1B) in length. Each secret string will be padded with random data to
|
A Secret can be up to 65,535 bytes (64KB - 1B) in length. Each secret string will be padded with random data to
|
||||||
a minimum of 64 bytes during encryption in order to protect short strings from ciphertext analysis.
|
a minimum of 64 bytes during encryption in order to protect short strings from ciphertext analysis.
|
||||||
"""
|
"""
|
||||||
device = models.ForeignKey(
|
assigned_object_type = models.ForeignKey(
|
||||||
to='dcim.Device',
|
to=ContentType,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.PROTECT
|
||||||
related_name='secrets'
|
)
|
||||||
|
assigned_object_id = models.PositiveIntegerField()
|
||||||
|
assigned_object = GenericForeignKey(
|
||||||
|
ct_field='assigned_object_type',
|
||||||
|
fk_field='assigned_object_id'
|
||||||
)
|
)
|
||||||
role = models.ForeignKey(
|
role = models.ForeignKey(
|
||||||
to='secrets.SecretRole',
|
to='secrets.SecretRole',
|
||||||
@ -310,34 +315,26 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
|
|||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
plaintext = None
|
plaintext = None
|
||||||
csv_headers = ['device', 'role', 'name', 'plaintext']
|
csv_headers = ['assigned_object_type', 'assigned_object_id', 'role', 'name', 'plaintext']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device', 'role', 'name']
|
ordering = ('role', 'name', 'pk')
|
||||||
unique_together = ['device', 'role', 'name']
|
unique_together = ('assigned_object_type', 'assigned_object_id', 'role', 'name')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.plaintext = kwargs.pop('plaintext', None)
|
self.plaintext = kwargs.pop('plaintext', None)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
try:
|
return self.name or 'Secret'
|
||||||
device = self.device
|
|
||||||
except Device.DoesNotExist:
|
|
||||||
device = None
|
|
||||||
if self.role and device and self.name:
|
|
||||||
return '{} for {} ({})'.format(self.role, self.device, self.name)
|
|
||||||
# Return role and device if no name is set
|
|
||||||
if self.role and device:
|
|
||||||
return '{} for {}'.format(self.role, self.device)
|
|
||||||
return 'Secret'
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('secrets:secret', args=[self.pk])
|
return reverse('secrets:secret', args=[self.pk])
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return (
|
return (
|
||||||
self.device,
|
f'{self.assigned_object_type.app_label}.{self.assigned_object_type.model}',
|
||||||
|
self.assigned_object_id,
|
||||||
self.role,
|
self.role,
|
||||||
self.name,
|
self.name,
|
||||||
self.plaintext or '',
|
self.plaintext or '',
|
||||||
|
@ -28,12 +28,15 @@ class SecretRoleTable(BaseTable):
|
|||||||
|
|
||||||
class SecretTable(BaseTable):
|
class SecretTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
device = tables.LinkColumn()
|
assigned_object = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
verbose_name='Assigned object'
|
||||||
|
)
|
||||||
tags = TagColumn(
|
tags = TagColumn(
|
||||||
url_name='secrets:secret_list'
|
url_name='secrets:secret_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Secret
|
model = Secret
|
||||||
fields = ('pk', 'device', 'role', 'name', 'last_updated', 'hash', 'tags')
|
fields = ('pk', 'assigned_object', 'role', 'name', 'last_updated', 'hash', 'tags')
|
||||||
default_columns = ('pk', 'device', 'role', 'name', 'last_updated')
|
default_columns = ('pk', 'assigned_object', 'role', 'name', 'last_updated')
|
||||||
|
@ -80,9 +80,9 @@ class SecretTest(APIViewTestCases.APIViewTestCase):
|
|||||||
SecretRole.objects.bulk_create(secret_roles)
|
SecretRole.objects.bulk_create(secret_roles)
|
||||||
|
|
||||||
secrets = (
|
secrets = (
|
||||||
Secret(device=device, role=secret_roles[0], name='Secret 1', plaintext='ABC'),
|
Secret(assigned_object=device, role=secret_roles[0], name='Secret 1', plaintext='ABC'),
|
||||||
Secret(device=device, role=secret_roles[0], name='Secret 2', plaintext='DEF'),
|
Secret(assigned_object=device, role=secret_roles[0], name='Secret 2', plaintext='DEF'),
|
||||||
Secret(device=device, role=secret_roles[0], name='Secret 3', plaintext='GHI'),
|
Secret(assigned_object=device, role=secret_roles[0], name='Secret 3', plaintext='GHI'),
|
||||||
)
|
)
|
||||||
for secret in secrets:
|
for secret in secrets:
|
||||||
secret.encrypt(self.master_key)
|
secret.encrypt(self.master_key)
|
||||||
@ -90,19 +90,22 @@ class SecretTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
self.create_data = [
|
self.create_data = [
|
||||||
{
|
{
|
||||||
'device': device.pk,
|
'assigned_object_type': 'dcim.device',
|
||||||
|
'assigned_object_id': device.pk,
|
||||||
'role': secret_roles[1].pk,
|
'role': secret_roles[1].pk,
|
||||||
'name': 'Secret 4',
|
'name': 'Secret 4',
|
||||||
'plaintext': 'JKL',
|
'plaintext': 'JKL',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device': device.pk,
|
'assigned_object_type': 'dcim.device',
|
||||||
|
'assigned_object_id': device.pk,
|
||||||
'role': secret_roles[1].pk,
|
'role': secret_roles[1].pk,
|
||||||
'name': 'Secret 5',
|
'name': 'Secret 5',
|
||||||
'plaintext': 'MNO',
|
'plaintext': 'MNO',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'device': device.pk,
|
'assigned_object_type': 'dcim.device',
|
||||||
|
'assigned_object_id': device.pk,
|
||||||
'role': secret_roles[1].pk,
|
'role': secret_roles[1].pk,
|
||||||
'name': 'Secret 6',
|
'name': 'Secret 6',
|
||||||
'plaintext': 'PQR',
|
'plaintext': 'PQR',
|
||||||
|
@ -3,6 +3,7 @@ from django.test import TestCase
|
|||||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||||
from secrets.filters import *
|
from secrets.filters import *
|
||||||
from secrets.models import Secret, SecretRole
|
from secrets.models import Secret, SecretRole
|
||||||
|
from virtualization.models import Cluster, ClusterType, VirtualMachine
|
||||||
|
|
||||||
|
|
||||||
class SecretRoleTestCase(TestCase):
|
class SecretRoleTestCase(TestCase):
|
||||||
@ -51,6 +52,15 @@ class SecretTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
|
cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
||||||
|
cluster = Cluster.objects.create(name='Cluster 1', type=cluster_type)
|
||||||
|
virtual_machines = (
|
||||||
|
VirtualMachine(name='Virtual Machine 1', cluster=cluster),
|
||||||
|
VirtualMachine(name='Virtual Machine 2', cluster=cluster),
|
||||||
|
VirtualMachine(name='Virtual Machine 3', cluster=cluster),
|
||||||
|
)
|
||||||
|
VirtualMachine.objects.bulk_create(virtual_machines)
|
||||||
|
|
||||||
roles = (
|
roles = (
|
||||||
SecretRole(name='Secret Role 1', slug='secret-role-1'),
|
SecretRole(name='Secret Role 1', slug='secret-role-1'),
|
||||||
SecretRole(name='Secret Role 2', slug='secret-role-2'),
|
SecretRole(name='Secret Role 2', slug='secret-role-2'),
|
||||||
@ -59,9 +69,12 @@ class SecretTestCase(TestCase):
|
|||||||
SecretRole.objects.bulk_create(roles)
|
SecretRole.objects.bulk_create(roles)
|
||||||
|
|
||||||
secrets = (
|
secrets = (
|
||||||
Secret(device=devices[0], role=roles[0], name='Secret 1', plaintext='SECRET DATA'),
|
Secret(assigned_object=devices[0], role=roles[0], name='Secret 1', plaintext='SECRET DATA'),
|
||||||
Secret(device=devices[1], role=roles[1], name='Secret 2', plaintext='SECRET DATA'),
|
Secret(assigned_object=devices[1], role=roles[1], name='Secret 2', plaintext='SECRET DATA'),
|
||||||
Secret(device=devices[2], role=roles[2], name='Secret 3', plaintext='SECRET DATA'),
|
Secret(assigned_object=devices[2], role=roles[2], name='Secret 3', plaintext='SECRET DATA'),
|
||||||
|
Secret(assigned_object=virtual_machines[0], role=roles[0], name='Secret 4', plaintext='SECRET DATA'),
|
||||||
|
Secret(assigned_object=virtual_machines[1], role=roles[1], name='Secret 5', plaintext='SECRET DATA'),
|
||||||
|
Secret(assigned_object=virtual_machines[2], role=roles[2], name='Secret 6', plaintext='SECRET DATA'),
|
||||||
)
|
)
|
||||||
# Must call save() to encrypt Secrets
|
# Must call save() to encrypt Secrets
|
||||||
for s in secrets:
|
for s in secrets:
|
||||||
@ -78,9 +91,9 @@ class SecretTestCase(TestCase):
|
|||||||
def test_role(self):
|
def test_role(self):
|
||||||
roles = SecretRole.objects.all()[:2]
|
roles = SecretRole.objects.all()[:2]
|
||||||
params = {'role_id': [roles[0].pk, roles[1].pk]}
|
params = {'role_id': [roles[0].pk, roles[1].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
params = {'role': [roles[0].slug, roles[1].slug]}
|
params = {'role': [roles[0].slug, roles[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
def test_device(self):
|
def test_device(self):
|
||||||
devices = Device.objects.all()[:2]
|
devices = Device.objects.all()[:2]
|
||||||
@ -88,3 +101,10 @@ class SecretTestCase(TestCase):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
params = {'device': [devices[0].name, devices[1].name]}
|
params = {'device': [devices[0].name, devices[1].name]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_virtual_machine(self):
|
||||||
|
virtual_machines = VirtualMachine.objects.all()[:2]
|
||||||
|
params = {'virtual_machine_id': [virtual_machines[0].pk, virtual_machines[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'virtual_machine': [virtual_machines[0].name, virtual_machines[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
@ -69,13 +69,14 @@ class SecretTestCase(
|
|||||||
|
|
||||||
# Create one secret per device to allow bulk-editing of names (which must be unique per device/role)
|
# Create one secret per device to allow bulk-editing of names (which must be unique per device/role)
|
||||||
Secret.objects.bulk_create((
|
Secret.objects.bulk_create((
|
||||||
Secret(device=devices[0], role=secretroles[0], name='Secret 1', ciphertext=b'1234567890'),
|
Secret(assigned_object=devices[0], role=secretroles[0], name='Secret 1', ciphertext=b'1234567890'),
|
||||||
Secret(device=devices[1], role=secretroles[0], name='Secret 2', ciphertext=b'1234567890'),
|
Secret(assigned_object=devices[1], role=secretroles[0], name='Secret 2', ciphertext=b'1234567890'),
|
||||||
Secret(device=devices[2], role=secretroles[0], name='Secret 3', ciphertext=b'1234567890'),
|
Secret(assigned_object=devices[2], role=secretroles[0], name='Secret 3', ciphertext=b'1234567890'),
|
||||||
))
|
))
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'device': devices[1].pk,
|
'assigned_object_type': 'dcim.device',
|
||||||
|
'assigned_object_id': devices[1].pk,
|
||||||
'role': secretroles[1].pk,
|
'role': secretroles[1].pk,
|
||||||
'name': 'Secret X',
|
'name': 'Secret X',
|
||||||
}
|
}
|
||||||
@ -100,11 +101,12 @@ class SecretTestCase(
|
|||||||
def test_import_objects(self):
|
def test_import_objects(self):
|
||||||
self.add_permissions('secrets.add_secret')
|
self.add_permissions('secrets.add_secret')
|
||||||
|
|
||||||
|
device = Device.objects.get(name='Device 1')
|
||||||
csv_data = (
|
csv_data = (
|
||||||
"device,role,name,plaintext",
|
"assigned_object_type,assigned_object_id,role,name,plaintext",
|
||||||
"Device 1,Secret Role 1,Secret 4,abcdefghij",
|
f"device,{device.pk},Secret Role 1,Secret 4,abcdefghij",
|
||||||
"Device 1,Secret Role 1,Secret 5,abcdefghij",
|
f"device,{device.pk},Secret Role 1,Secret 5,abcdefghij",
|
||||||
"Device 1,Secret Role 1,Secret 6,abcdefghij",
|
f"device,{device.pk},Secret Role 1,Secret 6,abcdefghij",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set the session_key cookie on the request
|
# Set the session_key cookie on the request
|
||||||
|
@ -58,7 +58,7 @@ class SecretRoleBulkDeleteView(BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class SecretListView(ObjectListView):
|
class SecretListView(ObjectListView):
|
||||||
queryset = Secret.objects.prefetch_related('role', 'device')
|
queryset = Secret.objects.prefetch_related('role', 'tags')
|
||||||
filterset = filters.SecretFilterSet
|
filterset = filters.SecretFilterSet
|
||||||
filterset_form = forms.SecretFilterForm
|
filterset_form = forms.SecretFilterForm
|
||||||
table = tables.SecretTable
|
table = tables.SecretTable
|
||||||
@ -198,13 +198,13 @@ class SecretBulkImportView(BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class SecretBulkEditView(BulkEditView):
|
class SecretBulkEditView(BulkEditView):
|
||||||
queryset = Secret.objects.prefetch_related('role', 'device')
|
queryset = Secret.objects.prefetch_related('role')
|
||||||
filterset = filters.SecretFilterSet
|
filterset = filters.SecretFilterSet
|
||||||
table = tables.SecretTable
|
table = tables.SecretTable
|
||||||
form = forms.SecretBulkEditForm
|
form = forms.SecretBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class SecretBulkDeleteView(BulkDeleteView):
|
class SecretBulkDeleteView(BulkDeleteView):
|
||||||
queryset = Secret.objects.prefetch_related('role', 'device')
|
queryset = Secret.objects.prefetch_related('role')
|
||||||
filterset = filters.SecretFilterSet
|
filterset = filters.SecretFilterSet
|
||||||
table = tables.SecretTable
|
table = tables.SecretTable
|
||||||
|
@ -395,30 +395,16 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if request.user.is_authenticated %}
|
{% if perms.secrets.view_secret %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Secrets</strong>
|
<strong>Secrets</strong>
|
||||||
</div>
|
</div>
|
||||||
{% if secrets %}
|
{% include 'secrets/inc/assigned_secrets.html' %}
|
||||||
<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 %}
|
{% if perms.secrets.add_secret %}
|
||||||
<form id="secret_form">
|
|
||||||
{% csrf_token %}
|
|
||||||
</form>
|
|
||||||
<div class="panel-footer text-right noprint">
|
<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">
|
<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>
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add secret
|
||||||
Add secret
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
26
netbox/templates/secrets/inc/assigned_secrets.html
Normal file
26
netbox/templates/secrets/inc/assigned_secrets.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{% 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 %}
|
@ -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>
|
|
@ -11,7 +11,8 @@
|
|||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'secrets:secret_list' %}">Secrets</a></li>
|
<li><a href="{% url 'secrets:secret_list' %}">Secrets</a></li>
|
||||||
<li><a href="{% url 'secrets:secret_list' %}?role={{ secret.role.slug }}">{{ secret.role }}</a></li>
|
<li><a href="{% url 'secrets:secret_list' %}?role={{ secret.role.slug }}">{{ secret.role }}</a></li>
|
||||||
<li>{{ secret.device }}{% if secret.name %} ({{ secret.name }}){% endif %}</li>
|
<li><a href="{{ secret.assigned_object.get_absolute_url }}">{{ secret.assigned_object }}</a></li>
|
||||||
|
<li>{{ secret }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -50,9 +51,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Device</td>
|
<td>Assigned object</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:device' pk=secret.device.pk %}">{{ secret.device }}</a>
|
<a href="{{ secret.assigned_object.get_absolute_url }}">{{ secret.assigned_object }}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<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,21 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{% if perms.secrets.view_secret %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Secrets</strong>
|
||||||
|
</div>
|
||||||
|
{% include 'secrets/inc/assigned_secrets.html' %}
|
||||||
|
{% if perms.secrets.add_secret %}
|
||||||
|
<div class="panel-footer text-right noprint">
|
||||||
|
<a href="{% url 'secrets:secret_add' %}?virtual_machine={{ virtualmachine.pk }}&return_url={{ virtualmachine.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 %}
|
||||||
<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 +340,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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user