feat: move opkssh and warpgate into plugins (C3)

This commit is contained in:
LukeGus committed 2026-09-24 19:28:19 -05:00
1 parent 925da4a661
commit 98d367d7e2
336 files changed
+22380 -4746

No files matched your search

@@ -18,10 +18,9 @@ vi.mock("ssh2", async (importOriginal) => {
const mocks = vi.hoisted(() => ({
verifier: () => {},
createHostVerifier: vi.fn(),
getOPKSSHToken: vi.fn(),
invalidateOPKSSHToken: vi.fn(),
setupOPKSSHCertAuth: vi.fn(),
setupCACertAuth: vi.fn(),
getStepCaCert: vi.fn(),
invalidateStepCaCert: vi.fn(),
applyCertificateAuth: vi.fn(),
getVaultCert: vi.fn(),
deleteVaultCert: vi.fn(),
applyAgentAuth: vi.fn(),
@@ -41,13 +40,12 @@ vi.mock("../../../hosts/host-key-verifier.js", () => ({
createHostVerifier: mocks.createHostVerifier,
},
}));
vi.mock("../../../hosts/opkssh-auth.js", () => ({
getOPKSSHToken: mocks.getOPKSSHToken,
invalidateOPKSSHToken: mocks.invalidateOPKSSHToken,
vi.mock("../../../hosts/step-ca-auth.js", () => ({
getStepCaCert: mocks.getStepCaCert,
invalidateStepCaCert: mocks.invalidateStepCaCert,
}));
vi.mock("../../../hosts/opkssh-cert-auth.js", () => ({
setupOPKSSHCertAuth: mocks.setupOPKSSHCertAuth,
setupCACertAuth: mocks.setupCACertAuth,
vi.mock("@termix/plugin-sdk/ssh-certs", () => ({
applyCertificateAuth: mocks.applyCertificateAuth,
}));
vi.mock("../../../hosts/vault-signer-auth.js", () => ({
getVaultCert: mocks.getVaultCert,
@@ -135,12 +133,9 @@ beforeEach(() => {
config.agent = "agent-object";
return { socketPath: "/tmp/agent.sock" };
});
mocks.setupOPKSSHCertAuth.mockImplementation(async (config) => {
mocks.applyCertificateAuth.mockImplementation(async (config) => {
config.authHandler = "cert-handler";
});
mocks.setupCACertAuth.mockImplementation(async (config) => {
config.authHandler = "ca-handler";
});
});
describe("buildConnectConfig per auth type", () => {
@@ -186,7 +181,7 @@ describe("buildConnectConfig per auth type", () => {
expect(Buffer.isBuffer(config.privateKey)).toBe(true);
expect(config.passphrase).toBe("secret");
expect(config.password).toBe("fallback");
expect(mocks.setupCACertAuth).not.toHaveBeenCalled();
expect(mocks.applyCertificateAuth).not.toHaveBeenCalled();
});
it("key with a CA certificate grafts the certificate", async () => {
@@ -198,12 +193,15 @@ describe("buildConnectConfig per auth type", () => {
certPublicKey: "ssh-ed25519-cert-v01@openssh.com AAAA",
}),
);
expect(mocks.setupCACertAuth).toHaveBeenCalledOnce();
expect(config.authHandler).toBe("ca-handler");
expect(mocks.applyCertificateAuth).toHaveBeenCalledOnce();
expect(mocks.applyCertificateAuth.mock.calls[0][2]).toMatchObject({
certificate: "ssh-ed25519-cert-v01@openssh.com AAAA",
});
expect(config.authHandler).toBe("cert-handler");
});
it("key with a failing CA certificate falls back to key only", async () => {
mocks.setupCACertAuth.mockRejectedValueOnce(new Error("bad cert"));
mocks.applyCertificateAuth.mockRejectedValueOnce(new Error("bad cert"));
const { outcome } = await build(
host({ authType: "key", key: plainKey, certPublicKey: "junk" }),
);
@@ -275,27 +273,28 @@ describe("buildConnectConfig per auth type", () => {
expect(config.tryKeyboard).toBe(true);
});
it("opkssh without a cached token asks for a sign-in", async () => {
mocks.getOPKSSHToken.mockResolvedValueOnce(null);
const { outcome } = await build(host({ authType: "opkssh" }));
// OPKSSH is a plugin-registered SSH auth provider now: see
// plugins/opkssh/tests/backend/provider.test.ts.
it("stepca without a cached certificate asks for a sign-in", async () => {
mocks.getStepCaCert.mockReturnValueOnce(null);
const { outcome } = await build(host({ authType: "stepca" }));
expect(outcome).toMatchObject({
status: "interaction-required",
interaction: "opkssh",
interaction: "stepca",
});
expect(mocks.getOPKSSHToken).toHaveBeenCalledWith("user-1", 7);
expect(mocks.getStepCaCert).toHaveBeenCalledWith("user-1", 7);
});
it("opkssh and stepca use the cached certificate", async () => {
for (const authType of ["opkssh", "stepca"]) {
mocks.getOPKSSHToken.mockResolvedValueOnce({
privateKey: "k",
sshCert: "c",
});
const { config, outcome } = await build(host({ authType }));
expect(outcome.status).toBe("ready");
expect(config.authHandler).toBe("cert-handler");
}
expect(mocks.setupOPKSSHCertAuth).toHaveBeenCalledTimes(2);
it("stepca uses the cached certificate", async () => {
mocks.getStepCaCert.mockReturnValueOnce({
privateKey: "k",
sshCert: "c",
});
const { config, outcome } = await build(host({ authType: "stepca" }));
expect(outcome.status).toBe("ready");
expect(config.authHandler).toBe("cert-handler");
expect(mocks.applyCertificateAuth).toHaveBeenCalledOnce();
});
it("vault needs a profile, then a cached certificate", async () => {
@@ -466,18 +465,18 @@ describe("connectHost", () => {
});
it("throws the auth outcome instead of connecting", async () => {
mocks.getOPKSSHToken.mockResolvedValueOnce(null);
const attempt = connectHost(host({ authType: "opkssh" }), {
mocks.getStepCaCert.mockReturnValue(null);
const attempt = connectHost(host({ authType: "stepca" }), {
userId: "user-1",
purpose: "tmux",
});
await expect(attempt).rejects.toBeInstanceOf(SshConnectError);
await expect(
connectHost(host({ authType: "opkssh" }), {
connectHost(host({ authType: "stepca" }), {
userId: "user-1",
purpose: "tmux",
}).catch((error) => error.code),
).resolves.toBe("opkssh-required");
).resolves.toBe("stepca-required");
});
it("goes through the jump chain and forwards to the target", async () => {
@@ -629,22 +628,18 @@ describe("connectHost", () => {
it("lets the provider react to an auth failure", async () => {
FakeSshClient.nextBehaviour = ["auth-fail"];
mocks.getOPKSSHToken.mockResolvedValueOnce({
mocks.getStepCaCert.mockReturnValueOnce({
privateKey: "k",
sshCert: "c",
});
await expect(
connectHost(host({ authType: "opkssh" }), {
connectHost(host({ authType: "stepca" }), {
userId: "user-1",
purpose: "fleet",
}),
).rejects.toThrow(/All configured authentication methods failed/);
await vi.waitFor(() =>
expect(mocks.invalidateOPKSSHToken).toHaveBeenCalledWith(
"user-1",
7,
"SSH auth failed",
),
expect(mocks.invalidateStepCaCert).toHaveBeenCalledWith("user-1", 7),
);
});
+15 -19
View File
@@ -3,7 +3,6 @@ import type { WebSocket } from "ws";
const state = vi.hoisted(() => ({
sent: [] as Array<Record<string, unknown>>,
upsert: vi.fn(async () => undefined),
parseInfo: {
publicKeyLine: "ssh-ed25519 TESTKEY",
principals: ["alice"],
@@ -19,15 +18,6 @@ const state = vi.hoisted(() => ({
vi.mock("../../utils/logger.js", () => ({
sshLogger: { error: vi.fn(), warn: vi.fn(), info: vi.fn() },
}));
vi.mock("../../utils/data-crypto.js", () => ({
DataCrypto: { getUserDataKey: () => Buffer.alloc(32, 1) },
}));
vi.mock("../../utils/field-crypto.js", () => ({
FieldCrypto: {
encryptField: (value: string, _key: Buffer, _id: string, field: string) =>
`${field}:${value}`,
},
}));
vi.mock("../../database/repositories/factory.js", () => ({
createCurrentSettingsRepository: () => ({
get: async (key: string) =>
@@ -37,7 +27,6 @@ vi.mock("../../database/repositories/factory.js", () => ({
step_ca_provisioner: "oidc",
})[key],
}),
createCurrentOpksshTokenRepository: () => ({ upsert: state.upsert }),
}));
vi.mock("../../utils/step-ca-egress.js", () => ({
readStepCaPrivateAllowlist: async () => [],
@@ -92,8 +81,13 @@ vi.mock("../../hosts/step-ca-runtime.js", () => ({
},
}));
const { cancelStepCaAuth, completeStepCaAuth, startStepCaAuth } =
await import("../../hosts/step-ca-auth.js");
const {
cancelStepCaAuth,
completeStepCaAuth,
getStepCaCert,
invalidateStepCaCert,
startStepCaAuth,
} = await import("../../hosts/step-ca-auth.js");
function jwt(payload: object): string {
return `x.${Buffer.from(JSON.stringify(payload)).toString("base64url")}.y`;
@@ -124,7 +118,7 @@ async function start(): Promise<{ requestId: string; nonce: string }> {
beforeEach(() => {
state.sent.length = 0;
state.upsert.mockClear();
invalidateStepCaCert("user-1", 7);
state.runtimeComplete.mockClear();
state.command = null;
state.remoteResult = null;
@@ -147,9 +141,11 @@ describe("Step CA authentication", () => {
ok: true,
message: "Signed in. You can close this window.",
});
expect(state.upsert).toHaveBeenCalledWith(
expect.objectContaining({ userId: "user-1", hostId: 7 }),
);
expect(getStepCaCert("user-1", 7)).toEqual({
sshCert: "CERT",
privateKey: "PRIVATE",
});
expect(state.sent.at(-1)).toMatchObject({ type: "stepca_completed" });
});
it("rejects a token without the requested nonce", async () => {
@@ -159,7 +155,7 @@ describe("Step CA authentication", () => {
const result = await completeStepCaAuth({ state: requestId, code: "code" });
expect(result.ok).toBe(false);
expect(result.message).toMatch(/does not match/);
expect(state.upsert).not.toHaveBeenCalled();
expect(getStepCaCert("user-1", 7)).toBeNull();
});
it("routes a callback through Redis to the instance holding the WebSocket", async () => {
@@ -170,7 +166,7 @@ describe("Step CA authentication", () => {
await vi.waitFor(() => expect(state.runtimeComplete).toHaveBeenCalled(), {
timeout: 2000,
});
expect(state.upsert).toHaveBeenCalled();
expect(getStepCaCert("user-1", 7)).not.toBeNull();
});
it("returns a result produced by another instance", async () => {