Browse Source

Adapted the web UI to work with the new secrets API

Jeremy Stretch 8 years ago
parent
commit
616ca4fe1f

+ 64 - 48
netbox/project-static/js/secrets.js

@@ -4,13 +4,16 @@ $(document).ready(function() {
     $('button.unlock-secret').click(function (event) {
         var secret_id = $(this).attr('secret-id');
 
-        // Retrieve from storage or prompt for private key
-        var private_key = sessionStorage.getItem('private_key');
-        if (!private_key) {
-            $('#privkey_modal').modal('show');
+        // If we have an active cookie containing a session key, send the API request.
+        if (document.cookie.indexOf('session_key') > 0) {
+            console.log("Retrieving secret...");
+            unlock_secret(secret_id);
+        // Otherwise, prompt the user for a private key so we can request a session key.
         } else {
-            unlock_secret(secret_id, private_key);
+            console.log("No session key found. Prompt user for private key.");
+            $('#privkey_modal').modal('show');
         }
+
     });
 
     // Locking a secret
@@ -18,64 +21,50 @@ $(document).ready(function() {
         var secret_id = $(this).attr('secret-id');
         var secret_div = $('#secret_' + secret_id);
 
-        // Delete the plaintext
+        // Delete the plaintext from the DOM element.
         secret_div.html('********');
         $(this).hide();
         $(this).siblings('button.unlock-secret').show();
     });
 
-    // Adding/editing a secret
-    private_key_field = $('#id_private_key');
-    private_key_field.parents('form').submit(function(event) {
-        console.log("form submitted");
-        var private_key = sessionStorage.getItem('private_key');
-        if (private_key) {
-            private_key_field.val(private_key);
-        } else if ($('form .requires-private-key:first').val()) {
-            console.log("we need a key!");
-            $('#privkey_modal').modal('show');
-            return false;
-        }
-    });
-
-    // Saving a private RSA key locally
-    $('#submit_privkey').click(function() {
+    // Retrieve a session key
+    $('#request_session_key').click(function() {
         var private_key = $('#user_privkey').val();
-        sessionStorage.setItem('private_key', private_key);
+
+        // POST the user's private key to request a temporary session key.
+        console.log("Requesting a session key...");
+        get_session_key(private_key);
     });
 
-    // Generate a new public/private key pair via the API
-    $('#generate_keypair').click(function() {
-        $('#new_keypair_modal').modal('show');
+    // Retrieve a secret via the API
+    function unlock_secret(secret_id) {
         $.ajax({
-            url: netbox_api_path + 'secrets/generate-keys/',
+            url: netbox_api_path + 'secrets/secrets/' + secret_id + '/',
             type: 'GET',
             dataType: 'json',
             success: function (response, status) {
-                var public_key = response.public_key;
-                var private_key = response.private_key;
-                $('#new_pubkey').val(public_key);
-                $('#new_privkey').val(private_key);
+                console.log("Secret retrieved successfully");
+                $('#secret_' + secret_id).html(response.plaintext);
+                $('button.unlock-secret[secret-id=' + secret_id + ']').hide();
+                $('button.lock-secret[secret-id=' + secret_id + ']').show();
             },
             error: function (xhr, ajaxOptions, thrownError) {
-                alert("There was an error generating a new key pair.");
+                console.log("Error: " + xhr.responseText);
+                if (xhr.status == 403) {
+                    alert("Permission denied");
+                } else {
+                    var json = jQuery.parseJSON(xhr.responseText);
+                    alert("Secret retrieval failed: " + json['error']);
+                }
             }
         });
-    });
-
-    // Enter a newly generated public key
-    $('#use_new_pubkey').click(function() {
-        var new_pubkey = $('#new_pubkey');
-        if (new_pubkey.val()) {
-            $('#id_public_key').val(new_pubkey.val());
-        }
-    });
+    }
 
-    // Retrieve a secret via the API
-    function unlock_secret(secret_id, private_key) {
+    // Request a session key via the API
+    function get_session_key(private_key) {
         var csrf_token = $('input[name=csrfmiddlewaretoken]').val();
         $.ajax({
-            url: netbox_api_path + 'secrets/secrets/' + secret_id + '/',
+            url: netbox_api_path + 'secrets/get-session-key/',
             type: 'POST',
             data: {
                 private_key: private_key
@@ -85,19 +74,46 @@ $(document).ready(function() {
                 xhr.setRequestHeader("X-CSRFToken", csrf_token);
             },
             success: function (response, status) {
-                $('#secret_' + secret_id).html(response.plaintext);
-                $('button.unlock-secret[secret-id=' + secret_id + ']').hide();
-                $('button.lock-secret[secret-id=' + secret_id + ']').show();
+                console.log("Received a new session key; valid until " + response.expiration_time);
+                alert('Session key received! You may now unlock secrets.');
             },
             error: function (xhr, ajaxOptions, thrownError) {
                 if (xhr.status == 403) {
                     alert("Permission denied");
                 } else {
                     var json = jQuery.parseJSON(xhr.responseText);
-                    alert("Decryption failed: " + json['error']);
+                    alert("Failed to retrieve a session key: " + json['error']);
                 }
             }
         });
     }
 
+    // Generate a new public/private key pair via the API
+    $('#generate_keypair').click(function() {
+        $('#new_keypair_modal').modal('show');
+        $.ajax({
+            url: netbox_api_path + 'secrets/generate-keys/',
+            type: 'GET',
+            dataType: 'json',
+            success: function (response, status) {
+                var public_key = response.public_key;
+                var private_key = response.private_key;
+                $('#new_pubkey').val(public_key);
+                $('#new_privkey').val(private_key);
+            },
+            error: function (xhr, ajaxOptions, thrownError) {
+                alert("There was an error generating a new key pair.");
+            }
+        });
+    });
+
+    // Accept a new RSA key pair generated via the API
+    $('#use_new_pubkey').click(function() {
+        var new_pubkey = $('#new_pubkey');
+
+        if (new_pubkey.val()) {
+            $('#id_public_key').val(new_pubkey.val());
+        }
+    });
+
 });

+ 2 - 7
netbox/secrets/forms.py

@@ -47,9 +47,8 @@ class SecretRoleForm(BootstrapMixin, forms.ModelForm):
 #
 
 class SecretForm(BootstrapMixin, forms.ModelForm):
-    private_key = forms.CharField(required=False, widget=forms.HiddenInput())
     plaintext = forms.CharField(max_length=65535, required=False, label='Plaintext',
-                                widget=forms.PasswordInput(attrs={'class': 'requires-private-key'}))
+                                widget=forms.PasswordInput())
     plaintext2 = forms.CharField(max_length=65535, required=False, label='Plaintext (verify)',
                                  widget=forms.PasswordInput())
 
@@ -59,9 +58,6 @@ class SecretForm(BootstrapMixin, forms.ModelForm):
 
     def clean(self):
 
-        if self.cleaned_data['plaintext']:
-            validate_rsa_key(self.cleaned_data['private_key'])
-
         if self.cleaned_data['plaintext'] != self.cleaned_data['plaintext2']:
             raise forms.ValidationError({
                 'plaintext2': "The two given plaintext values do not match. Please check your input."
@@ -86,8 +82,7 @@ class SecretFromCSVForm(forms.ModelForm):
 
 
 class SecretImportForm(BootstrapMixin, BulkImportForm):
-    private_key = forms.CharField(widget=forms.HiddenInput())
-    csv = CSVDataField(csv_form=SecretFromCSVForm, widget=forms.Textarea(attrs={'class': 'requires-private-key'}))
+    csv = CSVDataField(csv_form=SecretFromCSVForm)
 
 
 class SecretBulkEditForm(BootstrapMixin, BulkEditForm):

+ 39 - 16
netbox/secrets/views.py

@@ -1,3 +1,5 @@
+import base64
+
 from django.contrib import messages
 from django.contrib.auth.decorators import permission_required, login_required
 from django.contrib.auth.mixins import PermissionRequiredMixin
@@ -12,7 +14,7 @@ from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, Obje
 
 from . import filters, forms, tables
 from .decorators import userkey_required
-from .models import SecretRole, Secret, UserKey
+from .models import SecretRole, Secret, SessionKey, UserKey
 
 
 #
@@ -110,32 +112,44 @@ def secret_add(request, pk):
 def secret_edit(request, pk):
 
     secret = get_object_or_404(Secret, pk=pk)
-    uk = UserKey.objects.get(user=request.user)
 
     if request.method == 'POST':
         form = forms.SecretForm(request.POST, instance=secret)
         if form.is_valid():
 
-            # Re-encrypt the Secret if a plaintext has been specified.
-            if form.cleaned_data['plaintext']:
+            # Re-encrypt the Secret if a plaintext and session key have been provided.
+            session_key = request.COOKIES.get('session_key', None)
+            if form.cleaned_data['plaintext'] and session_key is not None:
 
-                # Retrieve the master key from the current user's UserKey
-                master_key = uk.get_master_key(form.cleaned_data['private_key'])
-                if master_key is None:
-                    form.add_error(None, "Invalid private key! Unable to encrypt secret data.")
+                # Retrieve the master key using the provided session key
+                session_key = base64.b64decode(session_key)
+                master_key = None
+                try:
+                    sk = SessionKey.objects.get(user=request.user)
+                    master_key = sk.get_master_key(session_key)
+                except SessionKey.DoesNotExist:
+                    form.add_error(None, "No session key found for this user.")
 
                 # Create and encrypt the new Secret
-                else:
+                if master_key is not None:
                     secret = form.save(commit=False)
                     secret.plaintext = str(form.cleaned_data['plaintext'])
                     secret.encrypt(master_key)
                     secret.save()
+                    messages.success(request, u"Modified secret {}.".format(secret))
+                    return redirect('secrets:secret', pk=secret.pk)
+                else:
+                    form.add_error(None, "Invalid session key. Unable to encrypt secret data.")
 
+            # We can't save the plaintext without a session key.
+            elif form.cleaned_data['plaintext']:
+                form.add_error(None, "No session key was provided with the request. Unable to encrypt secret data.")
+
+            # If no new plaintext was specified, a session key is not needed.
             else:
                 secret = form.save()
-
-            messages.success(request, u"Modified secret {}.".format(secret))
-            return redirect('secrets:secret', pk=secret.pk)
+                messages.success(request, u"Modified secret {}.".format(secret))
+                return redirect('secrets:secret', pk=secret.pk)
 
     else:
         form = forms.SecretForm(instance=secret)
@@ -157,19 +171,28 @@ class SecretDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 @userkey_required()
 def secret_import(request):
 
-    uk = UserKey.objects.get(user=request.user)
+    session_key = request.COOKIES.get('session_key', None)
 
     if request.method == 'POST':
         form = forms.SecretImportForm(request.POST)
+
+        if session_key is None:
+            form.add_error(None, "No session key was provided with the request. Unable to encrypt secret data.")
+
         if form.is_valid():
 
             new_secrets = []
 
-            # Retrieve the master key from the current user's UserKey
-            master_key = uk.get_master_key(form.cleaned_data['private_key'])
+            session_key = base64.b64decode(session_key)
+            master_key = None
+            try:
+                sk = SessionKey.objects.get(user=request.user)
+                master_key = sk.get_master_key(session_key)
+            except SessionKey.DoesNotExist:
+                form.add_error(None, "No session key found for this user.")
+
             if master_key is None:
                 form.add_error(None, "Invalid private key! Unable to encrypt secret data.")
-
             else:
                 try:
                     with transaction.atomic():

+ 4 - 5
netbox/templates/secrets/inc/private_key_modal.html

@@ -10,16 +10,15 @@
             </div>
             <div class="modal-body">
                 <p>
-                    Your private RSA key is needed to complete this action. Once you've provided your key, it will
-                    remain cached locally until you close this browser tab.
+                    You do not have an active session key. To request one, please provide your private RSA key below.
+                    Once retrieved, your session key will be saved for future requests.
                 </p>
                 <div class="form-group">
                     <textarea class="form-control" id="user_privkey" style="height: 300px;"></textarea>
                 </div>
                 <div class="form-group text-right">
-                    <button id="submit_privkey" class="btn btn-primary unlock-secret" data-dismiss="modal">
-                        <i class="fa fa-save" aria-hidden="True"></i>
-                        Save RSA Key
+                    <button id="request_session_key" class="btn btn-primary unlock-secret" data-dismiss="modal">
+                        Request session key
                     </button>
                 </div>
             </div>