mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
feat: move opkssh and warpgate into plugins (C3)
This commit is contained in:
1 parent
925da4a661
commit
98d367d7e2
336 files changed
+22380
-4746
No files matched your search
@@ -8,112 +8,86 @@
|
||||
|
||||
import { getErrorMessage } from "../utils/error-message.js";
|
||||
import { sshLogger } from "../utils/logger.js";
|
||||
import {
|
||||
registerKeyboardInteractiveInterceptor,
|
||||
registerSshAuthProvider,
|
||||
} from "../hosts/connect/auth-provider-registry.js";
|
||||
import { applyCertificateAuth } from "@termix/plugin-sdk/ssh-certs";
|
||||
import { registerSshAuthProvider } from "../hosts/connect/auth-provider-registry.js";
|
||||
import type {
|
||||
SshAuthProvider,
|
||||
SshConnectHost,
|
||||
} from "../hosts/connect/types.js";
|
||||
|
||||
const OPKSSH_REQUIRED_MESSAGE =
|
||||
"OPKSSH authentication required. Please open a Terminal connection to this host first to complete browser-based authentication. Your session will be cached for 24 hours.";
|
||||
const STEPCA_REQUIRED_MESSAGE =
|
||||
"Step CA authentication required. Please open a Terminal connection to this host first to complete browser-based authentication.";
|
||||
|
||||
const VAULT_REQUIRED_MESSAGE =
|
||||
"Vault SSH signer authentication required. Please open a Terminal connection first.";
|
||||
|
||||
const AUTH_FAILED_PATTERN = /All configured authentication methods failed/i;
|
||||
|
||||
function issuedCertificateProvider(
|
||||
type: "opkssh" | "stepca",
|
||||
labelKey: string,
|
||||
): SshAuthProvider {
|
||||
return {
|
||||
type,
|
||||
pluginId: "core",
|
||||
labelKey,
|
||||
needsUserInteraction: true,
|
||||
supportsBackground: false,
|
||||
interaction: "opkssh",
|
||||
prepare: async (config, host, env) => {
|
||||
const { getOPKSSHToken } = await import("../hosts/opkssh-auth.js");
|
||||
const token = await getOPKSSHToken(env.userId, env.hostId);
|
||||
if (!token) {
|
||||
env.log("info", "No valid certificate found, requesting sign-in");
|
||||
return {
|
||||
status: "interaction-required",
|
||||
interaction: "opkssh",
|
||||
message: OPKSSH_REQUIRED_MESSAGE,
|
||||
flag: "requiresOPKSSHAuth",
|
||||
};
|
||||
}
|
||||
try {
|
||||
const { setupOPKSSHCertAuth } =
|
||||
await import("../hosts/opkssh-cert-auth.js");
|
||||
await setupOPKSSHCertAuth(config, env.client, token, host.username);
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "error",
|
||||
code: "failed",
|
||||
message: "OPKSSH authentication failed: " + getErrorMessage(error),
|
||||
};
|
||||
}
|
||||
env.log("info", "Using cached SSH certificate");
|
||||
return { status: "ready" };
|
||||
},
|
||||
onAuthFailed: (_host, env, context) => {
|
||||
if (!AUTH_FAILED_PATTERN.test(context.error.message)) return;
|
||||
sshLogger.warn("OPKSSH authentication failed - invalidating token", {
|
||||
operation: "opkssh_auth_failed",
|
||||
hostId: env.hostId,
|
||||
userId: env.userId,
|
||||
});
|
||||
void import("../hosts/opkssh-auth.js")
|
||||
.then(({ invalidateOPKSSHToken }) =>
|
||||
invalidateOPKSSHToken(env.userId, env.hostId, "SSH auth failed"),
|
||||
)
|
||||
.catch(() => {});
|
||||
const stepCaProvider: SshAuthProvider = {
|
||||
type: "stepca",
|
||||
pluginId: "core",
|
||||
labelKey: "hosts.filterAuthStepca",
|
||||
needsUserInteraction: true,
|
||||
supportsBackground: false,
|
||||
interaction: "stepca",
|
||||
prepare: async (config, host, env) => {
|
||||
const { getStepCaCert } = await import("../hosts/step-ca-auth.js");
|
||||
const cert = getStepCaCert(env.userId, env.hostId);
|
||||
if (!cert) {
|
||||
env.log("info", "No valid certificate found, requesting sign-in");
|
||||
return {
|
||||
status: "interaction-required",
|
||||
interaction: "opkssh",
|
||||
message:
|
||||
"OPKSSH authentication failed or expired. Please authenticate again.",
|
||||
interaction: "stepca",
|
||||
message: STEPCA_REQUIRED_MESSAGE,
|
||||
};
|
||||
},
|
||||
startInteraction: async (request) => {
|
||||
if (type === "stepca") {
|
||||
const { startStepCaAuth } = await import("../hosts/step-ca-auth.js");
|
||||
await startStepCaAuth(
|
||||
request.userId,
|
||||
request.hostId,
|
||||
request.host.username,
|
||||
request.socket,
|
||||
request.requestOrigin,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { startOPKSSHAuth } = await import("../hosts/opkssh-auth.js");
|
||||
await startOPKSSHAuth(
|
||||
request.userId,
|
||||
request.hostId,
|
||||
request.host.name || request.host.ip,
|
||||
request.socket,
|
||||
request.requestOrigin,
|
||||
}
|
||||
try {
|
||||
await applyCertificateAuth(
|
||||
config,
|
||||
env.client,
|
||||
{ privateKey: cert.privateKey, certificate: cert.sshCert },
|
||||
host.username,
|
||||
);
|
||||
},
|
||||
cancelInteraction: async (request) => {
|
||||
if (!request.requestId) return;
|
||||
if (type === "stepca") {
|
||||
const { cancelStepCaAuth } = await import("../hosts/step-ca-auth.js");
|
||||
cancelStepCaAuth(request.requestId);
|
||||
return;
|
||||
}
|
||||
const { cancelAuthSession } = await import("../hosts/opkssh-auth.js");
|
||||
cancelAuthSession(request.requestId);
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "error",
|
||||
code: "failed",
|
||||
message: "Step CA authentication failed: " + getErrorMessage(error),
|
||||
};
|
||||
}
|
||||
env.log("info", "Using cached SSH certificate");
|
||||
return { status: "ready" };
|
||||
},
|
||||
onAuthFailed: (_host, env, context) => {
|
||||
if (!AUTH_FAILED_PATTERN.test(context.error.message)) return;
|
||||
void import("../hosts/step-ca-auth.js")
|
||||
.then(({ invalidateStepCaCert }) =>
|
||||
invalidateStepCaCert(env.userId, env.hostId),
|
||||
)
|
||||
.catch(() => {});
|
||||
return {
|
||||
status: "interaction-required",
|
||||
interaction: "stepca",
|
||||
message:
|
||||
"Step CA authentication failed or expired. Please authenticate again.",
|
||||
};
|
||||
},
|
||||
startInteraction: async (request) => {
|
||||
const { startStepCaAuth } = await import("../hosts/step-ca-auth.js");
|
||||
await startStepCaAuth(
|
||||
request.userId,
|
||||
request.hostId,
|
||||
request.host.username,
|
||||
request.socket,
|
||||
request.requestOrigin,
|
||||
);
|
||||
},
|
||||
cancelInteraction: async (request) => {
|
||||
if (!request.requestId) return;
|
||||
const { cancelStepCaAuth } = await import("../hosts/step-ca-auth.js");
|
||||
cancelStepCaAuth(request.requestId);
|
||||
},
|
||||
};
|
||||
|
||||
function vaultProfileId(host: SshConnectHost): number | undefined {
|
||||
return host.vaultProfile?.id ?? undefined;
|
||||
@@ -149,12 +123,10 @@ const vaultProvider: SshAuthProvider = {
|
||||
};
|
||||
}
|
||||
try {
|
||||
const { setupOPKSSHCertAuth } =
|
||||
await import("../hosts/opkssh-cert-auth.js");
|
||||
await setupOPKSSHCertAuth(
|
||||
await applyCertificateAuth(
|
||||
config,
|
||||
env.client,
|
||||
{ privateKey: cert.privateKey, sshCert: cert.sshCert },
|
||||
{ privateKey: cert.privateKey, certificate: cert.sshCert },
|
||||
host.username,
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -214,52 +186,14 @@ const vaultProvider: SshAuthProvider = {
|
||||
},
|
||||
};
|
||||
|
||||
const WARPGATE_PATTERN = /warpgate\s+authentication/i;
|
||||
|
||||
let sshRegistered = false;
|
||||
|
||||
export function registerLegacySshAuthProviders(): void {
|
||||
if (sshRegistered) return;
|
||||
sshRegistered = true;
|
||||
|
||||
registerSshAuthProvider(
|
||||
issuedCertificateProvider("opkssh", "hosts.filterAuthOpkssh"),
|
||||
);
|
||||
registerSshAuthProvider(
|
||||
issuedCertificateProvider("stepca", "hosts.filterAuthStepca"),
|
||||
);
|
||||
registerSshAuthProvider(stepCaProvider);
|
||||
registerSshAuthProvider(vaultProvider);
|
||||
|
||||
// Warpgate is a flag on a host, not an auth type: it shows up as a
|
||||
// keyboard-interactive round with a sign-in URL.
|
||||
registerKeyboardInteractiveInterceptor({
|
||||
id: "warpgate",
|
||||
pluginId: "core",
|
||||
detect: ({ name, instructions, prompts }) => {
|
||||
const texts = prompts.map((p) => p.prompt);
|
||||
const isWarpgate =
|
||||
WARPGATE_PATTERN.test(name) ||
|
||||
WARPGATE_PATTERN.test(instructions) ||
|
||||
texts.some((p) => WARPGATE_PATTERN.test(p));
|
||||
if (!isWarpgate) return null;
|
||||
|
||||
const fullText = `${name}\n${instructions}\n${texts.join("\n")}`;
|
||||
const urlMatch = fullText.match(/https?:\/\/[^\s\n]+/i);
|
||||
if (!urlMatch) return null;
|
||||
const keyMatch = fullText.match(
|
||||
/security key[:\s]+([a-z0-9](?:\s+[a-z0-9]){3}|[a-z0-9]{4})/i,
|
||||
);
|
||||
return {
|
||||
kind: "warpgate",
|
||||
url: urlMatch[0],
|
||||
securityKey: keyMatch ? keyMatch[1] : "N/A",
|
||||
instructions,
|
||||
};
|
||||
},
|
||||
// Warpgate asks for the password before its browser round; answer it
|
||||
// silently so the only thing the user sees is the Warpgate dialog.
|
||||
autoAnswerPasswords: (host) => host.useWarpgate === true,
|
||||
});
|
||||
}
|
||||
|
||||
/** Test helper. */
|
||||
|
||||
Reference in new issue
Block a user