Add server template apply modes, bulk revocation, explicit DN order and detail tabs
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user