// XCA-style editors for advanced extension list fields. 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 openEditor(page, buttonId, title) { await page.locator(buttonId).click(); const dialog = page.getByRole("dialog", { name: title, exact: true }); await expect(dialog).toBeVisible(); return dialog; } async function addRow(dialog, type, value) { const index = await dialog.locator(".list-entry").count(); await dialog.getByRole("button", { name: "Add", exact: true }).click(); const row = dialog.locator(".list-entry").nth(index); await row.locator("select").selectOption(type); await row.locator(".list-value").fill(value); return row; } test("advanced extension list editors", async ({ page }) => { const user = unique("list-editor"); const vaultName = unique("List Editors"); const certName = unique("constraints.example"); const crlUrls = ["https://ca.example.com/root.crl", "https://backup.example.com/root.crl"]; const aiaEntries = [ ["OCSP;URI", "https://ocsp.example.com"], ["caIssuers;URI", "https://ca.example.com/issuer.crt"], ]; const aiaText = aiaEntries.map(([type, value]) => `${type}:${value}`).join(", "); const cpsUrls = ["https://example.com/cps", "https://example.com/policy"]; 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 settingsResponse = page.waitForResponse( (response) => response.url().includes("/api/vaults/") && response.url().endsWith("/settings") && response.ok() ); await page.goto(`${vaultUrl}/certificates/new`); await settingsResponse; await (await field(page, "Internal name")).fill(certName); await (await field(page, "Common name (CN)")).fill(certName); await (await field(page, "Basic constraints")).selectOption("ca"); await page.locator("#new-key").check(); // URI-list edits can be cancelled without changing the original textarea. let dialog = await openEditor(page, "#edit-crl-urls", "CRL distribution point URLs"); await expect(dialog.getByLabel("Critical", { exact: true })).not.toBeChecked(); await addRow(dialog, "URI", "https://discarded.example.com/root.crl"); await dialog.getByRole("button", { name: "Cancel", exact: true }).click(); await expect(dialog).toHaveCount(0); await expect(page.locator("#crl-urls")).toHaveValue(""); dialog = await openEditor(page, "#edit-crl-urls", "CRL distribution point URLs"); await expect(dialog.locator(".list-entry")).toHaveCount(0); await addRow(dialog, "URI", crlUrls[0]); await addRow(dialog, "URI", crlUrls[1]); await dialog.getByLabel("Critical", { exact: true }).check(); await dialog.getByRole("button", { name: "Apply", exact: true }).click(); await expect(page.locator("#crl-urls")).toHaveValue(crlUrls.join("\n")); dialog = await openEditor(page, "#edit-crl-urls", "CRL distribution point URLs"); await expect(dialog.locator(".list-entry")).toHaveCount(2); await expect(dialog.locator(".list-entry").nth(1).locator(".list-value")).toHaveValue(crlUrls[1]); await expect(dialog.getByLabel("Critical", { exact: true })).toBeChecked(); await dialog.getByRole("button", { name: "Cancel", exact: true }).click(); // AIA combines the access method and XCA general-name type in each row. dialog = await openEditor(page, "#edit-aia", "Authority information access"); await expect(dialog.getByLabel("Critical", { exact: true })).not.toBeChecked(); for (const [type, value] of aiaEntries) { await addRow(dialog, type, value); } await dialog.getByLabel("Critical", { exact: true }).check(); await dialog.getByRole("button", { name: "Apply", exact: true }).click(); await expect(page.locator("#aia")).toHaveValue(aiaText); dialog = await openEditor(page, "#edit-aia", "Authority information access"); await expect(dialog.locator(".list-entry")).toHaveCount(2); await expect(dialog.locator(".list-entry").nth(0).locator("select")).toHaveValue("OCSP;URI"); await expect(dialog.locator(".list-entry").nth(1).locator("select")).toHaveValue("caIssuers;URI"); await expect(dialog.locator(".list-entry").nth(1).locator(".list-value")).toHaveValue(aiaEntries[1][1]); await expect(dialog.getByLabel("Critical", { exact: true })).toBeChecked(); await dialog.getByRole("button", { name: "Cancel", exact: true }).click(); // Name constraints use typed rows and share one extension-level critical flag. dialog = await openEditor(page, "#edit-nc-permitted", "Name constraints · permitted"); await expect(dialog.getByLabel("Critical", { exact: true })).toBeChecked(); await addRow(dialog, "DNS", ".example.com"); await addRow(dialog, "DNS", "example.net"); await dialog.getByLabel("Critical", { exact: true }).uncheck(); await dialog.getByRole("button", { name: "Apply", exact: true }).click(); await expect(page.locator("#nc-permitted")).toHaveValue("DNS:.example.com, DNS:example.net"); dialog = await openEditor(page, "#edit-nc-excluded", "Name constraints · excluded"); await expect(dialog.getByLabel("Critical", { exact: true })).not.toBeChecked(); await dialog.getByLabel("Critical", { exact: true }).check(); await addRow(dialog, "DNS", ".bad.example"); await dialog.getByRole("button", { name: "Apply", exact: true }).click(); await expect(page.locator("#nc-excluded")).toHaveValue("DNS:.bad.example"); dialog = await openEditor(page, "#edit-nc-permitted", "Name constraints · permitted"); await expect(dialog.locator(".list-entry")).toHaveCount(2); await expect(dialog.getByLabel("Critical", { exact: true })).toBeChecked(); await dialog.getByRole("button", { name: "Cancel", exact: true }).click(); // CPS URIs use comma-separated plain URLs and support row deletion. await (await field(page, "Certificate policy OIDs")).fill("1.3.6.1.4.1.1"); dialog = await openEditor(page, "#edit-policy-cps", "Policy CPS URIs"); await expect(dialog.getByLabel("Critical", { exact: true })).not.toBeChecked(); await dialog.getByLabel("Critical", { exact: true }).check(); await addRow(dialog, "URI", cpsUrls[0]); await addRow(dialog, "URI", cpsUrls[1]); const discarded = await addRow(dialog, "URI", "https://discarded.example.com/cps"); await discarded.getByRole("button", { name: "Delete", exact: true }).click(); await expect(dialog.locator(".list-entry")).toHaveCount(2); await dialog.getByRole("button", { name: "Apply", exact: true }).click(); await expect(page.locator("#policy-cps")).toHaveValue(cpsUrls.join(", ")); await expect(await field(page, "Common name (CN)")).toHaveValue(certName); await expect(page.locator("#nc-excluded")).toHaveValue("DNS:.bad.example"); await page.getByRole("button", { name: "Validate extensions" }).click(); await expect(page.getByText("Extension preview")).toBeVisible(); await expect(page.locator("pre.pem")).toContainText("Authority Information Access"); await expect(page.locator("pre.pem")).toContainText("Name Constraints"); await expect(page.locator("pre.pem")).toContainText("Certificate Policies"); await page.getByRole("button", { name: "Create certificate" }).click(); await expect(page.getByRole("heading", { name: certName })).toBeVisible(); const certApiUrl = page.url().replace(/\/vaults\//, "/api/vaults/"); const detail = await page.evaluate(async (url) => { const response = await fetch(url); return response.json(); }, certApiUrl); const extensions = detail.extension_spec; expect(extensions.crl_distribution_points).toEqual({ critical: true, urls: crlUrls }); expect(extensions.authority_information_access).toEqual({ critical: true, descriptions: aiaEntries.map(([type, value]) => ({ method: type.startsWith("OCSP") ? "ocsp" : "ca_issuers", location: { type: "uri", value }, })), }); expect(extensions.name_constraints).toEqual({ critical: true, permitted: [ { type: "dns", value: ".example.com" }, { type: "dns", value: "example.net" }, ], excluded: [{ type: "dns", value: ".bad.example" }], }); expect(extensions.certificate_policies).toEqual({ critical: true, policies: [{ oid: "1.3.6.1.4.1.1", cps: cpsUrls }], }); // The shared advanced-extension component also works in template forms. await page.goto(`${vaultUrl}/templates/new`); await expect(page.getByRole("heading", { name: "New template" })).toBeVisible(); dialog = await openEditor(page, "#edit-crl-urls", "CRL distribution point URLs"); await addRow(dialog, "URI", "https://template.example.com/root.crl"); await dialog.getByRole("button", { name: "Apply", exact: true }).click(); await expect(page.locator("#crl-urls")).toHaveValue("https://template.example.com/root.crl"); dialog = await openEditor(page, "#edit-aia", "Authority information access"); await addRow(dialog, "OCSP;URI", "https://template.example.com/ocsp"); await dialog.getByRole("button", { name: "Apply", exact: true }).click(); await expect(page.locator("#aia")).toHaveValue("OCSP;URI:https://template.example.com/ocsp"); });