mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 21:03:47 +00:00
281 lines
8.5 KiB
TypeScript
281 lines
8.5 KiB
TypeScript
import { getDb } from "../database/db/index.js";
|
|
import { hosts, sshCredentials, vaultProfiles } from "../database/db/schema.js";
|
|
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";
|
|
import type { SSHHost } from "../../types/index.js";
|
|
|
|
const sshLogger = logger;
|
|
|
|
/**
|
|
* Resolve a host with its credentials server-side by hostId.
|
|
* This avoids passing credentials through the frontend.
|
|
*/
|
|
export async function resolveHostById(
|
|
hostId: number,
|
|
userId: string,
|
|
): Promise<SSHHost | null> {
|
|
const { PermissionManager } = await import("../utils/permission-manager.js");
|
|
const access = await PermissionManager.getInstance().canAccessHost(
|
|
userId,
|
|
hostId,
|
|
"read",
|
|
);
|
|
if (!access.hasAccess) return null;
|
|
|
|
const db = getDb();
|
|
|
|
const hostResults = await SimpleDBOps.select(
|
|
db.select().from(hosts).where(eq(hosts.id, hostId)),
|
|
"ssh_data",
|
|
userId,
|
|
);
|
|
|
|
if (hostResults.length === 0) return null;
|
|
|
|
const host = hostResults[0] as Record<string, unknown>;
|
|
|
|
// Parse JSON fields
|
|
if (typeof host.jumpHosts === "string" && host.jumpHosts) {
|
|
try {
|
|
host.jumpHosts = JSON.parse(host.jumpHosts as string);
|
|
} catch {
|
|
host.jumpHosts = [];
|
|
}
|
|
}
|
|
if (typeof host.tunnelConnections === "string") {
|
|
try {
|
|
host.tunnelConnections = JSON.parse(host.tunnelConnections as string);
|
|
} catch {
|
|
host.tunnelConnections = [];
|
|
}
|
|
}
|
|
if (typeof host.statsConfig === "string" && host.statsConfig) {
|
|
try {
|
|
host.statsConfig = JSON.parse(host.statsConfig as string);
|
|
} catch {
|
|
host.statsConfig = undefined;
|
|
}
|
|
}
|
|
if (typeof host.terminalConfig === "string" && host.terminalConfig) {
|
|
try {
|
|
host.terminalConfig = JSON.parse(host.terminalConfig as string);
|
|
} catch {
|
|
host.terminalConfig = undefined;
|
|
}
|
|
}
|
|
if (typeof host.socks5ProxyChain === "string" && host.socks5ProxyChain) {
|
|
try {
|
|
host.socks5ProxyChain = JSON.parse(host.socks5ProxyChain as string);
|
|
} catch {
|
|
host.socks5ProxyChain = [];
|
|
}
|
|
}
|
|
if (typeof host.quickActions === "string" && host.quickActions) {
|
|
try {
|
|
host.quickActions = JSON.parse(host.quickActions as string);
|
|
} catch {
|
|
host.quickActions = [];
|
|
}
|
|
}
|
|
|
|
// Resolve credential if using credential-based auth
|
|
if (host.credentialId) {
|
|
const ownerId = (host.userId || userId) as string;
|
|
try {
|
|
// Try user's own override credential first
|
|
if (userId !== ownerId) {
|
|
try {
|
|
const { hostAccess } = await import("../database/db/schema.js");
|
|
const accessRecords = await db
|
|
.select()
|
|
.from(hostAccess)
|
|
.where(
|
|
and(eq(hostAccess.hostId, hostId), eq(hostAccess.userId, userId)),
|
|
)
|
|
.limit(1);
|
|
const overrideCredId = accessRecords[0]?.overrideCredentialId as
|
|
| number
|
|
| null;
|
|
if (overrideCredId) {
|
|
const userCreds = await SimpleDBOps.select(
|
|
db
|
|
.select()
|
|
.from(sshCredentials)
|
|
.where(
|
|
and(
|
|
eq(sshCredentials.id, overrideCredId),
|
|
eq(sshCredentials.userId, userId),
|
|
),
|
|
),
|
|
"ssh_credentials",
|
|
userId,
|
|
);
|
|
if (userCreds.length > 0) {
|
|
const cred = userCreds[0] as Record<string, unknown>;
|
|
host.password = cred.password;
|
|
host.key = cred.key;
|
|
host.keyPassword = cred.keyPassword;
|
|
host.keyType = cred.keyType;
|
|
host.username = pickResolvedUsername(
|
|
host.username,
|
|
cred.username,
|
|
host.overrideCredentialUsername,
|
|
);
|
|
host.authType = cred.key
|
|
? "key"
|
|
: cred.password
|
|
? "password"
|
|
: "none";
|
|
host.username = await expandOidcUsername(
|
|
host.username as string | undefined,
|
|
userId,
|
|
);
|
|
return host as unknown as SSHHost;
|
|
}
|
|
}
|
|
} catch {
|
|
// fall through to shared credential
|
|
}
|
|
|
|
try {
|
|
const { SharedCredentialManager } =
|
|
await import("../utils/shared-credential-manager.js");
|
|
const sharedCredManager = SharedCredentialManager.getInstance();
|
|
const sharedCred = await sharedCredManager.getSharedCredentialForUser(
|
|
hostId,
|
|
userId,
|
|
);
|
|
if (sharedCred) {
|
|
host.password = sharedCred.password;
|
|
host.key = sharedCred.key;
|
|
host.keyPassword = sharedCred.keyPassword;
|
|
host.keyType = sharedCred.keyType;
|
|
host.username = pickResolvedUsername(
|
|
host.username,
|
|
sharedCred.username,
|
|
host.overrideCredentialUsername,
|
|
);
|
|
host.authType = sharedCred.key
|
|
? "key"
|
|
: sharedCred.password
|
|
? "password"
|
|
: "none";
|
|
host.username = await expandOidcUsername(
|
|
host.username as string | undefined,
|
|
userId,
|
|
);
|
|
return host as unknown as SSHHost;
|
|
}
|
|
} catch (e) {
|
|
sshLogger.warn("Failed to get shared credential", {
|
|
operation: "host_resolver_shared_credential",
|
|
hostId,
|
|
error: e instanceof Error ? e.message : "Unknown",
|
|
});
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
const credentials = await SimpleDBOps.select(
|
|
db
|
|
.select()
|
|
.from(sshCredentials)
|
|
.where(
|
|
and(
|
|
eq(sshCredentials.id, host.credentialId as number),
|
|
eq(sshCredentials.userId, ownerId),
|
|
),
|
|
),
|
|
"ssh_credentials",
|
|
ownerId,
|
|
);
|
|
|
|
if (credentials.length > 0) {
|
|
const cred = credentials[0] as Record<string, unknown>;
|
|
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;
|
|
host.keyType = cred.keyType;
|
|
// CA-signed certificate for cert-based auth
|
|
(host as Record<string, unknown>).certPublicKey =
|
|
cred.certPublicKey || null;
|
|
host.username = pickResolvedUsername(
|
|
host.username,
|
|
cred.username,
|
|
host.overrideCredentialUsername,
|
|
);
|
|
host.authType = host.key ? "key" : host.password ? "password" : "none";
|
|
}
|
|
} catch (e) {
|
|
sshLogger.warn("Failed to resolve credential for host", {
|
|
operation: "host_resolver_credential",
|
|
hostId,
|
|
error: e instanceof Error ? e.message : "Unknown",
|
|
});
|
|
}
|
|
}
|
|
|
|
host.username = await expandOidcUsername(
|
|
host.username as string | undefined,
|
|
userId,
|
|
);
|
|
|
|
// Resolve a Vault SSH signer profile (shared settings, no secrets). The
|
|
// certificate itself is obtained per-user at connect time via Vault OIDC.
|
|
if (host.vaultProfileId) {
|
|
try {
|
|
const profiles = await db
|
|
.select()
|
|
.from(vaultProfiles)
|
|
.where(eq(vaultProfiles.id, host.vaultProfileId as number))
|
|
.limit(1);
|
|
if (profiles.length > 0) {
|
|
(host as Record<string, unknown>).vaultProfile = profiles[0];
|
|
host.authType = "vault";
|
|
}
|
|
} catch (e) {
|
|
sshLogger.warn("Failed to resolve vault profile for host", {
|
|
operation: "host_resolver_vault_profile",
|
|
hostId,
|
|
error: e instanceof Error ? e.message : "Unknown",
|
|
});
|
|
}
|
|
}
|
|
|
|
return host as unknown as SSHHost;
|
|
}
|
|
|
|
/**
|
|
* Check if a user has access to a host (owner or shared access).
|
|
*/
|
|
export async function checkHostAccess(
|
|
hostId: number,
|
|
userId: string,
|
|
hostUserId: string,
|
|
requiredPermission: "read" | "execute" = "execute",
|
|
): Promise<boolean> {
|
|
if (userId === hostUserId) return true;
|
|
|
|
try {
|
|
const { PermissionManager } =
|
|
await import("../utils/permission-manager.js");
|
|
const permissionManager = PermissionManager.getInstance();
|
|
const accessInfo = await permissionManager.canAccessHost(
|
|
userId,
|
|
hostId,
|
|
requiredPermission,
|
|
);
|
|
return accessInfo.hasAccess;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|