mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
feat: move step-ca into a plugin (C4)
This commit is contained in:
1 parent
98d367d7e2
commit
b027717c46
128 files changed
+3805
-1781
No files matched your search
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* The OPKSSH redirect URI identity providers were registered with before
|
||||
* The OPKSSH and Step CA redirect URIs identity providers were registered with before
|
||||
* 2.9 must keep reaching the plugin's callback, query intact.
|
||||
*/
|
||||
|
||||
@@ -49,3 +49,16 @@ describe("the old OPKSSH callback", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the old Step CA callback", () => {
|
||||
it("forwards to the plugin's callback with the query", async () => {
|
||||
const response = await fetch(
|
||||
`${base}/host/step-ca-callback?code=abc&state=xyz`,
|
||||
{ redirect: "manual" },
|
||||
);
|
||||
expect(response.status).toBe(307);
|
||||
expect(response.headers.get("location")).toBe(
|
||||
"/plugin-api/step-ca/callback?code=abc&state=xyz",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -18,8 +18,6 @@ vi.mock("ssh2", async (importOriginal) => {
|
||||
const mocks = vi.hoisted(() => ({
|
||||
verifier: () => {},
|
||||
createHostVerifier: vi.fn(),
|
||||
getStepCaCert: vi.fn(),
|
||||
invalidateStepCaCert: vi.fn(),
|
||||
applyCertificateAuth: vi.fn(),
|
||||
getVaultCert: vi.fn(),
|
||||
deleteVaultCert: vi.fn(),
|
||||
@@ -40,10 +38,6 @@ vi.mock("../../../hosts/host-key-verifier.js", () => ({
|
||||
createHostVerifier: mocks.createHostVerifier,
|
||||
},
|
||||
}));
|
||||
vi.mock("../../../hosts/step-ca-auth.js", () => ({
|
||||
getStepCaCert: mocks.getStepCaCert,
|
||||
invalidateStepCaCert: mocks.invalidateStepCaCert,
|
||||
}));
|
||||
vi.mock("@termix/plugin-sdk/ssh-certs", () => ({
|
||||
applyCertificateAuth: mocks.applyCertificateAuth,
|
||||
}));
|
||||
@@ -276,25 +270,20 @@ describe("buildConnectConfig per auth type", () => {
|
||||
// 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: "stepca",
|
||||
});
|
||||
expect(mocks.getStepCaCert).toHaveBeenCalledWith("user-1", 7);
|
||||
});
|
||||
// Step CA is the step-ca plugin's provider: see
|
||||
// plugins/step-ca/tests/backend/step-ca.test.ts.
|
||||
|
||||
it("stepca uses the cached certificate", async () => {
|
||||
mocks.getStepCaCert.mockReturnValueOnce({
|
||||
privateKey: "k",
|
||||
sshCert: "c",
|
||||
it("a stepca host without the step-ca plugin names it", async () => {
|
||||
setSshAuthTypeOwnerSource(() => [
|
||||
{ type: "stepca", pluginId: "step-ca", pluginName: "Step CA" },
|
||||
]);
|
||||
const { outcome } = await build(host({ authType: "stepca" }));
|
||||
expect(outcome).toEqual({
|
||||
status: "error",
|
||||
code: "provider-missing",
|
||||
message: "This host uses stepca, which needs the Step CA plugin",
|
||||
});
|
||||
const { config, outcome } = await build(host({ authType: "stepca" }));
|
||||
expect(outcome.status).toBe("ready");
|
||||
expect(config.authHandler).toBe("cert-handler");
|
||||
expect(mocks.applyCertificateAuth).toHaveBeenCalledOnce();
|
||||
setSshAuthTypeOwnerSource(() => []);
|
||||
});
|
||||
|
||||
it("vault needs a profile, then a cached certificate", async () => {
|
||||
@@ -465,18 +454,20 @@ describe("connectHost", () => {
|
||||
});
|
||||
|
||||
it("throws the auth outcome instead of connecting", async () => {
|
||||
mocks.getStepCaCert.mockReturnValue(null);
|
||||
const attempt = connectHost(host({ authType: "stepca" }), {
|
||||
mocks.getVaultCert.mockResolvedValue(null);
|
||||
const vaultHost = () =>
|
||||
host({ authType: "vault", vaultProfile: { id: 3 } });
|
||||
const attempt = connectHost(vaultHost(), {
|
||||
userId: "user-1",
|
||||
purpose: "tmux",
|
||||
});
|
||||
await expect(attempt).rejects.toBeInstanceOf(SshConnectError);
|
||||
await expect(
|
||||
connectHost(host({ authType: "stepca" }), {
|
||||
connectHost(vaultHost(), {
|
||||
userId: "user-1",
|
||||
purpose: "tmux",
|
||||
}).catch((error) => error.code),
|
||||
).resolves.toBe("stepca-required");
|
||||
).resolves.toBe("vault-required");
|
||||
});
|
||||
|
||||
it("goes through the jump chain and forwards to the target", async () => {
|
||||
@@ -628,18 +619,18 @@ describe("connectHost", () => {
|
||||
|
||||
it("lets the provider react to an auth failure", async () => {
|
||||
FakeSshClient.nextBehaviour = ["auth-fail"];
|
||||
mocks.getStepCaCert.mockReturnValueOnce({
|
||||
mocks.getVaultCert.mockResolvedValueOnce({
|
||||
privateKey: "k",
|
||||
sshCert: "c",
|
||||
});
|
||||
await expect(
|
||||
connectHost(host({ authType: "stepca" }), {
|
||||
connectHost(host({ authType: "vault", vaultProfile: { id: 3 } }), {
|
||||
userId: "user-1",
|
||||
purpose: "fleet",
|
||||
}),
|
||||
).rejects.toThrow(/All configured authentication methods failed/);
|
||||
await vi.waitFor(() =>
|
||||
expect(mocks.invalidateStepCaCert).toHaveBeenCalledWith("user-1", 7),
|
||||
expect(mocks.deleteVaultCert).toHaveBeenCalledWith("user-1", 3),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { WebSocket } from "ws";
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
sent: [] as Array<Record<string, unknown>>,
|
||||
parseInfo: {
|
||||
publicKeyLine: "ssh-ed25519 TESTKEY",
|
||||
principals: ["alice"],
|
||||
validAfter: new Date(Date.now() - 60_000),
|
||||
validBefore: new Date(Date.now() + 60 * 60_000),
|
||||
},
|
||||
idToken: "",
|
||||
command: null as Record<string, string> | null,
|
||||
remoteResult: null as { ok: boolean; message: string } | null,
|
||||
runtimeComplete: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/logger.js", () => ({
|
||||
sshLogger: { error: vi.fn(), warn: vi.fn(), info: vi.fn() },
|
||||
}));
|
||||
vi.mock("../../database/repositories/factory.js", () => ({
|
||||
createCurrentSettingsRepository: () => ({
|
||||
get: async (key: string) =>
|
||||
({
|
||||
step_ca_url: "https://ca.example",
|
||||
step_ca_fingerprint: "a".repeat(64),
|
||||
step_ca_provisioner: "oidc",
|
||||
})[key],
|
||||
}),
|
||||
}));
|
||||
vi.mock("../../utils/step-ca-egress.js", () => ({
|
||||
readStepCaPrivateAllowlist: async () => [],
|
||||
}));
|
||||
vi.mock("../../utils/step-ca-client.js", () => ({
|
||||
fetchRootCertificate: async () => "ROOT",
|
||||
findOidcProvisioner: async () => ({
|
||||
clientID: "client",
|
||||
configurationEndpoint: "https://idp.example/.well-known/openid",
|
||||
}),
|
||||
discoverOidcEndpoints: async () => ({
|
||||
authorizationEndpoint: "https://idp.example/auth",
|
||||
tokenEndpoint: "https://idp.example/token",
|
||||
}),
|
||||
createPkce: () => ({ verifier: "verifier", challenge: "challenge" }),
|
||||
generateSshKeyPair: () => ({
|
||||
publicKeyLine: "ssh-ed25519 TESTKEY",
|
||||
privateKeyPem: "PRIVATE",
|
||||
}),
|
||||
buildAuthorizationUrl: (input: Record<string, string>) => {
|
||||
const url = new URL("https://idp.example/auth");
|
||||
url.searchParams.set("state", input.state);
|
||||
url.searchParams.set("nonce", input.nonce);
|
||||
return url.toString();
|
||||
},
|
||||
exchangeCodeForIdToken: async () => state.idToken,
|
||||
decodeJwtClaims: (token: string) => {
|
||||
const payload = token.split(".")[1];
|
||||
return payload
|
||||
? JSON.parse(Buffer.from(payload, "base64url").toString("utf8"))
|
||||
: {};
|
||||
},
|
||||
signSshCertificate: async () => "CERT",
|
||||
parseSshCertificate: () => state.parseInfo,
|
||||
}));
|
||||
vi.mock("../../hosts/step-ca-runtime.js", () => ({
|
||||
stepCaRuntime: {
|
||||
register: vi.fn(async () => undefined),
|
||||
remove: vi.fn(async () => undefined),
|
||||
submit: vi.fn(async () => true),
|
||||
takeCommand: vi.fn(async () => {
|
||||
const command = state.command;
|
||||
state.command = null;
|
||||
return command;
|
||||
}),
|
||||
complete: state.runtimeComplete,
|
||||
takeResult: vi.fn(async () => {
|
||||
const result = state.remoteResult;
|
||||
state.remoteResult = null;
|
||||
return result;
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
function fakeWs(): WebSocket {
|
||||
return {
|
||||
send: (raw: string) => state.sent.push(JSON.parse(raw)),
|
||||
once: vi.fn(),
|
||||
} as unknown as WebSocket;
|
||||
}
|
||||
|
||||
async function start(): Promise<{ requestId: string; nonce: string }> {
|
||||
await startStepCaAuth(
|
||||
"user-1",
|
||||
7,
|
||||
"alice",
|
||||
fakeWs(),
|
||||
"https://termix.example",
|
||||
);
|
||||
const chooser = state.sent.at(-1)!;
|
||||
const url = new URL(String(chooser.url));
|
||||
return {
|
||||
requestId: String(chooser.requestId),
|
||||
nonce: url.searchParams.get("nonce")!,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
state.sent.length = 0;
|
||||
invalidateStepCaCert("user-1", 7);
|
||||
state.runtimeComplete.mockClear();
|
||||
state.command = null;
|
||||
state.remoteResult = null;
|
||||
state.parseInfo = {
|
||||
publicKeyLine: "ssh-ed25519 TESTKEY",
|
||||
principals: ["alice"],
|
||||
validAfter: new Date(Date.now() - 60_000),
|
||||
validBefore: new Date(Date.now() + 60 * 60_000),
|
||||
};
|
||||
});
|
||||
|
||||
describe("Step CA authentication", () => {
|
||||
it("stores only a certificate bound to the nonce, key, principal and validity window", async () => {
|
||||
const { requestId, nonce } = await start();
|
||||
state.idToken = jwt({ nonce, email: "alice@example.com" });
|
||||
|
||||
await expect(
|
||||
completeStepCaAuth({ state: requestId, code: "code" }),
|
||||
).resolves.toEqual({
|
||||
ok: true,
|
||||
message: "Signed in. You can close this window.",
|
||||
});
|
||||
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 () => {
|
||||
const { requestId } = await start();
|
||||
state.idToken = jwt({ email: "alice@example.com" });
|
||||
|
||||
const result = await completeStepCaAuth({ state: requestId, code: "code" });
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.message).toMatch(/does not match/);
|
||||
expect(getStepCaCert("user-1", 7)).toBeNull();
|
||||
});
|
||||
|
||||
it("routes a callback through Redis to the instance holding the WebSocket", async () => {
|
||||
const { requestId, nonce } = await start();
|
||||
state.idToken = jwt({ nonce });
|
||||
state.command = { state: requestId, code: "remote-code" };
|
||||
|
||||
await vi.waitFor(() => expect(state.runtimeComplete).toHaveBeenCalled(), {
|
||||
timeout: 2000,
|
||||
});
|
||||
expect(getStepCaCert("user-1", 7)).not.toBeNull();
|
||||
});
|
||||
|
||||
it("returns a result produced by another instance", async () => {
|
||||
state.remoteResult = { ok: true, message: "remote success" };
|
||||
await expect(
|
||||
completeStepCaAuth({ state: "remote-state", code: "code" }),
|
||||
).resolves.toEqual({ ok: true, message: "remote success" });
|
||||
});
|
||||
|
||||
it("cancels local sessions", async () => {
|
||||
const { requestId } = await start();
|
||||
expect(cancelStepCaAuth(requestId)).toBe(true);
|
||||
expect(cancelStepCaAuth(requestId)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,7 @@ const state = vi.hoisted(() => ({
|
||||
delivered: [] as Array<{ channelId: number; title: string }>,
|
||||
failChannel: null as number | null,
|
||||
fetchCalls: [] as Array<{ url: string; allowlist: readonly string[] }>,
|
||||
lastTls: null as Record<string, unknown> | null,
|
||||
lastSignal: null as AbortSignal | null,
|
||||
}));
|
||||
|
||||
@@ -61,7 +62,9 @@ vi.mock("../../utils/safe-outbound-fetch.js", () => ({
|
||||
url: string,
|
||||
init: RequestInit,
|
||||
allowlist: readonly string[],
|
||||
tls: Record<string, unknown> = {},
|
||||
) => {
|
||||
state.lastTls = tls;
|
||||
state.lastSignal = init.signal ?? null;
|
||||
state.fetchCalls.push({ url, allowlist });
|
||||
return new Response("ok", { status: 200 });
|
||||
@@ -184,6 +187,18 @@ describe("ctx.fetch", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("passes a pinned CA and the fingerprint bootstrap flag to the guard", async () => {
|
||||
const ctx = contextFor(["network:outbound"]);
|
||||
await ctx.fetch("https://ca.lan/root/abc", {
|
||||
tls: { rejectUnauthorized: false },
|
||||
});
|
||||
expect(state.lastTls).toEqual({ rejectUnauthorized: false });
|
||||
await ctx.fetch("https://ca.lan/provisioners", { tls: { ca: "PEM" } });
|
||||
expect(state.lastTls).toEqual({ ca: "PEM" });
|
||||
await ctx.fetch("https://example.com");
|
||||
expect(state.lastTls).toEqual({});
|
||||
});
|
||||
|
||||
it("aborts the request, and a streamed body, when the caller's signal fires", async () => {
|
||||
const ctx = contextFor(["network:outbound"]);
|
||||
const controller = new AbortController();
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import crypto from "crypto";
|
||||
import {
|
||||
buildAuthorizationUrl,
|
||||
certificateFingerprint,
|
||||
createPkce,
|
||||
decodeJwtClaims,
|
||||
generateSshKeyPair,
|
||||
normalizeCaUrl,
|
||||
normalizeFingerprint,
|
||||
parseSshCertificate,
|
||||
} from "../../utils/step-ca-client.js";
|
||||
import {
|
||||
generateCa,
|
||||
signUserCertificate,
|
||||
} from "../../database/routes/ssh-certificate.js";
|
||||
|
||||
describe("step-ca client helpers", () => {
|
||||
it("normalizes the CA url and fingerprint the way step does", () => {
|
||||
expect(normalizeCaUrl(" https://ca.internal:9000/ ")).toBe(
|
||||
"https://ca.internal:9000",
|
||||
);
|
||||
expect(() => normalizeCaUrl("http://ca.internal")).toThrow(/https/);
|
||||
const fp = "AB:cd".repeat(16).replace(/:/g, "") + "";
|
||||
expect(normalizeFingerprint("AB:cd".repeat(16))).toBe(fp.toLowerCase());
|
||||
expect(() => normalizeFingerprint("abcd")).toThrow(/SHA-256/);
|
||||
});
|
||||
|
||||
it("fingerprints a PEM certificate by the sha256 of its DER", () => {
|
||||
const der = crypto.randomBytes(64);
|
||||
const pem = `-----BEGIN CERTIFICATE-----\n${der.toString("base64")}\n-----END CERTIFICATE-----\n`;
|
||||
expect(certificateFingerprint(pem)).toBe(
|
||||
crypto.createHash("sha256").update(der).digest("hex"),
|
||||
);
|
||||
});
|
||||
|
||||
it("builds a PKCE authorization request", () => {
|
||||
const { verifier, challenge } = createPkce();
|
||||
expect(challenge).toBe(
|
||||
crypto.createHash("sha256").update(verifier).digest("base64url"),
|
||||
);
|
||||
const url = new URL(
|
||||
buildAuthorizationUrl({
|
||||
authorizationEndpoint: "https://idp.example/auth?tenant=x",
|
||||
clientId: "cid",
|
||||
redirectUri: "https://termix.example/callback",
|
||||
state: "s",
|
||||
nonce: "n",
|
||||
codeChallenge: challenge,
|
||||
}),
|
||||
);
|
||||
expect(url.searchParams.get("tenant")).toBe("x");
|
||||
expect(url.searchParams.get("response_type")).toBe("code");
|
||||
expect(url.searchParams.get("code_challenge_method")).toBe("S256");
|
||||
expect(url.searchParams.get("scope")).toContain("openid");
|
||||
});
|
||||
|
||||
it("generates an ed25519 key whose public line a CA can certify, and reads the cert back", () => {
|
||||
const { publicKeyLine, privateKeyPem } = generateSshKeyPair();
|
||||
expect(publicKeyLine).toMatch(/^ssh-ed25519 [A-Za-z0-9+/=]+$/);
|
||||
expect(privateKeyPem).toContain("BEGIN PRIVATE KEY");
|
||||
|
||||
const ca = generateCa();
|
||||
const cert = signUserCertificate({
|
||||
userPublicKeyLine: publicKeyLine,
|
||||
caPrivateKeyPem: ca.privateKeyPem,
|
||||
caPublicKeyLine: ca.publicKeyLine,
|
||||
keyId: "alice@example",
|
||||
principals: ["alice", "ops"],
|
||||
validAfter: 1_700_000_000,
|
||||
validBefore: 1_700_057_600,
|
||||
});
|
||||
expect(cert).not.toBeNull();
|
||||
const info = parseSshCertificate(cert!);
|
||||
expect(info).toMatchObject({
|
||||
keyType: "ssh-ed25519-cert-v01@openssh.com",
|
||||
publicKeyLine,
|
||||
keyId: "alice@example",
|
||||
principals: ["alice", "ops"],
|
||||
});
|
||||
expect(info.validAfter.toISOString()).toBe("2023-11-14T22:13:20.000Z");
|
||||
expect(info.validBefore.getTime() - info.validAfter.getTime()).toBe(
|
||||
16 * 3600 * 1000,
|
||||
);
|
||||
expect(() => parseSshCertificate(publicKeyLine)).toThrow(/certificate/);
|
||||
expect(() =>
|
||||
parseSshCertificate("ssh-ed25519-cert-v01@openssh.com AAAA"),
|
||||
).toThrow(/certificate/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("decodeJwtClaims", () => {
|
||||
it("reads the payload without verifying and tolerates junk", () => {
|
||||
const payload = Buffer.from(
|
||||
JSON.stringify({ email: "a@b.c", nonce: "n1" }),
|
||||
).toString("base64url");
|
||||
expect(decodeJwtClaims(`x.${payload}.y`)).toEqual({
|
||||
email: "a@b.c",
|
||||
nonce: "n1",
|
||||
});
|
||||
expect(decodeJwtClaims("not-a-jwt")).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* The Step CA settings migration: a 2.8 CA configuration and its private
|
||||
* endpoint allowlist come through into the step-ca plugin's admin settings,
|
||||
* the old redirect URI stays in use, and a second run changes nothing.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
legacy: {} as Record<string, string>,
|
||||
pluginRows: [] as Array<{ key: string; value: string | null }>,
|
||||
pluginInstalled: true,
|
||||
}));
|
||||
|
||||
vi.mock("../../database/repositories/factory.js", () => ({
|
||||
createCurrentPluginRepository: () => ({
|
||||
findById: async (id: string) =>
|
||||
state.pluginInstalled && id === "step-ca" ? { id } : null,
|
||||
}),
|
||||
createCurrentSettingsRepository: () => ({
|
||||
get: async (key: string) => state.legacy[key] ?? null,
|
||||
}),
|
||||
createCurrentPluginSettingsRepository: () => ({
|
||||
get: async (
|
||||
_pluginId: string,
|
||||
_scope: string,
|
||||
_scopeId: string | null,
|
||||
key: string,
|
||||
) => state.pluginRows.find((row) => row.key === key) ?? null,
|
||||
set: async (
|
||||
_pluginId: string,
|
||||
_scope: string,
|
||||
_scopeId: string | null,
|
||||
key: string,
|
||||
value: string | null,
|
||||
) => {
|
||||
state.pluginRows = state.pluginRows.filter((row) => row.key !== key);
|
||||
state.pluginRows.push({ key, value });
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/logger.js", () => ({
|
||||
databaseLogger: { info: vi.fn(), warn: vi.fn() },
|
||||
}));
|
||||
|
||||
import { runStepCaSettingsMigration } from "../../utils/crypto-migration/step-ca-settings-migration.js";
|
||||
|
||||
const value = (key: string) => {
|
||||
const row = state.pluginRows.find((candidate) => candidate.key === key);
|
||||
return row?.value == null ? undefined : JSON.parse(row.value);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
state.legacy = {
|
||||
step_ca_url: "https://ca.internal:9000",
|
||||
step_ca_fingerprint: "a".repeat(64),
|
||||
step_ca_provisioner: "oidc",
|
||||
step_ca_private_endpoint_allowlist: JSON.stringify([
|
||||
"ca.internal",
|
||||
"SSO.internal",
|
||||
]),
|
||||
};
|
||||
state.pluginRows = [];
|
||||
state.pluginInstalled = true;
|
||||
});
|
||||
|
||||
describe("runStepCaSettingsMigration", () => {
|
||||
it("moves the 2.8 settings and keeps the old redirect URI", async () => {
|
||||
const result = await runStepCaSettingsMigration();
|
||||
expect(result.moved).toEqual([
|
||||
"step_ca_url",
|
||||
"step_ca_fingerprint",
|
||||
"step_ca_provisioner",
|
||||
"step_ca_private_endpoint_allowlist",
|
||||
]);
|
||||
expect(result.legacyCallback).toBe(true);
|
||||
expect(value("caUrl")).toBe("https://ca.internal:9000");
|
||||
expect(value("fingerprint")).toBe("a".repeat(64));
|
||||
expect(value("provisioner")).toBe("oidc");
|
||||
expect(value("privateEndpoints")).toBe("ca.internal, sso.internal");
|
||||
expect(value("legacyCallback")).toBe(true);
|
||||
});
|
||||
|
||||
it("changes nothing the second time", async () => {
|
||||
await runStepCaSettingsMigration();
|
||||
const before = JSON.stringify(state.pluginRows);
|
||||
const again = await runStepCaSettingsMigration();
|
||||
expect(again).toEqual({ moved: [], legacyCallback: false });
|
||||
expect(JSON.stringify(state.pluginRows)).toBe(before);
|
||||
});
|
||||
|
||||
it("never overwrites a value set in the plugin", async () => {
|
||||
state.pluginRows = [
|
||||
{ key: "caUrl", value: JSON.stringify("https://new.ca") },
|
||||
{ key: "legacyCallback", value: JSON.stringify(false) },
|
||||
];
|
||||
await runStepCaSettingsMigration();
|
||||
expect(value("caUrl")).toBe("https://new.ca");
|
||||
expect(value("legacyCallback")).toBe(false);
|
||||
expect(value("provisioner")).toBe("oidc");
|
||||
});
|
||||
|
||||
it("leaves an install that never used Step CA alone", async () => {
|
||||
state.legacy = {};
|
||||
const result = await runStepCaSettingsMigration();
|
||||
expect(result).toEqual({ moved: [], legacyCallback: false });
|
||||
expect(state.pluginRows).toEqual([]);
|
||||
});
|
||||
|
||||
it("does nothing until the plugin row exists", async () => {
|
||||
state.pluginInstalled = false;
|
||||
await runStepCaSettingsMigration();
|
||||
expect(state.pluginRows).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user