Allow saving empty secrets via secret manager and slash command (#4580) * Allow saving empty secrets via secret manager and slash command Closes #4577 * Fix logic on cancel * Fix /secrets-read with empty secret value
Signed| @@ -2,7 +2,7 @@ import { DOMPurify, moment } from '../lib.js'; | |||
| 2 | import { event_types, eventSource, getRequestHeaders } from '../script.js'; | 2 | import { event_types, eventSource, getRequestHeaders } from '../script.js'; |
| 3 | import { t } from './i18n.js'; | 3 | import { t } from './i18n.js'; |
| 4 | import { chat_completion_sources } from './openai.js'; | 4 | import { chat_completion_sources } from './openai.js'; |
| 5 | import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js'; | 5 | import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; |
| 6 | import { SlashCommand } from './slash-commands/SlashCommand.js'; | 6 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 7 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; | 7 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; |
| 8 | import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; | 8 | import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| @@ -293,11 +293,13 @@ export let secret_state = {}; | |||
| 293 | * @param {string} key Secret key | 293 | * @param {string} key Secret key |
| 294 | * @param {string} value Secret value to write | 294 | * @param {string} value Secret value to write |
| 295 | * @param {string} [label] (Optional) Label for the key. If not provided, generated automatically. | 295 | * @param {string} [label] (Optional) Label for the key. If not provided, generated automatically. |
| 296 | * @param {Object} [options] Additional options | ||
| 297 | * @param {boolean} [options.allowEmpty] Whether to allow writing empty values. If false and value is empty, the secret will be deleted. | ||
| 296 | * @return {Promise<string?>} The ID of the newly created secret key, or null if no value is provided. | 298 | * @return {Promise<string?>} The ID of the newly created secret key, or null if no value is provided. |
| 297 | */ | 299 | */ |
| 298 | export async function writeSecret(key, value, label) { | 300 | export async function writeSecret(key, value, label, { allowEmpty } = {}) { |
| 299 | try { | 301 | try { |
| 300 | if (!value) { | 302 | if (!value && !allowEmpty) { |
| 301 | console.warn(`No value provided for ${key} in writeSecret, redirecting to deleteSecret`); | 303 | console.warn(`No value provided for ${key} in writeSecret, redirecting to deleteSecret`); |
| 302 | await deleteSecret(key); | 304 | await deleteSecret(key); |
| 303 | return null; | 305 | return null; |
| @@ -558,7 +560,8 @@ async function openKeyManagerDialog(key) { | |||
| 558 | const template = $(await renderTemplateAsync('secretKeyManager', { name, key })); | 560 | const template = $(await renderTemplateAsync('secretKeyManager', { name, key })); |
| 559 | template.find('button[data-action="add-secret"]').on('click', async function () { | 561 | template.find('button[data-action="add-secret"]').on('click', async function () { |
| 560 | let label = ''; | 562 | let label = ''; |
| 561 | const value = await Popup.show.input(t`Add Secret`, t`Enter the secret value:`, '', { | 563 | let result = POPUP_RESULT.CANCELLED; |
| 564 | const value = await Popup.show.input(t`Add Secret`, t`Enter the secret value (can be empty):`, '', { | ||
| 562 | customInputs: [{ | 565 | customInputs: [{ |
| 563 | id: 'newSecretLabel', | 566 | id: 'newSecretLabel', |
| 564 | type: 'text', | 567 | type: 'text', |
| @@ -567,13 +570,20 @@ async function openKeyManagerDialog(key) { | |||
| 567 | onClose: popup => { | 570 | onClose: popup => { |
| 568 | if (popup.result) { | 571 | if (popup.result) { |
| 569 | label = popup.inputResults.get('newSecretLabel').toString().trim(); | 572 | label = popup.inputResults.get('newSecretLabel').toString().trim(); |
| 573 | result = popup.result; | ||
| 570 | } | 574 | } |
| 571 | }, | 575 | }, |
| 572 | }); | 576 | }); |
| 573 | if (!value) { | 577 | if (!value) { |
| 578 | if (result !== POPUP_RESULT.AFFIRMATIVE) { | ||
| 579 | return; | ||
| 580 | } | ||
| 581 | const allowEmpty = await Popup.show.confirm(t`No value entered`, t`No value was entered for the secret. Do you want to add an empty secret?`); | ||
| 582 | if (!allowEmpty) { | ||
| 574 | return; | 583 | return; |
| 575 | } | 584 | } |
| 576 | await writeSecret(key, value, label); | 585 | } |
| 586 | await writeSecret(key, value, label, { allowEmpty: true }); | ||
| 577 | await renderSecretsList(); | 587 | await renderSecretsList(); |
| 578 | }); | 588 | }); |
| 579 | 589 | ||
| @@ -821,6 +831,13 @@ function registerSecretSlashCommands() { | |||
| 821 | isRequired: false, | 831 | isRequired: false, |
| 822 | typeList: [ARGUMENT_TYPE.STRING], | 832 | typeList: [ARGUMENT_TYPE.STRING], |
| 823 | }), | 833 | }), |
| 834 | SlashCommandNamedArgument.fromProps({ | ||
| 835 | name: 'empty', | ||
| 836 | description: t`Whether to allow empty values.`, | ||
| 837 | isRequired: false, | ||
| 838 | typeList: [ARGUMENT_TYPE.BOOLEAN], | ||
| 839 | defaultValue: String(false), | ||
| 840 | }), | ||
| 824 | ], | 841 | ], |
| 825 | unnamedArgumentList: [ | 842 | unnamedArgumentList: [ |
| 826 | SlashCommandArgument.fromProps({ | 843 | SlashCommandArgument.fromProps({ |
| @@ -831,6 +848,7 @@ function registerSecretSlashCommands() { | |||
| 831 | ], | 848 | ], |
| 832 | callback: async (args, value) => { | 849 | callback: async (args, value) => { |
| 833 | const quiet = isTrueBoolean(args?.quiet?.toString()); | 850 | const quiet = isTrueBoolean(args?.quiet?.toString()); |
| 851 | const allowEmpty = isTrueBoolean(args?.empty?.toString()); | ||
| 834 | const key = args?.key?.toString()?.trim() || resolveSecretKey(); | 852 | const key = args?.key?.toString()?.trim() || resolveSecretKey(); |
| 835 | 853 | ||
| 836 | if (!key) { | 854 | if (!key) { |
| @@ -849,7 +867,7 @@ function registerSecretSlashCommands() { | |||
| 849 | } | 867 | } |
| 850 | 868 | ||
| 851 | const valueStr = value?.toString()?.trim(); | 869 | const valueStr = value?.toString()?.trim(); |
| 852 | if (!valueStr) { | 870 | if (!valueStr && !allowEmpty) { |
| 853 | if (!quiet) { | 871 | if (!quiet) { |
| 854 | toastr.error(t`No value provided for the secret key: ${key}`); | 872 | toastr.error(t`No value provided for the secret key: ${key}`); |
| 855 | } | 873 | } |
| @@ -857,7 +875,7 @@ function registerSecretSlashCommands() { | |||
| 857 | } | 875 | } |
| 858 | 876 | ||
| 859 | const label = args?.label?.toString()?.trim() || getLabel(); | 877 | const label = args?.label?.toString()?.trim() || getLabel(); |
| 860 | const id = await writeSecret(key, valueStr, label); | 878 | const id = await writeSecret(key, valueStr, label, { allowEmpty }); |
| 861 | 879 | ||
| 862 | if (!quiet) { | 880 | if (!quiet) { |
| 863 | toastr.success(t`Secret has been written for the key: ${key}`); | 881 | toastr.success(t`Secret has been written for the key: ${key}`); |
| @@ -561,12 +561,13 @@ router.post('/find', (request, response) => { | |||
| 561 | } | 561 | } |
| 562 | 562 | ||
| 563 | const manager = new SecretManager(request.user.directories); | 563 | const manager = new SecretManager(request.user.directories); |
| 564 | const secretValue = manager.readSecret(key, id); | 564 | const state = manager.getSecretState(); |
| 565 | 565 | ||
| 566 | if (!secretValue) { | 566 | if (!state[key]) { |
| 567 | return response.sendStatus(404); | 567 | return response.sendStatus(404); |
| 568 | } | 568 | } |
| 569 | 569 | ||
| 570 | const secretValue = manager.readSecret(key, id); | ||
| 570 | return response.send({ value: secretValue }); | 571 | return response.send({ value: secretValue }); |
| 571 | } catch (error) { | 572 | } catch (error) { |
| 572 | console.error('Error finding secret:', error); | 573 | console.error('Error finding secret:', error); |