Remove bulk actions from certificate list
This commit is contained in:
1 parent
555f3f28f7
commit
73e6a11463
5 files changed
+26
-297
No files matched your search
@@ -103,8 +103,7 @@ explicit/mandatory DN entries, default key type/size/curve, default validity).
|
|||||||
issuer chain.
|
issuer chain.
|
||||||
- Renew with a new or kept serial, optionally revoke/replace the old
|
- Renew with a new or kept serial, optionally revoke/replace the old
|
||||||
certificate. Revoke with a reason; manage the CA's revocation list manually,
|
certificate. Revoke with a reason; manage the CA's revocation list manually,
|
||||||
including revocation dates and invalidity dates. Certificate lists support
|
including revocation dates and invalidity dates. CA properties
|
||||||
bulk revocation with a shared reason and invalidity date. CA properties
|
|
||||||
(CRL days and default signing template, including built-ins) are editable
|
(CRL days and default signing template, including built-ins) are editable
|
||||||
from the CA detail page, and the wizard offers the CA default template
|
from the CA detail page, and the wizard offers the CA default template
|
||||||
explicitly without overwriting edits.
|
explicitly without overwriting edits.
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
// 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);
|
|
||||||
});
|
|
||||||
@@ -79,12 +79,18 @@ test("core PKI workflow", async ({ page }) => {
|
|||||||
// Certificate list links to the detail route in both views.
|
// Certificate list links to the detail route in both views.
|
||||||
const certDetailUrl = /\/vaults\/[^/]+\/certificates\/[^/]+$/;
|
const certDetailUrl = /\/vaults\/[^/]+\/certificates\/[^/]+$/;
|
||||||
await page.goto(`${vaultUrl}/certificates`);
|
await page.goto(`${vaultUrl}/certificates`);
|
||||||
|
await expect(page.getByRole("button", { name: "Select all revocable" })).toHaveCount(0);
|
||||||
|
await expect(page.getByRole("button", { name: "Revoke selected" })).toHaveCount(0);
|
||||||
|
await expect(page.locator("input[data-cert-id]")).toHaveCount(0);
|
||||||
await page.getByRole("link", { name: leafName, exact: true }).click();
|
await page.getByRole("link", { name: leafName, exact: true }).click();
|
||||||
await expect(page).toHaveURL(certDetailUrl);
|
await expect(page).toHaveURL(certDetailUrl);
|
||||||
await expect(page.getByRole("heading", { name: leafName })).toBeVisible();
|
await expect(page.getByRole("heading", { name: leafName })).toBeVisible();
|
||||||
|
|
||||||
await page.goto(`${vaultUrl}/certificates`);
|
await page.goto(`${vaultUrl}/certificates`);
|
||||||
await page.getByRole("button", { name: "Plain view" }).click();
|
await page.getByRole("button", { name: "Plain view" }).click();
|
||||||
|
await expect(page.getByRole("button", { name: "Select all revocable" })).toHaveCount(0);
|
||||||
|
await expect(page.getByRole("button", { name: "Revoke selected" })).toHaveCount(0);
|
||||||
|
await expect(page.locator("input[data-cert-id]")).toHaveCount(0);
|
||||||
await page.getByRole("link", { name: leafName, exact: true }).click();
|
await page.getByRole("link", { name: leafName, exact: true }).click();
|
||||||
await expect(page).toHaveURL(certDetailUrl);
|
await expect(page).toHaveURL(certDetailUrl);
|
||||||
await expect(page.getByRole("heading", { name: leafName })).toBeVisible();
|
await expect(page.getByRole("heading", { name: leafName })).toBeVisible();
|
||||||
|
|||||||
@@ -297,11 +297,6 @@ pub async fn certificate(vault_id: &str, cert_id: &str) -> Result<CertificateDet
|
|||||||
check(response).await
|
check(response).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn bulk_revoke_certificates(vault_id: &str, payload: &serde_json::Value) -> Result<serde_json::Value, String> {
|
|
||||||
let response = send("POST", &format!("/api/vaults/{vault_id}/certificates/bulk-revoke"), Some(payload)).await?;
|
|
||||||
check(response).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn preview_certificate(vault_id: &str, payload: &serde_json::Value) -> Result<serde_json::Value, String> {
|
pub async fn preview_certificate(vault_id: &str, payload: &serde_json::Value) -> Result<serde_json::Value, String> {
|
||||||
let response = send("POST", &format!("/api/vaults/{vault_id}/certificates/preview"), Some(payload)).await?;
|
let response = send("POST", &format!("/api/vaults/{vault_id}/certificates/preview"), Some(payload)).await?;
|
||||||
check(response).await
|
check(response).await
|
||||||
|
|||||||
+19
-175
@@ -1,16 +1,12 @@
|
|||||||
//! Certificate list, flat or as an issuer tree.
|
//! Certificate list, flat or as an issuer tree.
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use crate::components::nav::{app_nav, spinner, whoami};
|
use crate::components::nav::{app_nav, spinner, whoami};
|
||||||
use crate::components::ui::{page_header, toast_error};
|
use crate::components::ui::{page_header, toast_error};
|
||||||
use crate::dom;
|
|
||||||
use crate::model::{fmt_date, fmt_fp, status_badge, CertificateSummary, Me};
|
use crate::model::{fmt_date, fmt_fp, status_badge, CertificateSummary, Me};
|
||||||
|
|
||||||
pub fn CertificatesPage(vault_id: String) -> View {
|
pub fn CertificatesPage(vault_id: String) -> View {
|
||||||
@@ -21,11 +17,6 @@ pub fn CertificatesPage(vault_id: String) -> View {
|
|||||||
let loading = create_signal(true);
|
let loading = create_signal(true);
|
||||||
let filter = create_signal(String::new());
|
let filter = create_signal(String::new());
|
||||||
let plain = create_signal(false);
|
let plain = create_signal(false);
|
||||||
let selected = create_signal(Vec::<String>::new());
|
|
||||||
let bulk_reason = create_signal("unspecified".to_string());
|
|
||||||
let bulk_invalidity = create_signal(String::new());
|
|
||||||
let bulk_busy = create_signal(false);
|
|
||||||
let info = create_signal(None::<String>);
|
|
||||||
|
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
match api::me().await {
|
match api::me().await {
|
||||||
@@ -59,97 +50,6 @@ pub fn CertificatesPage(vault_id: String) -> View {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let refresh = move || {
|
|
||||||
spawn_local(async move {
|
|
||||||
if let Ok(page) = api::certificates(id).await {
|
|
||||||
if certificates.is_alive() {
|
|
||||||
certificates.set(page.results);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let toggle = move |event: web_sys::Event| {
|
|
||||||
if let Some(target) = event
|
|
||||||
.target()
|
|
||||||
.and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
|
|
||||||
{
|
|
||||||
if let Some(cert_id) = target.get_attribute("data-cert-id") {
|
|
||||||
let mut current = selected.get_clone();
|
|
||||||
if target.checked() {
|
|
||||||
if !current.contains(&cert_id) {
|
|
||||||
current.push(cert_id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
current.retain(|value| value != &cert_id);
|
|
||||||
}
|
|
||||||
selected.set(current);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let select_all = move |_| {
|
|
||||||
let all = certificates
|
|
||||||
.get_clone()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|cert| cert.issuer_id.is_some())
|
|
||||||
.map(|cert| cert.id)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
selected.set(all);
|
|
||||||
};
|
|
||||||
let clear_selection = move |_| selected.set(Vec::new());
|
|
||||||
let bulk_revoke = move |_| {
|
|
||||||
let ids = selected.get_clone();
|
|
||||||
if ids.is_empty() {
|
|
||||||
error.set(Some("select at least one certificate".to_string()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !dom::confirm(&format!("Revoke {} selected certificate(s)?", ids.len())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let payload = json!({
|
|
||||||
"certificate_ids": ids,
|
|
||||||
"reason": bulk_reason.get_clone(),
|
|
||||||
"invalidity_date": if bulk_invalidity.get_clone().is_empty() {
|
|
||||||
serde_json::Value::Null
|
|
||||||
} else {
|
|
||||||
json!(bulk_invalidity.get_clone())
|
|
||||||
},
|
|
||||||
});
|
|
||||||
bulk_busy.set(true);
|
|
||||||
spawn_local(async move {
|
|
||||||
match api::bulk_revoke_certificates(id, &payload).await {
|
|
||||||
Ok(value) => {
|
|
||||||
let revoked = value
|
|
||||||
.get("revoked")
|
|
||||||
.and_then(|v| v.as_array())
|
|
||||||
.map(|v| v.len())
|
|
||||||
.unwrap_or(0);
|
|
||||||
let skipped = value
|
|
||||||
.get("skipped")
|
|
||||||
.and_then(|v| v.as_array())
|
|
||||||
.map(|v| v.len())
|
|
||||||
.unwrap_or(0);
|
|
||||||
let errors = value
|
|
||||||
.get("errors")
|
|
||||||
.and_then(|v| v.as_array())
|
|
||||||
.map(|v| v.len())
|
|
||||||
.unwrap_or(0);
|
|
||||||
info.set(Some(format!(
|
|
||||||
"Revoked {revoked} certificate(s); {skipped} already revoked; {errors} error(s)."
|
|
||||||
)));
|
|
||||||
selected.set(Vec::new());
|
|
||||||
bulk_invalidity.set(String::new());
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
Err(message) if message == api::LOGIN_REQUIRED => {}
|
|
||||||
Err(message) => error.set(Some(message)),
|
|
||||||
}
|
|
||||||
if bulk_busy.is_alive() {
|
|
||||||
bulk_busy.set(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let content = move || {
|
let content = move || {
|
||||||
if loading.get() {
|
if loading.get() {
|
||||||
return spinner();
|
return spinner();
|
||||||
@@ -167,11 +67,10 @@ pub fn CertificatesPage(vault_id: String) -> View {
|
|||||||
|| cert.fingerprint_sha256.to_lowercase().contains(&query)
|
|| cert.fingerprint_sha256.to_lowercase().contains(&query)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let chosen = selected.get_clone();
|
|
||||||
let html = if plain.get() {
|
let html = if plain.get() {
|
||||||
flat_html(&visible, id, &chosen)
|
flat_html(&visible, id)
|
||||||
} else {
|
} else {
|
||||||
tree_html(&visible, id, &chosen)
|
tree_html(&visible, id)
|
||||||
};
|
};
|
||||||
view! {
|
view! {
|
||||||
(page_header(
|
(page_header(
|
||||||
@@ -191,31 +90,7 @@ pub fn CertificatesPage(vault_id: String) -> View {
|
|||||||
aria-label="Filter certificates", bind:value=filter)
|
aria-label="Filter certificates", bind:value=filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div(class="card mb-3") {
|
div(class="card", dangerously_set_inner_html=html) {}
|
||||||
div(class="card-body") {
|
|
||||||
div(class="d-flex flex-wrap gap-2 align-items-end") {
|
|
||||||
div(class="me-auto") {
|
|
||||||
div(class="small text-secondary", aria-live="polite") { (format!("{} selected", selected.get_clone().len())) }
|
|
||||||
div(class="d-flex gap-2 mt-1") {
|
|
||||||
button(class="btn btn-sm btn-outline-light", on:click=select_all) { "Select all revocable" }
|
|
||||||
button(class="btn btn-sm btn-outline-light", on:click=clear_selection) { "Clear" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div() {
|
|
||||||
label(class="form-label small mb-1") { "Reason" }
|
|
||||||
select(class="form-select form-select-sm", bind:value=bulk_reason) { (reason_options()) }
|
|
||||||
}
|
|
||||||
div() {
|
|
||||||
label(class="form-label small mb-1") { "Invalid since (optional)" }
|
|
||||||
input(class="form-control form-control-sm", r#type="datetime-local", bind:value=bulk_invalidity)
|
|
||||||
}
|
|
||||||
button(class="btn btn-warning", disabled=bulk_busy.get() || selected.get_clone().is_empty(), on:click=bulk_revoke) {
|
|
||||||
(if bulk_busy.get() { "Revoking…" } else { "Revoke selected" })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div(class="card", on:change=toggle, dangerously_set_inner_html=html) {}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -228,17 +103,20 @@ pub fn CertificatesPage(vault_id: String) -> View {
|
|||||||
(app_nav("/", Some(id), nav))
|
(app_nav("/", Some(id), nav))
|
||||||
main(class="container page-shell py-4") {
|
main(class="container page-shell py-4") {
|
||||||
(toast_error(error))
|
(toast_error(error))
|
||||||
(crate::components::toast::toast(info, true))
|
|
||||||
(content)
|
(content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn certificate_href(vault_id: &str, cert_id: &str) -> String {
|
fn certificate_href(vault_id: &str, cert_id: &str) -> String {
|
||||||
format!("/vaults/{}/certificates/{}", escape(vault_id), escape(cert_id))
|
format!(
|
||||||
|
"/vaults/{}/certificates/{}",
|
||||||
|
escape(vault_id),
|
||||||
|
escape(cert_id)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flat_html(rows: &[CertificateSummary], vault_id: &str, selected: &[String]) -> String {
|
fn flat_html(rows: &[CertificateSummary], vault_id: &str) -> String {
|
||||||
if rows.is_empty() {
|
if rows.is_empty() {
|
||||||
return "<p class=\"empty-state\">No certificates match.</p>".to_string();
|
return "<p class=\"empty-state\">No certificates match.</p>".to_string();
|
||||||
}
|
}
|
||||||
@@ -246,8 +124,7 @@ fn flat_html(rows: &[CertificateSummary], vault_id: &str, selected: &[String]) -
|
|||||||
for cert in rows {
|
for cert in rows {
|
||||||
let (badge_class, badge_label) = status_badge(cert);
|
let (badge_class, badge_label) = status_badge(cert);
|
||||||
body.push_str(&format!(
|
body.push_str(&format!(
|
||||||
"<tr><td>{}</td><td><a href=\"{}\">{}</a><div class=\"item-sub\">{}</div></td><td>{}</td><td>{}</td><td class=\"fingerprint\">{}</td><td>{}</td><td><span class=\"badge {}\">{}</span>{}</td><td class=\"fingerprint\">{}</td></tr>",
|
"<tr><td><a href=\"{}\">{}</a><div class=\"item-sub\">{}</div></td><td>{}</td><td>{}</td><td class=\"fingerprint\">{}</td><td>{}</td><td><span class=\"badge {}\">{}</span>{}</td><td class=\"fingerprint\">{}</td></tr>",
|
||||||
checkbox(cert, selected),
|
|
||||||
certificate_href(vault_id, &cert.id),
|
certificate_href(vault_id, &cert.id),
|
||||||
escape(&cert.name),
|
escape(&cert.name),
|
||||||
escape(&cert.subject),
|
escape(&cert.subject),
|
||||||
@@ -262,11 +139,11 @@ fn flat_html(rows: &[CertificateSummary], vault_id: &str, selected: &[String]) -
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
format!(
|
format!(
|
||||||
"<div class=\"table-responsive\"><table class=\"table table-hover align-middle mb-0\"><thead><tr><th scope=\"col\"><span class=\"visually-hidden\">Select</span></th><th scope=\"col\">Name</th><th scope=\"col\">Issuer</th><th scope=\"col\">Type</th><th scope=\"col\">Serial</th><th scope=\"col\">Expires</th><th scope=\"col\">Status</th><th scope=\"col\">SHA-256</th></tr></thead><tbody>{body}</tbody></table></div>"
|
"<div class=\"table-responsive\"><table class=\"table table-hover align-middle mb-0\"><thead><tr><th scope=\"col\">Name</th><th scope=\"col\">Issuer</th><th scope=\"col\">Type</th><th scope=\"col\">Serial</th><th scope=\"col\">Expires</th><th scope=\"col\">Status</th><th scope=\"col\">SHA-256</th></tr></thead><tbody>{body}</tbody></table></div>"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tree_html(rows: &[CertificateSummary], vault_id: &str, selected: &[String]) -> String {
|
fn tree_html(rows: &[CertificateSummary], vault_id: &str) -> String {
|
||||||
if rows.is_empty() {
|
if rows.is_empty() {
|
||||||
return "<p class=\"empty-state\">No certificates match.</p>".to_string();
|
return "<p class=\"empty-state\">No certificates match.</p>".to_string();
|
||||||
}
|
}
|
||||||
@@ -287,7 +164,7 @@ fn tree_html(rows: &[CertificateSummary], vault_id: &str, selected: &[String]) -
|
|||||||
roots.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
roots.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||||
let mut html = String::from("<div class=\"card-body\"><ul class=\"cert-tree\">");
|
let mut html = String::from("<div class=\"card-body\"><ul class=\"cert-tree\">");
|
||||||
for root in &roots {
|
for root in &roots {
|
||||||
html.push_str(&node_html(root, &children, vault_id, selected));
|
html.push_str(&node_html(root, &children, vault_id));
|
||||||
}
|
}
|
||||||
html.push_str("</ul></div>");
|
html.push_str("</ul></div>");
|
||||||
html
|
html
|
||||||
@@ -297,7 +174,6 @@ fn node_html(
|
|||||||
cert: &CertificateSummary,
|
cert: &CertificateSummary,
|
||||||
children: &HashMap<String, Vec<CertificateSummary>>,
|
children: &HashMap<String, Vec<CertificateSummary>>,
|
||||||
vault_id: &str,
|
vault_id: &str,
|
||||||
selected: &[String],
|
|
||||||
) -> String {
|
) -> String {
|
||||||
let (badge_class, badge_label) = status_badge(cert);
|
let (badge_class, badge_label) = status_badge(cert);
|
||||||
let sub = if cert.subject == cert.issuer_name {
|
let sub = if cert.subject == cert.issuer_name {
|
||||||
@@ -306,8 +182,7 @@ fn node_html(
|
|||||||
format!("issuer: {}", cert.issuer_name)
|
format!("issuer: {}", cert.issuer_name)
|
||||||
};
|
};
|
||||||
let mut row = format!(
|
let mut row = format!(
|
||||||
"<div class=\"cert-row\">{}<a class=\"item-name\" href=\"{}\">{}</a>{} <span class=\"badge {}\">{}</span><span class=\"item-sub\">{}</span><span class=\"item-sub\">expires {}</span><span class=\"item-sub fingerprint\">{}</span></div>",
|
"<div class=\"cert-row\"><a class=\"item-name\" href=\"{}\">{}</a>{} <span class=\"badge {}\">{}</span><span class=\"item-sub\">{}</span><span class=\"item-sub\">expires {}</span><span class=\"item-sub fingerprint\">{}</span></div>",
|
||||||
checkbox(cert, selected),
|
|
||||||
certificate_href(vault_id, &cert.id),
|
certificate_href(vault_id, &cert.id),
|
||||||
escape(&cert.name),
|
escape(&cert.name),
|
||||||
if cert.is_ca { " <span class=\"badge badge-ca\">CA</span>" } else { "" },
|
if cert.is_ca { " <span class=\"badge badge-ca\">CA</span>" } else { "" },
|
||||||
@@ -322,7 +197,7 @@ fn node_html(
|
|||||||
if !kids.is_empty() {
|
if !kids.is_empty() {
|
||||||
row = format!(
|
row = format!(
|
||||||
"<li><details class=\"cert-node\" open><summary>{row}</summary><ul class=\"cert-tree\">{}</ul></details></li>",
|
"<li><details class=\"cert-node\" open><summary>{row}</summary><ul class=\"cert-tree\">{}</ul></details></li>",
|
||||||
kids.iter().map(|child| node_html(child, children, vault_id, selected)).collect::<String>()
|
kids.iter().map(|child| node_html(child, children, vault_id)).collect::<String>()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
row = format!("<li>{row}</li>");
|
row = format!("<li>{row}</li>");
|
||||||
@@ -333,39 +208,6 @@ fn node_html(
|
|||||||
row
|
row
|
||||||
}
|
}
|
||||||
|
|
||||||
fn checkbox(cert: &CertificateSummary, selected: &[String]) -> String {
|
|
||||||
if cert.issuer_id.is_none() {
|
|
||||||
return "<input class=\"form-check-input\" type=\"checkbox\" disabled aria-label=\"Self-signed certificates cannot be revoked\" title=\"Self-signed certificates cannot be revoked\">".to_string();
|
|
||||||
}
|
|
||||||
let checked = if selected.contains(&cert.id) {
|
|
||||||
" checked"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
format!(
|
|
||||||
"<input class=\"form-check-input\" type=\"checkbox\" data-cert-id=\"{}\" aria-label=\"Select {} for revocation\"{}>",
|
|
||||||
escape(&cert.id),
|
|
||||||
escape(&cert.name),
|
|
||||||
checked
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reason_options() -> Vec<View> {
|
|
||||||
[
|
|
||||||
"unspecified",
|
|
||||||
"keyCompromise",
|
|
||||||
"CACompromise",
|
|
||||||
"affiliationChanged",
|
|
||||||
"superseded",
|
|
||||||
"cessationOfOperation",
|
|
||||||
"certificateHold",
|
|
||||||
"privilegeWithdrawn",
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.map(|reason| view! { option(value=*reason) { (*reason) } })
|
|
||||||
.collect::<Vec<View>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn escape(value: &str) -> String {
|
fn escape(value: &str) -> String {
|
||||||
value
|
value
|
||||||
.replace('&', "&")
|
.replace('&', "&")
|
||||||
@@ -390,12 +232,14 @@ mod tests {
|
|||||||
fn certificate_links_point_at_the_detail_route() {
|
fn certificate_links_point_at_the_detail_route() {
|
||||||
let cert = cert("cert-1", "Leaf");
|
let cert = cert("cert-1", "Leaf");
|
||||||
|
|
||||||
let flat = flat_html(std::slice::from_ref(&cert), "vault-1", &[]);
|
let flat = flat_html(std::slice::from_ref(&cert), "vault-1");
|
||||||
assert!(flat.contains("href=\"/vaults/vault-1/certificates/cert-1\""));
|
assert!(flat.contains("href=\"/vaults/vault-1/certificates/cert-1\""));
|
||||||
assert!(!flat.contains("href=\"/vaults/vault-1/cert-1\""));
|
assert!(!flat.contains("href=\"/vaults/vault-1/cert-1\""));
|
||||||
|
assert!(!flat.contains("type=\"checkbox\""));
|
||||||
|
|
||||||
let tree = tree_html(std::slice::from_ref(&cert), "vault-1", &[]);
|
let tree = tree_html(std::slice::from_ref(&cert), "vault-1");
|
||||||
assert!(tree.contains("href=\"/vaults/vault-1/certificates/cert-1\""));
|
assert!(tree.contains("href=\"/vaults/vault-1/certificates/cert-1\""));
|
||||||
assert!(!tree.contains("href=\"/vaults/vault-1/cert-1\""));
|
assert!(!tree.contains("href=\"/vaults/vault-1/cert-1\""));
|
||||||
|
assert!(!tree.contains("type=\"checkbox\""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user