Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -99,7 +102,8 @@
},
"jupyterlab": {
"extension": true,
"outputDir": "jupyter_secrets_manager/labextension"
"outputDir": "jupyter_secrets_manager/labextension",
"schemaDir": "schema"
},
"eslintIgnore": [
"node_modules",
Expand Down
14 changes: 14 additions & 0 deletions schema/manager.json
Original file line number Diff line number Diff line change
@@ -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": false
}
},
"additionalProperties": false
}
53 changes: 48 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,18 +26,58 @@ const inMemoryConnector: JupyterFrontEndPlugin<void> = {
/**
* The secret manager extension.
*/
const manager: JupyterFrontEndPlugin<ISecretsManager> = {
const managerPlugin: JupyterFrontEndPlugin<ISecretsManager> = {
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
});
Comment on lines +41 to +47
Copy link
Collaborator Author

@brichet brichet May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read the page config and setup the fields visibility at instantiation if required, to lock the setting.


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;
}
Comment on lines +53 to +56
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the visibility setting from the schema if it locked.


// Otherwise listen to it to update the field visibility.
const updateFieldVisibility = () => {
const showSecretField =
Copy link
Member

@trungleduc trungleduc May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const showSecretField =
if(!showSecretFieldsConfig ){
return;
}
const showSecretField =

Copy link
Collaborator Author

@brichet brichet May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think this is necessary ?
This function shouldn't be called because of the return statement just above(on the same condition), and even if it is, that should not change anything because the settings is locked in the manager.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed!

settings.get('ShowSecretFields').composite ?? false;
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];
81 changes: 75 additions & 6 deletions src/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>();
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 === false) {
Private.lockFieldsVisibility();
}
Comment on lines +29 to +33
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock the visibility setting if it is explicitly false at startup (coming from page config)

}

/**
Expand All @@ -33,14 +45,44 @@ export class SecretsManager implements ISecretsManager {
this._ready.resolve();
}

/**
* A promise that resolves when the connector is set.
*/
get ready(): Promise<void> {
return this._ready.promise;
}

/**
* A promise that locks the connector access during storage.
*/
protected get storing(): Promise<void> {
return this._storing.promise;
}

/**
* A signal emitting when the field visibility setting has changed.
*/
get fieldVisibilityChanged(): ISignal<this, boolean> {
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.
*/
Expand Down Expand Up @@ -179,6 +221,7 @@ export class SecretsManager implements ISecretsManager {

private _ready = new PromiseDelegate<void>();
private _storing: PromiseDelegate<void>;
private _fieldsVisibilityChanged = new Signal<this, boolean>(this);
}

/**
Expand Down Expand Up @@ -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<ISecret | undefined> {
if (!connector?.fetch) {
Expand All @@ -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
Expand All @@ -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<any> {
if (!connector?.save) {
Expand All @@ -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<void> {
if (!connector?.remove) {
Expand All @@ -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;
}
Comment on lines +394 to +400
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not update the visibility if locked.


/**
* The secret path type.
*/
export type SecretPath = {
namespace: string;
id: string;
Expand Down
9 changes: 9 additions & 0 deletions src/token.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<ISecretsManager, boolean>;
/**
* Get the visibility of the secret fields.
*/
readonly secretFieldsVisibility: boolean;
/**
* Get a secret given its namespace and ID.
*/
Expand Down
6 changes: 4 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
Loading