From 6ce2cf9db0ef7dd377f0918f4889664ea3a13e34 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 22 Mar 2016 12:17:49 -0400 Subject: [PATCH] Reworked secrets API to allow optional decryption by POSTing a private key --- netbox/project-static/js/secrets.js | 2 +- netbox/secrets/api/serializers.py | 2 +- netbox/secrets/api/urls.py | 1 - netbox/secrets/api/views.py | 124 +++++++++++++++++++--------- 4 files changed, 86 insertions(+), 43 deletions(-) diff --git a/netbox/project-static/js/secrets.js b/netbox/project-static/js/secrets.js index 264014715..21d8656e6 100644 --- a/netbox/project-static/js/secrets.js +++ b/netbox/project-static/js/secrets.js @@ -71,7 +71,7 @@ $(document).ready(function() { function unlock_secret(secret_id, private_key) { var csrf_token = $('input[name=csrfmiddlewaretoken]').val(); $.ajax({ - url: '/api/secrets/secrets/' + secret_id + '/decrypt/', + url: '/api/secrets/secrets/' + secret_id + '/', type: 'POST', data: { private_key: private_key diff --git a/netbox/secrets/api/serializers.py b/netbox/secrets/api/serializers.py index 2c016a5a7..ea245e753 100644 --- a/netbox/secrets/api/serializers.py +++ b/netbox/secrets/api/serializers.py @@ -31,7 +31,7 @@ class SecretSerializer(serializers.ModelSerializer): class Meta: model = Secret - fields = ['id', 'device', 'role', 'name', 'hash', 'created', 'last_modified'] + fields = ['id', 'device', 'role', 'name', 'plaintext', 'hash', 'created', 'last_modified'] class SecretNestedSerializer(SecretSerializer): diff --git a/netbox/secrets/api/urls.py b/netbox/secrets/api/urls.py index e38cf3393..6acae580d 100644 --- a/netbox/secrets/api/urls.py +++ b/netbox/secrets/api/urls.py @@ -8,7 +8,6 @@ urlpatterns = [ # Secrets url(r'^secrets/$', SecretListView.as_view(), name='secret_list'), url(r'^secrets/(?P\d+)/$', SecretDetailView.as_view(), name='secret_detail'), - url(r'^secrets/(?P\d+)/decrypt/$', SecretDecryptView.as_view(), name='secret_decrypt'), # Secret roles url(r'^secret-roles/$', SecretRoleListView.as_view(), name='secretrole_list'), diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index 34d0229fe..b75ef9f4a 100644 --- a/netbox/secrets/api/views.py +++ b/netbox/secrets/api/views.py @@ -1,11 +1,10 @@ from Crypto.PublicKey import RSA -from django.http import HttpResponseForbidden from django.shortcuts import get_object_or_404 from rest_framework import generics -from rest_framework.exceptions import ValidationError -from rest_framework.permissions import IsAuthenticated +from rest_framework import status +from rest_framework.permissions import IsAuthenticated, BasePermission from rest_framework.response import Response from rest_framework.views import APIView @@ -14,6 +13,16 @@ from secrets.models import Secret, SecretRole, UserKey from .serializers import SecretRoleSerializer, SecretSerializer +ERR_USERKEY_MISSING = "No UserKey found for the current user." +ERR_USERKEY_INACTIVE = "UserKey has not been activated for decryption." +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): """ List all secret roles @@ -30,55 +39,90 @@ class SecretRoleDetailView(generics.RetrieveAPIView): serializer_class = SecretRoleSerializer -class SecretListView(generics.ListAPIView): +class SecretListView(generics.GenericAPIView): """ - List secrets (filterable) + List secrets (filterable). If a private key is POSTed, attempt to decrypt each Secret. """ - queryset = Secret.objects.select_related('role') + queryset = Secret.objects.select_related('device', 'role') serializer_class = SecretSerializer filter_class = SecretFilter - permission_classes = [IsAuthenticated] + permission_classes = [SecretViewPermission] + def get(self, request, private_key=None, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) -class SecretDetailView(generics.RetrieveAPIView): - """ - Retrieve a single Secret - """ - queryset = Secret.objects.select_related('role') - serializer_class = SecretSerializer - permission_classes = [IsAuthenticated] + # Attempt to decrypt each Secret if a private key was provided. + if private_key is not None: + try: + uk = UserKey.objects.get(user=request.user) + except UserKey.DoesNotExist: + return Response( + {'error': ERR_USERKEY_MISSING}, + status=status.HTTP_400_BAD_REQUEST + ) + if not uk.is_active(): + return Response( + {'error': ERR_USERKEY_INACTIVE}, + status=status.HTTP_400_BAD_REQUEST + ) + master_key = uk.get_master_key(private_key) + if master_key is not None: + for s in queryset: + s.decrypt(master_key) + else: + return Response( + {'error': ERR_PRIVKEY_INVALID}, + status=status.HTTP_400_BAD_REQUEST + ) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) -class SecretDecryptView(APIView): - """ - Retrieve the plaintext from a stored Secret. The request must include a valid private key. - """ - permission_classes = [IsAuthenticated] - - def post(self, request, pk): - - secret = get_object_or_404(Secret, pk=pk) + def post(self, request, *args, **kwargs): private_key = request.POST.get('private_key') - if not private_key: - raise ValidationError("Private key is missing from request.") + return self.get(request, private_key=private_key) - # Retrieve the Secret's plaintext with the user's private key - try: - uk = UserKey.objects.get(user=request.user) - except UserKey.DoesNotExist: - return HttpResponseForbidden(reason="No UserKey found.") - if not uk.is_active(): - return HttpResponseForbidden(reason="UserKey is inactive.") - # Attempt to decrypt the Secret. - master_key = uk.get_master_key(private_key) - if master_key is None: - raise ValidationError("Invalid secret key.") - secret.decrypt(master_key) +class SecretDetailView(generics.GenericAPIView): + """ + Retrieve a single Secret. If a private key is POSTed, attempt to decrypt the Secret. + """ + queryset = Secret.objects.select_related('device', 'role') + serializer_class = SecretSerializer + permission_classes = [SecretViewPermission] - return Response({ - 'plaintext': secret.plaintext, - }) + def get(self, request, pk, private_key=None, *args, **kwargs): + secret = get_object_or_404(Secret, pk=pk) + + # Attempt to decrypt the Secret if a private key was provided. + if private_key is not None: + try: + uk = UserKey.objects.get(user=request.user) + except UserKey.DoesNotExist: + return Response( + {'error': ERR_USERKEY_MISSING}, + status=status.HTTP_400_BAD_REQUEST + ) + if not uk.is_active(): + return Response( + {'error': ERR_USERKEY_INACTIVE}, + status=status.HTTP_400_BAD_REQUEST + ) + master_key = uk.get_master_key(private_key) + if master_key is not None: + secret.decrypt(master_key) + else: + return Response( + {'error': ERR_PRIVKEY_INVALID}, + status=status.HTTP_400_BAD_REQUEST + ) + + serializer = self.get_serializer(secret) + return Response(serializer.data) + + def post(self, request, pk, *args, **kwargs): + private_key = request.POST.get('private_key') + return self.get(request, pk, private_key=private_key) class RSAKeyGeneratorView(APIView):