Browse Source

Introduced per-role decryption permissions

Jeremy Stretch 9 years ago
parent
commit
2cb99c6012

+ 14 - 18
netbox/secrets/api/views.py

@@ -4,7 +4,7 @@ from django.shortcuts import get_object_or_404
 
 from rest_framework import generics
 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.settings import api_settings
 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."
 
 
-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
@@ -47,10 +42,10 @@ class SecretListView(generics.GenericAPIView):
 
       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
     filter_class = SecretFilter
-    permission_classes = [SecretViewPermission]
     renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [FreeRADIUSClientsRenderer]
 
     def get(self, request, private_key=None, *args, **kwargs):
@@ -74,7 +69,8 @@ class SecretListView(generics.GenericAPIView):
             master_key = uk.get_master_key(private_key)
             if master_key is not None:
                 for s in queryset:
-                    s.decrypt(master_key)
+                    if s.decryptable_by(request.user):
+                        s.decrypt(master_key)
             else:
                 return Response(
                     {'error': ERR_PRIVKEY_INVALID},
@@ -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"
     """
-    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
-    permission_classes = [SecretViewPermission]
     renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [FreeRADIUSClientsRenderer]
 
     def get(self, request, pk, *args, **kwargs):
@@ -114,14 +110,14 @@ class SecretDetailView(generics.GenericAPIView):
                     {'error': ERR_USERKEY_INACTIVE},
                     status=status.HTTP_400_BAD_REQUEST
                 )
-            master_key = uk.get_master_key(private_key)
-            if master_key is not None:
+            if secret.decryptable_by(request.user):
+                master_key = uk.get_master_key(private_key)
+                if master_key is None:
+                    return Response(
+                        {'error': ERR_PRIVKEY_INVALID},
+                        status=status.HTTP_400_BAD_REQUEST
+                    )
                 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)

+ 32 - 0
netbox/secrets/migrations/0004_auto_20160407_1548.py

@@ -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),
+        ),
+    ]

+ 9 - 4
netbox/secrets/models.py

@@ -4,7 +4,7 @@ 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.auth.models import Group, User
 from django.core.exceptions import ValidationError
 from django.core.urlresolvers import reverse
 from django.db import models
@@ -164,6 +164,8 @@ class SecretRole(models.Model):
     """
     name = models.CharField(max_length=50, 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:
         ordering = ['name']
@@ -189,9 +191,6 @@ class Secret(models.Model):
 
     class Meta:
         ordering = ['device', 'role', 'name']
-        permissions = (
-            ('view_secret', "Can view secrets"),
-        )
 
     def __init__(self, *args, **kwargs):
         self.plaintext = kwargs.pop('plaintext', None)
@@ -279,3 +278,9 @@ class Secret(models.Model):
         if not self.hash:
             raise Exception("Hash has not been generated for this secret.")
         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()