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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ As the extension works in the browser, the `S3` buckets need to have certain `CO

More information about `CORS` [here](https://docs.aws.amazon.com/AmazonS3/latest/userguide/cors.html) and the various ways to configure it [here](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html).

### Set up credentials

User credentials can be set by accessing `Settings` -> `Settings Editor` -> `Credentials Provider`. Users need to offer a bucket name, region, endpoint, access key ID and secret access key, as well as optionally a path to the folder within the bucket that should act as root.

The extension uses [`jupyter-secrets-manager`](https://github.com/jupyterlab-contrib/jupyter-secrets-manager) to deal with the secret input fields.

## Requirements

- JupyterLab >= 4.2.5
Expand Down
21 changes: 14 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,22 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.511.0",
"@aws-sdk/s3-request-presigner": "^3.511.0",
"@jupyterlab/application": "^4.2.5",
"@jupyterlab/coreutils": "^6.2.5",
"@jupyterlab/settingregistry": "^4.2.5",
"@jupyterlab/statedb": "^4.2.5",
"@lumino/coreutils": "^2.1.2"
"@jupyterlab/application": "^4.4.0",
"@jupyterlab/apputils": "^4.5.0",
"@jupyterlab/coreutils": "^6.4.0",
"@jupyterlab/filebrowser": "^4.4.0",
"@jupyterlab/settingregistry": "^4.4.0",
"@jupyterlab/statedb": "^4.4.0",
"@jupyterlab/translation": "^4.4.0",
"@jupyterlab/ui-components": "^4.4.0",
"@lumino/commands": "^2.3.0",
"@lumino/coreutils": "^2.2.0",
"@lumino/widgets": "^2.7.0",
"jupyter-secrets-manager": "^0.4.0"
},
"devDependencies": {
"@jupyterlab/builder": "^4.2.5",
"@jupyterlab/testutils": "^4.2.5",
"@jupyterlab/builder": "^4.4.0",
"@jupyterlab/testutils": "^4.4.0",
"@types/jest": "^29.2.0",
"@types/json-schema": "^7.0.11",
"@types/react": "^18.0.26",
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ classifiers = [
"Programming Language :: Python :: 3.12",
]
dependencies = [
"jupyter-secrets-manager >=0.4,<0.5"
]
dynamic = ["version", "description", "authors", "urls", "keywords"]

Expand Down
45 changes: 45 additions & 0 deletions schema/auth-file-browser.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"jupyter.lab.setting-icon": "jupydrive-s3:drive",
"jupyter.lab.setting-icon-label": "Drive Browser",
"title": "Credentials Provider",
"description": "jupydrive-s3 credentials provider.",
"type": "object",
"properties": {
"bucket": {
"type": "string",
"title": "Bucket",
"description": "The S3 bucket name.",
"default": "jupyter-drives-test-bucket-1"
},
"root": {
"type": "string",
"title": "Custom root path",
"description": "Path to folder within bucket, that should act as root. Defaults to bucket's top-level directory.",
"default": ""
},
"endpoint": {
"type": "string",
"title": "S3 endpoint",
"description": "The custom S3 endpoint (e.g. : https://s3.eu-north-1.amazonaws.com).",
"default": "https://example.com/s3"
},
"region": {
"type": "string",
"title": "Bucket region",
"description": "The S3 bucket region.",
"default": "eu-north-1"
},
"accessKeyId": {
"type": "string",
"title": "Access key ID",
"description": "The access key id used to access S3 bucket.",
"default": ""
},
"secretAccessKey": {
"type": "string",
"title": "Secret access key",
"default": ""
}
},
"additionalProperties": false
}
178 changes: 146 additions & 32 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,20 @@ import {
} from '@jupyterlab/filebrowser';
import { IStateDB } from '@jupyterlab/statedb';
import { editIcon } from '@jupyterlab/ui-components';

import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { ITranslator } from '@jupyterlab/translation';

import { CommandRegistry } from '@lumino/commands';
import { Widget } from '@lumino/widgets';
import { Drive } from './s3contents';

import { DriveIcon } from './icons';
import {
FilenameSearcher,
IScore,
folderIcon
} from '@jupyterlab/ui-components';
import { ReadonlyPartialJSONObject, Token } from '@lumino/coreutils';
import { S3ClientConfig } from '@aws-sdk/client-s3';
import { Drive } from './s3contents';
import { DriveIcon } from './icons';
import { SecretsManager, ISecretsManager } from 'jupyter-secrets-manager';

/**
* The command IDs used by the filebrowser plugin.
Expand Down Expand Up @@ -76,9 +74,11 @@ const DRIVE_STATE_ID = 'jupydrive-s3:drive-name-id';
*/
export interface IS3Auth {
factory: () => Promise<{
bucket: string;
name: string;
root: string;
config: S3ClientConfig;
secretsManager?: ISecretsManager;
token?: symbol;
}>;
}

Expand All @@ -87,34 +87,146 @@ export interface IS3Auth {
*/
export const IS3Auth = new Token<IS3Auth>('jupydrive-s3:auth-file-browser');

/**
* Authentification file browser ID.
*/
export const AUTH_FILEBROWSER_ID = 'jupydrive-s3:auth-file-browser';

/**
* The auth/credentials provider for the file browser.
*/
const authFileBrowser: JupyterFrontEndPlugin<IS3Auth> = {
id: 'jupydrive-s3:auth-file-browser',
description: 'The default file browser auth/credentials provider',
provides: IS3Auth,
activate: (): IS3Auth => {
return {
factory: async () => ({
bucket: process.env.JP_S3_BUCKET ?? 'jupyter-drives-test-bucket-1',
root: process.env.JP_S3_ROOT ?? '',
config: {
forcePathStyle: true,
endpoint: process.env.JP_S3_ENDPOINT ?? 'https://example.com/s3',
region: process.env.JP_S3_REGION ?? 'eu-west-1',
credentials: {
accessKeyId:
process.env.JP_S3_ACCESS_KEY_ID ?? 'abcdefghijklmnopqrstuvwxyz',
secretAccessKey:
process.env.JP_S3_SECRET_ACCESS_KEY ??
'SECRET123456789abcdefghijklmnopqrstuvwxyz'
}
const authFileBrowser: JupyterFrontEndPlugin<IS3Auth> = SecretsManager.sign(
AUTH_FILEBROWSER_ID,
token => ({
id: 'jupydrive-s3:auth-file-browser',
description: 'The default file browser auth/credentials provider',
requires: [ISettingRegistry, ISecretsManager],
provides: IS3Auth,
activate: (
app: JupyterFrontEnd,
settings: ISettingRegistry,
secretsManager: ISecretsManager
): IS3Auth => {
const secretFields: string[] = ['accessKeyId', 'secretAccessKey'];

function loadCredentials(setting: ISettingRegistry.ISettings) {
const bucket = setting.get('bucket').composite as string;
const root = setting.get('root').composite as string;
const endpoint = setting.get('endpoint').composite as string;
const region = setting.get('region').composite as string;

secretFields.forEach((key: string) => {
const secretValue = setting.get(key).composite as string;
// Save secret to secret manager.
secretsManager.set(
token,
AUTH_FILEBROWSER_ID,
AUTH_FILEBROWSER_ID + '::' + key,
{
namespace: AUTH_FILEBROWSER_ID,
id: AUTH_FILEBROWSER_ID + '::' + key,
value: secretValue
}
);
});

return {
name: bucket,
root: root,
config: {
forcePathStyle: true,
endpoint: endpoint,
region: region,
credentials: {
accessKeyId: '***',
secretAccessKey: '***'
}
},
secretsManager: secretsManager,
token: token
};
}

const attachSecretInput = (key: string) => {
const input = document.getElementById(
'jp-SettingsEditor-jupydrive-s3:auth-file-browser_' + key
) as HTMLInputElement;
if (input) {
input.type = 'password';
secretsManager.attach(
token,
AUTH_FILEBROWSER_ID,
AUTH_FILEBROWSER_ID + '::' + key,
input
);
} else {
setTimeout(() => attachSecretInput(key), 300);
}
})
};
}
};
};

// Watch for DOM changes to make sure secret inputs are attached.
const observer = new MutationObserver(() => {
secretFields.forEach((key: string) => attachSecretInput(key));
});

observer.observe(document.body, {
childList: true,
subtree: true
});

const getInitalSettings = async () => {
// Read the inital credentials settings.
const setting = await settings.load(authFileBrowser.id);
const initial = loadCredentials(setting);
if (initial.name !== '') {
return initial;
} else {
return null;
}
};

// Wait for the application to be restored and for the
// settings of credentials to be loaded.
Promise.all([app.restored, settings.load(authFileBrowser.id)]).then(
([_, settingCredentials]) => {
// Listen for the plugin setting changes using Signal.
settingCredentials.changed.connect(() => {
const s3Config = loadCredentials(settingCredentials);

// Connect to drive using new config.
const S3Drive = new Drive({ ...s3Config, secretsManager, token });
app.serviceManager.contents.addDrive(S3Drive);
});
}
);

return {
factory: async () => {
return (
(await getInitalSettings()) ?? {
name: process.env.JP_S3_BUCKET ?? 'jupyter-drives-test-bucket-1',
root: process.env.JP_S3_ROOT ?? '',
config: {
forcePathStyle: true,
endpoint:
process.env.JP_S3_ENDPOINT ?? 'https://example.com/s3',
region: process.env.JP_S3_REGION ?? 'eu-west-1',
credentials: {
accessKeyId:
process.env.JP_S3_ACCESS_KEY_ID ??
'abcdefghijklmnopqrstuvwxyz',
secretAccessKey:
process.env.JP_S3_SECRET_ACCESS_KEY ??
'SECRET123456789abcdefghijklmnopqrstuvwxyz'
}
}
}
);
}
};
}
})
);

/**
* The default file browser factory provider.
Expand All @@ -139,9 +251,11 @@ const defaultFileBrowser: JupyterFrontEndPlugin<IDefaultFileBrowser> = {
const auth = await s3auth.factory();
// create S3 drive
const S3Drive = new Drive({
name: auth.bucket,
name: auth.name,
root: auth.root,
config: auth.config
config: auth.config,
secretsManager: auth.secretsManager,
token: auth.token
});

app.serviceManager.contents.addDrive(S3Drive);
Expand Down
2 changes: 1 addition & 1 deletion src/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,7 @@ namespace Private {
) {
// transform base64 encoding to a utf-8 array for saving and storing in S3 bucket
const byteCharacters = atob(options.content);
const byteArrays = [];
const byteArrays: Uint8Array[] = [];

for (let offset = 0; offset < byteCharacters.length; offset += 512) {
const slice = byteCharacters.slice(offset, offset + 512);
Expand Down
Loading
Loading