mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-22 05:12:22 -06:00
Merge branch 'develop' into 2921-tags-select2
This commit is contained in:
@@ -18,7 +18,7 @@ class SecretRoleSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = SecretRole
|
||||
fields = ['id', 'name', 'slug', 'secret_count']
|
||||
fields = ['id', 'name', 'slug', 'description', 'secret_count']
|
||||
|
||||
|
||||
class SecretSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
|
||||
@@ -38,7 +38,7 @@ class SecretRoleViewSet(ModelViewSet):
|
||||
)
|
||||
serializer_class = serializers.SecretRoleSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
filterset_class = filters.SecretRoleFilter
|
||||
filterset_class = filters.SecretRoleFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -50,7 +50,7 @@ class SecretViewSet(ModelViewSet):
|
||||
'device__primary_ip4', 'device__primary_ip6', 'role', 'role__users', 'role__groups', 'tags',
|
||||
)
|
||||
serializer_class = serializers.SecretSerializer
|
||||
filterset_class = filters.SecretFilter
|
||||
filterset_class = filters.SecretFilterSet
|
||||
|
||||
master_key = None
|
||||
|
||||
|
||||
@@ -8,19 +8,19 @@ from .models import Secret, SecretRole
|
||||
|
||||
|
||||
__all__ = (
|
||||
'SecretFilter',
|
||||
'SecretRoleFilter',
|
||||
'SecretFilterSet',
|
||||
'SecretRoleFilterSet',
|
||||
)
|
||||
|
||||
|
||||
class SecretRoleFilter(NameSlugSearchFilterSet):
|
||||
class SecretRoleFilterSet(NameSlugSearchFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = SecretRole
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class SecretFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
class SecretFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
[
|
||||
{
|
||||
"model": "secrets.secretrole",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Login Credentials",
|
||||
"slug": "login-credentials",
|
||||
"users": [],
|
||||
"groups": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "secrets.secretrole",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "RADIUS Key",
|
||||
"slug": "radius-key",
|
||||
"users": [],
|
||||
"groups": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "secrets.secretrole",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "SNMPv2 Community",
|
||||
"slug": "snmpv2-community",
|
||||
"users": [],
|
||||
"groups": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "secrets.secretrole",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "SNMPv3 Credentials",
|
||||
"slug": "snmpv3-credentials",
|
||||
"users": [],
|
||||
"groups": []
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -42,7 +42,7 @@ class SecretRoleForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = SecretRole
|
||||
fields = [
|
||||
'name', 'slug', 'users', 'groups',
|
||||
'name', 'slug', 'description', 'users', 'groups',
|
||||
]
|
||||
widgets = {
|
||||
'users': StaticSelect2Multiple(),
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.14 on 2018-08-01 17:45
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('secrets', '0001_initial'), ('secrets', '0002_userkey_add_session_key'), ('secrets', '0003_unicode_literals')]
|
||||
replaces = [('secrets', '0001_initial'), ('secrets', '0002_userkey_add_session_key'), ('secrets', '0003_unicode_literals'), ('secrets', '0004_tags'), ('secrets', '0005_change_logging'), ('secrets', '0006_custom_tag_models')]
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('dcim', '0002_auto_20160622_1821'),
|
||||
('extras', '0019_tag_taggeditem'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('taggit', '0002_auto_20150616_2121'),
|
||||
('auth', '0007_alter_validators_add_error_messages'),
|
||||
]
|
||||
|
||||
@@ -24,27 +25,13 @@ class Migration(migrations.Migration):
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('groups', models.ManyToManyField(blank=True, related_name='secretroles', to='auth.Group')),
|
||||
('users', models.ManyToManyField(blank=True, related_name='secretroles', to=settings.AUTH_USER_MODEL)),
|
||||
('created', models.DateField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Secret',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateField(auto_now_add=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('name', models.CharField(blank=True, max_length=100)),
|
||||
('ciphertext', models.BinaryField(max_length=65568)),
|
||||
('hash', models.CharField(editable=False, max_length=128)),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='secrets', to='dcim.Device')),
|
||||
('role', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='secrets', to='secrets.SecretRole')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'role', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserKey',
|
||||
fields=[
|
||||
@@ -56,14 +43,10 @@ class Migration(migrations.Migration):
|
||||
('user', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='user_key', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'permissions': (('activate_userkey', 'Can activate user keys for decryption'),),
|
||||
'ordering': ['user__username'],
|
||||
'permissions': (('activate_userkey', 'Can activate user keys for decryption'),),
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='secret',
|
||||
unique_together=set([('device', 'role', 'name')]),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SessionKey',
|
||||
fields=[
|
||||
@@ -77,4 +60,22 @@ class Migration(migrations.Migration):
|
||||
'ordering': ['userkey__user__username'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Secret',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('name', models.CharField(blank=True, max_length=100)),
|
||||
('ciphertext', models.BinaryField(max_length=65568)),
|
||||
('hash', models.CharField(editable=False, max_length=128)),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='secrets', to='dcim.Device')),
|
||||
('role', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='secrets', to='secrets.SecretRole')),
|
||||
('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'role', 'name'],
|
||||
'unique_together': {('device', 'role', 'name')},
|
||||
},
|
||||
),
|
||||
]
|
||||
18
netbox/secrets/migrations/0007_secretrole_description.py
Normal file
18
netbox/secrets/migrations/0007_secretrole_description.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.6 on 2019-12-10 18:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('secrets', '0006_custom_tag_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='secretrole',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from Crypto.Cipher import AES, PKCS1_OAEP
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Util import strxor
|
||||
from django.conf import settings
|
||||
@@ -14,38 +14,21 @@ 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 CustomFieldModel, TaggedItem
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from .exceptions import InvalidKey
|
||||
from .hashers import SecretValidationHasher
|
||||
from .querysets import UserKeyQuerySet
|
||||
from .utils import encrypt_master_key, decrypt_master_key, generate_random_key
|
||||
|
||||
|
||||
def generate_random_key(bits=256):
|
||||
"""
|
||||
Generate a random encryption key. Sizes is given in bits and must be in increments of 32.
|
||||
"""
|
||||
if bits % 32:
|
||||
raise Exception("Invalid key size ({}). Key sizes must be in increments of 32 bits.".format(bits))
|
||||
return os.urandom(int(bits / 8))
|
||||
|
||||
|
||||
def encrypt_master_key(master_key, public_key):
|
||||
"""
|
||||
Encrypt a secret key with the provided public RSA key.
|
||||
"""
|
||||
key = RSA.importKey(public_key)
|
||||
cipher = PKCS1_OAEP.new(key)
|
||||
return cipher.encrypt(master_key)
|
||||
|
||||
|
||||
def decrypt_master_key(master_key_cipher, private_key):
|
||||
"""
|
||||
Decrypt a secret key with the provided private RSA key.
|
||||
"""
|
||||
key = RSA.importKey(private_key)
|
||||
cipher = PKCS1_OAEP.new(key)
|
||||
return cipher.decrypt(master_key_cipher)
|
||||
__all__ = (
|
||||
'Secret',
|
||||
'SecretRole',
|
||||
'SessionKey',
|
||||
'UserKey',
|
||||
)
|
||||
|
||||
|
||||
class UserKey(models.Model):
|
||||
@@ -270,6 +253,10 @@ class SecretRole(ChangeLoggedModel):
|
||||
slug = models.SlugField(
|
||||
unique=True
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
)
|
||||
users = models.ManyToManyField(
|
||||
to=User,
|
||||
related_name='secretroles',
|
||||
@@ -281,7 +268,7 @@ class SecretRole(ChangeLoggedModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
csv_headers = ['name', 'slug']
|
||||
csv_headers = ['name', 'slug', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
@@ -296,6 +283,7 @@ class SecretRole(ChangeLoggedModel):
|
||||
return (
|
||||
self.name,
|
||||
self.slug,
|
||||
self.description,
|
||||
)
|
||||
|
||||
def has_member(self, user):
|
||||
@@ -359,10 +347,14 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
if self.role and self.device and self.name:
|
||||
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 self.device:
|
||||
if self.role and device:
|
||||
return '{} for {}'.format(self.role, self.device)
|
||||
return 'Secret'
|
||||
|
||||
|
||||
@@ -19,16 +19,15 @@ SECRETROLE_ACTIONS = """
|
||||
|
||||
class SecretRoleTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
name = tables.LinkColumn()
|
||||
secret_count = tables.Column(verbose_name='Secrets')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = SecretRole
|
||||
fields = ('pk', 'name', 'secret_count', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'secret_count', 'description', 'slug', 'actions')
|
||||
|
||||
|
||||
#
|
||||
|
||||
38
netbox/secrets/tests/constants.py
Normal file
38
netbox/secrets/tests/constants.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# Dummy RSA key pair for testing use only
|
||||
PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA97wPWxpq5cClRu8Ssq609ZLfyx6E8ln/v/PdFZ7fxxmA4k+z
|
||||
1Q/Rn9/897PWy+1x2ZKlHjmaw1z7dS3PlGqdd453d1eY95xYVbFrIHs7yJy8lcDR
|
||||
2criwGEI68VP1FwcOkkwhicjtQZQS5fkkBIbRjA2wmt2PVT26YbOX2qCMItV1+me
|
||||
o/Ogh+uI1oNePJ8VYuGXbGNggf1qMY8fGhhhGY2b4PKuSTcsYjbg8adOGzFL9RXL
|
||||
I1X4PHNCzD/Y1vdM3jJXv+luk3TU+JIbzJeN5ZEEz+sIdlMPCAACaZAY/t9Kd/Lx
|
||||
Hr0o4K/6gqkZIukxFCK6sN53gibAXfaKc4xlqQIDAQABAoIBAQC4pDQVxNTTtQf6
|
||||
nImlH83EEto1++M+9pFFsi6fxLApJvsGsjzomke1Dy7uN93qVGk8rq3enzSYU58f
|
||||
sSs8BVKkH00vZ9ydAKxeAkREC1V9qkRsoTBHUY47sJcDkyZyssxfLNm7w0Q70h7a
|
||||
mLVEJBqr75eAxLN19vOpDk6Wkz3Bi0Dj27HLeme3hH5jLVQIIswWZnUDP3r/sdM/
|
||||
WA2GjoycPbug0r1FVZnxkFCrQ5yMfH3VzKBelj7356+5sc/TUXedDFN/DV2b90Ll
|
||||
+au7EEXecFYZwmX3SX2hpe6IWEpUW3B0fvm+Ipm8h7x68i7J0oi9EUXW2+UQYfOx
|
||||
dDLxTLvhAoGBAPtJJox4XcpzipSAzKxyV8K9ikUZCG2wJU7VHyZ5zpSXwl/husls
|
||||
brAzHQcnWayhxxuWeiQ6pLnRFPFXjlOH2FZqHXSLnfpDaymEksDPvo9GqRE3Q+F+
|
||||
lDRn72H1NLIj3Y3t5SwWRB34Dhy+gd5Ht9L3dCTH8cYvJGnmS4sH/z0NAoGBAPxh
|
||||
2rhS1B0S9mqqvpduUPxqUIWaztXaHC6ZikloOFcgVMdh9MRrpa2sa+bqcygyqrbH
|
||||
GZIIeGcWpmzeitWgSUNLMSIpdl/VoBSvZUMggdJyOHXayo/EhfFddGHdkfz0B0GW
|
||||
LzH8ow4JcYdhkTl4+xQstXJNVRJyw5ezFy35FHwNAoGAGZzjKP470R7lyS03r3wY
|
||||
Jelb5p8elM+XfemLO0i/HbY6QbuoZk9/GMac9tWz9jynJtC3smmn0KjXEaJzB2CZ
|
||||
VHWMewygFZo5mgnBS5XhPoldQjv310wnnw/Y/osXy/CL7KOK8Gt0lflqttNUOWvl
|
||||
+MLwO6+FnUXA2Gp42Lr/8SECgYANf2pEK2HewDHfmIwi6yp3pXPzAUmIlGanc1y6
|
||||
+lDxD/CYzTta+erdc/g9XFKWVsdciR9r+Pn/gW2bKve/3xer+qyBCDilfXZXRN4k
|
||||
jeuDhspQO0hUEg2b0AS2azQwlBiDQHX7tWg/CvBAbk5nBXpgJNf7aflfyDV/untF
|
||||
4SlgTQKBgGmcyU02lyM6ogGbzWqSsHgR1ZhYyTV9DekQx9GysLG1wT2QzgjxOw4K
|
||||
5PnVkOXr/ORqt+vJsYrtqBZQihmPPREKEwr2n8BRw0364z02wjvP04hDBHp4S5Ej
|
||||
PQeC5qErboVGMMpM2SamqGEfr+HJ/uRF6mEmm+xjI57aOvAwPW0B
|
||||
-----END RSA PRIVATE KEY-----"""
|
||||
|
||||
PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA97wPWxpq5cClRu8Ssq60
|
||||
9ZLfyx6E8ln/v/PdFZ7fxxmA4k+z1Q/Rn9/897PWy+1x2ZKlHjmaw1z7dS3PlGqd
|
||||
d453d1eY95xYVbFrIHs7yJy8lcDR2criwGEI68VP1FwcOkkwhicjtQZQS5fkkBIb
|
||||
RjA2wmt2PVT26YbOX2qCMItV1+meo/Ogh+uI1oNePJ8VYuGXbGNggf1qMY8fGhhh
|
||||
GY2b4PKuSTcsYjbg8adOGzFL9RXLI1X4PHNCzD/Y1vdM3jJXv+luk3TU+JIbzJeN
|
||||
5ZEEz+sIdlMPCAACaZAY/t9Kd/LxHr0o4K/6gqkZIukxFCK6sN53gibAXfaKc4xl
|
||||
qQIDAQAB
|
||||
-----END PUBLIC KEY-----"""
|
||||
@@ -6,45 +6,24 @@ from rest_framework import status
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
from secrets.models import Secret, SecretRole, SessionKey, UserKey
|
||||
from utilities.testing import APITestCase
|
||||
from .constants import PRIVATE_KEY, PUBLIC_KEY
|
||||
|
||||
# Dummy RSA key pair for testing use only
|
||||
PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA97wPWxpq5cClRu8Ssq609ZLfyx6E8ln/v/PdFZ7fxxmA4k+z
|
||||
1Q/Rn9/897PWy+1x2ZKlHjmaw1z7dS3PlGqdd453d1eY95xYVbFrIHs7yJy8lcDR
|
||||
2criwGEI68VP1FwcOkkwhicjtQZQS5fkkBIbRjA2wmt2PVT26YbOX2qCMItV1+me
|
||||
o/Ogh+uI1oNePJ8VYuGXbGNggf1qMY8fGhhhGY2b4PKuSTcsYjbg8adOGzFL9RXL
|
||||
I1X4PHNCzD/Y1vdM3jJXv+luk3TU+JIbzJeN5ZEEz+sIdlMPCAACaZAY/t9Kd/Lx
|
||||
Hr0o4K/6gqkZIukxFCK6sN53gibAXfaKc4xlqQIDAQABAoIBAQC4pDQVxNTTtQf6
|
||||
nImlH83EEto1++M+9pFFsi6fxLApJvsGsjzomke1Dy7uN93qVGk8rq3enzSYU58f
|
||||
sSs8BVKkH00vZ9ydAKxeAkREC1V9qkRsoTBHUY47sJcDkyZyssxfLNm7w0Q70h7a
|
||||
mLVEJBqr75eAxLN19vOpDk6Wkz3Bi0Dj27HLeme3hH5jLVQIIswWZnUDP3r/sdM/
|
||||
WA2GjoycPbug0r1FVZnxkFCrQ5yMfH3VzKBelj7356+5sc/TUXedDFN/DV2b90Ll
|
||||
+au7EEXecFYZwmX3SX2hpe6IWEpUW3B0fvm+Ipm8h7x68i7J0oi9EUXW2+UQYfOx
|
||||
dDLxTLvhAoGBAPtJJox4XcpzipSAzKxyV8K9ikUZCG2wJU7VHyZ5zpSXwl/husls
|
||||
brAzHQcnWayhxxuWeiQ6pLnRFPFXjlOH2FZqHXSLnfpDaymEksDPvo9GqRE3Q+F+
|
||||
lDRn72H1NLIj3Y3t5SwWRB34Dhy+gd5Ht9L3dCTH8cYvJGnmS4sH/z0NAoGBAPxh
|
||||
2rhS1B0S9mqqvpduUPxqUIWaztXaHC6ZikloOFcgVMdh9MRrpa2sa+bqcygyqrbH
|
||||
GZIIeGcWpmzeitWgSUNLMSIpdl/VoBSvZUMggdJyOHXayo/EhfFddGHdkfz0B0GW
|
||||
LzH8ow4JcYdhkTl4+xQstXJNVRJyw5ezFy35FHwNAoGAGZzjKP470R7lyS03r3wY
|
||||
Jelb5p8elM+XfemLO0i/HbY6QbuoZk9/GMac9tWz9jynJtC3smmn0KjXEaJzB2CZ
|
||||
VHWMewygFZo5mgnBS5XhPoldQjv310wnnw/Y/osXy/CL7KOK8Gt0lflqttNUOWvl
|
||||
+MLwO6+FnUXA2Gp42Lr/8SECgYANf2pEK2HewDHfmIwi6yp3pXPzAUmIlGanc1y6
|
||||
+lDxD/CYzTta+erdc/g9XFKWVsdciR9r+Pn/gW2bKve/3xer+qyBCDilfXZXRN4k
|
||||
jeuDhspQO0hUEg2b0AS2azQwlBiDQHX7tWg/CvBAbk5nBXpgJNf7aflfyDV/untF
|
||||
4SlgTQKBgGmcyU02lyM6ogGbzWqSsHgR1ZhYyTV9DekQx9GysLG1wT2QzgjxOw4K
|
||||
5PnVkOXr/ORqt+vJsYrtqBZQihmPPREKEwr2n8BRw0364z02wjvP04hDBHp4S5Ej
|
||||
PQeC5qErboVGMMpM2SamqGEfr+HJ/uRF6mEmm+xjI57aOvAwPW0B
|
||||
-----END RSA PRIVATE KEY-----"""
|
||||
|
||||
PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA97wPWxpq5cClRu8Ssq60
|
||||
9ZLfyx6E8ln/v/PdFZ7fxxmA4k+z1Q/Rn9/897PWy+1x2ZKlHjmaw1z7dS3PlGqd
|
||||
d453d1eY95xYVbFrIHs7yJy8lcDR2criwGEI68VP1FwcOkkwhicjtQZQS5fkkBIb
|
||||
RjA2wmt2PVT26YbOX2qCMItV1+meo/Ogh+uI1oNePJ8VYuGXbGNggf1qMY8fGhhh
|
||||
GY2b4PKuSTcsYjbg8adOGzFL9RXLI1X4PHNCzD/Y1vdM3jJXv+luk3TU+JIbzJeN
|
||||
5ZEEz+sIdlMPCAACaZAY/t9Kd/LxHr0o4K/6gqkZIukxFCK6sN53gibAXfaKc4xl
|
||||
qQIDAQAB
|
||||
-----END PUBLIC KEY-----"""
|
||||
class AppTest(APITestCase):
|
||||
|
||||
def test_root(self):
|
||||
|
||||
url = reverse('secrets-api:api-root')
|
||||
response = self.client.get('{}?format=api'.format(url), **self.header)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_choices(self):
|
||||
|
||||
url = reverse('secrets-api:field-choice-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class SecretRoleTest(APITestCase):
|
||||
|
||||
@@ -7,7 +7,7 @@ from secrets.models import Secret, SecretRole
|
||||
|
||||
class SecretRoleTestCase(TestCase):
|
||||
queryset = SecretRole.objects.all()
|
||||
filterset = SecretRoleFilter
|
||||
filterset = SecretRoleFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -35,7 +35,7 @@ class SecretRoleTestCase(TestCase):
|
||||
|
||||
class SecretTestCase(TestCase):
|
||||
queryset = Secret.objects.all()
|
||||
filterset = SecretFilter
|
||||
filterset = SecretFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -7,7 +7,8 @@ from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
|
||||
from secrets.hashers import SecretValidationHasher
|
||||
from secrets.models import UserKey, Secret, encrypt_master_key, decrypt_master_key, generate_random_key
|
||||
from secrets.models import Secret, UserKey
|
||||
from secrets.utils import encrypt_master_key, decrypt_master_key, generate_random_key
|
||||
|
||||
|
||||
class UserKeyTestCase(TestCase):
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import base64
|
||||
import urllib.parse
|
||||
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
from secrets.models import Secret, SecretRole
|
||||
from secrets.models import Secret, SecretRole, SessionKey, UserKey
|
||||
from utilities.testing import create_test_user
|
||||
from .constants import PRIVATE_KEY, PUBLIC_KEY
|
||||
|
||||
|
||||
class SecretRoleTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
user = create_test_user(permissions=['secrets.view_secretrole'])
|
||||
user = create_test_user(
|
||||
permissions=[
|
||||
'secrets.view_secretrole',
|
||||
'secrets.add_secretrole',
|
||||
]
|
||||
)
|
||||
self.client = Client()
|
||||
self.client.force_login(user)
|
||||
|
||||
@@ -28,11 +35,38 @@ class SecretRoleTestCase(TestCase):
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_secretrole_import(self):
|
||||
|
||||
csv_data = (
|
||||
"name,slug",
|
||||
"Secret Role 4,secret-role-4",
|
||||
"Secret Role 5,secret-role-5",
|
||||
"Secret Role 6,secret-role-6",
|
||||
)
|
||||
|
||||
response = self.client.post(reverse('secrets:secretrole_import'), {'csv': '\n'.join(csv_data)})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(SecretRole.objects.count(), 6)
|
||||
|
||||
|
||||
class SecretTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
user = create_test_user(permissions=['secrets.view_secret'])
|
||||
user = create_test_user(
|
||||
permissions=[
|
||||
'secrets.view_secret',
|
||||
'secrets.add_secret',
|
||||
]
|
||||
)
|
||||
|
||||
# Set up a master key
|
||||
userkey = UserKey(user=user, public_key=PUBLIC_KEY)
|
||||
userkey.save()
|
||||
master_key = userkey.get_master_key(PRIVATE_KEY)
|
||||
self.session_key = SessionKey(userkey=userkey)
|
||||
self.session_key.save(master_key)
|
||||
|
||||
self.client = Client()
|
||||
self.client.force_login(user)
|
||||
|
||||
@@ -75,3 +109,21 @@ class SecretTestCase(TestCase):
|
||||
secret = Secret.objects.first()
|
||||
response = self.client.get(secret.get_absolute_url(), follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_secret_import(self):
|
||||
|
||||
csv_data = (
|
||||
"device,role,name,plaintext",
|
||||
"Device 1,Secret Role 1,Secret 4,abcdefghij",
|
||||
"Device 1,Secret Role 1,Secret 5,abcdefghij",
|
||||
"Device 1,Secret Role 1,Secret 6,abcdefghij",
|
||||
)
|
||||
|
||||
# Set the session_key cookie on the request
|
||||
session_key = base64.b64encode(self.session_key.key).decode('utf-8')
|
||||
self.client.cookies['session_key'] = session_key
|
||||
|
||||
response = self.client.post(reverse('secrets:secret_import'), {'csv': '\n'.join(csv_data)})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(Secret.objects.count(), 6)
|
||||
|
||||
31
netbox/secrets/utils.py
Normal file
31
netbox/secrets/utils.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import os
|
||||
|
||||
from Crypto.Cipher import PKCS1_OAEP
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
|
||||
def generate_random_key(bits=256):
|
||||
"""
|
||||
Generate a random encryption key. Sizes is given in bits and must be in increments of 32.
|
||||
"""
|
||||
if bits % 32:
|
||||
raise Exception("Invalid key size ({}). Key sizes must be in increments of 32 bits.".format(bits))
|
||||
return os.urandom(int(bits / 8))
|
||||
|
||||
|
||||
def encrypt_master_key(master_key, public_key):
|
||||
"""
|
||||
Encrypt a secret key with the provided public RSA key.
|
||||
"""
|
||||
key = RSA.importKey(public_key)
|
||||
cipher = PKCS1_OAEP.new(key)
|
||||
return cipher.encrypt(master_key)
|
||||
|
||||
|
||||
def decrypt_master_key(master_key_cipher, private_key):
|
||||
"""
|
||||
Decrypt a secret key with the provided private RSA key.
|
||||
"""
|
||||
key = RSA.importKey(private_key)
|
||||
cipher = PKCS1_OAEP.new(key)
|
||||
return cipher.decrypt(master_key_cipher)
|
||||
@@ -70,8 +70,8 @@ class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
class SecretListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'secrets.view_secret'
|
||||
queryset = Secret.objects.prefetch_related('role', 'device')
|
||||
filter = filters.SecretFilter
|
||||
filter_form = forms.SecretFilterForm
|
||||
filterset = filters.SecretFilterSet
|
||||
filterset_form = forms.SecretFilterForm
|
||||
table = tables.SecretTable
|
||||
template_name = 'secrets/secret_list.html'
|
||||
|
||||
@@ -248,7 +248,7 @@ class SecretBulkImportView(BulkImportView):
|
||||
class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'secrets.change_secret'
|
||||
queryset = Secret.objects.prefetch_related('role', 'device')
|
||||
filter = filters.SecretFilter
|
||||
filterset = filters.SecretFilterSet
|
||||
table = tables.SecretTable
|
||||
form = forms.SecretBulkEditForm
|
||||
default_return_url = 'secrets:secret_list'
|
||||
@@ -257,6 +257,6 @@ class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'secrets.delete_secret'
|
||||
queryset = Secret.objects.prefetch_related('role', 'device')
|
||||
filter = filters.SecretFilter
|
||||
filterset = filters.SecretFilterSet
|
||||
table = tables.SecretTable
|
||||
default_return_url = 'secrets:secret_list'
|
||||
|
||||
Reference in New Issue
Block a user