From 0db867b23593b209ead8608e8f2449b2decd2b24 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Mon, 19 May 2025 19:13:43 +0200 Subject: [PATCH 1/3] Add a setting for the fields visibility, and a locker if it is hidden from page config --- package.json | 10 ++++-- schema/manager.json | 14 ++++++++ src/index.ts | 53 ++++++++++++++++++++++++++--- src/manager.ts | 81 +++++++++++++++++++++++++++++++++++++++++---- yarn.lock | 6 ++-- 5 files changed, 148 insertions(+), 16 deletions(-) create mode 100644 schema/manager.json diff --git a/package.json b/package.json index d891df8..188a4e0 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", - "src/**/*.{ts,tsx}" + "src/**/*.{ts,tsx}", + "schema/*.json" ], "main": "lib/index.js", "types": "lib/index.d.ts", @@ -58,9 +59,11 @@ "dependencies": { "@jupyterlab/application": "^4.0.0", "@jupyterlab/coreutils": "^6.0.0", + "@jupyterlab/settingregistry": "^4.0.0", "@jupyterlab/statedb": "^4.0.0", "@lumino/algorithm": "^2.0.0", - "@lumino/coreutils": "^2.1.2" + "@lumino/coreutils": "^2.1.2", + "@lumino/signaling": "^2.1.2" }, "devDependencies": { "@jupyterlab/builder": "^4.0.0", @@ -99,7 +102,8 @@ }, "jupyterlab": { "extension": true, - "outputDir": "jupyter_secrets_manager/labextension" + "outputDir": "jupyter_secrets_manager/labextension", + "schemaDir": "schema" }, "eslintIgnore": [ "node_modules", diff --git a/schema/manager.json b/schema/manager.json new file mode 100644 index 0000000..71eda7c --- /dev/null +++ b/schema/manager.json @@ -0,0 +1,14 @@ +{ + "title": "Secrets manager", + "description": "The secrets manager settings", + "type": "object", + "properties": { + "ShowSecretFields": { + "type": "boolean", + "title": "Show secret fields", + "description": "Whether to show the secret fields in the UI or not", + "default": true + } + }, + "additionalProperties": false +} diff --git a/src/index.ts b/src/index.ts index 99c1b84..82b9c6f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,9 +2,12 @@ import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; + import { SecretsManager } from './manager'; import { ISecretsManager } from './token'; import { InMemoryConnector } from './connectors'; +import { PageConfig } from '@jupyterlab/coreutils'; /** * A basic secret connector extension, that should be disabled to provide a new @@ -23,18 +26,58 @@ const inMemoryConnector: JupyterFrontEndPlugin = { /** * The secret manager extension. */ -const manager: JupyterFrontEndPlugin = { +const managerPlugin: JupyterFrontEndPlugin = { id: 'jupyter-secrets-manager:manager', description: 'A JupyterLab extension to manage secrets.', autoStart: true, provides: ISecretsManager, - activate: (app: JupyterFrontEnd): ISecretsManager => { - console.log('JupyterLab extension jupyter-secrets-manager is activated!'); - return new SecretsManager(); + optional: [ISettingRegistry], + activate: ( + app: JupyterFrontEnd, + settingRegistry: ISettingRegistry + ): ISecretsManager => { + // Check if the fields are hidden from page config. + let showSecretFieldsConfig = true; + if (PageConfig.getOption('secretsManager-showFields') === 'false') { + showSecretFieldsConfig = false; + } + + const manager = new SecretsManager({ + showSecretFields: showSecretFieldsConfig + }); + + settingRegistry + .load(managerPlugin.id) + .then(settings => { + // If the fields are hidden from the manager, remove the setting. + if (!showSecretFieldsConfig) { + delete settings.schema.properties?.['ShowSecretFields']; + return; + } + + // Otherwise listen to it to update the field visibility. + const updateFieldVisibility = () => { + const showSecretField = + settings.get('ShowSecretFields').composite ?? true; + manager.secretFieldsVisibility = showSecretField as boolean; + }; + + settings.changed.connect(() => updateFieldVisibility()); + updateFieldVisibility(); + }) + .catch(reason => { + console.error( + `Failed to load settings for ${managerPlugin.id}`, + reason + ); + }); + + console.debug('JupyterLab extension jupyter-secrets-manager is activated!'); + return manager; } }; export * from './connectors'; export * from './manager'; export * from './token'; -export default [inMemoryConnector, manager]; +export default [inMemoryConnector, managerPlugin]; diff --git a/src/manager.ts b/src/manager.ts index 8ae0157..779717b 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -8,17 +8,29 @@ import { ISecretsList, ISecretsManager } from './token'; +import { ISignal, Signal } from '@lumino/signaling'; + +interface IOptions { + showSecretFields?: boolean; +} /** * The default secrets manager. */ export class SecretsManager implements ISecretsManager { /** - * the secrets manager constructor. + * The secrets manager constructor. */ - constructor() { + constructor(options: IOptions) { this._storing = new PromiseDelegate(); this._storing.resolve(); + Private.setSecretFieldsVisibility(options.showSecretFields ?? false); + + // If the secret fields are hidden from constructor, this setting comes from + // PageConfig, we need to lock the fields visibility. + if (!options.showSecretFields) { + Private.lockFieldsVisibility(); + } } /** @@ -33,14 +45,44 @@ export class SecretsManager implements ISecretsManager { this._ready.resolve(); } + /** + * A promise that resolves when the connector is set. + */ get ready(): Promise { return this._ready.promise; } + /** + * A promise that locks the connector access during storage. + */ protected get storing(): Promise { return this._storing.promise; } + /** + * A signal emitting when the field visibility setting has changed. + */ + get fieldVisibilityChanged(): ISignal { + return this._fieldsVisibilityChanged; + } + + /** + * Get the visibility of the secret fields. + */ + get secretFieldsVisibility(): boolean { + return Private.getSecretFieldsVisibility(); + } + + /** + * Set the visibility of the secret fields. + * The visibility cannot be set if it is locked (from page config). + */ + set secretFieldsVisibility(value: boolean) { + if (Private.setSecretFieldsVisibility(value)) { + this._fieldsVisibilityChanged.emit(Private.getSecretFieldsVisibility()); + } + } + /** * Get a secret given its namespace and ID. */ @@ -179,6 +221,7 @@ export class SecretsManager implements ISecretsManager { private _ready = new PromiseDelegate(); private _storing: PromiseDelegate; + private _fieldsVisibilityChanged = new Signal(this); } /** @@ -293,7 +336,7 @@ namespace Private { } /** - * Actually fetch the secret from the connector. + * Fetch the secret from the connector. */ export async function get(id: string): Promise { if (!connector?.fetch) { @@ -303,7 +346,7 @@ namespace Private { } /** - * Actually list the secret from the connector. + * List the secret from the connector. */ export async function list( namespace: string @@ -314,7 +357,7 @@ namespace Private { return connector.list(namespace); } /** - * Actually save the secret using the connector. + * Save the secret using the connector. */ export async function set(id: string, secret: ISecret): Promise { if (!connector?.save) { @@ -324,7 +367,7 @@ namespace Private { } /** - * Actually remove the secrets using the connector. + * Remove the secrets using the connector. */ export async function remove(id: string): Promise { if (!connector?.remove) { @@ -333,6 +376,32 @@ namespace Private { return connector.remove(id); } + /** + * Lock the fields visibility value. + */ + let fieldsVisibilityLocked = false; + export function lockFieldsVisibility() { + fieldsVisibilityLocked = true; + } + + /** + * Get/set the fields visibility. + */ + let secretFieldsVisibility = false; + export function getSecretFieldsVisibility(): boolean { + return secretFieldsVisibility; + } + export function setSecretFieldsVisibility(value: boolean): boolean { + if (!fieldsVisibilityLocked && value !== secretFieldsVisibility) { + secretFieldsVisibility = value; + return true; + } + return false; + } + + /** + * The secret path type. + */ export type SecretPath = { namespace: string; id: string; diff --git a/yarn.lock b/yarn.lock index d188893..b53dcfa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2495,7 +2495,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/settingregistry@npm:^4.3.5": +"@jupyterlab/settingregistry@npm:^4.0.0, @jupyterlab/settingregistry@npm:^4.3.5": version: 4.3.5 resolution: "@jupyterlab/settingregistry@npm:4.3.5" dependencies: @@ -2915,7 +2915,7 @@ __metadata: languageName: node linkType: hard -"@lumino/signaling@npm:^1.10.0 || ^2.0.0, @lumino/signaling@npm:^2.1.3": +"@lumino/signaling@npm:^1.10.0 || ^2.0.0, @lumino/signaling@npm:^2.1.2, @lumino/signaling@npm:^2.1.3": version: 2.1.3 resolution: "@lumino/signaling@npm:2.1.3" dependencies: @@ -6802,10 +6802,12 @@ __metadata: "@jupyterlab/application": ^4.0.0 "@jupyterlab/builder": ^4.0.0 "@jupyterlab/coreutils": ^6.0.0 + "@jupyterlab/settingregistry": ^4.0.0 "@jupyterlab/statedb": ^4.0.0 "@jupyterlab/testutils": ^4.0.0 "@lumino/algorithm": ^2.0.0 "@lumino/coreutils": ^2.1.2 + "@lumino/signaling": ^2.1.2 "@types/jest": ^29.2.0 "@types/json-schema": ^7.0.11 "@types/react": ^18.0.26 From 07662ab9404ba647d2496fc2654871ea562db895 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Mon, 19 May 2025 22:14:45 +0200 Subject: [PATCH 2/3] Add the visibility and signal to the manager interface --- schema/manager.json | 2 +- src/index.ts | 2 +- src/token.ts | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/schema/manager.json b/schema/manager.json index 71eda7c..ffbfba2 100644 --- a/schema/manager.json +++ b/schema/manager.json @@ -7,7 +7,7 @@ "type": "boolean", "title": "Show secret fields", "description": "Whether to show the secret fields in the UI or not", - "default": true + "default": false } }, "additionalProperties": false diff --git a/src/index.ts b/src/index.ts index 82b9c6f..62c4d45 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,7 +58,7 @@ const managerPlugin: JupyterFrontEndPlugin = { // Otherwise listen to it to update the field visibility. const updateFieldVisibility = () => { const showSecretField = - settings.get('ShowSecretFields').composite ?? true; + settings.get('ShowSecretFields').composite ?? false; manager.secretFieldsVisibility = showSecretField as boolean; }; diff --git a/src/token.ts b/src/token.ts index 94b018e..4b49174 100644 --- a/src/token.ts +++ b/src/token.ts @@ -1,6 +1,7 @@ import { JupyterFrontEndPlugin } from '@jupyterlab/application'; import { IDataConnector } from '@jupyterlab/statedb'; import { Token } from '@lumino/coreutils'; +import { ISignal } from '@lumino/signaling'; /** * The secret object interface. @@ -36,6 +37,14 @@ export interface ISecretsManager { * This is to prevent misconfiguration of competing plugins or MITM attacks. */ setConnector(value: ISecretsConnector): void; + /** + * A signal emitting when the field visibility setting has changed. + */ + readonly fieldVisibilityChanged: ISignal; + /** + * Get the visibility of the secret fields. + */ + readonly secretFieldsVisibility: boolean; /** * Get a secret given its namespace and ID. */ From 5cca6ed500ff680197757bc8da8bd8d2fc82b855 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Tue, 20 May 2025 11:50:00 +0200 Subject: [PATCH 3/3] Lock the visibility of secret fields only if explicitly provided in the options --- src/manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/manager.ts b/src/manager.ts index 779717b..6bab69b 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -28,7 +28,7 @@ export class SecretsManager implements ISecretsManager { // If the secret fields are hidden from constructor, this setting comes from // PageConfig, we need to lock the fields visibility. - if (!options.showSecretFields) { + if (options.showSecretFields === false) { Private.lockFieldsVisibility(); } }