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:
bincxz
2026-06-07 13:21:54 +08:00
parent 80d9b33c59
commit 4b07b4826a
3 changed files with 80 additions and 10 deletions

View File

@@ -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);

View File

@@ -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,
);
});

View File

@@ -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[];