Skip to content

Commit 2862614

Browse files
Allow connection to servers with no token and no password (microsoft#7708)
1 parent dfec12f commit 2862614

7 files changed

Lines changed: 80 additions & 11 deletions

File tree

news/2 Fixes/7137.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix regression to allow connection to servers with no token and no password and add functional test for this scenario

news/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
docopt~=0.6.2
2-
pytest~=3.4.1
2+
pytest>=5.2.0

src/client/datascience/jupyter/jupyterPasswordConnect.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,17 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect {
5353
sessionCookieName = sessionResult.sessionCookieName;
5454
sessionCookieValue = sessionResult.sessionCookieValue;
5555
}
56+
} else {
57+
// If userPassword is undefined or '' then the user didn't pick a password. In this case return back that we should just try to connect
58+
// like a standard connection. Might be the case where there is no token and no password
59+
return { emptyPassword: true, xsrfCookie: '', sessionCookieName: '', sessionCookieValue: '' };
5660
}
5761
userPassword = undefined;
5862

5963
// If we found everything return it all back if not, undefined as partial is useless
6064
if (xsrfCookie && sessionCookieName && sessionCookieValue) {
6165
sendTelemetryEvent(Telemetry.GetPasswordSuccess);
62-
return { xsrfCookie, sessionCookieName, sessionCookieValue };
66+
return { xsrfCookie, sessionCookieName, sessionCookieValue, emptyPassword: false };
6367
} else {
6468
sendTelemetryEvent(Telemetry.GetPasswordFailure);
6569
return undefined;
@@ -69,14 +73,14 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect {
6973
// For HTTPS connections respect our allowUnauthorized setting by adding in an agent to enable that on the request
7074
private addAllowUnauthorized(url: string, allowUnauthorized: boolean, options: nodeFetch.RequestInit): nodeFetch.RequestInit {
7175
if (url.startsWith('https') && allowUnauthorized) {
72-
const requestAgent = new HttpsAgent({rejectUnauthorized: false});
73-
return {...options, agent: requestAgent};
76+
const requestAgent = new HttpsAgent({ rejectUnauthorized: false });
77+
return { ...options, agent: requestAgent };
7478
}
7579

7680
return options;
7781
}
7882

79-
private async getUserPassword() : Promise<string | undefined> {
83+
private async getUserPassword(): Promise<string | undefined> {
8084
// First get the proposed URI from the user
8185
return this.appShell.showInputBox({
8286
prompt: localize.DataScience.jupyterSelectPasswordPrompt(),
@@ -112,7 +116,7 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect {
112116
allowUnauthorized: boolean,
113117
xsrfCookie: string,
114118
password: string,
115-
fetchFunction: (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => Promise<nodeFetch.Response>): Promise<{sessionCookieName: string | undefined; sessionCookieValue: string | undefined}> {
119+
fetchFunction: (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => Promise<nodeFetch.Response>): Promise<{ sessionCookieName: string | undefined; sessionCookieValue: string | undefined }> {
116120
let sessionCookieName: string | undefined;
117121
let sessionCookieValue: string | undefined;
118122
// Create the form params that we need
@@ -138,7 +142,7 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect {
138142
}
139143
}
140144

141-
return {sessionCookieName, sessionCookieValue};
145+
return { sessionCookieName, sessionCookieValue };
142146
}
143147

144148
private getCookies(response: nodeFetch.Response): Map<string, string> {

src/client/datascience/jupyter/jupyterSessionManager.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,13 @@ export class JupyterSessionManager implements IJupyterSessionManager {
116116
if (connInfo.token === '' || connInfo.token === 'null') {
117117
serverSettings = { ...serverSettings, token: '' };
118118
const pwSettings = await this.jupyterPasswordConnect.getPasswordConnectionInfo(connInfo.baseUrl, connInfo.allowUnauthorized ? true : false);
119-
if (pwSettings) {
119+
if (pwSettings && !pwSettings.emptyPassword) {
120120
cookieString = this.getSessionCookieString(pwSettings);
121121
const requestHeaders = { Cookie: cookieString, 'X-XSRFToken': pwSettings.xsrfCookie };
122122
requestInit = { ...requestInit, headers: requestHeaders };
123123
requiresWebSocket = true;
124+
} else if (pwSettings && pwSettings.emptyPassword) {
125+
serverSettings = { ...serverSettings, token: connInfo.token };
124126
} else {
125127
// Failed to get password info, notify the user
126128
throw new Error(localize.DataScience.passwordFailure());

src/client/datascience/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export interface IJupyterDebugger {
145145
}
146146

147147
export interface IJupyterPasswordConnectInfo {
148+
emptyPassword: boolean;
148149
xsrfCookie: string;
149150
sessionCookieName: string;
150151
sessionCookieValue: string;

src/test/datascience/notebook.functional.test.ts

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import { injectable } from 'inversify';
99
import * as os from 'os';
1010
import * as path from 'path';
1111
import { Readable, Writable } from 'stream';
12+
import * as TypeMoq from 'typemoq';
1213
import * as uuid from 'uuid/v4';
1314
import { Disposable, Uri } from 'vscode';
1415
import { CancellationToken, CancellationTokenSource } from 'vscode-jsonrpc';
1516

17+
import { IApplicationShell } from '../../client/common/application/types';
1618
import { Cancellation, CancellationError } from '../../client/common/cancellation';
1719
import { EXTENSION_ROOT_DIR } from '../../client/common/constants';
1820
import { traceError, traceInfo } from '../../client/common/logger';
@@ -59,8 +61,6 @@ suite('DataScience notebook tests', () => {
5961
setup(() => {
6062
ioc = new DataScienceIocContainer();
6163
ioc.registerDataScienceTypes();
62-
jupyterExecution = ioc.serviceManager.get<IJupyterExecution>(IJupyterExecution);
63-
processFactory = ioc.serviceManager.get<IProcessServiceFactory>(IProcessServiceFactory);
6464
});
6565

6666
teardown(async () => {
@@ -199,8 +199,14 @@ suite('DataScience notebook tests', () => {
199199
});
200200
}
201201

202-
function runTest(name: string, func: () => Promise<void>, _notebookProc?: ChildProcess) {
202+
function runTest(name: string, func: () => Promise<void>, _notebookProc?: ChildProcess, rebindFunc?: () => void) {
203203
test(name, async () => {
204+
// Give tests a chance to rebind IOC services before we fetch jupyterExecution and processFactory
205+
if (rebindFunc) {
206+
rebindFunc();
207+
}
208+
jupyterExecution = ioc.serviceManager.get<IJupyterExecution>(IJupyterExecution);
209+
processFactory = ioc.serviceManager.get<IProcessServiceFactory>(IProcessServiceFactory);
204210
console.log(`Starting test ${name} ...`);
205211
if (await jupyterExecution.isNotebookSupported()) {
206212
return func();
@@ -282,6 +288,53 @@ suite('DataScience notebook tests', () => {
282288
}
283289
});
284290

291+
// Connect to a server that doesn't have a token or password, customers use this and we regressed it once
292+
runTest('Remote No Auth', async () => {
293+
const python = await getNotebookCapableInterpreter(ioc, processFactory);
294+
const procService = await processFactory.create();
295+
296+
if (procService && python) {
297+
const connectionFound = createDeferred();
298+
const configFile = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience', 'serverConfigFiles', 'remoteNoAuth.py');
299+
const exeResult = procService.execObservable(python.path, ['-m', 'jupyter', 'notebook', `--config=${configFile}`], { env: process.env, throwOnStdErr: false });
300+
disposables.push(exeResult);
301+
302+
exeResult.out.subscribe((output: Output<string>) => {
303+
traceInfo(`remote jupyter output: ${output.out}`);
304+
const connectionURL = getIPConnectionInfo(output.out);
305+
if (connectionURL) {
306+
connectionFound.resolve(connectionURL);
307+
}
308+
});
309+
310+
const connString = await connectionFound.promise;
311+
const uri = connString as string;
312+
313+
// We have a connection string here, so try to connect jupyterExecution to the notebook server
314+
const server = await jupyterExecution.connectToNotebookServer({ uri, useDefaultConfig: true, purpose: '' });
315+
const notebook = server ? await server.createNotebook(Uri.parse(Identifiers.InteractiveWindowIdentity)) : undefined;
316+
if (!notebook) {
317+
assert.fail('Failed to connect to remote password server');
318+
} else {
319+
await verifySimple(notebook, `a=1${os.EOL}a`, 1);
320+
}
321+
// Have to dispose here otherwise the process may exit before hand and mess up cleanup.
322+
await server!.dispose();
323+
}
324+
}, undefined, () => {
325+
const dummyDisposable = {
326+
dispose: () => { return; }
327+
};
328+
const appShell = TypeMoq.Mock.ofType<IApplicationShell>();
329+
appShell.setup(a => a.showErrorMessage(TypeMoq.It.isAnyString())).returns((e) => { throw e; });
330+
appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(''));
331+
appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_a1: string, a2: string, _a3: string) => Promise.resolve(a2));
332+
appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_a1: string, a2: string, _a3: string, _a4: string) => Promise.resolve(a2));
333+
appShell.setup(a => a.showInputBox(TypeMoq.It.isAny())).returns(() => Promise.resolve(''));
334+
appShell.setup(a => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable);
335+
ioc.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, appShell.object);
336+
});
337+
285338
runTest('Remote Password', async () => {
286339
const python = await getNotebookCapableInterpreter(ioc, processFactory);
287340
const procService = await processFactory.create();
@@ -368,6 +421,8 @@ suite('DataScience notebook tests', () => {
368421
});
369422

370423
test('Not installed', async () => {
424+
jupyterExecution = ioc.serviceManager.get<IJupyterExecution>(IJupyterExecution);
425+
processFactory = ioc.serviceManager.get<IProcessServiceFactory>(IProcessServiceFactory);
371426
// Rewire our data we use to search for processes
372427
class EmptyInterpreterService implements IInterpreterService {
373428
public get hasInterpreters(): Promise<boolean> {
@@ -1001,6 +1056,8 @@ plt.show()`,
10011056
});
10021057

10031058
test('Notebook launch failure', async function () {
1059+
jupyterExecution = ioc.serviceManager.get<IJupyterExecution>(IJupyterExecution);
1060+
processFactory = ioc.serviceManager.get<IProcessServiceFactory>(IProcessServiceFactory);
10041061
if (!ioc.mockJupyter) {
10051062
// tslint:disable-next-line: no-invalid-this
10061063
this.skip();
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# With these settings you can connect to a server with no token and no password
2+
c.NotebookApp.token = ''
3+
c.NotebookApp.open_browser = False
4+
c.NotebookApp.disable_check_xsrf = True

0 commit comments

Comments
 (0)