Introduced per-role decryption permissions

This commit is contained in:
Jeremy Stretch 2016-04-07 12:37:09 -04:00
parent 8a39f254ad
commit 2cb99c6012
3 changed files with 55 additions and 22 deletions

View File

@ -4,7 +4,7 @@ from django.shortcuts import get_object_or_404
from rest_framework import generics from rest_framework import generics
from rest_framework import status from rest_framework import status
from rest_framework.permissions import IsAuthenticated, BasePermission from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.views import APIView from rest_framework.views import APIView
@ -20,11 +20,6 @@ ERR_USERKEY_INACTIVE = "UserKey has not been activated for decryption."
ERR_PRIVKEY_INVALID = "Invalid private key." ERR_PRIVKEY_INVALID = "Invalid private key."
class SecretViewPermission(BasePermission):
def has_permission(self, request, view):
return request.user.has_perm('secrets.view_secret')
class SecretRoleListView(generics.ListAPIView): class SecretRoleListView(generics.ListAPIView):
""" """
List all secret roles List all secret roles
@ -47,10 +42,10 @@ class SecretListView(generics.GenericAPIView):
e.g. curl -X GET http://netbox/api/secrets/secrets/ --data-binary "@/path/to/private_key" e.g. curl -X GET http://netbox/api/secrets/secrets/ --data-binary "@/path/to/private_key"
""" """
queryset = Secret.objects.select_related('device__primary_ip', 'role') queryset = Secret.objects.select_related('device__primary_ip', 'role')\
.prefetch_related('role__users', 'role__groups')
serializer_class = SecretSerializer serializer_class = SecretSerializer
filter_class = SecretFilter filter_class = SecretFilter
permission_classes = [SecretViewPermission]
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [FreeRADIUSClientsRenderer] renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [FreeRADIUSClientsRenderer]
def get(self, request, private_key=None, *args, **kwargs): def get(self, request, private_key=None, *args, **kwargs):
@ -74,6 +69,7 @@ class SecretListView(generics.GenericAPIView):
master_key = uk.get_master_key(private_key) master_key = uk.get_master_key(private_key)
if master_key is not None: if master_key is not None:
for s in queryset: for s in queryset:
if s.decryptable_by(request.user):
s.decrypt(master_key) s.decrypt(master_key)
else: else:
return Response( return Response(
@ -91,9 +87,9 @@ class SecretDetailView(generics.GenericAPIView):
e.g. curl -X GET http://netbox/api/secrets/secrets/123/ --data-binary "@/path/to/private_key" e.g. curl -X GET http://netbox/api/secrets/secrets/123/ --data-binary "@/path/to/private_key"
""" """
queryset = Secret.objects.select_related('device__primary_ip', 'role') queryset = Secret.objects.select_related('device__primary_ip', 'role')\
.prefetch_related('role__users', 'role__groups')
serializer_class = SecretSerializer serializer_class = SecretSerializer
permission_classes = [SecretViewPermission]
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [FreeRADIUSClientsRenderer] renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [FreeRADIUSClientsRenderer]
def get(self, request, pk, *args, **kwargs): def get(self, request, pk, *args, **kwargs):
@ -114,14 +110,14 @@ class SecretDetailView(generics.GenericAPIView):
{'error': ERR_USERKEY_INACTIVE}, {'error': ERR_USERKEY_INACTIVE},
status=status.HTTP_400_BAD_REQUEST status=status.HTTP_400_BAD_REQUEST
) )
if secret.decryptable_by(request.user):
master_key = uk.get_master_key(private_key) master_key = uk.get_master_key(private_key)
if master_key is not None: if master_key is None:
secret.decrypt(master_key)
else:
return Response( return Response(
{'error': ERR_PRIVKEY_INVALID}, {'error': ERR_PRIVKEY_INVALID},
status=status.HTTP_400_BAD_REQUEST status=status.HTTP_400_BAD_REQUEST
) )
secret.decrypt(master_key)
serializer = self.get_serializer(secret) serializer = self.get_serializer(secret)
return Response(serializer.data) return Response(serializer.data)

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.1 on 2016-04-07 15:48
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('auth', '0007_alter_validators_add_error_messages'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('secrets', '0003_auto_20160321_1524'),
]
operations = [
migrations.AlterModelOptions(
name='secret',
options={'ordering': ['device', 'role', 'name']},
),
migrations.AddField(
model_name='secretrole',
name='groups',
field=models.ManyToManyField(blank=True, related_name='secretroles', to='auth.Group'),
),
migrations.AddField(
model_name='secretrole',
name='users',
field=models.ManyToManyField(blank=True, related_name='secretroles', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -4,7 +4,7 @@ from Crypto.PublicKey import RSA
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 User from django.contrib.auth.models import Group, User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models from django.db import models
@ -164,6 +164,8 @@ class SecretRole(models.Model):
""" """
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
users = models.ManyToManyField(User, related_name='secretroles', blank=True)
groups = models.ManyToManyField(Group, related_name='secretroles', blank=True)
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@ -189,9 +191,6 @@ class Secret(models.Model):
class Meta: class Meta:
ordering = ['device', 'role', 'name'] ordering = ['device', 'role', 'name']
permissions = (
('view_secret', "Can view secrets"),
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.plaintext = kwargs.pop('plaintext', None) self.plaintext = kwargs.pop('plaintext', None)
@ -279,3 +278,9 @@ class Secret(models.Model):
if not self.hash: if not self.hash:
raise Exception("Hash has not been generated for this secret.") raise Exception("Hash has not been generated for this secret.")
return check_password(plaintext, self.hash, preferred=SecretValidationHasher()) return check_password(plaintext, self.hash, preferred=SecretValidationHasher())
def decryptable_by(self, user):
"""
Check whether the given user has permission to decrypt this Secret.
"""
return user in self.role.users.all() or user.groups.filter(pk__in=self.role.groups.all()).exists()