91 lines
2.9 KiB
Rust
91 lines
2.9 KiB
Rust
//! `POST /api/unattended-password` — agent-side reporting of the per-boot
|
|
//! "permanent password" used for unattended access (no logged-in user to
|
|
//! click the approval popup). hello-agent generates a random password
|
|
//! every time the service starts and posts it here so the admin UI can
|
|
//! surface it for support staff.
|
|
//!
|
|
//! Auth: same per-peer signed-API gate as `/api/sysinfo` and
|
|
//! `/api/heartbeat` — see [`crate::api::device_auth`]. Managed peers
|
|
//! (`peer.managed = 1`) must carry a valid Ed25519 signature; stock
|
|
//! clients keep posting unsigned and the first valid signature TOFU-
|
|
//! promotes the peer.
|
|
|
|
use crate::api::device_auth::{self, AuthOutcome};
|
|
use crate::api::error::ApiError;
|
|
use crate::api::state::AppState;
|
|
use axum::body::Bytes;
|
|
use axum::extract::Extension;
|
|
use axum::http::HeaderMap;
|
|
use serde_json::Value;
|
|
use std::sync::Arc;
|
|
|
|
/// Body: `{"id": "...", "uuid": "...", "password": "..."}`
|
|
/// Response (bare string, like sysinfo): `"OK"` or `"ID_NOT_FOUND"`.
|
|
pub async fn unattended_password(
|
|
Extension(state): Extension<Arc<AppState>>,
|
|
headers: HeaderMap,
|
|
body: Bytes,
|
|
) -> Result<String, ApiError> {
|
|
let outcome = device_auth::verify(
|
|
&state,
|
|
"POST",
|
|
"/api/unattended-password",
|
|
&headers,
|
|
&body,
|
|
)
|
|
.await?;
|
|
|
|
let payload: Value = serde_json::from_slice(&body)
|
|
.map_err(|_| ApiError::BadRequest("invalid json".into()))?;
|
|
let body_id = payload
|
|
.get("id")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or_default();
|
|
let uuid = payload
|
|
.get("uuid")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or_default();
|
|
let password = payload
|
|
.get("password")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or_default();
|
|
if body_id.is_empty() || uuid.is_empty() || password.is_empty() {
|
|
return Err(ApiError::BadRequest(
|
|
"id, uuid, and password are required".into(),
|
|
));
|
|
}
|
|
|
|
// Bind the trusted identity to the body. For signed requests the body
|
|
// id must match the header id, or the agent is trying to overwrite
|
|
// someone else's displayed password. For unsigned requests we just
|
|
// need to ensure the peer isn't already locked down as managed.
|
|
let id = match outcome {
|
|
AuthOutcome::Verified { id: signed_id } => {
|
|
if body_id != signed_id {
|
|
return Err(ApiError::Unauthorized);
|
|
}
|
|
signed_id
|
|
}
|
|
AuthOutcome::LegacyUnsigned => {
|
|
device_auth::enforce_managed_for_id(&state, body_id).await?;
|
|
body_id.to_string()
|
|
}
|
|
};
|
|
|
|
let peer = state
|
|
.db
|
|
.get_peer(&id)
|
|
.await
|
|
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
|
if peer.is_none() {
|
|
return Ok("ID_NOT_FOUND".to_string());
|
|
}
|
|
|
|
state
|
|
.db
|
|
.set_unattended_password(&id, uuid, password)
|
|
.await
|
|
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
|
Ok("OK".to_string())
|
|
}
|