Skip to content

Commit a360878

Browse files
committed
feat: reworked credential management
Store passwords in the system keychain. Signed-off-by: Gordon Smith <GordonJSmith@gmail.com>
1 parent 3e0d035 commit a360878

21 files changed

Lines changed: 999 additions & 89 deletions

.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 & 1 deletion
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

ecl-sample/.vscode/launch.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515
"rejectUnauthorized": true,
1616
"resultLimit": 100,
1717
"timeoutSecs": 60,
18-
"user": "vscode_user",
19-
"password": ""
18+
"user": "gordon"
2019
},
2120
{
2221
"name": "play",
@@ -29,8 +28,7 @@
2928
"rejectUnauthorized": false,
3029
"resultLimit": 100,
3130
"timeoutSecs": 60,
32-
"user": "vscode_user",
33-
"password": ""
31+
"user": "vscode_user"
3432
},
3533
{
3634
"name": "localhost (https)",

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"private": true,
33
"name": "ecl",
4-
"version": "2.32.1",
4+
"version": "2.32.2",
55
"publisher": "hpcc-systems",
66
"displayName": "ECL Language",
77
"description": "ECL (Enterprise Control Language) support for Visual Studio Code",
@@ -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 forget password)%",
651+
"description": "%Logout (and forget password) from HPCC Platform%",
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",
@@ -1481,7 +1517,7 @@
14811517
"ecl.pingInterval": {
14821518
"type": "number",
14831519
"scope": "resource",
1484-
"default": 5,
1520+
"default": 60,
14851521
"description": "%Ping interval (secs, -1 to disable)%"
14861522
},
14871523
"dashy.libraryLocation": {
@@ -1671,11 +1707,6 @@
16711707
"type": "string",
16721708
"description": "%User ID%",
16731709
"default": ""
1674-
},
1675-
"password": {
1676-
"type": "string",
1677-
"description": "%User password%",
1678-
"default": ""
16791710
}
16801711
}
16811712
}
@@ -1694,8 +1725,7 @@
16941725
"rejectUnauthorized": true,
16951726
"resultLimit": 100,
16961727
"timeoutSecs": 60,
1697-
"user": "vscode_user",
1698-
"password": ""
1728+
"user": "vscode_user"
16991729
}
17001730
],
17011731
"configurationSnippets": [
@@ -1715,8 +1745,7 @@
17151745
"rejectUnauthorized": true,
17161746
"resultLimit": 100,
17171747
"timeoutSecs": 60,
1718-
"user": "vscode_user",
1719-
"password": ""
1748+
"user": "vscode_user"
17201749
}
17211750
},
17221751
{
@@ -1735,8 +1764,7 @@
17351764
"rejectUnauthorized": false,
17361765
"resultLimit": 100,
17371766
"timeoutSecs": 60,
1738-
"user": "vscode_user",
1739-
"password": ""
1767+
"user": "vscode_user"
17401768
}
17411769
}
17421770
]

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 forget password)": "Logout (and forget password)",
96+
"Logout (and forget password) from HPCC Platform": "Logout (and forget password) from HPCC Platform",
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/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.clearAllCredentials();
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)