Implement signed API communication to improve security
build / build-linux-amd64 (push) Successful in 1m54s
build / build-linux-amd64 (push) Successful in 1m54s
This commit is contained in:
@@ -109,6 +109,56 @@ pub async fn delete(
|
||||
notice_then_table(&state, lang, if ok { "ok" } else { "error" }, &msg).await
|
||||
}
|
||||
|
||||
/// Flip `peer.managed` between 0 and 1. Same effect as calling the JSON
|
||||
/// API `PUT /api/peers/:id/managed`, but rendered as an HTMX action so the
|
||||
/// table refreshes in place. The handler reads the current value, flips
|
||||
/// it, and writes back — this avoids a stale-toggle race where the row
|
||||
/// the admin clicked on showed a stale state (e.g. TOFU just promoted it
|
||||
/// in the background) and a "set to N" command would no-op silently.
|
||||
pub async fn toggle_managed(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
admin: AuthedUser,
|
||||
lang: Lang,
|
||||
Path(peer_id): Path<String>,
|
||||
) -> Result<Html<String>, ApiError> {
|
||||
require_admin(&admin)?;
|
||||
let row = state
|
||||
.db
|
||||
.peer_get_auth(&peer_id)
|
||||
.await
|
||||
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
||||
let (_pk, was_managed) = match row {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
return notice_then_table(
|
||||
&state,
|
||||
lang,
|
||||
"error",
|
||||
&tf1(lang, "devices.managed_no_peer", &peer_id),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
};
|
||||
let new_value = !was_managed;
|
||||
state
|
||||
.db
|
||||
.peer_set_managed(&peer_id, new_value)
|
||||
.await
|
||||
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
||||
hbb_common::log::info!(
|
||||
"admin {} set peer {} managed={} via dashboard",
|
||||
admin.name,
|
||||
peer_id,
|
||||
new_value
|
||||
);
|
||||
let key = if new_value {
|
||||
"devices.managed_now_on"
|
||||
} else {
|
||||
"devices.managed_now_off"
|
||||
};
|
||||
notice_then_table(&state, lang, "ok", &tf1(lang, key, &peer_id)).await
|
||||
}
|
||||
|
||||
/// Per-device detail page: hardware / OS inventory reported by hello-agent
|
||||
/// alongside the standard sysinfo (CPU/RAM/OS/hostname). Replaces the
|
||||
/// devices list in `#devices-region` via HTMX; a "Back to devices" button
|
||||
@@ -189,6 +239,7 @@ async fn render_table(state: &Arc<AppState>, lang: Lang) -> Result<String, ApiEr
|
||||
<th class="text-left font-medium px-3 py-2">{c_ver}</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_last}</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_conns}</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_auth}</th>
|
||||
<th class="text-right font-medium px-3 py-2 w-1">{c_actions}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -202,12 +253,13 @@ async fn render_table(state: &Arc<AppState>, lang: Lang) -> Result<String, ApiEr
|
||||
c_ver = t(lang, "devices.col_version"),
|
||||
c_last = t(lang, "devices.col_last_heartbeat"),
|
||||
c_conns = t(lang, "devices.col_conns"),
|
||||
c_auth = t(lang, "devices.col_auth"),
|
||||
c_actions = t(lang, "common.actions"),
|
||||
);
|
||||
if devices.is_empty() {
|
||||
let _ = write!(
|
||||
s,
|
||||
r##"<tr><td colspan="10" class="px-3 py-4 text-slate-500 text-center text-xs">{}</td></tr>"##,
|
||||
r##"<tr><td colspan="11" class="px-3 py-4 text-slate-500 text-center text-xs">{}</td></tr>"##,
|
||||
t(lang, "devices.no_devices"),
|
||||
);
|
||||
}
|
||||
@@ -332,6 +384,57 @@ fn render_device_row(
|
||||
dot = dot_class,
|
||||
id = html_escape(&d.id),
|
||||
);
|
||||
|
||||
// Auth badge: `Signed` (emerald) when peer.managed=1 — heartbeat /
|
||||
// sysinfo posts must carry a valid Ed25519 signature; `—` (slate) when
|
||||
// managed=0 and the device still posts unsigned bodies. The tooltip
|
||||
// gives the operator the one-line explanation so they know what
|
||||
// flipping the flag will do.
|
||||
let auth_cell = if d.managed {
|
||||
format!(
|
||||
r##"<td class="px-3 py-2 whitespace-nowrap">
|
||||
<span class="inline-flex items-center gap-1 rounded border border-emerald-700/50 bg-emerald-900/30 px-2 py-0.5 text-xs text-emerald-300" title="{tt}">{label}</span>
|
||||
</td>"##,
|
||||
tt = html_escape(t(lang, "devices.auth_signed_tooltip")),
|
||||
label = t(lang, "devices.auth_signed"),
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
r##"<td class="px-3 py-2 whitespace-nowrap">
|
||||
<span class="inline-flex items-center gap-1 rounded border border-slate-700 bg-slate-800/40 px-2 py-0.5 text-xs text-slate-400" title="{tt}">{label}</span>
|
||||
</td>"##,
|
||||
tt = html_escape(t(lang, "devices.auth_unsigned_tooltip")),
|
||||
label = t(lang, "devices.auth_unsigned"),
|
||||
)
|
||||
};
|
||||
// Auth toggle: the menu entry's label flips based on current state,
|
||||
// and only the off→on transition needs no confirm (it strengthens
|
||||
// security). on→off removes the signature requirement and reintroduces
|
||||
// the spoofing surface, so we require a confirm on that direction.
|
||||
let toggle_managed_item = if d.managed {
|
||||
format!(
|
||||
r##"<button class="w-full text-left px-2 py-1 text-xs hover:bg-slate-800 rounded"
|
||||
hx-post="/admin/pages/devices/{id}/toggle-managed"
|
||||
hx-target="#devices-region" hx-swap="innerHTML"
|
||||
hx-confirm="{confirm}">
|
||||
{label}
|
||||
</button>"##,
|
||||
id = html_escape(&d.id),
|
||||
confirm = html_escape(&tf1(lang, "devices.confirm_managed_off", &d.id)),
|
||||
label = t(lang, "devices.mark_unsigned"),
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
r##"<button class="w-full text-left px-2 py-1 text-xs hover:bg-slate-800 rounded"
|
||||
hx-post="/admin/pages/devices/{id}/toggle-managed"
|
||||
hx-target="#devices-region" hx-swap="innerHTML">
|
||||
{label}
|
||||
</button>"##,
|
||||
id = html_escape(&d.id),
|
||||
label = t(lang, "devices.mark_managed"),
|
||||
)
|
||||
};
|
||||
|
||||
let _ = write!(
|
||||
s,
|
||||
r##"<tr class="hover:bg-slate-800/40">
|
||||
@@ -344,6 +447,7 @@ fn render_device_row(
|
||||
<td class="px-3 py-2 text-slate-400 whitespace-nowrap">{ver}</td>
|
||||
<td class="px-3 py-2 text-slate-500 text-xs">{last}</td>
|
||||
<td class="px-3 py-2 text-slate-400">{n}</td>
|
||||
{auth_cell}
|
||||
<td class="px-3 py-2">
|
||||
<details class="text-right relative">
|
||||
<summary class="cursor-pointer list-none text-xs text-slate-400 hover:text-slate-200 select-none">···</summary>
|
||||
@@ -358,6 +462,8 @@ fn render_device_row(
|
||||
{details}
|
||||
</button>
|
||||
<hr class="border-slate-700 my-1" />
|
||||
{toggle_managed_item}
|
||||
<hr class="border-slate-700 my-1" />
|
||||
<button class="w-full text-left px-2 py-1 text-xs hover:bg-slate-800 rounded"
|
||||
hx-post="/admin/pages/devices/{id}/disconnect"
|
||||
hx-target="#devices-region" hx-swap="innerHTML"
|
||||
@@ -394,6 +500,8 @@ fn render_device_row(
|
||||
ver = html_escape(&version_label),
|
||||
last = html_escape(&d.last_heartbeat_at),
|
||||
n = conn_count,
|
||||
auth_cell = auth_cell,
|
||||
toggle_managed_item = toggle_managed_item,
|
||||
connect_web = t(lang, "devices.connect_web"),
|
||||
details = t(lang, "devices.details"),
|
||||
confirm_disc = html_escape(&tf1(lang, "devices.confirm_disconnect", &d.id)),
|
||||
|
||||
Reference in New Issue
Block a user