mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-17 12:42:52 -06:00
Adapted the web UI to work with the new secrets API
This commit is contained in:
parent
a9fe39459a
commit
616ca4fe1f
@ -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']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -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):
|
||||||
|
@ -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():
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user