mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 21:03:47 +00:00
Fix credential auth optional password (#1009)
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user