Add server template apply modes, bulk revocation, explicit DN order and detail tabs

This commit is contained in:
2026-10-23 05:33:43 +00:00
parent 5eb7866f5b
commit 763268bd40
13 changed files with 1477 additions and 633 deletions
+23
View File
@@ -49,6 +49,9 @@ test("advanced extensions, validation and vault export", async ({ page }) => {
await page.getByRole("button", { name: "Create certificate" }).click();
await expect(page.getByRole("heading", { name: certName })).toBeVisible();
const certUrl = page.url();
const certApiUrl = certUrl.replace(/\/vaults\//, "/api/vaults/");
await page.locator(".nav-tabs").getByRole("button", { name: "Extensions", exact: true }).click();
await expect(page.getByText("CRL Distribution Points", { exact: false })).toBeVisible();
await expect(page.getByText("1.2.3.4", { exact: false })).toBeVisible();
@@ -59,4 +62,24 @@ test("advanced extensions, validation and vault export", async ({ page }) => {
await page.getByRole("button", { name: "Export ZIP" }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/\.zip$/);
// Import preview selection: a duplicate is deselected by default, but can
// be selected explicitly and then reported as skipped by the commit.
await page.goto(`${vaultUrl}/import`);
await expect(page.getByRole("heading", { name: "Import" })).toBeVisible();
const pem = await page.evaluate(async (url) => {
const response = await fetch(`${url}/export`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ format: "pem" }),
});
const payload = await response.json();
return atob(payload.content_base64);
}, certApiUrl);
await page.locator("textarea[placeholder='-----BEGIN CERTIFICATE-----']").fill(pem);
await page.getByRole("button", { name: "Preview", exact: true }).click();
await expect(page.getByText("duplicate", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "All", exact: true }).click();
await page.getByRole("button", { name: "Import", exact: true }).click();
await expect(page.getByText(/skipped 1/)).toBeVisible();
});
+115
View File
@@ -0,0 +1,115 @@
// Bulk revocation through the certificate list.
const { test, expect } = require("@playwright/test");
function unique(prefix) {
return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
}
async function login(page, username) {
await page.goto("/login");
await page.getByRole("link", { name: "Sign in with default" }).click();
await page.getByPlaceholder("Enter any user/subject").fill(username);
await page.getByRole("button", { name: "Sign-in" }).click();
await expect(page.getByRole("heading", { name: "Vaults" })).toBeVisible();
}
async function api(page, method, url, body) {
return page.evaluate(
async ({ method, url, body }) => {
const response = await fetch(url, {
method,
headers: { "content-type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await response.text();
return { status: response.status, json: text ? JSON.parse(text) : null };
},
{ method, url, body }
);
}
test("bulk revocation from the certificate list", async ({ page }) => {
const user = unique("bulk");
const vaultName = unique("Bulk Vault");
await login(page, user);
const vault = await api(page, "POST", "/api/vaults", { name: vaultName });
expect(vault.status).toBe(201);
const vaultId = vault.json.id;
const caKey = await api(page, "POST", `/api/vaults/${vaultId}/keys`, {
name: "Bulk CA key",
key_type: "rsa",
size: 2048,
});
expect(caKey.status).toBe(201);
const now = new Date();
const later = new Date(now.getTime() + 365 * 24 * 3600 * 1000);
const ca = await api(page, "POST", `/api/vaults/${vaultId}/certificates`, {
name: "Bulk CA",
key_id: caKey.json.id,
subject: [
{ name: "C", value: "DE" },
{ name: "O", value: "KCA E2E" },
{ name: "CN", value: "Bulk CA" },
],
not_before: now.toISOString(),
not_after: later.toISOString(),
extensions: {
basic_constraints: { critical: true, ca: true, path_length: null },
key_usage: { critical: true, usages: ["key_cert_sign", "crl_sign"] },
subject_key_identifier: { critical: false },
authority_key_identifier: { critical: false },
},
});
expect(ca.status).toBe(201);
const caId = ca.json.id;
async function leaf(name) {
const key = await api(page, "POST", `/api/vaults/${vaultId}/keys`, {
name: `${name} key`,
key_type: "rsa",
size: 2048,
});
expect(key.status).toBe(201);
const cert = await api(page, "POST", `/api/vaults/${vaultId}/certificates`, {
name,
key_id: key.json.id,
signer_id: caId,
subject: [
{ name: "C", value: "DE" },
{ name: "O", value: "KCA E2E" },
{ name: "CN", value: name },
],
not_before: now.toISOString(),
not_after: later.toISOString(),
extensions: {
basic_constraints: { critical: true, ca: false, path_length: null },
key_usage: { critical: true, usages: ["digital_signature"] },
subject_alt_name: { critical: false, entries: [{ type: "dns", value: name }] },
subject_key_identifier: { critical: false },
authority_key_identifier: { critical: false },
},
});
expect(cert.status).toBe(201);
return cert.json;
}
await leaf("bulk-one.example");
await leaf("bulk-two.example");
await page.goto(`/vaults/${vaultId}/certificates`);
await expect(page.getByRole("heading", { name: "Certificates" })).toBeVisible();
await page.evaluate(() => {
window.confirm = () => true;
});
await page.getByRole("button", { name: "Select all revocable" }).click();
await expect(page.getByText("2 selected")).toBeVisible();
await page.locator("select").first().selectOption("keyCompromise");
await page.getByRole("button", { name: "Revoke selected" }).click();
await expect(page.getByText(/Revoked 2 certificate\(s\); 0 already revoked; 0 error\(s\)/)).toBeVisible();
const revocations = await api(page, "GET", `/api/vaults/${vaultId}/certificates`);
const revoked = revocations.json.results.filter((cert) => cert.revoked);
expect(revoked).toHaveLength(2);
});
+10 -1
View File
@@ -54,6 +54,10 @@ test("core PKI workflow", async ({ page }) => {
await page.getByRole("button", { name: "Create certificate" }).click();
await expect(page.getByText("CA certificate", { exact: false })).toBeVisible();
const caUrl = page.url();
await page.locator(".nav-tabs").getByRole("button", { name: "CA", exact: true }).click();
await (await field(page, "CA · default signing template")).selectOption("builtin:tls-server");
await page.getByRole("button", { name: "Save CA" }).click();
await expect(page.getByText("CA properties saved.")).toBeVisible();
// Leaf signed by the CA
await page.goto(`${vaultUrl}/certificates/new`);
@@ -63,10 +67,13 @@ test("core PKI workflow", async ({ page }) => {
await (await field(page, "Organization (O)")).fill("KCA E2E");
await (await field(page, "Country (C)")).fill("DE");
await page.locator("#new-key").check();
await expect(page.getByText("CA default template: TLS server")).toBeVisible();
await page.getByRole("button", { name: "Apply CA default" }).click();
await expect(page.locator("label").filter({ hasText: "TLS server" }).locator("..").locator("input[type=checkbox]")).toBeChecked();
await page.locator("input[placeholder*='DNS:example.com']").fill(`DNS:${leafName}`);
await page.locator("label").filter({ hasText: "TLS server" }).locator("..").locator("input[type=checkbox]").check();
await page.getByRole("button", { name: "Create certificate" }).click();
await expect(page.getByRole("heading", { name: leafName })).toBeVisible();
await page.locator(".nav-tabs").getByRole("button", { name: "Extensions", exact: true }).click();
await expect(page.getByText("serverAuth", { exact: false })).toBeVisible();
// Search
@@ -77,12 +84,14 @@ test("core PKI workflow", async ({ page }) => {
// Revoke
await page.getByRole("link", { name: leafName, exact: true }).click();
await page.locator(".nav-tabs").getByRole("button", { name: "Revoke", exact: true }).click();
await page.getByRole("button", { name: "Revoke certificate" }).click();
await expect(page.getByText("revoked", { exact: true })).toBeVisible();
// CRL from the CA
await page.goto(caUrl);
await expect(page.getByRole("heading", { name: caName })).toBeVisible();
await page.locator(".nav-tabs").getByRole("button", { name: "CA", exact: true }).click();
await page.getByRole("button", { name: "Generate CRL" }).click();
await expect(page).toHaveURL(/\/crls\//);
await expect(page.getByText(/CRL number \d+ · 1 revoked/)).toBeVisible();
+85
View File
@@ -0,0 +1,85 @@
// Vault settings: explicit DN order, mandatory DN and template application.
const { test, expect } = require("@playwright/test");
function unique(prefix) {
return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
}
async function field(page, label) {
return page.locator("label").filter({ hasText: label }).first().locator("..").locator("input, select, textarea").first();
}
async function login(page, username) {
await page.goto("/login");
await page.getByRole("link", { name: "Sign in with default" }).click();
await page.getByPlaceholder("Enter any user/subject").fill(username);
await page.getByRole("button", { name: "Sign-in" }).click();
await expect(page.getByRole("heading", { name: "Vaults" })).toBeVisible();
}
async function api(page, method, url, body) {
return page.evaluate(
async ({ method, url, body }) => {
const response = await fetch(url, {
method,
headers: { "content-type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await response.text();
return { status: response.status, json: text ? JSON.parse(text) : null };
},
{ method, url, body }
);
}
test("explicit DN order, mandatory DN and template apply", async ({ page }) => {
const user = unique("settings");
const vaultName = unique("Settings Vault");
const certName = unique("ordered.example");
await login(page, user);
await page.getByRole("button", { name: "New vault" }).click();
await page.getByPlaceholder("Production PKI").fill(vaultName);
await page.getByRole("button", { name: "Create vault" }).click();
await page.getByRole("link", { name: vaultName }).click();
const vaultUrl = page.url();
const vaultId = vaultUrl.split("/").pop();
const settings = await api(page, "GET", `/api/vaults/${vaultId}/settings`);
const updated = await api(page, "PATCH", `/api/vaults/${vaultId}/settings`, {
version: settings.json.version,
settings: { explicit_dn: ["CN", "O"], mandatory_dn: ["CN"] },
});
expect(updated.status).toBe(200);
await page.goto(`${vaultUrl}/certificates/new`);
await expect(page.getByText("Additional subject attributes")).toBeVisible();
const labels = await page.locator("label").allTextContents();
const cnIndex = labels.findIndex((label) => label.trim() === "Common name (CN)");
const oIndex = labels.findIndex((label) => label.trim() === "Organization (O)");
expect(cnIndex).toBeGreaterThan(-1);
expect(oIndex).toBeGreaterThan(cnIndex);
await expect(page.locator("label").filter({ hasText: "Country (C)" })).toHaveCount(0);
// Applying a template uses the server merge endpoint and updates typed fields.
await page.locator("select").nth(2).selectOption("builtin:tls-server");
await page.locator("select").nth(3).selectOption("extensions");
await page.getByRole("button", { name: "Apply", exact: true }).click();
await expect(page.locator("label").filter({ hasText: "TLS server" }).locator("..").locator("input[type=checkbox]")).toBeChecked();
await (await field(page, "Internal name")).fill(certName);
await (await field(page, "Organization (O)")).fill("KCA E2E");
await page.locator("#new-key").check();
await page.getByRole("button", { name: "Create certificate" }).click();
await expect(page.getByText("missing mandatory subject entries: CN")).toBeVisible();
await (await field(page, "Common name (CN)")).fill(certName);
await page.locator("label").filter({ hasText: "No well-defined expiration" }).locator("..").locator("input[type=checkbox]").check();
await page.getByRole("button", { name: "Create certificate" }).click();
await expect(page.getByRole("heading", { name: certName })).toBeVisible();
await expect(page.getByText("No expiration", { exact: true })).toBeVisible();
const cert = await api(page, "GET", `/api/vaults/${vaultId}/certificates`);
const created = cert.json.results.find((item) => item.name === certName);
// RFC 4514 renders the most specific RDN first, so CN follows O.
expect(created.subject).toBe(`O=KCA E2E,CN=${certName}`);
});