-
Notifications
You must be signed in to change notification settings - Fork 6
Isolate secrets from others extensions #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
823fe44
887fdd8
3787fad
6cced74
51cd331
f55089e
e3c3a3e
5c5a2b5
309dd1b
6e4a826
142a5f2
d9fe0e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,3 +1,5 @@ | ||||||||||||||
| import { JupyterFrontEndPlugin } from '@jupyterlab/application'; | ||||||||||||||
| import { PageConfig } from '@jupyterlab/coreutils'; | ||||||||||||||
| import { PromiseDelegate } from '@lumino/coreutils'; | ||||||||||||||
|
|
||||||||||||||
| import { | ||||||||||||||
|
|
@@ -8,64 +10,79 @@ import { | |||||||||||||
| } from './token'; | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * The secrets manager namespace. | ||||||||||||||
| */ | ||||||||||||||
| export namespace SecretsManager { | ||||||||||||||
| /** | ||||||||||||||
| * Secrets manager constructor's options. | ||||||||||||||
| */ | ||||||||||||||
| export interface IOptions { | ||||||||||||||
| connector: ISecretsConnector; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * The default secrets manager implementation. | ||||||||||||||
| * The default secrets manager. | ||||||||||||||
| */ | ||||||||||||||
| export class SecretsManager implements ISecretsManager { | ||||||||||||||
| /** | ||||||||||||||
| * the secrets manager constructor. | ||||||||||||||
| */ | ||||||||||||||
| constructor(options: SecretsManager.IOptions) { | ||||||||||||||
| this._connector = options.connector; | ||||||||||||||
| constructor() { | ||||||||||||||
| this._ready = new PromiseDelegate<void>(); | ||||||||||||||
| this._ready.resolve(); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Set the connector to use with the manager. | ||||||||||||||
| * | ||||||||||||||
| * NOTE: | ||||||||||||||
| * If several extensions try to set the connector, the manager will be locked. | ||||||||||||||
| * This is to prevent malicious extensions to get passwords when they are saved. | ||||||||||||||
| */ | ||||||||||||||
| setConnector(value: ISecretsConnector): void { | ||||||||||||||
| Private.setConnector(value); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| get ready(): Promise<void> { | ||||||||||||||
| return this._ready.promise; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Get a secret given its namespace and ID. | ||||||||||||||
| */ | ||||||||||||||
| async get(namespace: string, id: string): Promise<ISecret | undefined> { | ||||||||||||||
| return this._get(Private.buildSecretId(namespace, id)); | ||||||||||||||
| async get( | ||||||||||||||
brichet marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
| token: symbol, | ||||||||||||||
| namespace: string, | ||||||||||||||
| id: string | ||||||||||||||
| ): Promise<ISecret | undefined> { | ||||||||||||||
| this._check(token, namespace); | ||||||||||||||
| return this._get(Private.buildConnectorId(namespace, id)); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Set a secret given its namespace and ID. | ||||||||||||||
| */ | ||||||||||||||
| async set(namespace: string, id: string, secret: ISecret): Promise<any> { | ||||||||||||||
| return this._set(Private.buildSecretId(namespace, id), secret); | ||||||||||||||
| async set( | ||||||||||||||
| token: symbol, | ||||||||||||||
| namespace: string, | ||||||||||||||
| id: string, | ||||||||||||||
| secret: ISecret | ||||||||||||||
| ): Promise<any> { | ||||||||||||||
| this._check(token, namespace); | ||||||||||||||
| return this._set(Private.buildConnectorId(namespace, id), secret); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * List the secrets for a namespace as a ISecretsList. | ||||||||||||||
| */ | ||||||||||||||
| async list(namespace: string): Promise<ISecretsList | undefined> { | ||||||||||||||
| if (!this._connector.list) { | ||||||||||||||
| async list( | ||||||||||||||
| token: symbol, | ||||||||||||||
| namespace: string | ||||||||||||||
| ): Promise<ISecretsList | undefined> { | ||||||||||||||
| this._check(token, namespace); | ||||||||||||||
| const connector = Private.getConnector(); | ||||||||||||||
| if (!connector?.list) { | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
| await this._ready.promise; | ||||||||||||||
| return await this._connector.list(namespace); | ||||||||||||||
| return connector.list(namespace); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Remove a secret given its namespace and ID. | ||||||||||||||
| */ | ||||||||||||||
| async remove(namespace: string, id: string): Promise<void> { | ||||||||||||||
| return this._remove(Private.buildSecretId(namespace, id)); | ||||||||||||||
| async remove(token: symbol, namespace: string, id: string): Promise<void> { | ||||||||||||||
| this._check(token, namespace); | ||||||||||||||
| return this._remove(Private.buildConnectorId(namespace, id)); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
|
|
@@ -74,17 +91,19 @@ export class SecretsManager implements ISecretsManager { | |||||||||||||
| * is programmatically filled. | ||||||||||||||
| */ | ||||||||||||||
| async attach( | ||||||||||||||
| token: symbol, | ||||||||||||||
| namespace: string, | ||||||||||||||
| id: string, | ||||||||||||||
| input: HTMLInputElement, | ||||||||||||||
| callback?: (value: string) => void | ||||||||||||||
| ): Promise<void> { | ||||||||||||||
| const attachedId = Private.buildSecretId(namespace, id); | ||||||||||||||
| this._check(token, namespace); | ||||||||||||||
| const attachedId = Private.buildConnectorId(namespace, id); | ||||||||||||||
| const attachedInput = this._attachedInputs.get(attachedId); | ||||||||||||||
|
||||||||||||||
|
|
||||||||||||||
| // Detach the previous input. | ||||||||||||||
| if (attachedInput) { | ||||||||||||||
| this.detach(namespace, id); | ||||||||||||||
| this.detach(token, namespace, id); | ||||||||||||||
| } | ||||||||||||||
| this._attachedInputs.set(attachedId, input); | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -108,14 +127,16 @@ export class SecretsManager implements ISecretsManager { | |||||||||||||
| /** | ||||||||||||||
| * Detach the input previously attached with its namespace and ID. | ||||||||||||||
| */ | ||||||||||||||
| detach(namespace: string, id: string): void { | ||||||||||||||
| this._detach(Private.buildSecretId(namespace, id)); | ||||||||||||||
| detach(token: symbol, namespace: string, id: string): void { | ||||||||||||||
| this._check(token, namespace); | ||||||||||||||
| this._detach(Private.buildConnectorId(namespace, id)); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Detach all attached input for a namespace. | ||||||||||||||
| */ | ||||||||||||||
| async detachAll(namespace: string): Promise<void> { | ||||||||||||||
| async detachAll(token: symbol, namespace: string): Promise<void> { | ||||||||||||||
| this._check(token, namespace); | ||||||||||||||
| for (const id of this._attachedInputs.keys()) { | ||||||||||||||
| if (id.startsWith(`${namespace}:`)) { | ||||||||||||||
| this._detach(id); | ||||||||||||||
|
|
@@ -127,31 +148,34 @@ export class SecretsManager implements ISecretsManager { | |||||||||||||
| * Actually fetch the secret from the connector. | ||||||||||||||
| */ | ||||||||||||||
| private async _get(id: string): Promise<ISecret | undefined> { | ||||||||||||||
|
||||||||||||||
| if (!this._connector.fetch) { | ||||||||||||||
| const connector = Private.getConnector(); | ||||||||||||||
| if (!connector?.fetch) { | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
| await this._ready.promise; | ||||||||||||||
| return this._connector.fetch(id); | ||||||||||||||
| return connector.fetch(id); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Actually save the secret using the connector. | ||||||||||||||
| */ | ||||||||||||||
| private async _set(id: string, secret: ISecret): Promise<any> { | ||||||||||||||
| if (!this._connector.save) { | ||||||||||||||
| const connector = Private.getConnector(); | ||||||||||||||
| if (!connector?.save) { | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
| return this._connector.save(id, secret); | ||||||||||||||
| return connector.save(id, secret); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Actually remove the secrets using the connector. | ||||||||||||||
| */ | ||||||||||||||
| async _remove(id: string): Promise<void> { | ||||||||||||||
| if (!this._connector.remove) { | ||||||||||||||
| const connector = Private.getConnector(); | ||||||||||||||
| if (!connector?.remove) { | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
| this._connector.remove(id); | ||||||||||||||
| return connector.remove(id); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| private _onInput = async (e: Event): Promise<void> => { | ||||||||||||||
|
|
@@ -184,16 +208,122 @@ export class SecretsManager implements ISecretsManager { | |||||||||||||
| this._attachedInputs.delete(attachedId); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| private _connector: ISecretsConnector; | ||||||||||||||
| private _check(token: symbol, namespace: string): void { | ||||||||||||||
| if (Private.isLocked() || Private.namespace.get(token) !== namespace) { | ||||||||||||||
| throw new Error( | ||||||||||||||
| `The secrets namespace ${namespace} is not available with the provided token` | ||||||||||||||
| ); | ||||||||||||||
|
||||||||||||||
| throw new Error( | |
| `The secrets namespace ${namespace} is not available with the provided token` | |
| ); | |
| Private.lock( | |
| `The secrets namespace ${namespace} is not available with the provided token` | |
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't know if we want to lock the manager on any failing attempt. It should not occurs, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't feel strongly about this, but the only times I expect this to happen are in development so I erred on the side of security. I'm happy to defer to your judgement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could use a strict flag that can be set when starting jupyterlab.
Uh oh!
There was an error while loading. Please reload this page.