mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
Changed Secret parent from a GenericForeignKey to ForeignKey(Device)
This commit is contained in:
parent
3f279dc58b
commit
a6108f2fa8
@ -2,7 +2,7 @@
|
||||
|
||||
# Secret
|
||||
|
||||
A secret represents a single credential or other string which must be stored securely. Each secret is assigned to a parent object with NetBox, such as a device. The plaintext value of a secret is encrypted to a ciphertext immediately prior to storage within the database using a 256-bit AES master key. A SHA256 hash of the plaintext is also stored along with each ciphertext to validate the decrypted plaintext.
|
||||
A secret represents a single credential or other string which must be stored securely. Each secret is assigned to a device within NetBox. The plaintext value of a secret is encrypted to a ciphertext immediately prior to storage within the database using a 256-bit AES master key. A SHA256 hash of the plaintext is also stored along with each ciphertext to validate the decrypted plaintext.
|
||||
|
||||
Each secret can also store an optional name parameter, which is not encrypted. This may be useful for storing user names.
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.validators import MinValueValidator
|
||||
@ -8,7 +7,6 @@ from django.db import models
|
||||
from django.db.models import Q, ObjectDoesNotExist
|
||||
|
||||
from extras.rpc import RPC_CLIENTS
|
||||
from secrets.models import Secret
|
||||
from utilities.fields import NullableCharField
|
||||
|
||||
|
||||
@ -420,7 +418,6 @@ class Device(models.Model):
|
||||
primary_ip = models.OneToOneField('ipam.IPAddress', related_name='primary_for', on_delete=models.SET_NULL, blank=True, null=True, verbose_name='Primary IP')
|
||||
ro_snmp = models.CharField(max_length=50, blank=True, verbose_name='SNMP (RO)')
|
||||
comments = models.TextField(blank=True)
|
||||
secrets = GenericRelation(Secret)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
@ -70,8 +70,7 @@ urlpatterns = [
|
||||
url(r'^devices/(?P<pk>\d+)/inventory/$', views.device_inventory, name='device_inventory'),
|
||||
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.device_lldp_neighbors, name='device_lldp_neighbors'),
|
||||
url(r'^devices/(?P<pk>\d+)/ip-addresses/assign/$', views.ipaddress_assign, name='ipaddress_assign'),
|
||||
url(r'^devices/(?P<parent_pk>\d+)/add-secret/$', secret_add, {'parent_model': 'dcim.Device'},
|
||||
name='device_addsecret'),
|
||||
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
|
||||
|
||||
# Console ports
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.consoleport_add, name='consoleport_add'),
|
||||
|
@ -66,6 +66,6 @@ class SecretRoleAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(Secret)
|
||||
class SecretAdmin(admin.ModelAdmin):
|
||||
list_display = ['parent', 'role', 'name', 'created', 'last_modified']
|
||||
fields = ['parent', 'role', 'name', 'hash', 'created', 'last_modified']
|
||||
readonly_fields = ['parent', 'hash', 'created', 'last_modified']
|
||||
list_display = ['device', 'role', 'name', 'created', 'last_modified']
|
||||
fields = ['device', 'role', 'name', 'hash', 'created', 'last_modified']
|
||||
readonly_fields = ['device', 'hash', 'created', 'last_modified']
|
||||
|
@ -1,5 +1,6 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.api.serializers import DeviceNestedSerializer
|
||||
from secrets.models import Secret, SecretRole
|
||||
|
||||
|
||||
@ -24,13 +25,13 @@ class SecretRoleNestedSerializer(SecretRoleSerializer):
|
||||
# Secrets
|
||||
#
|
||||
|
||||
# TODO: Serialize parent info
|
||||
class SecretSerializer(serializers.ModelSerializer):
|
||||
device = DeviceNestedSerializer()
|
||||
role = SecretRoleNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Secret
|
||||
fields = ['id', 'role', 'name', 'hash', 'created', 'last_modified']
|
||||
fields = ['id', 'device', 'role', 'name', 'hash', 'created', 'last_modified']
|
||||
|
||||
|
||||
class SecretNestedSerializer(SecretSerializer):
|
||||
|
@ -2,10 +2,10 @@ from Crypto.Cipher import PKCS1_OAEP
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
from django import forms
|
||||
from django.apps import apps
|
||||
from django.db.models import Count
|
||||
|
||||
from utilities.forms import BootstrapMixin, ConfirmationForm, CSVDataField
|
||||
from dcim.models import Device
|
||||
from utilities.forms import BootstrapMixin, BulkImportForm, ConfirmationForm, CSVDataField
|
||||
from .models import Secret, SecretRole, UserKey
|
||||
|
||||
|
||||
@ -53,51 +53,26 @@ class SecretForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
|
||||
class SecretFromCSVForm(forms.ModelForm):
|
||||
parent_name = forms.CharField()
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found.'})
|
||||
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid secret role.'})
|
||||
plaintext = forms.CharField()
|
||||
|
||||
class Meta:
|
||||
model = Secret
|
||||
fields = ['parent_name', 'role', 'name', 'plaintext']
|
||||
fields = ['device', 'role', 'name', 'plaintext']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
s = super(SecretFromCSVForm, self).save(*args, **kwargs)
|
||||
s.plaintext = str(self.cleaned_data['plaintext'])
|
||||
return s
|
||||
|
||||
|
||||
class SecretImportForm(forms.Form, BootstrapMixin):
|
||||
class SecretImportForm(BulkImportForm, BootstrapMixin):
|
||||
private_key = forms.CharField(widget=forms.HiddenInput())
|
||||
parent_type = forms.ChoiceField(label='Parent Type', choices=(
|
||||
('dcim.Device', 'Device'),
|
||||
))
|
||||
csv = CSVDataField(csv_form=SecretFromCSVForm)
|
||||
|
||||
def clean(self):
|
||||
parent_type = self.cleaned_data.get('parent_type')
|
||||
records = self.cleaned_data.get('csv')
|
||||
if not records or not parent_type:
|
||||
return
|
||||
|
||||
secrets = []
|
||||
parent_cls = apps.get_model(parent_type)
|
||||
|
||||
for i, record in enumerate(records, start=1):
|
||||
secret_form = SecretFromCSVForm(data=record)
|
||||
if secret_form.is_valid():
|
||||
s = secret_form.save(commit=False)
|
||||
# Set parent
|
||||
try:
|
||||
s.parent = parent_cls.objects.get(name=secret_form.cleaned_data['parent_name'])
|
||||
except parent_cls.DoesNotExist:
|
||||
self.add_error('csv', "Invalid parent object ({})".format(secret_form.cleaned_data['parent_name']))
|
||||
# Set plaintext
|
||||
s.plaintext = str(secret_form.cleaned_data['plaintext'])
|
||||
secrets.append(s)
|
||||
else:
|
||||
for field, errors in secret_form.errors.items():
|
||||
for e in errors:
|
||||
self.add_error('csv', "Record {} {}: {}".format(i, field, e))
|
||||
|
||||
self.cleaned_data['csv'] = secrets
|
||||
|
||||
|
||||
class SecretBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
31
netbox/secrets/migrations/0002_auto_20160321_1448.py
Normal file
31
netbox/secrets/migrations/0002_auto_20160321_1448.py
Normal file
@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.1 on 2016-03-21 14:48
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0003_auto_20160304_1642'),
|
||||
('secrets', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='secret',
|
||||
name='content_type',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='secret',
|
||||
name='object_id',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='secret',
|
||||
name='device',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='secrets', to='dcim.Device'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
19
netbox/secrets/migrations/0003_auto_20160321_1524.py
Normal file
19
netbox/secrets/migrations/0003_auto_20160321_1524.py
Normal file
@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.1 on 2016-03-21 15:24
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('secrets', '0002_auto_20160321_1448'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='secret',
|
||||
options={'ordering': ['device', 'role', 'name'], 'permissions': (('view_secret', 'Can view secrets'),)},
|
||||
),
|
||||
]
|
@ -5,13 +5,13 @@ from Crypto.PublicKey import RSA
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import make_password, check_password
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.utils.encoding import force_bytes
|
||||
|
||||
from dcim.models import Device
|
||||
|
||||
|
||||
def generate_master_key():
|
||||
"""
|
||||
@ -176,9 +176,7 @@ class Secret(models.Model):
|
||||
A secret string of up to 255 bytes in length, stored as both an AES256-encrypted ciphertext and an irreversible
|
||||
salted SHA256 hash (for plaintext validation).
|
||||
"""
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField()
|
||||
parent = GenericForeignKey('content_type', 'object_id')
|
||||
device = models.ForeignKey(Device, related_name='secrets')
|
||||
role = models.ForeignKey('SecretRole', related_name='secrets', on_delete=models.PROTECT)
|
||||
name = models.CharField(max_length=100, blank=True)
|
||||
ciphertext = models.BinaryField(editable=False, max_length=65568) # 16B IV + 2B pad length + {62-65550}B padded
|
||||
@ -189,7 +187,7 @@ class Secret(models.Model):
|
||||
plaintext = None
|
||||
|
||||
class Meta:
|
||||
ordering = ['role', 'name']
|
||||
ordering = ['device', 'role', 'name']
|
||||
permissions = (
|
||||
('view_secret', "Can view secrets"),
|
||||
)
|
||||
@ -199,8 +197,8 @@ class Secret(models.Model):
|
||||
super(Secret, self).__init__(*args, **kwargs)
|
||||
|
||||
def __unicode__(self):
|
||||
if self.role and self.parent:
|
||||
return "{} for {}".format(self.role, self.parent)
|
||||
if self.role and self.device:
|
||||
return "{} for {}".format(self.role, self.device)
|
||||
return "Secret"
|
||||
|
||||
def get_absolute_url(self):
|
||||
|
@ -9,14 +9,14 @@ from .models import Secret
|
||||
#
|
||||
|
||||
class SecretTable(tables.Table):
|
||||
parent = tables.LinkColumn('secrets:secret', args=[Accessor('pk')], verbose_name='Parent')
|
||||
device = tables.LinkColumn('secrets:secret', args=[Accessor('pk')], verbose_name='Device')
|
||||
role = tables.Column(verbose_name='Role')
|
||||
name = tables.Column(verbose_name='Name')
|
||||
last_modified = tables.DateTimeColumn(verbose_name='Last modified')
|
||||
|
||||
class Meta:
|
||||
model = Secret
|
||||
fields = ('parent', 'role', 'name', 'last_modified')
|
||||
fields = ('device', 'role', 'name', 'last_modified')
|
||||
empty_text = "No secrets found."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
@ -28,4 +28,4 @@ class SecretBulkEditTable(SecretTable):
|
||||
|
||||
class Meta(SecretTable.Meta):
|
||||
model = None # django_tables2 bugfix
|
||||
fields = ('pk', 'parent', 'role', 'name')
|
||||
fields = ('pk', 'device', 'role', 'name')
|
||||
|
@ -1,4 +1,3 @@
|
||||
from django.apps import apps
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import permission_required, login_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
@ -8,6 +7,7 @@ from django.db.models import ProtectedError
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from dcim.models import Device
|
||||
from utilities.error_handlers import handle_protectederror
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.views import BulkEditView, BulkDeleteView, ObjectListView
|
||||
@ -25,7 +25,7 @@ from .tables import SecretTable, SecretBulkEditTable
|
||||
|
||||
@method_decorator(login_required, name='dispatch')
|
||||
class SecretListView(ObjectListView):
|
||||
queryset = Secret.objects.select_related('role').prefetch_related('parent')
|
||||
queryset = Secret.objects.select_related('role').prefetch_related('device')
|
||||
filter = SecretFilter
|
||||
filter_form = SecretFilterForm
|
||||
table = SecretTable
|
||||
@ -46,13 +46,12 @@ def secret(request, pk):
|
||||
|
||||
@permission_required('secrets.add_secret')
|
||||
@userkey_required()
|
||||
def secret_add(request, parent_model, parent_pk):
|
||||
def secret_add(request, pk):
|
||||
|
||||
# Retrieve parent object
|
||||
parent_cls = apps.get_model(parent_model)
|
||||
parent = get_object_or_404(parent_cls, pk=parent_pk)
|
||||
# Retrieve device
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
|
||||
secret = Secret(parent=parent)
|
||||
secret = Secret(device=device)
|
||||
uk = UserKey.objects.get(user=request.user)
|
||||
|
||||
if request.method == 'POST':
|
||||
@ -83,7 +82,7 @@ def secret_add(request, parent_model, parent_pk):
|
||||
return render(request, 'secrets/secret_edit.html', {
|
||||
'secret': secret,
|
||||
'form': form,
|
||||
'cancel_url': parent.get_absolute_url(),
|
||||
'cancel_url': device.get_absolute_url(),
|
||||
})
|
||||
|
||||
|
||||
|
@ -132,7 +132,7 @@
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
<div class="panel-footer text-right">
|
||||
<a href="{% url 'dcim:device_addsecret' parent_pk=device.pk %}" class="btn btn-xs btn-primary">
|
||||
<a href="{% url 'dcim:device_addsecret' pk=device.pk %}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add secret
|
||||
</a>
|
||||
|
@ -7,7 +7,7 @@
|
||||
<div class="col-md-12">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'secrets:secret_list' %}">Secrets</a></li>
|
||||
<li>{{ secret.parent }}</li>
|
||||
<li><a href="{% url 'dcim:device' pk=secret.device.pk %}">{{ secret.device }}</a></li>
|
||||
<li>{{ secret.role }}{% if secret.name %} ({{ secret.name }}){% endif %}</li>
|
||||
</ol>
|
||||
</div>
|
||||
@ -35,9 +35,9 @@
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<tr>
|
||||
<td>Parent</td>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ secret.parent.get_absolute_url }}">{{ secret.parent }}</a>
|
||||
<a href="{% url 'dcim:device' pk=secret.device.pk %}">{{ secret.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -7,7 +7,7 @@
|
||||
{% for secret in selected_objects %}
|
||||
<tr>
|
||||
<td><a href="{% url 'secrets:secret' pk=secret.pk %}">{{ secret }}</a></td>
|
||||
<td>{{ secret.parent }}</td>
|
||||
<td>{{ secret.device }}</td>
|
||||
<td>{{ secret.role }}</td>
|
||||
<td>{{ secret.name }}</td>
|
||||
</tr>
|
||||
|
@ -30,9 +30,9 @@
|
||||
<div class="panel-heading"><strong>Secret Attributes</strong></div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Parent</label>
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ secret.parent }}</p>
|
||||
<p class="form-control-static">{{ secret.device }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.role %}
|
||||
|
@ -37,8 +37,8 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Parent</td>
|
||||
<td>Name of the parent object</td>
|
||||
<td>Device</td>
|
||||
<td>Name of the parent device</td>
|
||||
<td>edge-router1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
Loading…
Reference in New Issue
Block a user