Skip to content

Commit d5c4b34

Browse files
committed
with-authorization-class
1 parent 2d6e248 commit d5c4b34

9 files changed

Lines changed: 82 additions & 91 deletions

File tree

src/api/index.ts

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,14 @@ import {
1212
schemas,
1313
checkingConnection,
1414
inactiveServerIds,
15-
serverManagerApi,
1615
} from "../extension";
1716
import { currentWorkspaceFolder, outputChannel, outputConsole } from "../utils";
1817

1918
const DEFAULT_API_VERSION = 1;
2019
const DEFAULT_SERVER_VERSION = "2016.2.0";
2120
import * as Atelier from "./atelier";
2221
import { isfsConfig } from "../utils/FileProviderUtil";
23-
import { IServerSpec } from "@intersystems-community/intersystems-servermanager";
22+
import { Authorization, IServerSpec } from "@intersystems-community/intersystems-servermanager";
2423

2524
// Map of the authRequest promises for each username@host:port/pathPrefix target to avoid concurrency issues
2625
const authRequestMap = new Map<string, Promise<any>>();
@@ -39,9 +38,8 @@ interface ConnectionSettings {
3938
superserverPort?: number;
4039
pathPrefix: string;
4140
ns: string;
42-
authMethod: "password" | "oauth2";
43-
username: string;
44-
password: string;
41+
authorization: Authorization;
42+
password?: string;
4543
docker: boolean;
4644
dockerService?: string;
4745
}
@@ -62,15 +60,18 @@ export class AtelierAPI {
6260
}
6361

