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

#1503: Initial work on generic secret assignments (WIP)

This commit is contained in:
Jeremy Stretch
2020-09-18 14:51:09 -04:00
parent 0cc2a6b2cf
commit ec095e58b7
7 changed files with 96 additions and 38 deletions

View File

@ -582,6 +582,12 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
images = GenericRelation(
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)
objects = RestrictedQuerySet.as_manager()

View File

@ -35,16 +35,6 @@ class SecretFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterS
to_field_name='slug',
label='Role (slug)',
)
device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(),
label='Device (ID)',
)
device = django_filters.ModelMultipleChoiceFilter(
field_name='device__name',
queryset=Device.objects.all(),
to_field_name='name',
label='Device (name)',
)
tag = TagFilter()
class Meta:

View File

@ -0,0 +1,49 @@
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')
Secret = apps.get_model('secrets', 'Secret')
device_ct = ContentType.objects.get(app_label='dcim', 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')},
),
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')},
),
migrations.RunPython(
code=device_to_generic_assignment,
reverse_code=migrations.RunPython.noop
),
migrations.RemoveField(
model_name='secret',
name='device',
),
]

View File

@ -6,13 +6,14 @@ from Crypto.Util import strxor
from django.conf import settings
from django.contrib.auth.hashers import make_password, check_password
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.db import models
from django.urls import reverse
from django.utils.encoding import force_bytes
from taggit.managers import TaggableManager
from dcim.models import Device
from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
from extras.utils import extras_features
from utilities.querysets import RestrictedQuerySet
@ -276,17 +277,26 @@ class SecretRole(ChangeLoggedModel):
class Secret(ChangeLoggedModel, CustomFieldModel):
"""
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
Device; Devices may have multiple Secrets associated with them. A name can optionally be defined along with the
ciphertext; this string is stored as plain text in the database.
SHA-256 hash is stored along with the ciphertext for validation upon decryption. Each Secret is assigned to exactly
one NetBox object, and objects may have multiple Secrets associated with them. A name can optionally be defined
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 minimum of 64 bytes during encryption in order to protect short strings from ciphertext analysis.
"""
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.CASCADE,
related_name='secrets'
assigned_object_type = models.ForeignKey(
to=ContentType,
on_delete=models.PROTECT,
blank=True,
null=True
)
assigned_object_id = models.PositiveIntegerField(
blank=True,
null=True
)
assigned_object = GenericForeignKey(
ct_field='assigned_object_type',
fk_field='assigned_object_id'
)
role = models.ForeignKey(
to='secrets.SecretRole',
@ -310,34 +320,26 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
objects = RestrictedQuerySet.as_manager()
plaintext = None
csv_headers = ['device', 'role', 'name', 'plaintext']
csv_headers = ['assigned_object_type', 'assigned_object_id', 'role', 'name', 'plaintext']
class Meta:
ordering = ['device', 'role', 'name']
unique_together = ['device', 'role', 'name']
ordering = ('role', 'name', 'pk')
unique_together = ('assigned_object_type', 'assigned_object_id', 'role', 'name')
def __init__(self, *args, **kwargs):
self.plaintext = kwargs.pop('plaintext', None)
super().__init__(*args, **kwargs)
def __str__(self):
try:
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'
return self.name or 'Secret'
def get_absolute_url(self):
return reverse('secrets:secret', args=[self.pk])
def to_csv(self):
return (
self.device,
f'{self.assigned_object_type.app_label}.{self.assigned_object_type.model}',
self.assigned_object_id,
self.role,
self.name,
self.plaintext or '',

View File

@ -28,12 +28,15 @@ class SecretRoleTable(BaseTable):
class SecretTable(BaseTable):
pk = ToggleColumn()
device = tables.LinkColumn()
assigned_object = tables.Column(
linkify=True,
verbose_name='Assigned object'
)
tags = TagColumn(
url_name='secrets:secret_list'
)
class Meta(BaseTable.Meta):
model = Secret
fields = ('pk', 'device', 'role', 'name', 'last_updated', 'hash', 'tags')
default_columns = ('pk', 'device', 'role', 'name', 'last_updated')
fields = ('pk', 'assigned_object', 'role', 'name', 'last_updated', 'hash', 'tags')
default_columns = ('pk', 'assigned_object', 'role', 'name', 'last_updated')

View File

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

View File

@ -11,7 +11,8 @@
<ol class="breadcrumb">
<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>{{ 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>
</div>
</div>
@ -50,9 +51,9 @@
</div>
<table class="table table-hover panel-body">
<tr>
<td>Device</td>
<td>Assigned object</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>
</tr>
<tr>