From 616ca4fe1fc9080b91acb974ea482906ba0039b8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 3 Feb 2017 16:14:42 -0500 Subject: [PATCH] Adapted the web UI to work with the new secrets API --- netbox/project-static/js/secrets.js | 122 ++++++++++-------- netbox/secrets/forms.py | 9 +- netbox/secrets/views.py | 55 +++++--- .../secrets/inc/private_key_modal.html | 9 +- 4 files changed, 114 insertions(+), 81 deletions(-) diff --git a/netbox/project-static/js/secrets.js b/netbox/project-static/js/secrets.js index 22710236b..40f4b5f0c 100644 --- a/netbox/project-static/js/secrets.js +++ b/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,31 +21,72 @@ $(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; - } + // Retrieve a session key + $('#request_session_key').click(function() { + var private_key = $('#user_privkey').val(); + + // POST the user's private key to request a temporary session key. + console.log("Requesting a session key..."); + get_session_key(private_key); }); - // Saving a private RSA key locally - $('#submit_privkey').click(function() { - var private_key = $('#user_privkey').val(); - sessionStorage.setItem('private_key', private_key); - }); + // Retrieve a secret via the API + function unlock_secret(secret_id) { + $.ajax({ + url: netbox_api_path + 'secrets/secrets/' + secret_id + '/', + type: 'GET', + dataType: 'json', + success: function (response, status) { + 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) { + 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']); + } + } + }); + } + + // 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/get-session-key/', + type: 'POST', + data: { + private_key: private_key + }, + dataType: 'json', + beforeSend: function(xhr, settings) { + xhr.setRequestHeader("X-CSRFToken", csrf_token); + }, + success: function (response, status) { + 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("Failed to retrieve a session key: " + json['error']); + } + } + }); + } // Generate a new public/private key pair via the API $('#generate_keypair').click(function() { @@ -63,41 +107,13 @@ $(document).ready(function() { }); }); - // Enter a newly generated public key + // 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()); } }); - // Retrieve a secret via the API - function unlock_secret(secret_id, private_key) { - var csrf_token = $('input[name=csrfmiddlewaretoken]').val(); - $.ajax({ - url: netbox_api_path + 'secrets/secrets/' + secret_id + '/', - type: 'POST', - data: { - private_key: private_key - }, - dataType: 'json', - beforeSend: function(xhr, settings) { - 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(); - }, - error: function (xhr, ajaxOptions, thrownError) { - if (xhr.status == 403) { - alert("Permission denied"); - } else { - var json = jQuery.parseJSON(xhr.responseText); - alert("Decryption failed: " + json['error']); - } - } - }); - } - }); diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index b4c64b485..15bfad9eb 100644 --- a/netbox/secrets/forms.py +++ b/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): diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index d67cd18a0..5a90eca6e 100644 --- a/netbox/secrets/views.py +++ b/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(): diff --git a/netbox/templates/secrets/inc/private_key_modal.html b/netbox/templates/secrets/inc/private_key_modal.html index d55e6425e..00d2455c1 100644 --- a/netbox/templates/secrets/inc/private_key_modal.html +++ b/netbox/templates/secrets/inc/private_key_modal.html @@ -10,16 +10,15 @@