189 lines
9.6 KiB
JavaScript
189 lines
9.6 KiB
JavaScript
// 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");
|
|
});
|