Skip to content

Commit fe7dd3f

Browse files
committed
Address review comments (zach)
1 parent b2cb10e commit fe7dd3f

File tree

8 files changed

+171
-75
lines changed

8 files changed

+171
-75
lines changed

src/cliConfig.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import * as vscode from "vscode";
2-
3-
import { type FeatureSet } from "./featureSet";
41
import { getHeaderArgs } from "./headers";
52
import { isKeyringSupported } from "./keyringStore";
63
import { escapeCommandArg } from "./util";
74

5+
import type { WorkspaceConfiguration } from "vscode";
6+
7+
import type { FeatureSet } from "./featureSet";
8+
89
export type CliAuth =
910
| { mode: "global-config"; configDir: string }
1011
| { mode: "url"; url: string };
@@ -13,7 +14,7 @@ export type CliAuth =
1314
* Returns the raw global flags from user configuration.
1415
*/
1516
export function getGlobalFlagsRaw(
16-
configs: Pick<vscode.WorkspaceConfiguration, "get">,
17+
configs: Pick<WorkspaceConfiguration, "get">,
1718
): string[] {
1819
return configs.get<string[]>("coder.globalFlags", []);
1920
}
@@ -23,7 +24,7 @@ export function getGlobalFlagsRaw(
2324
* Includes either `--global-config` or `--url` depending on the auth mode.
2425
*/
2526
export function getGlobalFlags(
26-
configs: Pick<vscode.WorkspaceConfiguration, "get">,
27+
configs: Pick<WorkspaceConfiguration, "get">,
2728
auth: CliAuth,
2829
): string[] {
2930
const authFlags =
@@ -39,28 +40,37 @@ export function getGlobalFlags(
3940
];
4041
}
4142

43+
/**
44+
* Returns true when the user has keyring enabled and the platform supports it.
45+
*/
46+
export function isKeyringEnabled(
47+
configs: Pick<WorkspaceConfiguration, "get">,
48+
): boolean {
49+
return isKeyringSupported() && configs.get<boolean>("coder.useKeyring", true);
50+
}
51+
4252
/**
4353
* Single source of truth: should the extension use the OS keyring for this session?
4454
* Requires CLI >= 2.29.0, macOS or Windows, and the coder.useKeyring setting enabled.
4555
*/
46-
export function shouldUseKeyring(featureSet: FeatureSet): boolean {
47-
return (
48-
featureSet.keyringAuth &&
49-
isKeyringSupported() &&
50-
vscode.workspace.getConfiguration().get<boolean>("coder.useKeyring", true)
51-
);
56+
export function shouldUseKeyring(
57+
configs: Pick<WorkspaceConfiguration, "get">,
58+
featureSet: FeatureSet,
59+
): boolean {
60+
return isKeyringEnabled(configs) && featureSet.keyringAuth;
5261
}
5362

5463
/**
5564
* Resolves how the CLI should authenticate: via the keyring (`--url`) or via
5665
* the global config directory (`--global-config`).
5766
*/
5867
export function resolveCliAuth(
68+
configs: Pick<WorkspaceConfiguration, "get">,
5969
featureSet: FeatureSet,
6070
deploymentUrl: string | undefined,
6171
configDir: string,
6272
): CliAuth {
63-
if (shouldUseKeyring(featureSet) && deploymentUrl) {
73+
if (shouldUseKeyring(configs, featureSet)) {
6474
return { mode: "url", url: deploymentUrl };
6575
}
6676
return { mode: "global-config", configDir };
@@ -70,7 +80,7 @@ export function resolveCliAuth(
7080
* Returns SSH flags for the `coder ssh` command from user configuration.
7181
*/
7282
export function getSshFlags(
73-
configs: Pick<vscode.WorkspaceConfiguration, "get">,
83+
configs: Pick<WorkspaceConfiguration, "get">,
7484
): string[] {
7585
// Make sure to match this default with the one in the package.json
7686
return configs.get<string[]>("coder.sshFlags", ["--disable-autostart"]);

src/commands.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -461,11 +461,9 @@ export class Commands {
461461
const version = semver.parse(await cliUtils.version(binary));
462462
const featureSet = featureSetForVersion(version);
463463
const configDir = this.pathResolver.getGlobalConfigDir(safeHost);
464-
const auth = resolveCliAuth(featureSet, baseUrl, configDir);
465-
const globalFlags = getGlobalFlags(
466-
vscode.workspace.getConfiguration(),
467-
auth,
468-
);
464+
const configs = vscode.workspace.getConfiguration();
465+
const auth = resolveCliAuth(configs, featureSet, baseUrl, configDir);
466+
const globalFlags = getGlobalFlags(configs, auth);
469467
terminal.sendText(
470468
`${escapeCommandArg(binary)} ${globalFlags.join(" ")} ssh ${app.workspace_name}`,
471469
);

src/core/cliManager.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import * as semver from "semver";
1010
import * as vscode from "vscode";
1111

1212
import { errToStr } from "../api/api-helper";
13-
import { shouldUseKeyring } from "../cliConfig";
14-
import { isKeyringSupported, type KeyringStore } from "../keyringStore";
13+
import { isKeyringEnabled, shouldUseKeyring } from "../cliConfig";
1514
import * as pgp from "../pgp";
1615
import { vscodeProposed } from "../vscodeProposed";
1716

@@ -23,6 +22,7 @@ import type { Api } from "coder/site/src/api/api";
2322
import type { IncomingMessage } from "node:http";
2423

2524
import type { FeatureSet } from "../featureSet";
25+
import type { KeyringStore } from "../keyringStore";
2626
import type { Logger } from "../logging/logger";
2727

2828
import type { PathResolver } from "./pathResolver";
@@ -717,16 +717,29 @@ export class CliManager {
717717
token: string | null,
718718
featureSet: FeatureSet,
719719
) {
720-
if (shouldUseKeyring(featureSet) && url && token !== null) {
720+
const configs = vscode.workspace.getConfiguration();
721+
if (shouldUseKeyring(configs, featureSet) && url && token !== null) {
721722
try {
722723
this.keyringStore.setToken(url, token);
723724
this.output.info("Stored token in OS keyring for", url);
724725
return;
725726
} catch (error) {
726-
this.output.warn(
727-
"Keyring write failed, falling back to file storage",
728-
error,
729-
);
727+
this.output.error("Failed to store token in OS keyring:", error);
728+
vscode.window
729+
.showErrorMessage(
730+
`Failed to store session token in OS keyring: ${errToStr(error)}. ` +
731+
"Disable keyring storage in settings to use plaintext files instead.",
732+
"Open Settings",
733+
)
734+
.then((action) => {
735+
if (action === "Open Settings") {
736+
vscode.commands.executeCommand(
737+
"workbench.action.openSettings",
738+
"coder.useKeyring",
739+
);
740+
}
741+
});
742+
throw error;
730743
}
731744
}
732745
await Promise.all([
@@ -739,7 +752,7 @@ export class CliManager {
739752
* Remove credentials for a deployment from both keyring and file storage.
740753
*/
741754
public async clearCredentials(safeHostname: string): Promise<void> {
742-
if (isKeyringSupported()) {
755+
if (isKeyringEnabled(vscode.workspace.getConfiguration())) {
743756
try {
744757
this.keyringStore.deleteToken(safeHostname);
745758
this.output.info("Removed keyring token for", safeHostname);

src/keyringStore.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type Logger } from "./logging/logger";
1+
import type { Logger } from "./logging/logger";
22

33
/** A single entry in the CLI's keyring credential map. */
44
interface CredentialEntry {
@@ -90,13 +90,20 @@ export function isKeyringSupported(): boolean {
9090
* Encoding (must match CLI):
9191
* macOS: base64-encoded JSON via setPassword/getPassword
9292
* Windows: raw UTF-8 JSON bytes via setSecret/getSecret
93+
*
94+
* Concurrency: setToken does read-modify-write on a shared entry, so concurrent
95+
* writes can clobber each other. Callers recover by re-writing on reconnection.
9396
*/
9497
export class KeyringStore {
9598
public constructor(
9699
private readonly logger: Logger,
97100
private readonly entryFactory: () => KeyringEntry = createDefaultEntry,
98101
) {}
99102

103+
/**
104+
* Store a token under the host extracted from deploymentUrl (includes port).
105+
* The CLI stores map keys as host+port, so we must write in the same format.
106+
*/
100107
public setToken(deploymentUrl: string, token: string): void {
101108
this.assertSupported();
102109
const entry = this.entryFactory();
@@ -106,6 +113,10 @@ export class KeyringStore {
106113
this.writeMap(entry, map);
107114
}
108115

116+
/**
117+
* Look up a token by safeHostname (hostname without port). VS Code identifies
118+
* deployments by safeHostname, so findMapKey bridges to the CLI's host+port keys.
119+
*/
109120
public getToken(safeHostname: string): string | undefined {
110121
this.assertSupported();
111122
const entry = this.entryFactory();
@@ -114,6 +125,7 @@ export class KeyringStore {
114125
return key !== undefined ? map[key].api_token : undefined;
115126
}
116127

128+
/** Remove a token by safeHostname, matching the same way as getToken. */
117129
public deleteToken(safeHostname: string): void {
118130
this.assertSupported();
119131
const entry = this.entryFactory();

src/login/loginCoordinator.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as vscode from "vscode";
44

55
import { CoderApi } from "../api/coderApi";
66
import { needToken } from "../api/utils";
7+
import { isKeyringEnabled } from "../cliConfig";
78
import { CertificateError } from "../error/certificateError";
89
import { OAuthAuthorizer } from "../oauth/authorizer";
910
import { buildOAuthTokenData } from "../oauth/utils";
@@ -243,12 +244,7 @@ export class LoginCoordinator implements vscode.Disposable {
243244
}
244245

245246
// Try keyring token (picks up tokens written by `coder login` in the terminal)
246-
let keyringToken: string | undefined;
247-
try {
248-
keyringToken = this.keyringStore.getToken(deployment.safeHostname);
249-
} catch (error) {
250-
this.logger.warn("Failed to read token from keyring", error);
251-
}
247+
const keyringToken = this.getKeyringToken(deployment.safeHostname);
252248
if (
253249
keyringToken &&
254250
keyringToken !== providedToken &&
@@ -307,6 +303,18 @@ export class LoginCoordinator implements vscode.Disposable {
307303
}
308304
}
309305

306+
private getKeyringToken(safeHostname: string): string | undefined {
307+
if (!isKeyringEnabled(vscode.workspace.getConfiguration())) {
308+
return undefined;
309+
}
310+
try {
311+
return this.keyringStore.getToken(safeHostname);
312+
} catch (error) {
313+
this.logger.warn("Failed to read token from keyring", error);
314+
return undefined;
315+
}
316+
}
317+
310318
/**
311319
* Shows auth error via dialog or logs it for autoLogin.
312320
*/

src/remote/remote.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -262,15 +262,22 @@ export class Remote {
262262
async (auth) => {
263263
workspaceClient.setCredentials(auth?.url, auth?.token);
264264
if (auth?.url) {
265-
await this.cliManager.configure(
266-
parts.safeHostname,
267-
auth.url,
268-
auth.token,
269-
featureSet,
270-
);
271-
this.logger.info(
272-
"Updated CLI config with new token for remote deployment",
273-
);
265+
try {
266+
await this.cliManager.configure(
267+
parts.safeHostname,
268+
auth.url,
269+
auth.token,
270+
featureSet,
271+
);
272+
this.logger.info(
273+
"Updated CLI config with new token for remote deployment",
274+
);
275+
} catch (error) {
276+
this.logger.error(
277+
"Failed to update CLI config for remote deployment",
278+
error,
279+
);
280+
}
274281
}
275282
},
276283
),
@@ -279,7 +286,12 @@ export class Remote {
279286
const configDir = this.pathResolver.getGlobalConfigDir(
280287
parts.safeHostname,
281288
);
282-
const cliAuth = resolveCliAuth(featureSet, baseUrlRaw, configDir);
289+
const cliAuth = resolveCliAuth(
290+
vscode.workspace.getConfiguration(),
291+
featureSet,
292+
baseUrlRaw,
293+
configDir,
294+
);
283295

284296
// Server versions before v0.14.1 don't support the vscodessh command!
285297
if (!featureSet.vscodessh) {

0 commit comments

Comments
 (0)