fix(terminal): resolve sudo autofill password through identity references (#1284)
Sudo autofill only read host.password and never resolved a host's reference to a Keychain identity (host.identityId). When the account password lived in a referenced identity, the autofill got nothing — while SSH login worked because it goes through resolveHostAuth, which resolves the identity. Add domain resolveHostAutofillPassword (same resolveHostAuth resolution: identity.password ?? host.password, honoring savePassword and dropping undecryptable placeholders) and use it as the terminal autofill password source. Login and autofill now share one resolution path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ import { buildCacheKey } from '../application/state/sftp/sharedRemoteHostCache';
|
||||
import type { DropEntry } from '../lib/sftpFileUtils';
|
||||
import { Host, KnownHost, TerminalSession } from '../types';
|
||||
import { resolveGroupDefaults, applyGroupDefaults } from '../domain/groupConfig';
|
||||
import { sanitizeCredentialValue } from '../domain/credentials';
|
||||
import { resolveHostAutofillPassword } from '../domain/sshAuth';
|
||||
import { materializeHostProxyProfile } from '../domain/proxyProfiles';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||
import { useI18n } from '../application/i18n/I18nProvider';
|
||||
@@ -66,11 +66,6 @@ import {
|
||||
type TerminalLayerProps,
|
||||
} from './terminalLayer/TerminalLayerSupport';
|
||||
|
||||
const resolveHostSudoAutofillPassword = (host: Host): string | undefined => {
|
||||
if (host.savePassword === false) return undefined;
|
||||
return sanitizeCredentialValue(host.password) || undefined;
|
||||
};
|
||||
|
||||
const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
||||
hosts,
|
||||
groupConfigs,
|
||||
@@ -614,11 +609,14 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
||||
for (const session of sessions) {
|
||||
const rawHost = hostMap.get(session.hostId);
|
||||
if (rawHost) {
|
||||
map.set(session.id, resolveHostSudoAutofillPassword(rawHost));
|
||||
// Resolve through identity references too (host.identityId), not just
|
||||
// host.password, so a password stored in a Keychain identity is filled
|
||||
// (issue #1284) — same resolution SSH login uses.
|
||||
map.set(session.id, resolveHostAutofillPassword({ host: rawHost, keys, identities }));
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [hostMap, sessions]);
|
||||
}, [hostMap, sessions, keys, identities]);
|
||||
|
||||
const handleTerminalFontSizeChange = useCallback((sessionId: string, nextFontSize: number) => {
|
||||
const sessionHost = sessionHostsMapRef.current.get(sessionId);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { resolveBridgeKeyAuth, resolveHostAuth } from "./sshAuth.ts";
|
||||
import type { Host, SSHKey } from "./models.ts";
|
||||
import { resolveBridgeKeyAuth, resolveHostAuth, resolveHostAutofillPassword } from "./sshAuth.ts";
|
||||
import type { Host, Identity, SSHKey } from "./models.ts";
|
||||
|
||||
const referenceKey: SSHKey = {
|
||||
id: "key-1",
|
||||
@@ -97,3 +97,59 @@ test("resolveHostAuth respects password auth over stale key selections", () => {
|
||||
assert.equal(resolved.key, undefined);
|
||||
assert.equal(resolved.keyId, undefined);
|
||||
});
|
||||
|
||||
const autofillBaseHost = {
|
||||
id: "h1",
|
||||
label: "Host",
|
||||
hostname: "h.example.test",
|
||||
username: "alice",
|
||||
} as Host;
|
||||
|
||||
test("resolveHostAutofillPassword uses the host's own saved password", () => {
|
||||
assert.equal(
|
||||
resolveHostAutofillPassword({ host: { ...autofillBaseHost, password: "direct-secret" }, keys: [] }),
|
||||
"direct-secret",
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveHostAutofillPassword resolves a referenced keychain identity's password", () => {
|
||||
// host stores no password of its own; the credential lives in a Keychain
|
||||
// identity it references (host.identityId) — the #1284 scenario.
|
||||
const identity = {
|
||||
id: "id-1",
|
||||
label: "alice@prod",
|
||||
username: "alice",
|
||||
authMethod: "password",
|
||||
password: "identity-secret",
|
||||
created: 1,
|
||||
} as Identity;
|
||||
assert.equal(
|
||||
resolveHostAutofillPassword({
|
||||
host: { ...autofillBaseHost, password: undefined, identityId: "id-1" },
|
||||
keys: [],
|
||||
identities: [identity],
|
||||
}),
|
||||
"identity-secret",
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveHostAutofillPassword returns undefined when the host opts out of saving", () => {
|
||||
assert.equal(
|
||||
resolveHostAutofillPassword({ host: { ...autofillBaseHost, password: "x", savePassword: false }, keys: [] }),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveHostAutofillPassword returns undefined when no password is available", () => {
|
||||
assert.equal(
|
||||
resolveHostAutofillPassword({ host: { ...autofillBaseHost, password: undefined }, keys: [] }),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveHostAutofillPassword ignores undecryptable password placeholders", () => {
|
||||
assert.equal(
|
||||
resolveHostAutofillPassword({ host: { ...autofillBaseHost, password: "enc:v1:djEwAAAA" }, keys: [] }),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -102,6 +102,22 @@ export const resolveHostAuth = (args: {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve the password to use for sudo autofill the same way SSH login does
|
||||
* (through resolveHostAuth), so a password stored in a referenced Keychain
|
||||
* identity (host.identityId) is found — not just host.password (issue #1284).
|
||||
* Returns undefined when the host opts out of saving its password, or none is
|
||||
* available (pure key auth, or an undecryptable placeholder).
|
||||
*/
|
||||
export const resolveHostAutofillPassword = (args: {
|
||||
host: Host;
|
||||
keys: SSHKey[];
|
||||
identities?: Identity[];
|
||||
}): string | undefined => {
|
||||
if (args.host.savePassword === false) return undefined;
|
||||
return sanitizeCredentialValue(resolveHostAuth(args).password) || undefined;
|
||||
};
|
||||
|
||||
export const resolveBridgeKeyAuth = (args: {
|
||||
key?: SSHKey | null;
|
||||
fallbackIdentityFilePaths?: string[];
|
||||
|
||||
Reference in New Issue
Block a user