Adapted the web UI to work with the new secrets API

This commit is contained in:
Jeremy Stretch 2017-02-03 16:14:42 -05:00
parent a9fe39459a
commit 616ca4fe1f
4 changed files with 114 additions and 81 deletions

View File

@ -4,13 +4,16 @@ $(document).ready(function() {
$('button.unlock-secret').click(function (event) { $('button.unlock-secret').click(function (event) {
var secret_id = $(this).attr('secret-id'); var secret_id = $(this).attr('secret-id');
// Retrieve from storage or prompt for private key // If we have an active cookie containing a session key, send the API request.
var private_key = sessionStorage.getItem('private_key'); if (document.cookie.indexOf('session_key') > 0) {
if (!private_key) { console.log("Retrieving secret...");
$('#privkey_modal').modal('show'); unlock_secret(secret_id);
// Otherwise, prompt the user for a private key so we can request a session key.
} else { } 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 // Locking a secret
@ -18,31 +21,72 @@ $(document).ready(function() {
var secret_id = $(this).attr('secret-id'); var secret_id = $(this).attr('secret-id');
var secret_div = $('#secret_' + secret_id); var secret_div = $('#secret_' + secret_id);
// Delete the plaintext // Delete the plaintext from the DOM element.
secret_div.html('********'); secret_div.html('********');
$(this).hide(); $(this).hide();
$(this).siblings('button.unlock-secret').show(); $(this).siblings('button.unlock-secret').show();
}); });
// Adding/editing a secret // Retrieve a session key
private_key_field = $('#id_private_key'); $('#request_session_key').click(function() {
private_key_field.parents('form').submit(function(event) { var private_key = $('#user_privkey').val();
console.log("form submitted");
var private_key = sessionStorage.getItem('private_key'); // POST the user's private key to request a temporary session key.
if (private_key) { console.log("Requesting a session key...");
private_key_field.val(private_key); get_session_key(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 // Retrieve a secret via the API
$('#submit_privkey').click(function() { function unlock_secret(secret_id) {
var private_key = $('#user_privkey').val(); $.ajax({
sessionStorage.setItem('private_key', private_key); 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 a new public/private key pair via the API
$('#generate_keypair').click(function() { $('#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() { $('#use_new_pubkey').click(function() {
var new_pubkey = $('#new_pubkey'); var new_pubkey = $('#new_pubkey');
if (new_pubkey.val()) { if (new_pubkey.val()) {
$('#id_public_key').val(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']);
}
}
});
}
}); });

View File

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

View File

@ -1,3 +1,5 @@
import base64
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import permission_required, login_required from django.contrib.auth.decorators import permission_required, login_required
from django.contrib.auth.mixins import PermissionRequiredMixin 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 . import filters, forms, tables
from .decorators import userkey_required from .decorators import userkey_required
from .models import SecretRole, Secret, UserKey from .models import SecretRole, Secret, SessionKey, UserKey
# #
@ -110,30 +112,42 @@ def secret_add(request, pk):
def secret_edit(request, pk): def secret_edit(request, pk):
secret = get_object_or_404(Secret, pk=pk) secret = get_object_or_404(Secret, pk=pk)
uk = UserKey.objects.get(user=request.user)
if request.method == 'POST': if request.method == 'POST':
form = forms.SecretForm(request.POST, instance=secret) form = forms.SecretForm(request.POST, instance=secret)
if form.is_valid(): if form.is_valid():
# Re-encrypt the Secret if a plaintext has been specified. # Re-encrypt the Secret if a plaintext and session key have been provided.
if form.cleaned_data['plaintext']: 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 # Retrieve the master key using the provided session key
master_key = uk.get_master_key(form.cleaned_data['private_key']) session_key = base64.b64decode(session_key)
if master_key is None: master_key = None
form.add_error(None, "Invalid private key! Unable to encrypt secret data.") 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 # Create and encrypt the new Secret
else: if master_key is not None:
secret = form.save(commit=False) secret = form.save(commit=False)
secret.plaintext = str(form.cleaned_data['plaintext']) secret.plaintext = str(form.cleaned_data['plaintext'])
secret.encrypt(master_key) secret.encrypt(master_key)
secret.save() 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: else:
secret = form.save() secret = form.save()
messages.success(request, u"Modified secret {}.".format(secret)) messages.success(request, u"Modified secret {}.".format(secret))
return redirect('secrets:secret', pk=secret.pk) return redirect('secrets:secret', pk=secret.pk)
@ -157,19 +171,28 @@ class SecretDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@userkey_required() @userkey_required()
def secret_import(request): def secret_import(request):
uk = UserKey.objects.get(user=request.user) session_key = request.COOKIES.get('session_key', None)
if request.method == 'POST': if request.method == 'POST':
form = forms.SecretImportForm(request.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(): if form.is_valid():
new_secrets = [] new_secrets = []
# Retrieve the master key from the current user's UserKey session_key = base64.b64decode(session_key)
master_key = uk.get_master_key(form.cleaned_data['private_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: if master_key is None:
form.add_error(None, "Invalid private key! Unable to encrypt secret data.") form.add_error(None, "Invalid private key! Unable to encrypt secret data.")
else: else:
try: try:
with transaction.atomic(): with transaction.atomic():

View File

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