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 { PermissionManager } from "../../utils/permission-manager.js";
|
||||||
import { DataCrypto } from "../../utils/data-crypto.js";
|
import { DataCrypto } from "../../utils/data-crypto.js";
|
||||||
import { parseSSHKey } from "../../utils/ssh-key-utils.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 {
|
import {
|
||||||
isNonEmptyString,
|
isNonEmptyString,
|
||||||
isValidPort,
|
isValidPort,
|
||||||
@@ -447,7 +450,12 @@ router.post(
|
|||||||
sshDataObj.key = key || null;
|
sshDataObj.key = key || null;
|
||||||
sshDataObj.keyPassword = keyPassword || null;
|
sshDataObj.keyPassword = keyPassword || null;
|
||||||
sshDataObj.keyType = 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") {
|
} else if (effectiveAuthType === "agent") {
|
||||||
sshDataObj.password = null;
|
sshDataObj.password = null;
|
||||||
sshDataObj.key = null;
|
sshDataObj.key = null;
|
||||||
@@ -656,7 +664,7 @@ router.post(
|
|||||||
|
|
||||||
const cred = credentials[0];
|
const cred = credentials[0];
|
||||||
|
|
||||||
resolvedPassword = cred.password as string | undefined;
|
resolvedPassword = pickResolvedPassword(password, cred.password);
|
||||||
resolvedKey = cred.privateKey as string | undefined;
|
resolvedKey = cred.privateKey as string | undefined;
|
||||||
resolvedKeyPassword = cred.keyPassword as string | undefined;
|
resolvedKeyPassword = cred.keyPassword as string | undefined;
|
||||||
resolvedKeyType = cred.keyType as string | undefined;
|
resolvedKeyType = cred.keyType as string | undefined;
|
||||||
@@ -1043,7 +1051,12 @@ router.put(
|
|||||||
if (keyType) {
|
if (keyType) {
|
||||||
sshDataObj.keyType = 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") {
|
} else if (effectiveAuthType === "agent") {
|
||||||
sshDataObj.password = null;
|
sshDataObj.password = null;
|
||||||
sshDataObj.key = null;
|
sshDataObj.key = null;
|
||||||
@@ -2489,7 +2502,7 @@ async function resolveHostCredentials(
|
|||||||
const credential = credentials[0];
|
const credential = credentials[0];
|
||||||
const resolvedHost: Record<string, unknown> = {
|
const resolvedHost: Record<string, unknown> = {
|
||||||
...host,
|
...host,
|
||||||
password: credential.password,
|
password: pickResolvedPassword(host.password, credential.password),
|
||||||
key: credential.key,
|
key: credential.key,
|
||||||
keyPassword: credential.keyPassword,
|
keyPassword: credential.keyPassword,
|
||||||
keyType: credential.keyType,
|
keyType: credential.keyType,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
import {
|
import {
|
||||||
pickResolvedUsername,
|
pickResolvedUsername,
|
||||||
|
pickResolvedPassword,
|
||||||
expandOidcUsername,
|
expandOidcUsername,
|
||||||
} from "./credential-username.js";
|
} 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", () => {
|
describe("expandOidcUsername", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
|
|||||||
@@ -21,6 +21,19 @@ export function pickResolvedUsername(
|
|||||||
return cred;
|
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
|
* Expands the `$oidc.preferred_username` placeholder in an SSH username to the
|
||||||
* connecting user's OIDC identifier. Returns the username unchanged if it does
|
* 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 cookieParser from "cookie-parser";
|
||||||
import { Client, type ConnectConfig } from "ssh2";
|
import { Client, type ConnectConfig } from "ssh2";
|
||||||
import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js";
|
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 { getDb } from "../database/db/index.js";
|
||||||
import { hosts, sshCredentials } from "../database/db/schema.js";
|
import { hosts, sshCredentials } from "../database/db/schema.js";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
@@ -961,9 +964,10 @@ async function resolveHostCredentials(
|
|||||||
host.overrideCredentialUsername,
|
host.overrideCredentialUsername,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (credential.password) {
|
baseHost.password = pickResolvedPassword(
|
||||||
baseHost.password = credential.password;
|
host.password,
|
||||||
}
|
credential.password,
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
credential.key ||
|
credential.key ||
|
||||||
(credential as Record<string, unknown>).privateKey
|
(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 { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||||
import { logger } from "../utils/logger.js";
|
import { logger } from "../utils/logger.js";
|
||||||
import {
|
import {
|
||||||
|
pickResolvedPassword,
|
||||||
pickResolvedUsername,
|
pickResolvedUsername,
|
||||||
expandOidcUsername,
|
expandOidcUsername,
|
||||||
} from "./credential-username.js";
|
} from "./credential-username.js";
|
||||||
@@ -190,7 +191,7 @@ export async function resolveHostById(
|
|||||||
|
|
||||||
if (credentials.length > 0) {
|
if (credentials.length > 0) {
|
||||||
const cred = credentials[0] as Record<string, unknown>;
|
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
|
// Prefer the normalised private key; fall back to raw key field
|
||||||
host.key = (cred.privateKey || cred.key) as string | null;
|
host.key = (cred.privateKey || cred.key) as string | null;
|
||||||
host.keyPassword = cred.keyPassword;
|
host.keyPassword = cred.keyPassword;
|
||||||
|
|||||||
@@ -592,6 +592,17 @@ export function HostEditor({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|||||||
@@ -45,19 +45,19 @@ describe("buildHostEditorPayload auth field isolation", () => {
|
|||||||
expect(payload.password).toBe("newpass");
|
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 = {
|
const form = {
|
||||||
...createHostEditorForm(null),
|
...createHostEditorForm(null),
|
||||||
authType: "credential" as const,
|
authType: "credential" as const,
|
||||||
credentialId: "7",
|
credentialId: "7",
|
||||||
password: "leftover",
|
password: "host-specific-password",
|
||||||
key: "leftover-key",
|
key: "leftover-key",
|
||||||
};
|
};
|
||||||
|
|
||||||
const payload = buildHostEditorPayload(form, sshOnly);
|
const payload = buildHostEditorPayload(form, sshOnly);
|
||||||
|
|
||||||
expect(payload.credentialId).toBe(7);
|
expect(payload.credentialId).toBe(7);
|
||||||
expect(payload.password).toBeNull();
|
expect(payload.password).toBe("host-specific-password");
|
||||||
expect(payload.key).toBeNull();
|
expect(payload.key).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -273,7 +273,8 @@ export function buildHostEditorPayload(
|
|||||||
pin: form.pin,
|
pin: form.pin,
|
||||||
authType: form.authType,
|
authType: form.authType,
|
||||||
useWarpgate: form.useWarpgate,
|
useWarpgate: form.useWarpgate,
|
||||||
password: usesPassword || usesKey ? form.password || null : null,
|
password:
|
||||||
|
usesPassword || usesKey || usesCredential ? form.password || null : null,
|
||||||
key: usesKey
|
key: usesKey
|
||||||
? form.key === "existing_key"
|
? form.key === "existing_key"
|
||||||
? undefined
|
? undefined
|
||||||
|
|||||||
Reference in New Issue
Block a user