From e18d26d1e0887f9f74aa993ef70c048d68066269 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 25 Jan 2023 12:54:44 +0000 Subject: [PATCH 01/17] [skip ci] Updated CHANGELOG.md --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cb3b91..6ddec6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [Unreleased](https://github.com/Onemind-Services-LLC/netbox-secrets/tree/HEAD) + +[Full Changelog](https://github.com/Onemind-Services-LLC/netbox-secrets/compare/v1.7.3...HEAD) + +**Closed issues:** + +- \[Bug\]: Secret role secrets count is always 0 [\#24](https://github.com/Onemind-Services-LLC/netbox-secrets/issues/24) + +## [v1.7.3](https://github.com/Onemind-Services-LLC/netbox-secrets/tree/v1.7.3) (2023-01-23) + +[Full Changelog](https://github.com/Onemind-Services-LLC/netbox-secrets/compare/v1.7.2...v1.7.3) + +**Merged pull requests:** + +- Prepare for Pypi [\#23](https://github.com/Onemind-Services-LLC/netbox-secrets/pull/23) ([abhi1693](https://github.com/abhi1693)) + ## [v1.7.2](https://github.com/Onemind-Services-LLC/netbox-secrets/tree/v1.7.2) (2023-01-19) [Full Changelog](https://github.com/Onemind-Services-LLC/netbox-secrets/compare/v1.7.1...v1.7.2) From 1e8e6b4bca4b518ffc88c9421bf74fade5cc1ac8 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 26 Jan 2023 03:15:28 +0530 Subject: [PATCH 02/17] added migrations check --- configuration/configuration.py | 2 ++ test.sh | 1 + 2 files changed, 3 insertions(+) diff --git a/configuration/configuration.py b/configuration/configuration.py index cdc99a7..5227423 100644 --- a/configuration/configuration.py +++ b/configuration/configuration.py @@ -84,3 +84,5 @@ def _read_secret(secret_name, default=None): # symbols. NetBox will not run without this defined. For more information, see # https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY SECRET_KEY = _read_secret('secret_key', environ.get('SECRET_KEY', '')) + +DEVELOPER = True diff --git a/test.sh b/test.sh index 6bbaf9d..da0144b 100755 --- a/test.sh +++ b/test.sh @@ -16,6 +16,7 @@ doco="docker compose --file docker-compose.yml" test_netbox_unit_tests() { echo "⏱ Running NetBox Unit Tests" + $doco run --rm netbox python manage.py makemigrations netbox_secrets --check $doco run --rm netbox python manage.py test netbox_secrets } From d5ec7b038dba79a2e4f096fe3c3fabaea0262a5a Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 26 Jan 2023 20:05:14 +0530 Subject: [PATCH 03/17] improved user key UI --- netbox_secrets/forms/secrets.py | 19 +- ...rkey_created_alter_userkey_last_updated.py | 23 +++ netbox_secrets/models/secrets.py | 14 +- netbox_secrets/project-static/src/secrets.ts | 12 +- .../static/netbox_secrets/secrets.js | 6 +- .../static/netbox_secrets/secrets.js.map | 4 +- netbox_secrets/template_content.py | 24 ++- .../templates/netbox_secrets/userkey.html | 168 +++++++++++++----- .../netbox_secrets/userkey_edit.html | 124 ++++++++----- netbox_secrets/views.py | 45 +---- 10 files changed, 273 insertions(+), 166 deletions(-) create mode 100644 netbox_secrets/migrations/0006_alter_userkey_created_alter_userkey_last_updated.py diff --git a/netbox_secrets/forms/secrets.py b/netbox_secrets/forms/secrets.py index 392453f..182a330 100644 --- a/netbox_secrets/forms/secrets.py +++ b/netbox_secrets/forms/secrets.py @@ -3,12 +3,12 @@ from django import forms from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ - -from netbox.forms import NetBoxModelBulkEditForm, NetBoxModelFilterSetForm, NetBoxModelForm, NetBoxModelImportForm from netbox_secrets.constants import * from netbox_secrets.models import Secret, SecretRole, UserKey + +from netbox.forms import NetBoxModelBulkEditForm, NetBoxModelFilterSetForm, NetBoxModelForm, NetBoxModelImportForm from utilities.forms import ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, \ - SlugField + SlugField, SmallTextarea def validate_rsa_key(key, is_secret=True): @@ -158,12 +158,19 @@ class SecretFilterForm(NetBoxModelFilterSetForm): # class UserKeyForm(forms.ModelForm): + public_key = forms.CharField( + widget=SmallTextarea( + attrs={ + 'class': 'form-control', + } + ), + label='Public Key (PEM format)', + help_text='Enter your public RSA key. Keep the private one with you; you will need it for decryption. Please note that passphrase-protected keys are not supported.' + ) + class Meta: model = UserKey fields = ['public_key'] - labels = { - 'public_key': '' - } def clean_public_key(self): key = self.cleaned_data['public_key'] diff --git a/netbox_secrets/migrations/0006_alter_userkey_created_alter_userkey_last_updated.py b/netbox_secrets/migrations/0006_alter_userkey_created_alter_userkey_last_updated.py new file mode 100644 index 0000000..799d023 --- /dev/null +++ b/netbox_secrets/migrations/0006_alter_userkey_created_alter_userkey_last_updated.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.5 on 2023-01-26 14:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('netbox_secrets', '0005_alter_secret_custom_field_data_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='userkey', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='userkey', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + ] diff --git a/netbox_secrets/models/secrets.py b/netbox_secrets/models/secrets.py index 2f82fb8..17bcb1f 100644 --- a/netbox_secrets/models/secrets.py +++ b/netbox_secrets/models/secrets.py @@ -4,7 +4,6 @@ from Crypto.PublicKey import RSA from Crypto.Util import strxor from django.conf import settings -from django.utils.translation import gettext_lazy as _ from django.contrib.auth.hashers import make_password, check_password from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation @@ -14,7 +13,8 @@ from django.urls import reverse from django.utils.encoding import force_bytes -from netbox.models import OrganizationalModel, NetBoxModel +from netbox.models import NetBoxModel +from netbox.models.features import ChangeLoggingMixin, WebhooksMixin from utilities.querysets import RestrictedQuerySet from netbox_secrets.exceptions import InvalidKey from netbox_secrets.hashers import SecretValidationHasher @@ -31,19 +31,13 @@ plugin_settings = settings.PLUGINS_CONFIG.get('netbox_secrets', {}) -class UserKey(models.Model): +class UserKey(ChangeLoggingMixin, WebhooksMixin): """ A UserKey stores a user's personal RSA (public) encryption key, which is used to generate their unique encrypted copy of the master encryption key. The encrypted instance of the master key can be decrypted only with the user's matching (private) decryption key. """ id = models.BigAutoField(primary_key=True) - created = models.DateField( - auto_now_add=True - ) - last_updated = models.DateTimeField( - auto_now=True - ) user = models.OneToOneField( to=User, on_delete=models.CASCADE, @@ -52,8 +46,6 @@ class UserKey(models.Model): ) public_key = models.TextField( verbose_name='RSA public key', - help_text=_( - 'Enter your public RSA key. Keep the private one with you; you will need it for decryption. Please note that passphrase-protected keys are not supported.') ) master_key_cipher = models.BinaryField( max_length=512, diff --git a/netbox_secrets/project-static/src/secrets.ts b/netbox_secrets/project-static/src/secrets.ts index ba7002f..18dbba0 100644 --- a/netbox_secrets/project-static/src/secrets.ts +++ b/netbox_secrets/project-static/src/secrets.ts @@ -171,11 +171,17 @@ function initLockUnlock() { function requestSessionKey(privateKey: string) { apiPostForm('/api/plugins/secrets/get-session-key/', { private_key: privateKey, + preserve: true }).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(); + // If the session key has been added from the user key page, reload the page. + if (window.location.pathname === '/plugins/secrets/user-key/') { + window.location.reload(); + }else { + // 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; diff --git a/netbox_secrets/static/netbox_secrets/secrets.js b/netbox_secrets/static/netbox_secrets/secrets.js index 9bdb8a1..3d2e450 100644 --- a/netbox_secrets/static/netbox_secrets/secrets.js +++ b/netbox_secrets/static/netbox_secrets/secrets.js @@ -1,6 +1,6 @@ -(()=>{var R=Object.create;var E=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var P=Object.getPrototypeOf,C=Object.prototype.hasOwnProperty;var B=e=>E(e,"__esModule",{value:!0});var D=(e,r)=>()=>(r||e((r={exports:{}}).exports,r),r.exports);var H=(e,r,o)=>{if(r&&typeof r=="object"||typeof r=="function")for(let t of M(r))!C.call(e,t)&&t!=="default"&&E(e,t,{get:()=>r[t],enumerable:!(o=I(r,t))||o.enumerable});return e},_=e=>H(B(E(e!=null?R(P(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var p=(e,r,o)=>new Promise((t,i)=>{var c=a=>{try{s(o.next(a))}catch(l){i(l)}},n=a=>{try{s(o.throw(a))}catch(l){i(l)}},s=a=>a.done?t(a.value):Promise.resolve(a.value).then(c,n);s((o=o.apply(e,r)).next())});var T=D(v=>{"use strict";v.parse=O;v.serialize=F;var q=decodeURIComponent,$=encodeURIComponent,K=/; */,m=/^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;function O(e,r){if(typeof e!="string")throw new TypeError("argument str must be a string");for(var o={},t=r||{},i=e.split(K),c=t.decode||q,n=0;n{if(f(n))d("danger","Error",n.error).show();else{let{private_key:s,public_key:a}=n;o!==null&&t!==null&&(o.value=a,t.value=s)}})}function c(){let n=document.getElementById("id_public_key");o!==null&&(n.value=o.value,n.innerText=o.value)}e.addEventListener("shown.bs.modal",()=>i()),r.addEventListener("click",()=>c())}function L(e,r){let o=document.querySelector(`button.unlock-secret[secret-id='${e}']`),t=document.querySelector(`button.lock-secret[secret-id='${e}']`),i=document.querySelector(`button.copy-secret[secret-id='${e}']`);o!==null&&(r==="unlock"&&o.classList.add("d-none"),r==="lock"&&o.classList.remove("d-none")),t!==null&&(r==="unlock"&&t.classList.remove("d-none"),r==="lock"&&t.classList.add("d-none")),i!==null&&(r==="unlock"&&i.classList.remove("d-none"),r==="lock"&&i.classList.add("d-none"))}function N(){let e=new window.Modal("#privkey_modal");function r(t){let i=document.getElementById(`secret_${t}`);typeof t=="string"&&t!==""&&g(`/api/plugins/secrets/secrets/${t}/`).then(c=>{if(f(c))c.error.toLowerCase().includes("invalid session key")?e.show():d("danger","Error",c.error).show();else{let{plaintext:n}=c;i!==null&&n!==null?(k(i)?i.value=n:i.innerText=n,L(t,"unlock")):e.show()}})}function o(t){if(typeof t=="string"&&t!==""){let i=document.getElementById(`secret_${t}`);k(i)?i.value="********":i.innerText="********",L(t,"lock")}}for(let t of document.querySelectorAll("button.unlock-secret"))t.addEventListener("click",()=>r(t.getAttribute("secret-id")));for(let t of document.querySelectorAll("button.lock-secret"))t.addEventListener("click",()=>o(t.getAttribute("secret-id")))}function z(e){x("/api/plugins/secrets/get-session-key/",{private_key:e}).then(r=>{if(!f(r))d("success","Session Key Received","You may now unlock secrets.").show();else{let o=r.error;S(r)&&(o+=` -${r.exception}`),d("danger","Failed to Retrieve Session Key",o).show()}})}function J(){for(let e of document.querySelectorAll("#request_session_key")){let r=function(){for(let o of document.querySelectorAll("#user_privkey"))z(o.value),o.value=""};e.addEventListener("click",r)}}function X(){let e=new window.Modal("#privkey_modal");function r(o){document.cookie.indexOf("session_key")===-1&&(o.preventDefault(),e.show())}for(let o of document.querySelectorAll(".requires-session-key")){let t=o.closest("form");t!==null&&t.addEventListener("submit",r)}}function b(){for(let e of[G,N,J,X])e()}document.readyState!=="loading"?b():document.addEventListener("DOMContentLoaded",b);})(); +(()=>{var R=Object.create;var E=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var P=Object.getPrototypeOf,C=Object.prototype.hasOwnProperty;var B=e=>E(e,"__esModule",{value:!0});var D=(e,r)=>()=>(r||e((r={exports:{}}).exports,r),r.exports);var H=(e,r,o)=>{if(r&&typeof r=="object"||typeof r=="function")for(let t of M(r))!C.call(e,t)&&t!=="default"&&E(e,t,{get:()=>r[t],enumerable:!(o=I(r,t))||o.enumerable});return e},_=e=>H(B(E(e!=null?R(P(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var p=(e,r,o)=>new Promise((t,i)=>{var c=a=>{try{s(o.next(a))}catch(l){i(l)}},n=a=>{try{s(o.throw(a))}catch(l){i(l)}},s=a=>a.done?t(a.value):Promise.resolve(a.value).then(c,n);s((o=o.apply(e,r)).next())});var T=D(v=>{"use strict";v.parse=O;v.serialize=F;var q=decodeURIComponent,$=encodeURIComponent,K=/; */,m=/^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;function O(e,r){if(typeof e!="string")throw new TypeError("argument str must be a string");for(var o={},t=r||{},i=e.split(K),c=t.decode||q,n=0;n{if(f(n))d("danger","Error",n.error).show();else{let{private_key:s,public_key:a}=n;o!==null&&t!==null&&(o.value=a,t.value=s)}})}function c(){let n=document.getElementById("id_public_key");o!==null&&(n.value=o.value,n.innerText=o.value)}e.addEventListener("shown.bs.modal",()=>i()),r.addEventListener("click",()=>c())}function L(e,r){let o=document.querySelector(`button.unlock-secret[secret-id='${e}']`),t=document.querySelector(`button.lock-secret[secret-id='${e}']`),i=document.querySelector(`button.copy-secret[secret-id='${e}']`);o!==null&&(r==="unlock"&&o.classList.add("d-none"),r==="lock"&&o.classList.remove("d-none")),t!==null&&(r==="unlock"&&t.classList.remove("d-none"),r==="lock"&&t.classList.add("d-none")),i!==null&&(r==="unlock"&&i.classList.remove("d-none"),r==="lock"&&i.classList.add("d-none"))}function N(){let e=new window.Modal("#privkey_modal");function r(t){let i=document.getElementById(`secret_${t}`);typeof t=="string"&&t!==""&&g(`/api/plugins/secrets/secrets/${t}/`).then(c=>{if(f(c))c.error.toLowerCase().includes("invalid session key")?e.show():d("danger","Error",c.error).show();else{let{plaintext:n}=c;i!==null&&n!==null?(k(i)?i.value=n:i.innerText=n,L(t,"unlock")):e.show()}})}function o(t){if(typeof t=="string"&&t!==""){let i=document.getElementById(`secret_${t}`);k(i)?i.value="********":i.innerText="********",L(t,"lock")}}for(let t of document.querySelectorAll("button.unlock-secret"))t.addEventListener("click",()=>r(t.getAttribute("secret-id")));for(let t of document.querySelectorAll("button.lock-secret"))t.addEventListener("click",()=>o(t.getAttribute("secret-id")))}function z(e){x("/api/plugins/secrets/get-session-key/",{private_key:e,preserve:!0}).then(r=>{if(!f(r))window.location.pathname==="/plugins/secrets/user-key/"?window.location.reload():d("success","Session Key Received","You may now unlock secrets.").show();else{let o=r.error;S(r)&&(o+=` +${r.exception}`),d("danger","Failed to Retrieve Session Key",o).show()}})}function J(){for(let e of document.querySelectorAll("#request_session_key")){let r=function(){for(let o of document.querySelectorAll("#user_privkey"))z(o.value),o.value=""};e.addEventListener("click",r)}}function X(){let e=new window.Modal("#privkey_modal");function r(o){document.cookie.indexOf("session_key")===-1&&(o.preventDefault(),e.show())}for(let o of document.querySelectorAll(".requires-session-key")){let t=o.closest("form");t!==null&&t.addEventListener("submit",r)}}function w(){for(let e of[G,N,J,X])e()}document.readyState!=="loading"?w():document.addEventListener("DOMContentLoaded",w);})(); /*! * cookie * Copyright(c) 2012-2014 Roman Shtylman diff --git a/netbox_secrets/static/netbox_secrets/secrets.js.map b/netbox_secrets/static/netbox_secrets/secrets.js.map index 89bc004..634df2d 100644 --- a/netbox_secrets/static/netbox_secrets/secrets.js.map +++ b/netbox_secrets/static/netbox_secrets/secrets.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../node_modules/cookie/index.js", "../src/bs.ts", "../src/util.ts", "../src/secrets.ts", "../src/index.ts"], - "sourcesContent": ["/*!\n * cookie\n * Copyright(c) 2012-2014 Roman Shtylman\n * Copyright(c) 2015 Douglas Christopher Wilson\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module exports.\n * @public\n */\n\nexports.parse = parse;\nexports.serialize = serialize;\n\n/**\n * Module variables.\n * @private\n */\n\nvar decode = decodeURIComponent;\nvar encode = encodeURIComponent;\nvar pairSplitRegExp = /; */;\n\n/**\n * RegExp to match field-content in RFC 7230 sec 3.2\n *\n * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]\n * field-vchar = VCHAR / obs-text\n * obs-text = %x80-FF\n */\n\nvar fieldContentRegExp = /^[\\u0009\\u0020-\\u007e\\u0080-\\u00ff]+$/;\n\n/**\n * Parse a cookie header.\n *\n * Parse the given cookie header string into an object\n * The object has the various cookies as keys(names) => values\n *\n * @param {string} str\n * @param {object} [options]\n * @return {object}\n * @public\n */\n\nfunction parse(str, options) {\n if (typeof str !== 'string') {\n throw new TypeError('argument str must be a string');\n }\n\n var obj = {}\n var opt = options || {};\n var pairs = str.split(pairSplitRegExp);\n var dec = opt.decode || decode;\n\n for (var i = 0; i < pairs.length; i++) {\n var pair = pairs[i];\n var eq_idx = pair.indexOf('=');\n\n // skip things that don't look like key=value\n if (eq_idx < 0) {\n continue;\n }\n\n var key = pair.substr(0, eq_idx).trim()\n var val = pair.substr(++eq_idx, pair.length).trim();\n\n // quoted values\n if ('\"' == val[0]) {\n val = val.slice(1, -1);\n }\n\n // only assign once\n if (undefined == obj[key]) {\n obj[key] = tryDecode(val, dec);\n }\n }\n\n return obj;\n}\n\n/**\n * Serialize data into a cookie header.\n *\n * Serialize the a name value pair into a cookie string suitable for\n * http headers. An optional options object specified cookie parameters.\n *\n * serialize('foo', 'bar', { httpOnly: true })\n * => \"foo=bar; httpOnly\"\n *\n * @param {string} name\n * @param {string} val\n * @param {object} [options]\n * @return {string}\n * @public\n */\n\nfunction serialize(name, val, options) {\n var opt = options || {};\n var enc = opt.encode || encode;\n\n if (typeof enc !== 'function') {\n throw new TypeError('option encode is invalid');\n }\n\n if (!fieldContentRegExp.test(name)) {\n throw new TypeError('argument name is invalid');\n }\n\n var value = enc(val);\n\n if (value && !fieldContentRegExp.test(value)) {\n throw new TypeError('argument val is invalid');\n }\n\n var str = name + '=' + value;\n\n if (null != opt.maxAge) {\n var maxAge = opt.maxAge - 0;\n\n if (isNaN(maxAge) || !isFinite(maxAge)) {\n throw new TypeError('option maxAge is invalid')\n }\n\n str += '; Max-Age=' + Math.floor(maxAge);\n }\n\n if (opt.domain) {\n if (!fieldContentRegExp.test(opt.domain)) {\n throw new TypeError('option domain is invalid');\n }\n\n str += '; Domain=' + opt.domain;\n }\n\n if (opt.path) {\n if (!fieldContentRegExp.test(opt.path)) {\n throw new TypeError('option path is invalid');\n }\n\n str += '; Path=' + opt.path;\n }\n\n if (opt.expires) {\n if (typeof opt.expires.toUTCString !== 'function') {\n throw new TypeError('option expires is invalid');\n }\n\n str += '; Expires=' + opt.expires.toUTCString();\n }\n\n if (opt.httpOnly) {\n str += '; HttpOnly';\n }\n\n if (opt.secure) {\n str += '; Secure';\n }\n\n if (opt.sameSite) {\n var sameSite = typeof opt.sameSite === 'string'\n ? opt.sameSite.toLowerCase() : opt.sameSite;\n\n switch (sameSite) {\n case true:\n str += '; SameSite=Strict';\n break;\n case 'lax':\n str += '; SameSite=Lax';\n break;\n case 'strict':\n str += '; SameSite=Strict';\n break;\n case 'none':\n str += '; SameSite=None';\n break;\n default:\n throw new TypeError('option sameSite is invalid');\n }\n }\n\n return str;\n}\n\n/**\n * Try decoding a string using a decoding function.\n *\n * @param {string} str\n * @param {function} decode\n * @private\n */\n\nfunction tryDecode(str, decode) {\n try {\n return decode(str);\n } catch (e) {\n return str;\n }\n}\n", "type ToastLevel = 'danger' | 'warning' | 'success' | 'info';\n\nexport function createToast(\n level: ToastLevel,\n title: string,\n message: string,\n extra?: string,\n): InstanceType {\n let iconName = 'mdi-alert';\n switch (level) {\n case 'warning':\n iconName = 'mdi-alert';\n break;\n case 'success':\n iconName = 'mdi-check-circle';\n break;\n case 'info':\n iconName = 'mdi-information';\n break;\n case 'danger':\n iconName = 'mdi-alert';\n break;\n }\n\n const container = document.createElement('div');\n container.setAttribute('class', 'toast-container position-fixed bottom-0 end-0 m-3');\n\n const main = document.createElement('div');\n main.setAttribute('class', `toast bg-${level}`);\n main.setAttribute('role', 'alert');\n main.setAttribute('aria-live', 'assertive');\n main.setAttribute('aria-atomic', 'true');\n\n const header = document.createElement('div');\n header.setAttribute('class', `toast-header bg-${level} text-body`);\n\n const icon = document.createElement('i');\n icon.setAttribute('class', `mdi ${iconName}`);\n\n const titleElement = document.createElement('strong');\n titleElement.setAttribute('class', 'me-auto ms-1');\n titleElement.innerText = title;\n\n const button = document.createElement('button');\n button.setAttribute('type', 'button');\n button.setAttribute('class', 'btn-close');\n button.setAttribute('data-bs-dismiss', 'toast');\n button.setAttribute('aria-label', 'Close');\n\n const body = document.createElement('div');\n body.setAttribute('class', 'toast-body');\n\n header.appendChild(icon);\n header.appendChild(titleElement);\n\n if (typeof extra !== 'undefined') {\n const extraElement = document.createElement('small');\n extraElement.setAttribute('class', 'text-muted');\n header.appendChild(extraElement);\n }\n\n header.appendChild(button);\n\n body.innerText = message.trim();\n\n main.appendChild(header);\n main.appendChild(body);\n container.appendChild(main);\n document.body.appendChild(container);\n\n const toast = new window.Toast(main);\n return toast;\n}\n", "import Cookie from 'cookie';\n\ntype APIRes = T | ErrorBase | APIError;\ntype Method = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';\ntype ReqData = URLSearchParams | Dict | undefined | unknown;\n\n/**\n * Type guard to determine if an API response is a detailed error.\n *\n * @param data API JSON Response\n * @returns Type guard for `data`.\n */\nexport function isApiError(data: Record): data is APIError {\n return 'error' in data && 'exception' in data;\n}\n\n/**\n * Type guard to determine if an API response is an error.\n *\n * @param data API JSON Response\n * @returns Type guard for `data`.\n */\nexport function hasError(data: Record): data is ErrorBase {\n return 'error' in data;\n}\n\n/**\n * Type guard to determine if an element is an `HTMLInputElement`.\n *\n * @param element HTML Element.\n */\nexport function isInputElement(element: HTMLElement): element is HTMLInputElement {\n return 'value' in element && 'required' in element;\n}\n\n/**\n * Retrieve the CSRF token from cookie storage.\n */\nexport function getCsrfToken(): string {\n const { csrftoken: csrfToken } = Cookie.parse(document.cookie);\n if (typeof csrfToken === 'undefined') {\n throw new Error('Invalid or missing CSRF token');\n }\n return csrfToken;\n}\n\n/**\n * Authenticate and interact with the NetBox API.\n *\n * @param url Request URL\n * @param method Request Method\n * @param data Data to `POST`, `PATCH`, or `PUT`, if applicable.\n * @returns JSON Response\n */\nexport async function apiRequest(\n url: string,\n method: Method,\n data?: D,\n): Promise> {\n const token = getCsrfToken();\n const headers = new Headers({ 'X-CSRFToken': token });\n\n let body;\n if (typeof data !== 'undefined') {\n body = JSON.stringify(data);\n headers.set('content-type', 'application/json');\n headers.set('Accept', 'application/json');\n }\n\n const res = await fetch(url, { method, body, headers, credentials: 'same-origin' });\n const contentType = res.headers.get('Content-Type');\n if (typeof contentType === 'string' && contentType.includes('text')) {\n const error = await res.text();\n return { error } as ErrorBase;\n }\n const json = (await res.json()) as R | APIError;\n if (!res.ok && Array.isArray(json)) {\n const error = json.join('\\n');\n return { error } as ErrorBase;\n } else if (!res.ok && 'detail' in json) {\n return { error: json.detail } as ErrorBase;\n }\n return json;\n}\n\n/**\n * `POST` an object as form data to the NetBox API.\n *\n * @param url Request URL\n * @param data Object to convert to form data\n * @returns JSON Response\n */\nexport async function apiPostForm(\n url: string,\n data: D,\n): Promise> {\n return await apiRequest(url, 'POST', data);\n}\n\n/**\n * `GET` data from the NetBox API.\n *\n * @param url Request URL\n * @returns JSON Response\n */\nexport async function apiGetBase(url: string): Promise> {\n return await apiRequest(url, 'GET');\n}\n", "import { createToast } from './bs';\nimport { apiGetBase, apiPostForm, isApiError, isInputElement, hasError } from './util';\n\nimport type { APISecret, APIKeyPair } from './types';\n\n/**\n * Initialize Generate Private Key Pair Elements.\n */\nfunction initGenerateKeyPair() {\n const element = document.getElementById('new_keypair_modal') as HTMLDivElement;\n const accept = document.getElementById('use_new_pubkey') as HTMLButtonElement;\n // If the elements are not loaded, stop.\n if (element === null || accept === null) {\n return;\n }\n const publicElem = element.querySelector('textarea#new_pubkey');\n const privateElem = element.querySelector('textarea#new_privkey');\n\n /**\n * Handle Generate Private Key Pair Modal opening.\n */\n function handleOpen() {\n // When the modal opens, set the `readonly` attribute on the textarea elements.\n for (const elem of [publicElem, privateElem]) {\n if (elem !== null) {\n elem.setAttribute('readonly', '');\n }\n }\n // Fetch the key pair from the API.\n apiGetBase('/api/plugins/secrets/generate-rsa-key-pair/').then(data => {\n if (!hasError(data)) {\n // If key pair generation was successful, set the textarea elements' value to the generated\n // values.\n const { private_key: priv, public_key: pub } = data;\n if (publicElem !== null && privateElem !== null) {\n publicElem.value = pub;\n privateElem.value = priv;\n }\n } else {\n // Otherwise, show an error.\n const toast = createToast('danger', 'Error', data.error);\n toast.show();\n }\n });\n }\n\n /**\n * Set the public key form field's value to the generated public key.\n */\n function handleAccept() {\n const publicKeyField = document.getElementById('id_public_key') as HTMLTextAreaElement;\n if (publicElem !== null) {\n publicKeyField.value = publicElem.value;\n publicKeyField.innerText = publicElem.value;\n }\n }\n element.addEventListener('shown.bs.modal', () => handleOpen());\n accept.addEventListener('click', () => handleAccept());\n}\n\n/**\n * Toggle copy/lock/unlock button visibility based on the action occurring.\n * @param id Secret ID.\n * @param action Lock or Unlock, so we know which buttons to display.\n */\nfunction toggleSecretButtons(id: string, action: 'lock' | 'unlock') {\n const unlockButton = document.querySelector(`button.unlock-secret[secret-id='${id}']`);\n const lockButton = document.querySelector(`button.lock-secret[secret-id='${id}']`);\n const copyButton = document.querySelector(`button.copy-secret[secret-id='${id}']`);\n // If we're unlocking, hide the unlock button. Otherwise, show it.\n if (unlockButton !== null) {\n if (action === 'unlock') unlockButton.classList.add('d-none');\n if (action === 'lock') unlockButton.classList.remove('d-none');\n }\n // If we're unlocking, show the lock button. Otherwise, hide it.\n if (lockButton !== null) {\n if (action === 'unlock') lockButton.classList.remove('d-none');\n if (action === 'lock') lockButton.classList.add('d-none');\n }\n // If we're unlocking, show the copy button. Otherwise, hide it.\n if (copyButton !== null) {\n if (action === 'unlock') copyButton.classList.remove('d-none');\n if (action === 'lock') copyButton.classList.add('d-none');\n }\n}\n\n/**\n * Initialize Lock & Unlock button event listeners & callbacks.\n */\nfunction initLockUnlock() {\n const privateKeyModal = new window.Modal('#privkey_modal');\n\n /**\n * Unlock a secret, or prompt the user for their private key, if a session key is not available.\n *\n * @param id Secret ID\n */\n function unlock(id: string | null) {\n const target = document.getElementById(`secret_${id}`) as HTMLDivElement | HTMLInputElement;\n if (typeof id === 'string' && id !== '') {\n apiGetBase(`/api/plugins/secrets/secrets/${id}/`).then(data => {\n if (!hasError(data)) {\n const { plaintext } = data;\n // `plaintext` is the plain text value of the secret. If it is null, it has not been\n // decrypted, likely due to a mission session key.\n\n if (target !== null && plaintext !== null) {\n // If `plaintext` is not null, we have the decrypted value. Set the target element's\n // inner text to the decrypted value and toggle copy/lock button visibility.\n if (isInputElement(target)) {\n target.value = plaintext;\n } else {\n target.innerText = plaintext;\n }\n\n toggleSecretButtons(id, 'unlock');\n } else {\n // Otherwise, we do _not_ have the decrypted value and need to prompt the user for\n // their private RSA key, in order to get a session key. The session key is then sent\n // as a cookie in future requests.\n privateKeyModal.show();\n }\n } else {\n if (data.error.toLowerCase().includes('invalid session key')) {\n // If, for some reason, a request was made but resulted in an API error that complains\n // of a missing session key, prompt the user for their session key.\n privateKeyModal.show();\n } else {\n // If we received an API error but it doesn't contain 'invalid session key', show the\n // user an error message.\n const toast = createToast('danger', 'Error', data.error);\n toast.show();\n }\n }\n });\n }\n }\n\n /**\n * Lock a secret and toggle visibility of the unlock button.\n * @param id Secret ID\n */\n function lock(id: string | null) {\n if (typeof id === 'string' && id !== '') {\n const target = document.getElementById(`secret_${id}`) as HTMLDivElement | HTMLInputElement;\n\n // Obscure the inner text of the secret element.\n if (isInputElement(target)) {\n target.value = '********';\n } else {\n target.innerText = '********';\n }\n\n // Toggle visibility of the copy/lock/unlock buttons.\n toggleSecretButtons(id, 'lock');\n }\n }\n\n for (const element of document.querySelectorAll('button.unlock-secret')) {\n element.addEventListener('click', () => unlock(element.getAttribute('secret-id')));\n }\n for (const element of document.querySelectorAll('button.lock-secret')) {\n element.addEventListener('click', () => lock(element.getAttribute('secret-id')));\n }\n}\n\n/**\n * Request a session key from the API.\n * @param privateKey RSA Private Key (valid JSON string)\n */\nfunction requestSessionKey(privateKey: string) {\n apiPostForm('/api/plugins/secrets/get-session-key/', {\n private_key: privateKey,\n }).then(res => {\n if (!hasError(res)) {\n // If the response received was not an error, show the user a success message.\n const toast = createToast('success', 'Session Key Received', 'You may now unlock secrets.');\n toast.show();\n } else {\n // Otherwise, show the user an error message.\n let message = res.error;\n if (isApiError(res)) {\n // If the error received was a standard API error containing a Python exception message,\n // append it to the error.\n message += `\\n${res.exception}`;\n }\n const toast = createToast('danger', 'Failed to Retrieve Session Key', message);\n toast.show();\n }\n });\n}\n\n/**\n * Initialize Request Session Key Elements.\n */\nfunction initGetSessionKey() {\n for (const element of document.querySelectorAll('#request_session_key')) {\n /**\n * Send the user's input private key to the API to get a session key, which will be stored as\n * a cookie for future requests.\n */\n function handleClick() {\n for (const pk of document.querySelectorAll('#user_privkey')) {\n requestSessionKey(pk.value);\n // Clear the private key form field value.\n pk.value = '';\n }\n }\n element.addEventListener('click', handleClick);\n }\n}\n\n/**\n * Initialize Secret Edit Form Handler.\n */\nfunction initSecretsEdit() {\n const privateKeyModal = new window.Modal('#privkey_modal');\n\n /**\n * Check the cookie store for a `session_key`. If not present, prompt the user to submit their\n * private key.\n */\n function handleSubmit(event: Event): void {\n if (document.cookie.indexOf('session_key') === -1) {\n event.preventDefault();\n privateKeyModal.show();\n }\n }\n\n for (const element of document.querySelectorAll('.requires-session-key')) {\n const form = element.closest('form');\n if (form !== null) {\n form.addEventListener('submit', handleSubmit);\n }\n }\n}\n\nexport function initSecrets() {\n for (const func of [initGenerateKeyPair, initLockUnlock, initGetSessionKey, initSecretsEdit]) {\n func();\n }\n}\n", "import { initSecrets } from './secrets';\n\nif (document.readyState !== 'loading') {\n initSecrets();\n} else {\n document.addEventListener('DOMContentLoaded', initSecrets);\n}\n"], - "mappings": "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", + "sourcesContent": ["/*!\n * cookie\n * Copyright(c) 2012-2014 Roman Shtylman\n * Copyright(c) 2015 Douglas Christopher Wilson\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module exports.\n * @public\n */\n\nexports.parse = parse;\nexports.serialize = serialize;\n\n/**\n * Module variables.\n * @private\n */\n\nvar decode = decodeURIComponent;\nvar encode = encodeURIComponent;\nvar pairSplitRegExp = /; */;\n\n/**\n * RegExp to match field-content in RFC 7230 sec 3.2\n *\n * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]\n * field-vchar = VCHAR / obs-text\n * obs-text = %x80-FF\n */\n\nvar fieldContentRegExp = /^[\\u0009\\u0020-\\u007e\\u0080-\\u00ff]+$/;\n\n/**\n * Parse a cookie header.\n *\n * Parse the given cookie header string into an object\n * The object has the various cookies as keys(names) => values\n *\n * @param {string} str\n * @param {object} [options]\n * @return {object}\n * @public\n */\n\nfunction parse(str, options) {\n if (typeof str !== 'string') {\n throw new TypeError('argument str must be a string');\n }\n\n var obj = {}\n var opt = options || {};\n var pairs = str.split(pairSplitRegExp);\n var dec = opt.decode || decode;\n\n for (var i = 0; i < pairs.length; i++) {\n var pair = pairs[i];\n var eq_idx = pair.indexOf('=');\n\n // skip things that don't look like key=value\n if (eq_idx < 0) {\n continue;\n }\n\n var key = pair.substr(0, eq_idx).trim()\n var val = pair.substr(++eq_idx, pair.length).trim();\n\n // quoted values\n if ('\"' == val[0]) {\n val = val.slice(1, -1);\n }\n\n // only assign once\n if (undefined == obj[key]) {\n obj[key] = tryDecode(val, dec);\n }\n }\n\n return obj;\n}\n\n/**\n * Serialize data into a cookie header.\n *\n * Serialize the a name value pair into a cookie string suitable for\n * http headers. An optional options object specified cookie parameters.\n *\n * serialize('foo', 'bar', { httpOnly: true })\n * => \"foo=bar; httpOnly\"\n *\n * @param {string} name\n * @param {string} val\n * @param {object} [options]\n * @return {string}\n * @public\n */\n\nfunction serialize(name, val, options) {\n var opt = options || {};\n var enc = opt.encode || encode;\n\n if (typeof enc !== 'function') {\n throw new TypeError('option encode is invalid');\n }\n\n if (!fieldContentRegExp.test(name)) {\n throw new TypeError('argument name is invalid');\n }\n\n var value = enc(val);\n\n if (value && !fieldContentRegExp.test(value)) {\n throw new TypeError('argument val is invalid');\n }\n\n var str = name + '=' + value;\n\n if (null != opt.maxAge) {\n var maxAge = opt.maxAge - 0;\n\n if (isNaN(maxAge) || !isFinite(maxAge)) {\n throw new TypeError('option maxAge is invalid')\n }\n\n str += '; Max-Age=' + Math.floor(maxAge);\n }\n\n if (opt.domain) {\n if (!fieldContentRegExp.test(opt.domain)) {\n throw new TypeError('option domain is invalid');\n }\n\n str += '; Domain=' + opt.domain;\n }\n\n if (opt.path) {\n if (!fieldContentRegExp.test(opt.path)) {\n throw new TypeError('option path is invalid');\n }\n\n str += '; Path=' + opt.path;\n }\n\n if (opt.expires) {\n if (typeof opt.expires.toUTCString !== 'function') {\n throw new TypeError('option expires is invalid');\n }\n\n str += '; Expires=' + opt.expires.toUTCString();\n }\n\n if (opt.httpOnly) {\n str += '; HttpOnly';\n }\n\n if (opt.secure) {\n str += '; Secure';\n }\n\n if (opt.sameSite) {\n var sameSite = typeof opt.sameSite === 'string'\n ? opt.sameSite.toLowerCase() : opt.sameSite;\n\n switch (sameSite) {\n case true:\n str += '; SameSite=Strict';\n break;\n case 'lax':\n str += '; SameSite=Lax';\n break;\n case 'strict':\n str += '; SameSite=Strict';\n break;\n case 'none':\n str += '; SameSite=None';\n break;\n default:\n throw new TypeError('option sameSite is invalid');\n }\n }\n\n return str;\n}\n\n/**\n * Try decoding a string using a decoding function.\n *\n * @param {string} str\n * @param {function} decode\n * @private\n */\n\nfunction tryDecode(str, decode) {\n try {\n return decode(str);\n } catch (e) {\n return str;\n }\n}\n", "type ToastLevel = 'danger' | 'warning' | 'success' | 'info';\n\nexport function createToast(\n level: ToastLevel,\n title: string,\n message: string,\n extra?: string,\n): InstanceType {\n let iconName = 'mdi-alert';\n switch (level) {\n case 'warning':\n iconName = 'mdi-alert';\n break;\n case 'success':\n iconName = 'mdi-check-circle';\n break;\n case 'info':\n iconName = 'mdi-information';\n break;\n case 'danger':\n iconName = 'mdi-alert';\n break;\n }\n\n const container = document.createElement('div');\n container.setAttribute('class', 'toast-container position-fixed bottom-0 end-0 m-3');\n\n const main = document.createElement('div');\n main.setAttribute('class', `toast bg-${level}`);\n main.setAttribute('role', 'alert');\n main.setAttribute('aria-live', 'assertive');\n main.setAttribute('aria-atomic', 'true');\n\n const header = document.createElement('div');\n header.setAttribute('class', `toast-header bg-${level} text-body`);\n\n const icon = document.createElement('i');\n icon.setAttribute('class', `mdi ${iconName}`);\n\n const titleElement = document.createElement('strong');\n titleElement.setAttribute('class', 'me-auto ms-1');\n titleElement.innerText = title;\n\n const button = document.createElement('button');\n button.setAttribute('type', 'button');\n button.setAttribute('class', 'btn-close');\n button.setAttribute('data-bs-dismiss', 'toast');\n button.setAttribute('aria-label', 'Close');\n\n const body = document.createElement('div');\n body.setAttribute('class', 'toast-body');\n\n header.appendChild(icon);\n header.appendChild(titleElement);\n\n if (typeof extra !== 'undefined') {\n const extraElement = document.createElement('small');\n extraElement.setAttribute('class', 'text-muted');\n header.appendChild(extraElement);\n }\n\n header.appendChild(button);\n\n body.innerText = message.trim();\n\n main.appendChild(header);\n main.appendChild(body);\n container.appendChild(main);\n document.body.appendChild(container);\n\n const toast = new window.Toast(main);\n return toast;\n}\n", "import Cookie from 'cookie';\n\ntype APIRes = T | ErrorBase | APIError;\ntype Method = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';\ntype ReqData = URLSearchParams | Dict | undefined | unknown;\n\n/**\n * Type guard to determine if an API response is a detailed error.\n *\n * @param data API JSON Response\n * @returns Type guard for `data`.\n */\nexport function isApiError(data: Record): data is APIError {\n return 'error' in data && 'exception' in data;\n}\n\n/**\n * Type guard to determine if an API response is an error.\n *\n * @param data API JSON Response\n * @returns Type guard for `data`.\n */\nexport function hasError(data: Record): data is ErrorBase {\n return 'error' in data;\n}\n\n/**\n * Type guard to determine if an element is an `HTMLInputElement`.\n *\n * @param element HTML Element.\n */\nexport function isInputElement(element: HTMLElement): element is HTMLInputElement {\n return 'value' in element && 'required' in element;\n}\n\n/**\n * Retrieve the CSRF token from cookie storage.\n */\nexport function getCsrfToken(): string {\n const { csrftoken: csrfToken } = Cookie.parse(document.cookie);\n if (typeof csrfToken === 'undefined') {\n throw new Error('Invalid or missing CSRF token');\n }\n return csrfToken;\n}\n\n/**\n * Authenticate and interact with the NetBox API.\n *\n * @param url Request URL\n * @param method Request Method\n * @param data Data to `POST`, `PATCH`, or `PUT`, if applicable.\n * @returns JSON Response\n */\nexport async function apiRequest(\n url: string,\n method: Method,\n data?: D,\n): Promise> {\n const token = getCsrfToken();\n const headers = new Headers({ 'X-CSRFToken': token });\n\n let body;\n if (typeof data !== 'undefined') {\n body = JSON.stringify(data);\n headers.set('content-type', 'application/json');\n headers.set('Accept', 'application/json');\n }\n\n const res = await fetch(url, { method, body, headers, credentials: 'same-origin' });\n const contentType = res.headers.get('Content-Type');\n if (typeof contentType === 'string' && contentType.includes('text')) {\n const error = await res.text();\n return { error } as ErrorBase;\n }\n const json = (await res.json()) as R | APIError;\n if (!res.ok && Array.isArray(json)) {\n const error = json.join('\\n');\n return { error } as ErrorBase;\n } else if (!res.ok && 'detail' in json) {\n return { error: json.detail } as ErrorBase;\n }\n return json;\n}\n\n/**\n * `POST` an object as form data to the NetBox API.\n *\n * @param url Request URL\n * @param data Object to convert to form data\n * @returns JSON Response\n */\nexport async function apiPostForm(\n url: string,\n data: D,\n): Promise> {\n return await apiRequest(url, 'POST', data);\n}\n\n/**\n * `GET` data from the NetBox API.\n *\n * @param url Request URL\n * @returns JSON Response\n */\nexport async function apiGetBase(url: string): Promise> {\n return await apiRequest(url, 'GET');\n}\n", "import { createToast } from './bs';\nimport { apiGetBase, apiPostForm, isApiError, isInputElement, hasError } from './util';\n\nimport type { APISecret, APIKeyPair } from './types';\n\n/**\n * Initialize Generate Private Key Pair Elements.\n */\nfunction initGenerateKeyPair() {\n const element = document.getElementById('new_keypair_modal') as HTMLDivElement;\n const accept = document.getElementById('use_new_pubkey') as HTMLButtonElement;\n // If the elements are not loaded, stop.\n if (element === null || accept === null) {\n return;\n }\n const publicElem = element.querySelector('textarea#new_pubkey');\n const privateElem = element.querySelector('textarea#new_privkey');\n\n /**\n * Handle Generate Private Key Pair Modal opening.\n */\n function handleOpen() {\n // When the modal opens, set the `readonly` attribute on the textarea elements.\n for (const elem of [publicElem, privateElem]) {\n if (elem !== null) {\n elem.setAttribute('readonly', '');\n }\n }\n // Fetch the key pair from the API.\n apiGetBase('/api/plugins/secrets/generate-rsa-key-pair/').then(data => {\n if (!hasError(data)) {\n // If key pair generation was successful, set the textarea elements' value to the generated\n // values.\n const { private_key: priv, public_key: pub } = data;\n if (publicElem !== null && privateElem !== null) {\n publicElem.value = pub;\n privateElem.value = priv;\n }\n } else {\n // Otherwise, show an error.\n const toast = createToast('danger', 'Error', data.error);\n toast.show();\n }\n });\n }\n\n /**\n * Set the public key form field's value to the generated public key.\n */\n function handleAccept() {\n const publicKeyField = document.getElementById('id_public_key') as HTMLTextAreaElement;\n if (publicElem !== null) {\n publicKeyField.value = publicElem.value;\n publicKeyField.innerText = publicElem.value;\n }\n }\n element.addEventListener('shown.bs.modal', () => handleOpen());\n accept.addEventListener('click', () => handleAccept());\n}\n\n/**\n * Toggle copy/lock/unlock button visibility based on the action occurring.\n * @param id Secret ID.\n * @param action Lock or Unlock, so we know which buttons to display.\n */\nfunction toggleSecretButtons(id: string, action: 'lock' | 'unlock') {\n const unlockButton = document.querySelector(`button.unlock-secret[secret-id='${id}']`);\n const lockButton = document.querySelector(`button.lock-secret[secret-id='${id}']`);\n const copyButton = document.querySelector(`button.copy-secret[secret-id='${id}']`);\n // If we're unlocking, hide the unlock button. Otherwise, show it.\n if (unlockButton !== null) {\n if (action === 'unlock') unlockButton.classList.add('d-none');\n if (action === 'lock') unlockButton.classList.remove('d-none');\n }\n // If we're unlocking, show the lock button. Otherwise, hide it.\n if (lockButton !== null) {\n if (action === 'unlock') lockButton.classList.remove('d-none');\n if (action === 'lock') lockButton.classList.add('d-none');\n }\n // If we're unlocking, show the copy button. Otherwise, hide it.\n if (copyButton !== null) {\n if (action === 'unlock') copyButton.classList.remove('d-none');\n if (action === 'lock') copyButton.classList.add('d-none');\n }\n}\n\n/**\n * Initialize Lock & Unlock button event listeners & callbacks.\n */\nfunction initLockUnlock() {\n const privateKeyModal = new window.Modal('#privkey_modal');\n\n /**\n * Unlock a secret, or prompt the user for their private key, if a session key is not available.\n *\n * @param id Secret ID\n */\n function unlock(id: string | null) {\n const target = document.getElementById(`secret_${id}`) as HTMLDivElement | HTMLInputElement;\n if (typeof id === 'string' && id !== '') {\n apiGetBase(`/api/plugins/secrets/secrets/${id}/`).then(data => {\n if (!hasError(data)) {\n const { plaintext } = data;\n // `plaintext` is the plain text value of the secret. If it is null, it has not been\n // decrypted, likely due to a mission session key.\n\n if (target !== null && plaintext !== null) {\n // If `plaintext` is not null, we have the decrypted value. Set the target element's\n // inner text to the decrypted value and toggle copy/lock button visibility.\n if (isInputElement(target)) {\n target.value = plaintext;\n } else {\n target.innerText = plaintext;\n }\n\n toggleSecretButtons(id, 'unlock');\n } else {\n // Otherwise, we do _not_ have the decrypted value and need to prompt the user for\n // their private RSA key, in order to get a session key. The session key is then sent\n // as a cookie in future requests.\n privateKeyModal.show();\n }\n } else {\n if (data.error.toLowerCase().includes('invalid session key')) {\n // If, for some reason, a request was made but resulted in an API error that complains\n // of a missing session key, prompt the user for their session key.\n privateKeyModal.show();\n } else {\n // If we received an API error but it doesn't contain 'invalid session key', show the\n // user an error message.\n const toast = createToast('danger', 'Error', data.error);\n toast.show();\n }\n }\n });\n }\n }\n\n /**\n * Lock a secret and toggle visibility of the unlock button.\n * @param id Secret ID\n */\n function lock(id: string | null) {\n if (typeof id === 'string' && id !== '') {\n const target = document.getElementById(`secret_${id}`) as HTMLDivElement | HTMLInputElement;\n\n // Obscure the inner text of the secret element.\n if (isInputElement(target)) {\n target.value = '********';\n } else {\n target.innerText = '********';\n }\n\n // Toggle visibility of the copy/lock/unlock buttons.\n toggleSecretButtons(id, 'lock');\n }\n }\n\n for (const element of document.querySelectorAll('button.unlock-secret')) {\n element.addEventListener('click', () => unlock(element.getAttribute('secret-id')));\n }\n for (const element of document.querySelectorAll('button.lock-secret')) {\n element.addEventListener('click', () => lock(element.getAttribute('secret-id')));\n }\n}\n\n/**\n * Request a session key from the API.\n * @param privateKey RSA Private Key (valid JSON string)\n */\nfunction requestSessionKey(privateKey: string) {\n apiPostForm('/api/plugins/secrets/get-session-key/', {\n private_key: privateKey,\n preserve: true\n }).then(res => {\n if (!hasError(res)) {\n // If the session key has been added from the user key page, reload the page.\n if (window.location.pathname === '/plugins/secrets/user-key/') {\n window.location.reload();\n }else {\n // If the response received was not an error, show the user a success message.\n const toast = createToast('success', 'Session Key Received', 'You may now unlock secrets.');\n toast.show();\n }\n } else {\n // Otherwise, show the user an error message.\n let message = res.error;\n if (isApiError(res)) {\n // If the error received was a standard API error containing a Python exception message,\n // append it to the error.\n message += `\\n${res.exception}`;\n }\n const toast = createToast('danger', 'Failed to Retrieve Session Key', message);\n toast.show();\n }\n });\n}\n\n/**\n * Initialize Request Session Key Elements.\n */\nfunction initGetSessionKey() {\n for (const element of document.querySelectorAll('#request_session_key')) {\n /**\n * Send the user's input private key to the API to get a session key, which will be stored as\n * a cookie for future requests.\n */\n function handleClick() {\n for (const pk of document.querySelectorAll('#user_privkey')) {\n requestSessionKey(pk.value);\n // Clear the private key form field value.\n pk.value = '';\n }\n }\n element.addEventListener('click', handleClick);\n }\n}\n\n/**\n * Initialize Secret Edit Form Handler.\n */\nfunction initSecretsEdit() {\n const privateKeyModal = new window.Modal('#privkey_modal');\n\n /**\n * Check the cookie store for a `session_key`. If not present, prompt the user to submit their\n * private key.\n */\n function handleSubmit(event: Event): void {\n if (document.cookie.indexOf('session_key') === -1) {\n event.preventDefault();\n privateKeyModal.show();\n }\n }\n\n for (const element of document.querySelectorAll('.requires-session-key')) {\n const form = element.closest('form');\n if (form !== null) {\n form.addEventListener('submit', handleSubmit);\n }\n }\n}\n\nexport function initSecrets() {\n for (const func of [initGenerateKeyPair, initLockUnlock, initGetSessionKey, initSecretsEdit]) {\n func();\n }\n}\n", "import { initSecrets } from './secrets';\n\nif (document.readyState !== 'loading') {\n initSecrets();\n} else {\n document.addEventListener('DOMContentLoaded', initSecrets);\n}\n"], + "mappings": "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", "names": [] } diff --git a/netbox_secrets/template_content.py b/netbox_secrets/template_content.py index ff25ac1..7b6ef87 100644 --- a/netbox_secrets/template_content.py +++ b/netbox_secrets/template_content.py @@ -40,22 +40,18 @@ def get_display_on(app_model): # Generate plugin extensions for the defined classes try: - for content_type in ContentType.objects.all(): - app_label = content_type.app_label - model = content_type.model - app_model_name = f'{app_label}.{model}' - - if app_model_name in plugin_settings.get('apps'): - klass_name = f'{app_label}_{model}_plugin_template_extension' - dynamic_klass = type( - klass_name, - (PluginTemplateExtension,), - {'model': app_model_name, get_display_on(app_model_name): secrets_panel} - ) - template_extensions.append(dynamic_klass) + for app_model in plugin_settings.get('apps'): + app_label, model = app_model.split('.') + klass_name = f'{app_label}_{model}_plugin_template_extension' + dynamic_klass = type( + klass_name, + (PluginTemplateExtension,), + {'model': app_model, get_display_on(app_model): secrets_panel} + ) + template_extensions.append(dynamic_klass) except OperationalError as e: # This happens when the database is not yet ready logger.warning(f'Database not ready, skipping plugin extensions: {e}') except Exception as e: # Unexpected error - logger.error(e) + raise Exception(f'Unexpected error: {e}') diff --git a/netbox_secrets/templates/netbox_secrets/userkey.html b/netbox_secrets/templates/netbox_secrets/userkey.html index b5d43d2..080fd2f 100644 --- a/netbox_secrets/templates/netbox_secrets/userkey.html +++ b/netbox_secrets/templates/netbox_secrets/userkey.html @@ -1,53 +1,131 @@ -{% extends 'base/layout.html' %} +{% extends 'generic/object.html' %} +{% load perms %} + +{% block breadcrumbs %} +{% endblock %} + +{% block object_identifier %} + {% if object %} + {{ block.super }} + {% endif %} +{% endblock %} {% block title %}User Key{% endblock %} -{% block content %} +{% block subtitle %} {% if object %} - -

- Your User Key is - {% if object.is_active %} - Active - {% else %} - Inactive + {{ block.super }} + {% endif %} +{% endblock %} + +{% block controls %} +
+
+ {# Extra buttons #} + {% block extra_controls %}{% endblock %} + + {% if object %} + {% if request.user|can_change:object %} + +  Edit + + {% endif %} {% endif %} -

-

- Created {{ object.created }} · Updated {{ object.last_updated|timesince }} ago -

- {% if not object.is_active %} - + +{% endblock %} + +{% block tabs %} + {% if object %} + {{ block.super }} + {% endif %} +{% endblock %} + +{% block content %} +
+
+
+
Overview
+
+ + + + + + {% if object.is_filled %} + + + + + {% endif %} +
Status + {% if object.is_active %} + Active + {% else %} + + {% if not object.is_filled %} + You don't have a user key on file. + {% else %} + Your user key is inactive. Ask an administrator to enable it for you. + {% endif %} + {% endif %} +
Public Key +
{{ object.public_key }}
+
+ {% if not object %} + + {% endif %} +
- {% endif %} -
{{ object.public_key }}
-
- {% if object.session_key %} - + {% if object.is_filled and object.is_active %} +
+
+
Session Key
+
+ + + + + + + + + +
Status + {% if object.session_key %} + Active + {% else %} + Inactive + {% endif %} +
Created{{ object.session_key.created|placeholder }}
+
+ +
-

Session Key: Active

- Created {{ object.session_key.created }} - {% else %} -

No Active Session Key

{% endif %} - {% else %} -

You don't have a user key on file.

-

- - - Create a User Key - -

- {% endif %} -{% endblock %} +
+{% endblock content %} diff --git a/netbox_secrets/templates/netbox_secrets/userkey_edit.html b/netbox_secrets/templates/netbox_secrets/userkey_edit.html index 6432c32..b0cc6dc 100644 --- a/netbox_secrets/templates/netbox_secrets/userkey_edit.html +++ b/netbox_secrets/templates/netbox_secrets/userkey_edit.html @@ -4,49 +4,83 @@ {% load static %} {% block content-wrapper %} - {% if object.is_active %} - - {% endif %} -
- {% csrf_token %} -
- {% render_field form.public_key %} -
-
-
- -
-
- Cancel - +
+
+ {# Link to model documentation #} + {% if settings.DOCS_ROOT and object.docs_url %} + -
- -