6462
public get config(): ConnectionSettings {
65-
const { serverName, active = false, https = false, pathPrefix = "", authMethod, username } = this._config;
63+
const { serverName, active = false, https = false, pathPrefix = "", authorization } = this._config;
6664
const ns = this.namespace || this._config.ns;
6765
const wsKey = this.configName.toLowerCase();
6866
const host = this.externalServer ? this._config.host : workspaceState.get(wsKey + ":host", this._config.host);
6967
const port = this.externalServer ? this._config.port : workspaceState.get(wsKey + ":port", this._config.port);
7068
const superserverPort = this.externalServer
7169
? this._config.superserverPort
7270
: workspaceState.get(wsKey + ":superserverPort", this._config.superserverPort);
73-
const password = workspaceState.get(wsKey + ":password", this._config.password);
71+
const password = workspaceState.get(wsKey + ":password", undefined);
72+
if (password !== undefined) {
73+
authorization.resolve(password);
74+
}
7475
const apiVersion = workspaceState.get(wsKey + ":apiVersion", DEFAULT_API_VERSION);
7576
const serverVersion = workspaceState.get(wsKey + ":serverVersion", DEFAULT_SERVER_VERSION);
7677
const docker = workspaceState.get(wsKey + ":docker", false);
@@ -86,9 +87,7 @@ export class AtelierAPI {
8687
superserverPort,
8788
pathPrefix,
8889
ns,
89-
authMethod,
90-
username,
91-
password,
90+
authorization,
9291
docker,
9392
dockerService,
9493
};
@@ -154,13 +153,9 @@ export class AtelierAPI {
154153
public setConnSpec(serverName: string, connSpec: IServerSpec): void {
155154
const {
156155
webServer: { scheme, host, port, pathPrefix = "" },
157-
authMethod = "password",
158-
username,
159-
password,
156+
authorization,
160157
} = connSpec;
161-
this._config.authMethod = authMethod;
162-
this._config.username = username;
163-
this._config.password = password;
158+
this._config.authorization = authorization;
164159
this._config.https = scheme == "https";
165160
this._config.host = host;
166161
this._config.port = port;
@@ -215,7 +210,8 @@ export class AtelierAPI {
215210

216211
/** Return the key for getting values from connection-specific Maps for this connection */
217212
private mapKey(): string {
218-
const { host, port, username } = this.config;
213+
const { host, port, authorization } = this.config;
214+
const username = authorization.resolved() ? authorization.username : "";
219215
let pathPrefix = this._config.pathPrefix || "";
220216
if (pathPrefix.length && !pathPrefix.startsWith("/")) {
221217
pathPrefix = "/" + pathPrefix;
@@ -245,9 +241,7 @@ export class AtelierAPI {
245241
if (serverName !== "") {
246242
const {
247243
webServer: { scheme, host, port, pathPrefix = "" },
248-
authMethod,
249-
username,
250-
password,
244+
authorization,
251245
superServer,
252246
} = getResolvedConnectionSpec(serverName, config("intersystems.servers", workspaceFolderName).get(serverName));
253247
this._config = {
@@ -260,9 +254,7 @@ export class AtelierAPI {
260254
host,
261255
port,
262256
superserverPort: superServer?.port,
263-
authMethod,
264-
username,
265-
password,
257+
authorization,
266258
pathPrefix,
267259
docker: false,
268260
};
@@ -272,9 +264,7 @@ export class AtelierAPI {
272264
if (resolvedSpec) {
273265
const {
274266
webServer: { scheme, host, port, pathPrefix = "" },
275-
authMethod,
276-
username,
277-
password,
267+
authorization,
278268
superServer,
279269
} = resolvedSpec;
280270
this._config = {
@@ -287,9 +277,7 @@ export class AtelierAPI {
287277
host,
288278
port,
289279
superserverPort: superServer?.port,
290-
authMethod,
291-
username,
292-
password,
280+
authorization,
293281
pathPrefix,
294282
docker: true,
295283
dockerService: conn["docker-compose"].service,
@@ -380,7 +368,9 @@ export class AtelierAPI {
380368
let authRequest = authRequestMap.get(mapKey);
381369
if (cookies.length || (method === "HEAD" && !originalPath)) {
382370
auth = Promise.resolve(cookies);
383-
headers["Authorization"] = serverManagerApi.getAuthorization(this.config);
371+
headers["Authorization"] = this.config.authorization.resolved()
372+
? this.config.authorization.httpAuthorizationHeader
373+
: "";
384374
} else if (!cookies.length) {
385375
if (!authRequest) {
386376
// Recursion point

src/commands/connectFolderToServerNamespace.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export async function connectFolderToServerNamespace(): Promise<void> {
8080
.serverInfo(false)
8181
.then((data) => data.result.content.namespaces)
8282
.catch(async (error) => {
83-
if (error?.statusCode == 401 && isUnauthenticated(api.config.username)) {
83+
if (error?.statusCode == 401 && isUnauthenticated(api.config.authorization.username)) {
8484
// Attempt to resolve username and password and try again
8585
const newSpec = await resolveUsernameAndPassword(api.config.serverName, connSpec);
8686
if (newSpec) {

src/commands/restDebugPanel.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as httpsModule from "https";
33
import * as vscode from "vscode";
44
import { AtelierAPI } from "../api";
55
import { handleError, webviewCSS } from "../utils";
6-
import { iscIcon, serverManagerApi } from "../extension";
6+
import { iscIcon } from "../extension";
77

88
interface WebviewMessage {
99
/** Whether this message was triggered by the user pressing the 'Start Debugging' button */
@@ -548,7 +548,9 @@ export class RESTDebugPanel {
548548
.trim();
549549
}
550550
});
551-
headers["authorization"] = headers["authorization"] ?? serverManagerApi.getAuthorization(api.config);
551+
headers["authorization"] =
552+
headers["authorization"] ??
553+
(api.config.authorization.resolved() ? api.config.authorization.httpAuthorizationHeader : "");
552554
const hasBody =
553555
typeof message.bodyContent == "string" && message.bodyContent != "" && message.bodyType != "No Body";
554556
if (hasBody) {

src/commands/serverActions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ type ServerAction = { detail: string; id: string; label: string; rawLink?: strin
2525
export async function serverActions(): Promise<void> {
2626
const { apiTarget, configName: workspaceFolder } = connectionTarget();
2727
const api = new AtelierAPI(apiTarget);
28-
const { active, host = "", ns = "", https, port = 0, pathPrefix, username, docker } = api.config;
28+
const { active, host = "", ns = "", https, port = 0, pathPrefix, authorization, docker } = api.config;
29+
const username = authorization.username;
2930
const explorerCount = (await explorerProvider.getChildren()).length;
3031
if (!explorerCount && (!docker || host === "")) {
3132
await vscode.commands.executeCommand("ObjectScriptExplorer.focus");

src/commands/webSocketTerminal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
225225
this._hideCursorWrite("\x1b]633;P;HasRichCommandDetection=True\x07");
226226
// Print the opening message
227227
this._hideCursorWrite(
228-
`\x1b[32mConnected to \x1b[0m\x1b[4m${api.config.host}:${api.config.port}${api.config.pathPrefix}\x1b[0m\x1b[32m as \x1b[0m\x1b[3m${api.config.username}\x1b[0m\r\n`
228+
`\x1b[32mConnected to \x1b[0m\x1b[4m${api.config.host}:${api.config.port}${api.config.pathPrefix}\x1b[0m\x1b[32m as \x1b[0m\x1b[3m${api.config.authorization.username}\x1b[0m\r\n`
229229
);
230230
// Add event handlers to the socket
231231
this._socket

src/extension.ts

Lines changed: 48 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ export let checkingConnection = false;
209209
export let serverManagerApi: serverManager.ServerManagerAPI;
210210

211211
/** Map of the intersystems.server connection specs we have resolved via the API to that extension */
212-
const resolvedConnSpecs = new Map<string, serverManager.IServerSpec>();
212+
const resolvedConnSpecs = new Map<string, serverManager.IServerSpec & { accessToken?: string }>();
213213

214214
/**
215215
* If servermanager extension is available, fetch the connection spec unless already cached.
@@ -255,8 +255,7 @@ export async function resolveConnectionSpec(
255255
superServer: {
256256
port: serverForUri.superserverPort,
257257
},
258-
username: serverForUri.username,
259-
password: serverForUri.password ? serverForUri.password : undefined,
258+
authorization: serverManagerApi.makeAuthorization(serverForUri.username, serverForUri.password),
260259
description: `Server for workspace folder '${serverName}'`,
261260
};
262261
}
@@ -269,14 +268,12 @@ export async function resolveConnectionSpec(
269268
}
270269
}
271270

272-
async function resolvePassword(serverSpec, ignoreUnauthenticated = false): Promise<void> {
273-
if (
274-
// Connection isn't unauthenticated
275-
(!isUnauthenticated(serverSpec.username) || ignoreUnauthenticated) &&
276-
// A password is missing
277-
typeof serverSpec.password == "undefined"
278-
) {
279-
const scopes = [serverSpec.name, serverSpec.username || ""];
271+
async function resolvePassword(
272+
serverSpec: Pick<serverManager.IServerSpec, "name" | "authorization">,
273+
ignoreUnauthenticated = false
274+
): Promise<string | undefined> {
275+
if (!(serverSpec.authorization.resolved() || ignoreUnauthenticated)) {
276+
const scopes = [serverSpec.name, serverSpec.authorization.username || ""];
280277

281278
// Handle Server Manager extension version < 3.8.0
282279
const account = serverManagerApi.getAccount ? serverManagerApi.getAccount(serverSpec) : undefined;
@@ -293,25 +290,28 @@ async function resolvePassword(serverSpec, ignoreUnauthenticated = false): Promi
293290
}
294291
if (session) {
295292
// If original spec lacked username use the one obtained from the user by the authprovider (exact case)
296-
serverSpec.username = serverSpec.username || session.scopes[1];
297-
serverSpec.password = session.accessToken;
293+
serverSpec.authorization.resolve(session.accessToken, session.scopes[1]);
294+
return session.accessToken;
298295
}
299296
}
300297
}
301298

302299
/** Resolve credentials for `serverName` and returned the complete connection spec if successful */
303-
export async function resolveUsernameAndPassword(serverName: string, oldSpec: any): Promise<any> {
304-
const newSpec: { name: string; username?: string; password?: string } = {
305-
name: serverName,
306-
username: oldSpec?.username,
307-
};
308-
await resolvePassword(newSpec, true);
309-
if (newSpec.password) {
300+
export async function resolveUsernameAndPassword(
301+
serverName: string,
302+
oldSpec: serverManager.IServerSpec
303+
): Promise<(serverManager.IServerSpec & { accessToken?: string }) | undefined> {
304+
const { authorization: _authorization, ...newSpec } = oldSpec;
305+
newSpec.name = serverName;
306+
const authorization = _authorization.clone();
307+
308+
const accessToken = await resolvePassword({ ...newSpec, authorization }, true);
309+
if (authorization.resolved()) {
310310
// Update the connection spec
311311
resolvedConnSpecs.set(serverName, {
312312
...oldSpec,
313-
username: newSpec.username,
314-
password: newSpec.password,
313+
authorization,
314+
accessToken,
315315
});
316316
return resolvedConnSpecs.get(serverName);
317317
}
@@ -364,7 +364,8 @@ export async function checkConnection(
364364
_onDidChangeConnection.fire();
365365
}
366366
let api = new AtelierAPI(apiTarget, false);
367-
const { active, host = "", port = 0, superserverPort = 0, ns = "", username } = api.config;
367+
const { active, host = "", port = 0, superserverPort = 0, ns = "", authorization } = api.config;
368+
const username = authorization.resolved() ? authorization.username : "";
368369
vscode.commands.executeCommand("setContext", "vscode-objectscript.connectActive", active);
369370
if (!panel.text) {
370371
panel.text = `${PANEL_LABEL}`;
@@ -470,8 +471,7 @@ export async function checkConnection(
470471
let success = false;
471472
message = "Not Authorized.";
472473
errorMessage = `Authorization error: Check your credentials in Settings, and that you have sufficient privileges on the /api/atelier web application on ${connInfo}`;
473-
const username = api.config.username;
474-
if (isUnauthenticated(username)) {
474+
if (isUnauthenticated(api.config.authorization.username)) {
475475
vscode.window.showErrorMessage(
476476
`Unauthenticated access rejected by '${api.serverId}'.${
477477
!api.config.serverName ? " Connection has been disabled." : ""
@@ -487,7 +487,7 @@ export async function checkConnection(
487487
const newSpec = await resolveUsernameAndPassword(api.config.serverName, oldSpec);
488488
if (newSpec) {
489489
// We were able to resolve credentials, so try again
490-
await workspaceState.update(wsKey + ":password", newSpec.password);
490+
await workspaceState.update(wsKey + ":password", newSpec.accessToken);
491491
api = new AtelierAPI(apiTarget, false);
492492
await api
493493
.serverInfo(true, serverInfoTimeout)
@@ -515,7 +515,7 @@ export async function checkConnection(
515515
vscode.window
516516
.showInputBox({
517517
password: true,
518-
title: `Not Authorized. Enter password to connect as user '${username}' to ${connInfo}`,
518+
title: `Not Authorized. Enter password to connect as user '${api.config.authorization.username}' to ${connInfo}`,
519519
prompt: !api.externalServer ? "If no password is entered the connection will be disabled." : "",
520520
ignoreFocusOut: true,
521521
})
@@ -1965,11 +1965,10 @@ export interface GeneralServerForUri {
19651965
namespace: string;
19661966
apiVersion: number;
19671967
serverVersion: string;
1968+
authorization: serverManager.Authorization;
19681969
}
19691970

1970-
export type ServerForUri = GeneralServerForUri &
1971-
HttpsAndScheme &
1972-
Required<Pick<serverManager.Authorization, "authMethod" | "username" | "password">>;
1971+
export type ServerForUri = GeneralServerForUri & HttpsAndScheme;
19731972

19741973
// This function is exported as one of our API functions but is also used internally
19751974
// for example to implement the async variant capable of resolving docker port number.
@@ -1989,13 +1988,28 @@ function serverForUri(uri: vscode.Uri): ServerForUri {
19891988
port,
19901989
superserverPort,
19911990
pathPrefix,
1992-
authMethod,
1993-
username,
1994-
password,
1991+
authorization,
19951992
ns,
19961993
apiVersion,
19971994
serverVersion,
19981995
} = api.config;
1996+
if (serverName !== "") {
1997+
const password = vscode.workspace
1998+
.getConfiguration(
1999+
`intersystems.servers.${serverName.toLowerCase()}`,
2000+
// objectscript(xml):// URIs are not in any workspace folder,
2001+
// so make sure we resolve the server definition with the proper
2002+
// granularity. This is needed to prevent other extensions like
2003+
// Language Server prompting for a passwoord when it's not needed.
2004+
[OBJECTSCRIPT_FILE_SCHEMA, OBJECTSCRIPTXML_FILE_SCHEMA].includes(uri.scheme)
2005+
? vscode.workspace.workspaceFolders?.find((f) => f.name.toLowerCase() == configNameLower)?.uri
2006+
: uri
2007+
)
2008+
.get("password") as string | undefined;
2009+
if (password !== undefined) {
2010+
authorization.resolve(password);
2011+
}
2012+
}
19992013
return {
20002014
serverName,
20012015
active,
@@ -2004,23 +2018,7 @@ function serverForUri(uri: vscode.Uri): ServerForUri {
20042018
port,
20052019
superserverPort,
20062020
pathPrefix,
2007-
authMethod,
2008-
username,
2009-
password:
2010-
serverName === ""
2011-
? password
2012-
: vscode.workspace
2013-
.getConfiguration(
2014-
`intersystems.servers.${serverName.toLowerCase()}`,
2015-
// objectscript(xml):// URIs are not in any workspace folder,
2016-
// so make sure we resolve the server definition with the proper
2017-
// granularity. This is needed to prevent other extensions like
2018-
// Language Server prompting for a passwoord when it's not needed.
2019-
[OBJECTSCRIPT_FILE_SCHEMA, OBJECTSCRIPTXML_FILE_SCHEMA].includes(uri.scheme)
2020-
? vscode.workspace.workspaceFolders?.find((f) => f.name.toLowerCase() == configNameLower)?.uri
2021-
: uri
2022-
)
2023-
.get("password"),
2021+
authorization,
20242022
namespace: ns,
20252023
apiVersion: active ? apiVersion : undefined,
20262024
serverVersion: active ? serverVersion : undefined,

0 commit comments

Comments
 (0)