Skip to content

Commit 17db48e

Browse files
authored
feat: reworked credential management (#473)
Store passwords in the system keychain. Signed-off-by: Gordon Smith <GordonJSmith@gmail.com>
1 parent 0c4e0a2 commit 17db48e

23 files changed

+1037
-183
lines changed

.vscode/launch.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
"runtimeExecutable": "${execPath}",
1818
"args": [
1919
"--disable-extensions",
20+
"--enable-extension",
21+
"GordonSmith.observable-js",
2022
"--extensionDevelopmentPath=${workspaceRoot}",
2123
"${workspaceRoot}/ecl-sample"
2224
],

README.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,28 @@ _Version 2.x introduces a new streamlined submission process. The "old" Run/Debu
159159

160160
## ECL
161161

162-
\n+## AI / Chat Integration
162+
## Secure Credential Storage
163+
164+
**Important:** As of version 2.33.0, the extension now uses VS Code's secure storage for HPCC Platform credentials instead of storing passwords in plaintext in your launch configuration files.
165+
166+
### What This Means for You
167+
168+
- **Your passwords are now encrypted** using your operating system's native credential manager (Windows Credential Manager, macOS Keychain, or Linux Secret Service)
169+
- **No more plaintext passwords** in workspace files that could accidentally be committed to version control
170+
- **Automatic migration**: Existing passwords are automatically moved to secure storage on first use
171+
- **Stay logged in** across VS Code sessions without re-entering credentials
172+
173+
### What You Need to Do
174+
175+
**Nothing!** The migration happens automatically. When you connect to an HPCC Platform:
176+
177+
1. If a password exists in your launch configuration, it will be securely stored and removed from the file
178+
2. For new connections, you'll be prompted for credentials which will be securely saved
179+
3. Your credentials persist across VS Code restarts
180+
181+
You can safely remove any `"password"` fields from your `launch.json` files if you prefer.
182+
183+
## AI / Chat Integration
163184

164185
The extension contributes experimental Language Model (AI) tools and a chat participant:
165186

@@ -343,9 +364,6 @@ The following Visual Studio Code settings are available for the ECL extension. T
343364
// Save file prior to submission
344365
"ecl.saveOnSubmit": false
345366

346-
// Ping interval (secs, -1 to disable)
347-
"ecl.pingInterval": 5
348-
349367
// Preferred version of ECL Watch (default v9).
350368
"ecl.preferredECLWatch": v9 | v5
351369
```

ecl-sample/.vscode/launch.json

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,20 @@
1515
"rejectUnauthorized": true,
1616
"resultLimit": 100,
1717
"timeoutSecs": 60,
18-
"user": "vscode_user",
19-
"password": ""
18+
"user": "gordon"
19+
},
20+
{
21+
"name": "localhost2",
22+
"type": "ecl",
23+
"request": "launch",
24+
"protocol": "http",
25+
"serverAddress": "localhost",
26+
"port": 8010,
27+
"targetCluster": "thor",
28+
"rejectUnauthorized": true,
29+
"resultLimit": 100,
30+
"timeoutSecs": 60,
31+
"user": "gordon2"
2032
},
2133
{
2234
"name": "play",
@@ -29,8 +41,7 @@
2941
"rejectUnauthorized": false,
3042
"resultLimit": 100,
3143
"timeoutSecs": 60,
32-
"user": "vscode_user",
33-
"password": ""
44+
"user": "vscode_user"
3445
},
3546
{
3647
"name": "localhost (https)",

package.json

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@
134134
"engines": {
135135
"vscode": "^1.105.0"
136136
},
137+
"releaseNotes": [
138+
{
139+
"previousVersion": "2.32.0",
140+
"message": "Credential storage has been upgraded to use VS Code's secure storage. Your existing credentials have been automatically migrated.",
141+
"learnMoreUrl": "README.md#secure-credential-storage"
142+
}
143+
],
137144
"galleryBanner": {
138145
"color": "#CFB69A",
139146
"theme": "light"
@@ -147,8 +154,7 @@
147154
"workspaceContains:*.ecllib",
148155
"workspaceContains:*.mod",
149156
"workspaceContains:*.eclnb",
150-
"workspaceContains:*.kel",
151-
"workspaceContains:*.dashy"
157+
"workspaceContains:*.kel"
152158
],
153159
"contributes": {
154160
"languageModelTools": [
@@ -428,6 +434,12 @@
428434
"title": "%Copy as ECL ID%",
429435
"description": "%Copy path as Qualified ECL ID%"
430436
},
437+
{
438+
"command": "ecl.clearStoredPasswords",
439+
"category": "ECL",
440+
"title": "%Clear All Passwords%",
441+
"description": "%Clear all stored HPCC Platform passwords from secure storage%"
442+
},
431443
{
432444
"command": "hpccPlatform.copyWUID",
433445
"category": "ECL",
@@ -625,6 +637,20 @@
625637
"dark": "resources/dark/server-process.svg"
626638
}
627639
},
640+
{
641+
"command": "hpccPlatform.login",
642+
"category": "ECL",
643+
"title": "%Login%",
644+
"description": "%Login to HPCC Platform%",
645+
"icon": "$(sign-in)"
646+
},
647+
{
648+
"command": "hpccPlatform.logout",
649+
"category": "ECL",
650+
"title": "%Logout and Clear Credentials%",
651+
"description": "%Logout from HPCC Platform and remove stored credentials%",
652+
"icon": "$(sign-out)"
653+
},
628654
{
629655
"command": "hpccPlatform.switchTargetCluster",
630656
"category": "ECL",
@@ -1046,6 +1072,16 @@
10461072
"when": "view == hpccPlatform",
10471073
"group": "navigation@60"
10481074
},
1075+
{
1076+
"command": "hpccPlatform.login",
1077+
"when": "view == hpccPlatform && !ecl.connected",
1078+
"group": "navigation@65"
1079+
},
1080+
{
1081+
"command": "hpccPlatform.logout",
1082+
"when": "view == hpccPlatform && ecl.connected",
1083+
"group": "navigation@65"
1084+
},
10491085
{
10501086
"command": "hpccResources.bundles.homepage",
10511087
"when": "view == hpccResources.bundles",
@@ -1478,12 +1514,6 @@
14781514
"default": false,
14791515
"description": "%Force global 'proxySupport' to 'fallback'%"
14801516
},
1481-
"ecl.pingInterval": {
1482-
"type": "number",
1483-
"scope": "resource",
1484-
"default": 5,
1485-
"description": "%Ping interval (secs, -1 to disable)%"
1486-
},
14871517
"dashy.libraryLocation": {
14881518
"type": "string",
14891519
"scope": "resource",
@@ -1671,11 +1701,6 @@
16711701
"type": "string",
16721702
"description": "%User ID%",
16731703
"default": ""
1674-
},
1675-
"password": {
1676-
"type": "string",
1677-
"description": "%User password%",
1678-
"default": ""
16791704
}
16801705
}
16811706
}
@@ -1694,8 +1719,7 @@
16941719
"rejectUnauthorized": true,
16951720
"resultLimit": 100,
16961721
"timeoutSecs": 60,
1697-
"user": "vscode_user",
1698-
"password": ""
1722+
"user": "vscode_user"
16991723
}
17001724
],
17011725
"configurationSnippets": [
@@ -1715,8 +1739,7 @@
17151739
"rejectUnauthorized": true,
17161740
"resultLimit": 100,
17171741
"timeoutSecs": 60,
1718-
"user": "vscode_user",
1719-
"password": ""
1742+
"user": "vscode_user"
17201743
}
17211744
},
17221745
{
@@ -1735,8 +1758,7 @@
17351758
"rejectUnauthorized": false,
17361759
"resultLimit": 100,
17371760
"timeoutSecs": 60,
1738-
"user": "vscode_user",
1739-
"password": ""
1761+
"user": "vscode_user"
17401762
}
17411763
}
17421764
]

package.nls.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"Build flags, to be passed to the eclcc compiler": "Build flags, to be passed to the eclcc compiler",
2323
"Bundles": "Bundles",
2424
"Bundles Homepage": "Bundles Homepage",
25+
"Cancel": "Cancel",
2526
"Cannot search: HPCC Platform not connected": "Cannot search: HPCC Platform not connected",
2627
"Check ECL Syntax": "Check ECL Syntax",
2728
"Checking ECL syntax": "Checking ECL syntax",
@@ -30,6 +31,11 @@
3031
"Check syntax with KEL grammar (fast)": "Check syntax with KEL grammar (fast)",
3132
"Check the syntax of ECL code?": "Check the syntax of ECL code?",
3233
"Clear all previously reported ECL Syntax Check results": "Clear all previously reported ECL Syntax Check results",
34+
"Clear All Passwords": "Clear All Passwords",
35+
"Clear all stored HPCC Platform passwords from secure storage": "Clear all stored HPCC Platform passwords from secure storage",
36+
"All stored passwords have been cleared.": "All stored passwords have been cleared.",
37+
"Failed to clear stored passwords: ": "Failed to clear stored passwords: ",
38+
"This will clear all stored HPCC Platform passwords. You will need to re-enter your credentials on the next connection. Are you sure?": "This will clear all stored HPCC Platform passwords. You will need to re-enter your credentials on the next connection. Are you sure?",
3339
"Client Tools": "Client Tools",
3440
"Client Tools Homepage": "Client Tools Homepage",
3541
"Compile": "Compile",
@@ -40,17 +46,20 @@
4046
"Copy as ECL ID": "Copy as ECL ID",
4147
"Copy path as Qualified ECL ID": "Copy path as Qualified ECL ID",
4248
"Copy WUID": "Copy WUID",
49+
"Credential storage has been upgraded to use VS Code's secure storage. Your existing credentials have been automatically migrated.": "Credential storage has been upgraded to use VS Code's secure storage. Your existing credentials have been automatically migrated.",
4350
"Dashy library location (bundled, latest, localPath)": "Dashy library location (bundled, latest, localPath)",
4451
"Dashy Library Path (libraryLocation === \"localPath\")": "Dashy Library Path (libraryLocation === \"localPath\")",
4552
"Database Name": "Database Name",
4653
"Debug level logging (requires restart)": "Debug level logging (requires restart)",
4754
"Default launch configuration": "Default launch configuration",
55+
"Dismiss": "Dismiss",
4856
"Default timeout (secs)": "Default timeout (secs)",
4957
"Delete Workunit": "Delete Workunit",
5058
"Digitally sign ECL file": "Digitally sign ECL file",
5159
"Down": "Down",
5260
"eclcc syntax check arguments": "eclcc syntax check arguments",
5361
"ECL Client Tools Terminal": "ECL Client Tools Terminal",
62+
"ECL Extension Updated": "ECL Extension Updated",
5463
"ECL Notebook": "ECL Notebook",
5564
"ECL syntax is invalid.": "ECL syntax is invalid.",
5665
"ECL syntax is valid.": "ECL syntax is valid.",
@@ -80,6 +89,16 @@
8089
"Language Reference Lookup": "Language Reference Lookup",
8190
"Language Reference Website": "Language Reference Website",
8291
"Launch ECL Watch": "Launch ECL Watch",
92+
"Login": "Login",
93+
"Login to HPCC Platform": "Login to HPCC Platform",
94+
"Login failed": "Login failed",
95+
"Logout and Clear Credentials": "Logout and Clear Credentials",
96+
"Logout from HPCC Platform and remove stored credentials": "Logout from HPCC Platform and remove stored credentials",
97+
"Logout failed": "Logout failed",
98+
"Learn More": "Learn More",
99+
"No HPCC Platform connection available": "No HPCC Platform connection available",
100+
"Successfully logged in to HPCC Platform": "Successfully logged in to HPCC Platform",
101+
"Successfully logged out from HPCC Platform": "Successfully logged out from HPCC Platform",
83102
"List recent HPCC workunits": "List recent HPCC workunits",
84103
"List Workunits Tool": "List Workunits Tool",
85104
"Max result limit for workunit results": "Max result limit for workunit results",
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, it, expect, beforeEach, vi } from "vitest";
2+
import * as vscode from "vscode";
3+
4+
// Mock the localize function
5+
vi.mock("../util/localize", () => ({
6+
default: (key: string) => key
7+
}));
8+
9+
describe("versionNotification", () => {
10+
let mockContext: any;
11+
let mockGlobalState: Map<string, any>;
12+
13+
beforeEach(() => {
14+
mockGlobalState = new Map();
15+
mockContext = {
16+
extension: {
17+
packageJSON: {
18+
version: "2.33.0"
19+
}
20+
},
21+
extensionUri: vscode.Uri.file("/mock/path"),
22+
globalState: {
23+
get: vi.fn((key: string) => mockGlobalState.get(key)),
24+
update: vi.fn((key: string, value: any) => {
25+
mockGlobalState.set(key, value);
26+
return Promise.resolve();
27+
})
28+
}
29+
};
30+
});
31+
32+
it("should store the current version on first activation", async () => {
33+
const { checkForUpgrade } = await import("../util/versionNotification");
34+
35+
await checkForUpgrade(mockContext);
36+
37+
expect(mockContext.globalState.update).toHaveBeenCalledWith(
38+
"ecl.lastVersion",
39+
"2.33.0"
40+
);
41+
});
42+
43+
it("should detect version upgrade", async () => {
44+
mockGlobalState.set("ecl.lastVersion", "2.32.0");
45+
46+
const showInformationMessageSpy = vi.spyOn(vscode.window, "showInformationMessage")
47+
.mockResolvedValue(undefined as any);
48+
49+
const { checkForUpgrade } = await import("../util/versionNotification");
50+
51+
await checkForUpgrade(mockContext);
52+
53+
expect(showInformationMessageSpy).toHaveBeenCalled();
54+
const call = showInformationMessageSpy.mock.calls[0];
55+
expect(call[0]).toContain("2.33.0");
56+
57+
showInformationMessageSpy.mockRestore();
58+
});
59+
60+
it("should not show notification when version hasn't changed", async () => {
61+
mockGlobalState.set("ecl.lastVersion", "2.33.0");
62+
63+
const showInformationMessageSpy = vi.spyOn(vscode.window, "showInformationMessage")
64+
.mockResolvedValue(undefined as any);
65+
66+
const { checkForUpgrade } = await import("../util/versionNotification");
67+
68+
await checkForUpgrade(mockContext);
69+
70+
expect(showInformationMessageSpy).not.toHaveBeenCalled();
71+
72+
showInformationMessageSpy.mockRestore();
73+
});
74+
});

src/debugger/launchRequestArguments.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,6 @@ import { locateAllClientTools as commsLocateAllClientTools } from "@hpcc-js/comm
44
export type LaunchProtocol = "http" | "https";
55
export type LaunchMode = "submit" | "submitNoArchive" | "compile" | "publish" | "debug";
66

7-
export enum LaunchConfigState {
8-
Unknown,
9-
Unreachable,
10-
Credentials,
11-
Ok
12-
}
13-
147
// This interface should always match the schema found in `package.json`.
158
export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
169
// Implicit

src/ecl/command.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import localize from "../util/localize";
1212
import { createDirectory, exists, writeFile } from "../util/fs";
1313
import { ECLR_EN_US, matchTopics, SLR_EN_US } from "./docs";
1414
import { SaveData } from "./saveData";
15+
import { credentialManager } from "../util/credentialManager";
1516

1617
const IMPORT_MARKER = "//Import:";
1718
const SKIP = localize("Skip");
@@ -45,6 +46,7 @@ export class ECLCommands {
4546
ctx.subscriptions.push(vscode.commands.registerCommand("ecl.verify", this.verify, this));
4647
ctx.subscriptions.push(vscode.commands.registerCommand("ecl.importModFile", this.importModFile, this));
4748
ctx.subscriptions.push(vscode.commands.registerCommand("ecl.copyAsEclID", this.copyAsEclID, this));
49+
ctx.subscriptions.push(vscode.commands.registerCommand("ecl.clearStoredPasswords", this.clearStoredPasswords, this));
4850
}
4951

5052
static attach(ctx: vscode.ExtensionContext): ECLCommands {
@@ -331,4 +333,22 @@ export class ECLCommands {
331333
vscode.env.clipboard.writeText(ids.join(os.EOL));
332334
}
333335
}
336+
337+
async clearStoredPasswords() {
338+
const confirm = await vscode.window.showWarningMessage(
339+
localize("This will clear all stored HPCC Platform passwords. You will need to re-enter your credentials on the next connection. Are you sure?"),
340+
{ modal: true },
341+
localize("Clear All Passwords"),
342+
localize("Cancel")
343+
);
344+
345+
if (confirm === localize("Clear All Passwords")) {
346+
try {
347+
await credentialManager.deleteAllCredentials();
348+
vscode.window.showInformationMessage(localize("All stored passwords have been cleared."));
349+
} catch (error: any) {
350+
vscode.window.showErrorMessage(localize("Failed to clear stored passwords: ") + error.message);
351+
}
352+
}
353+
}
334354
}

0 commit comments

Comments
 (0)