Fix credential auth optional password (#1009)

This commit is contained in:
ZacharyZcR
2026-07-10 08:03:14 +08:00
committed by GitHub
parent 37560fa133
commit 6b98b86969
8 changed files with 79 additions and 14 deletions
+18 -5
View File
@@ -25,7 +25,10 @@ import { AuthManager } from "../../utils/auth-manager.js";
import { PermissionManager } from "../../utils/permission-manager.js";
import { DataCrypto } from "../../utils/data-crypto.js";
import { parseSSHKey } from "../../utils/ssh-key-utils.js";
import { pickResolvedUsername } from "../../ssh/credential-username.js";
import {
pickResolvedPassword,
pickResolvedUsername,
} from "../../ssh/credential-username.js";
import {
isNonEmptyString,
isValidPort,
@@ -447,7 +450,12 @@ router.post(
sshDataObj.key = key || null;
sshDataObj.keyPassword = keyPassword || null;
sshDataObj.keyType = keyType;
sshDataObj.password = null;
sshDataObj.password = password || null;
} else if (effectiveAuthType === "credential") {
sshDataObj.password = password || null;
sshDataObj.key = null;
sshDataObj.keyPassword = null;
sshDataObj.keyType = null;
} else if (effectiveAuthType === "agent") {
sshDataObj.password = null;
sshDataObj.key = null;
@@ -656,7 +664,7 @@ router.post(
const cred = credentials[0];
resolvedPassword = cred.password as string | undefined;
resolvedPassword = pickResolvedPassword(password, cred.password);
resolvedKey = cred.privateKey as string | undefined;
resolvedKeyPassword = cred.keyPassword as string | undefined;
resolvedKeyType = cred.keyType as string | undefined;
@@ -1043,7 +1051,12 @@ router.put(
if (keyType) {
sshDataObj.keyType = keyType;
}
sshDataObj.password = null;
sshDataObj.password = password || null;
} else if (effectiveAuthType === "credential") {
sshDataObj.password = password || null;
sshDataObj.key = null;
sshDataObj.keyPassword = null;
sshDataObj.keyType = null;
} else if (effectiveAuthType === "agent") {
sshDataObj.password = null;
sshDataObj.key = null;
@@ -2489,7 +2502,7 @@ async function resolveHostCredentials(
const credential = credentials[0];
const resolvedHost: Record<string, unknown> = {
...host,
password: credential.password,
password: pickResolvedPassword(host.password, credential.password),
key: credential.key,
keyPassword: credential.keyPassword,
keyType: credential.keyType,
@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import {
pickResolvedUsername,
pickResolvedPassword,
expandOidcUsername,
} from "./credential-username.js";
@@ -30,6 +31,27 @@ describe("pickResolvedUsername", () => {
});
});
describe("pickResolvedPassword", () => {
it("keeps the host-specific password ahead of the credential password", () => {
expect(pickResolvedPassword("host-pass", "credential-pass")).toBe(
"host-pass",
);
});
it("falls back to the credential password when the host has none", () => {
expect(pickResolvedPassword("", "credential-pass")).toBe("credential-pass");
expect(pickResolvedPassword(undefined, "credential-pass")).toBe(
"credential-pass",
);
});
it("treats whitespace-only passwords as empty", () => {
expect(pickResolvedPassword(" ", "credential-pass")).toBe(
"credential-pass",
);
});
});
describe("expandOidcUsername", () => {
beforeEach(() => {
vi.resetModules();
+13
View File
@@ -21,6 +21,19 @@ export function pickResolvedUsername(
return cred;
}
/**
* A host can keep a per-host password while using a shared key credential.
* Prefer that host-specific value and fall back to the credential password.
*/
export function pickResolvedPassword(
hostPassword: unknown,
credentialPassword: unknown,
): string | undefined {
if (isNonEmptyString(hostPassword)) return hostPassword;
if (isNonEmptyString(credentialPassword)) return credentialPassword;
return undefined;
}
/**
* Expands the `$oidc.preferred_username` placeholder in an SSH username to the
* connecting user's OIDC identifier. Returns the username unchanged if it does
+8 -4
View File
@@ -4,7 +4,10 @@ import { createCorsMiddleware } from "../utils/cors-config.js";
import cookieParser from "cookie-parser";
import { Client, type ConnectConfig } from "ssh2";
import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js";
import { pickResolvedUsername } from "./credential-username.js";
import {
pickResolvedPassword,
pickResolvedUsername,
} from "./credential-username.js";
import { getDb } from "../database/db/index.js";
import { hosts, sshCredentials } from "../database/db/schema.js";
import { eq } from "drizzle-orm";
@@ -961,9 +964,10 @@ async function resolveHostCredentials(
host.overrideCredentialUsername,
);
if (credential.password) {
baseHost.password = credential.password;
}
baseHost.password = pickResolvedPassword(
host.password,
credential.password,
);
if (
credential.key ||
(credential as Record<string, unknown>).privateKey
+2 -1
View File
@@ -4,6 +4,7 @@ import { eq, and } from "drizzle-orm";
import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { logger } from "../utils/logger.js";
import {
pickResolvedPassword,
pickResolvedUsername,
expandOidcUsername,
} from "./credential-username.js";
@@ -190,7 +191,7 @@ export async function resolveHostById(
if (credentials.length > 0) {
const cred = credentials[0] as Record<string, unknown>;
host.password = cred.password;
host.password = pickResolvedPassword(host.password, cred.password);
// Prefer the normalised private key; fall back to raw key field
host.key = (cred.privateKey || cred.key) as string | null;
host.keyPassword = cred.keyPassword;
+11
View File
@@ -592,6 +592,17 @@ export function HostEditor({
/>
</div>
)}
<div className="flex flex-col gap-1.5">
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
{t("hosts.password")} ({t("common.optional")})
</label>
<PasswordInput
className="h-8 text-xs pr-8"
placeholder="••••••••"
value={form.password}
onChange={(e) => setField("password", e.target.value)}
/>
</div>
</>
)}
</div>
+3 -3
View File
@@ -45,19 +45,19 @@ describe("buildHostEditorPayload auth field isolation", () => {
expect(payload.password).toBe("newpass");
});
it("only sends the credentialId when authType is credential", () => {
it("sends credentialId and optional password when authType is credential", () => {
const form = {
...createHostEditorForm(null),
authType: "credential" as const,
credentialId: "7",
password: "leftover",
password: "host-specific-password",
key: "leftover-key",
};
const payload = buildHostEditorPayload(form, sshOnly);
expect(payload.credentialId).toBe(7);
expect(payload.password).toBeNull();
expect(payload.password).toBe("host-specific-password");
expect(payload.key).toBeNull();
});
+2 -1
View File
@@ -273,7 +273,8 @@ export function buildHostEditorPayload(
pin: form.pin,
authType: form.authType,
useWarpgate: form.useWarpgate,
password: usesPassword || usesKey ? form.password || null : null,
password:
usesPassword || usesKey || usesCredential ? form.password || null : null,
key: usesKey
? form.key === "existing_key"
? undefined