From eb951fdaf1d22a67cb301cceaa48abd89c38bedd Mon Sep 17 00:00:00 2001 From: checktheroads Date: Sat, 17 Apr 2021 17:18:13 -0700 Subject: [PATCH] migrate secrets to bootstrap 5 and deprecate jquery functions --- netbox/project-static/dist/netbox.js | Bin 438446 -> 444724 bytes netbox/project-static/dist/netbox.js.map | Bin 1249918 -> 1264902 bytes netbox/project-static/src/global.d.ts | 24 ++- netbox/project-static/src/netbox.ts | 7 +- netbox/project-static/src/secrets.ts | 192 +++++++++++++++--- netbox/project-static/src/util.ts | 51 ++++- .../secrets/inc/private_key_modal.html | 22 +- netbox/templates/secrets/secret.html | 6 +- netbox/templates/secrets/secret_import.html | 4 - netbox/templates/secrets/secretrole.html | 62 +++--- 10 files changed, 287 insertions(+), 81 deletions(-) diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 0537d116684a8a8a2753a7622eaa6f522d880877..bd8ae108ce9148efe7bab8dc8e3a139e5711e01c 100644 GIT binary patch delta 6283 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 delta 1899 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 lsJs = { results: T[]; }; -type APIError = { +type ErrorBase = { error: string; +}; + +type APIError = { exception: string; netbox_version: string; python_version: string; -}; +} & ErrorBase; type APIObjectBase = { id: number; @@ -39,6 +42,23 @@ type APIReference = { _depth: number; }; +type APISecret = { + assigned_object: APIObjectBase; + assigned_object_id: number; + assigned_object_type: string; + created: string; + custom_fields: Record; + display: string; + hash: string; + id: number; + last_updated: string; + name: string; + plaintext: Nullable; + role: APIObjectBase; + tags: number[]; + url: string; +}; + interface ObjectWithGroup extends APIObjectBase { group: Nullable; } diff --git a/netbox/project-static/src/netbox.ts b/netbox/project-static/src/netbox.ts index a8d9023fa..465ecdf9f 100644 --- a/netbox/project-static/src/netbox.ts +++ b/netbox/project-static/src/netbox.ts @@ -7,7 +7,7 @@ import { initSpeedSelector, initForms } from './forms'; import { initRackElevation } from './buttons'; import { initClipboard } from './clipboard'; import { initSearchBar } from './search'; -// import { initGenerateKeyPair } from './secrets'; +import { initGenerateKeyPair, initLockUnlock, initGetSessionKey } from './secrets'; import { getElements } from './util'; const INITIALIZERS = [ @@ -21,7 +21,9 @@ const INITIALIZERS = [ initColorSelect, initRackElevation, initClipboard, - // initGenerateKeyPair, + initGenerateKeyPair, + initLockUnlock, + initGetSessionKey, ] as (() => void)[]; /** @@ -35,7 +37,6 @@ function initBootstrap(): void { new Tooltip(tooltip, { container: 'body', boundary: 'window' }); } for (const modal of getElements('[data-bs-toggle="modal"]')) { - // for (const modal of getElements('div.modal')) { new Modal(modal); } initMessageToasts(); diff --git a/netbox/project-static/src/secrets.ts b/netbox/project-static/src/secrets.ts index 88c255fc7..d2104f959 100644 --- a/netbox/project-static/src/secrets.ts +++ b/netbox/project-static/src/secrets.ts @@ -1,47 +1,50 @@ -import { apiGetBase, getElements, isApiError } from './util'; +import { Modal } from 'bootstrap'; +import { apiGetBase, apiPostForm, getElements, isApiError, hasError } from './util'; +import { createToast } from './toast'; + /** - * - * $('#generate_keypair').click(function() { - $('#new_keypair_modal').modal('show'); - $.ajax({ - url: netbox_api_path + 'secrets/generate-rsa-key-pair/', - 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."); - } - }); - }); + * Initialize Generate Private Key Pair Elements. */ export function initGenerateKeyPair() { const element = document.getElementById('new_keypair_modal') as HTMLDivElement; const accept = document.getElementById('use_new_pubkey') as HTMLButtonElement; + // If the elements are not loaded, stop. + if (element === null || accept === null) { + return; + } const publicElem = element.querySelector('textarea#new_pubkey'); const privateElem = element.querySelector('textarea#new_privkey'); + /** + * Handle Generate Private Key Pair Modal opening. + */ function handleOpen() { + // When the modal opens, set the `readonly` attribute on the textarea elements. for (const elem of [publicElem, privateElem]) { if (elem !== null) { elem.setAttribute('readonly', ''); } } - + // Fetch the key pair from the API. apiGetBase('/api/secrets/generate-rsa-key-pair').then(data => { - if (!isApiError(data)) { + if (!hasError(data)) { + // If key pair generation was successful, set the textarea elements' value to the generated + // values. const { private_key: priv, public_key: pub } = data; if (publicElem !== null && privateElem !== null) { publicElem.value = pub; privateElem.value = priv; } + } else { + // Otherwise, show an error. + const toast = createToast('danger', 'Error', data.error); + toast.show(); } }); } + /** + * Set the public key form field's value to the generated public key. + */ function handleAccept() { const publicKeyField = document.getElementById('id_public_key') as HTMLTextAreaElement; if (publicElem !== null) { @@ -53,10 +56,147 @@ export function initGenerateKeyPair() { accept.addEventListener('click', handleAccept); } -export function initLockUnlock() { - for (const element of getElements('button.unlock-secret')) { - function handleClick() { - const { secretId } = element.dataset; - } +/** + * Toggle copy/lock/unlock button visibility based on the action occurring. + * @param id Secret ID. + * @param action Lock or Unlock, so we know which buttons to display. + */ +function toggleSecretButtons(id: string, action: 'lock' | 'unlock') { + const unlockButton = document.querySelector(`button.unlock-secret[secret-id='${id}']`); + const lockButton = document.querySelector(`button.lock-secret[secret-id='${id}']`); + const copyButton = document.querySelector(`button.copy-secret[secret-id='${id}']`); + + // If we're unlocking, hide the unlock button. Otherwise, show it. + if (unlockButton !== null) { + if (action === 'unlock') unlockButton.classList.add('d-none'); + if (action === 'lock') unlockButton.classList.remove('d-none'); + } + // If we're unlocking, show the lock button. Otherwise, hide it. + if (lockButton !== null) { + if (action === 'unlock') lockButton.classList.remove('d-none'); + if (action === 'lock') lockButton.classList.add('d-none'); + } + // If we're unlocking, show the copy button. Otherwise, hide it. + if (copyButton !== null) { + if (action === 'unlock') copyButton.classList.remove('d-none'); + if (action === 'lock') copyButton.classList.add('d-none'); + } +} + +/** + * Initialize Lock & Unlock button event listeners & callbacks. + */ +export function initLockUnlock() { + const privateKeyModalElem = document.getElementById('privkey_modal'); + if (privateKeyModalElem === null) { + return; + } + const privateKeyModal = new Modal(privateKeyModalElem); + + /** + * Unlock a secret, or prompt the user for their private key, if a session key is not available. + * + * @param id Secret ID + */ + function unlock(id: string | null) { + const target = document.getElementById(`secret_${id}`); + if (typeof id === 'string' && id !== '') { + apiGetBase(`/api/secrets/secrets/${id}`).then(data => { + if (!hasError(data)) { + const { plaintext } = data; + // `plaintext` is the plain text value of the secret. If it is null, it has not been + // decrypted, likely due to a mission session key. + + if (target !== null && plaintext !== null) { + // If `plaintext` is not null, we have the decrypted value. Set the target element's + // inner text to the decrypted value and toggle copy/lock button visibility. + target.innerText = plaintext; + toggleSecretButtons(id, 'unlock'); + } else { + // Otherwise, we do _not_ have the decrypted value and need to prompt the user for + // their private RSA key, in order to get a session key. The session key is then sent + // as a cookie in future requests. + privateKeyModal.show(); + } + } else { + if (data.error.toLowerCase().includes('invalid session key')) { + // If, for some reason, a request was made but resulted in an API error that complains + // of a missing session key, prompt the user for their session key. + privateKeyModal.show(); + } else { + // If we received an API error but it doesn't contain 'invalid session key', show the + // user an error message. + const toast = createToast('danger', 'Error', data.error); + toast.show(); + } + } + }); + } + } + /** + * Lock a secret and toggle visibility of the unlock button. + * @param id Secret ID + */ + function lock(id: string | null) { + if (typeof id === 'string' && id !== '') { + const target = document.getElementById(`secret_${id}`); + if (target !== null) { + // Obscure the inner text of the secret element. + target.innerText = '********'; + } + // Toggle visibility of the copy/lock/unlock buttons. + toggleSecretButtons(id, 'lock'); + } + } + + for (const element of getElements('button.unlock-secret')) { + element.addEventListener('click', () => unlock(element.getAttribute('secret-id'))); + } + for (const element of getElements('button.lock-secret')) { + element.addEventListener('click', () => lock(element.getAttribute('secret-id'))); + } +} + +/** + * Request a session key from the API. + * @param privateKey RSA Private Key (valid JSON string) + */ +function requestSessionKey(privateKey: string) { + apiPostForm('/api/secrets/get-session-key/', { private_key: privateKey }).then(res => { + if (!hasError(res)) { + // If the response received was not an error, show the user a success message. + const toast = createToast('success', 'Session Key Received', 'You may now unlock secrets.'); + toast.show(); + } else { + // Otherwise, show the user an error message. + let message = res.error; + if (isApiError(res)) { + // If the error received was a standard API error containing a Python exception message, + // append it to the error. + message += `\n${res.exception}`; + } + const toast = createToast('danger', 'Failed to Retrieve Session Key', message); + toast.show(); + } + }); +} + +/** + * Initialize Request Session Key Elements. + */ +export function initGetSessionKey() { + for (const element of getElements('#request_session_key')) { + /** + * Send the user's input private key to the API to get a session key, which will be stored as + * a cookie for future requests. + */ + function handleClick() { + for (const pk of getElements('#user_privkey')) { + requestSessionKey(pk.value); + // Clear the private key form field value. + pk.value = ''; + } + } + element.addEventListener('click', handleClick); } } diff --git a/netbox/project-static/src/util.ts b/netbox/project-static/src/util.ts index b8cf37fdd..960fd30e2 100644 --- a/netbox/project-static/src/util.ts +++ b/netbox/project-static/src/util.ts @@ -1,6 +1,9 @@ import Cookie from 'cookie'; - export function isApiError(data: Record): data is APIError { + return 'error' in data && 'exception' in data; +} + +export function hasError(data: Record): data is ErrorBase { return 'error' in data; } @@ -34,13 +37,55 @@ export function getCsrfToken(): string { export async function apiGetBase>( url: string, -): Promise { +): Promise { const token = getCsrfToken(); const res = await fetch(url, { method: 'GET', headers: { 'X-CSRFToken': token }, + credentials: 'same-origin', }); + const contentType = res.headers.get('Content-Type'); + if (typeof contentType === 'string' && contentType.includes('text')) { + const error = await res.text(); + return { error } as ErrorBase; + } + const json = (await res.json()) as T | APIError; + if (!res.ok && Array.isArray(json)) { + const error = json.join('\n'); + return { error } as ErrorBase; + } + return json; +} + +export async function apiPostForm< + T extends Record, + R extends Record +>(url: string, data: T): Promise { + const token = getCsrfToken(); + const body = new URLSearchParams(); + for (const [k, v] of Object.entries(data)) { + body.append(k, String(v)); + } + const res = await fetch(url, { + method: 'POST', + body, + headers: { 'X-CSRFToken': token }, + }); + + const contentType = res.headers.get('Content-Type'); + if (typeof contentType === 'string' && contentType.includes('text')) { + let error = await res.text(); + if (contentType.includes('text/html')) { + error = res.statusText; + } + return { error } as ErrorBase; + } + + const json = (await res.json()) as R | APIError; + if (!res.ok && 'detail' in json) { + return { error: json.detail as string } as ErrorBase; + } return json; } @@ -50,7 +95,7 @@ export async function apiGetBase>( */ export async function getApiData( url: string, -): Promise | APIError> { +): Promise | ErrorBase | APIError> { return await apiGetBase>(url); } diff --git a/netbox/templates/secrets/inc/private_key_modal.html b/netbox/templates/secrets/inc/private_key_modal.html index 5b1d4550b..86f240277 100644 --- a/netbox/templates/secrets/inc/private_key_modal.html +++ b/netbox/templates/secrets/inc/private_key_modal.html @@ -2,26 +2,26 @@ diff --git a/netbox/templates/secrets/secret.html b/netbox/templates/secrets/secret.html index b79fc5026..c84a6c342 100644 --- a/netbox/templates/secrets/secret.html +++ b/netbox/templates/secrets/secret.html @@ -53,15 +53,15 @@
Secret
-
********
+
********
- -
diff --git a/netbox/templates/secrets/secret_import.html b/netbox/templates/secrets/secret_import.html index 0e540eb94..e8789da66 100644 --- a/netbox/templates/secrets/secret_import.html +++ b/netbox/templates/secrets/secret_import.html @@ -5,7 +5,3 @@ {{ block.super }} {% include 'secrets/inc/private_key_modal.html' %} {% endblock %} - -{% block javascript %} - -{% endblock %} diff --git a/netbox/templates/secrets/secretrole.html b/netbox/templates/secrets/secretrole.html index 0e284fb75..2116d8a3c 100644 --- a/netbox/templates/secrets/secretrole.html +++ b/netbox/templates/secrets/secretrole.html @@ -3,33 +3,35 @@ {% load plugins %} {% block breadcrumbs %} -
  • Secret Roles
  • -
  • {{ object }}
  • + + {% endblock %} {% block content %} -
    +
    -
    -
    - Secret Role +
    +
    + Secret Role +
    +
    + + + + + + + + + + + + + +
    Name{{ object.name }}
    Description{{ object.description|placeholder }}
    Secrets + {{ secrets_table.rows|length }} +
    - - - - - - - - - - - - - -
    Name{{ object.name }}
    Description{{ object.description|placeholder }}
    Secrets - {{ secrets_table.rows|length }} -
    {% plugin_left_page object %}
    @@ -40,15 +42,17 @@
    -
    -
    - Secrets +
    +
    + Secrets +
    +
    + {% include 'inc/table.html' with table=secrets_table %}
    - {% include 'inc/table.html' with table=secrets_table %} {% if perms.secrets.add_secret %} -