This commit is contained in:
lda committed 2026-04-12 00:45:32 +07:00
1 parent b8b8d951f1
commit ac54ec7c74
12 files changed
+634 -5

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
use axum::Json;
use axum::extract::{Query, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use serde::{Deserialize, Serialize};
use tracing::warn;
use crate::api::json_error;
use crate::runtime::AppState;
use crate::state::AuditEventFilter;
#[derive(Debug, Deserialize)]
pub struct ListAuditEventsQuery {
pub agent_id: Option<String>,
pub request_id: Option<String>,
pub event_type: Option<String>,
pub outcome: Option<String>,
pub since_unix: Option<u64>,
pub until_unix: Option<u64>,
pub limit: Option<usize>,
}
#[derive(Debug, Serialize)]
pub struct AuditEventResponse {
pub event_id: String,
pub ts_unix: u64,
pub actor_type: String,
pub actor_id: Option<String>,
pub agent_id: Option<String>,
pub request_id: Option<String>,
pub event_type: String,
pub outcome: String,
pub latency_ms: Option<u64>,
pub message: String,
pub metadata: serde_json::Value,
}
pub async fn list_audit_events(
State(state): State<AppState>,
Query(query): Query<ListAuditEventsQuery>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let filter = AuditEventFilter {
agent_id: query.agent_id,
request_id: query.request_id,
event_type: query.event_type,
outcome: query.outcome,
since_unix: query.since_unix,
until_unix: query.until_unix,
limit: query.limit.unwrap_or(100),
};
match state.store.list_audit_events(filter).await {
Ok(events) => {
let body = events
.into_iter()
.map(|event| AuditEventResponse {
event_id: event.event_id,
ts_unix: event.ts_unix,
actor_type: event.actor_type,
actor_id: event.actor_id,
agent_id: event.agent_id,
request_id: event.request_id,
event_type: event.event_type,
outcome: event.outcome,
latency_ms: event.latency_ms,
message: event.message,
metadata: event.metadata,
})
.collect::<Vec<_>>();
Ok((StatusCode::OK, Json(body)))
}
Err(err) => {
warn!(error = %err, "failed to list audit events");
Err(json_error(
StatusCode::INTERNAL_SERVER_ERROR,
"list_audit_events_failed",
&err.to_string(),
))
}
}
}
+105
View File
@@ -3,12 +3,14 @@ use axum::extract::{Path as AxumPath, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use serde::{Deserialize, Serialize};
use std::time::Instant;
use tracing::{info, info_span, warn};
use uuid::Uuid;
use wakey_agent::protocol::{AgentCommand, ErrorPayload, RequestId, ServerMessage};
use crate::api::json_error;
use crate::runtime::{AgentReply, AppState};
use crate::state::AuditEventInput;
#[derive(Debug, Serialize)]
pub struct AgentStatus {
@@ -56,6 +58,7 @@ pub async fn run_command(
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let request_id_string = format!("req-{}", Uuid::new_v4());
let command = command_kind(&req.command);
let started = Instant::now();
let span = info_span!(
"relay_command",
agent_id = %agent_id,
@@ -93,6 +96,23 @@ pub async fn run_command(
.insert(request_id_string.clone(), pending_tx);
info!("dispatching command to agent");
if let Err(err) = state
.store
.append_audit_event(AuditEventInput {
actor_type: "admin_api".into(),
actor_id: None,
agent_id: Some(agent_id.clone()),
request_id: Some(request_id_string.clone()),
event_type: "command_dispatch".into(),
outcome: "sent".into(),
latency_ms: None,
message: "dispatched command to connected agent".into(),
metadata: serde_json::json!({ "command": command }),
})
.await
{
warn!(error = %err, "failed to append audit event for command dispatch");
}
if let Err(err) = tx.send(ServerMessage::Command {
request_id,
@@ -100,6 +120,23 @@ pub async fn run_command(
}) {
state.pending.lock().await.remove(&request_id_string);
warn!(error = %err, "failed sending command to agent session");
if let Err(audit_err) = state
.store
.append_audit_event(AuditEventInput {
actor_type: "admin_api".into(),
actor_id: None,
agent_id: Some(agent_id.clone()),
request_id: Some(request_id_string.clone()),
event_type: "command_dispatch".into(),
outcome: "send_failed".into(),
latency_ms: Some(started.elapsed().as_millis() as u64),
message: err.to_string(),
metadata: serde_json::json!({ "command": command }),
})
.await
{
warn!(error = %audit_err, "failed to append audit event for command send failure");
}
return Err(json_error(
StatusCode::BAD_GATEWAY,
"agent_send_failed",
@@ -116,6 +153,23 @@ pub async fn run_command(
let response = match outcome {
Ok(Ok(AgentReply::Result(result))) => {
info!("agent command completed");
if let Err(err) = state
.store
.append_audit_event(AuditEventInput {
actor_type: "admin_api".into(),
actor_id: None,
agent_id: Some(agent_id.clone()),
request_id: Some(request_id_string.clone()),
event_type: "command_result".into(),
outcome: "ok".into(),
latency_ms: Some(started.elapsed().as_millis() as u64),
message: "agent command completed".into(),
metadata: serde_json::json!({ "command": command }),
})
.await
{
warn!(error = %err, "failed to append audit event for command success");
}
RelayCommandResponse {
request_id: request_id_string,
status: "ok".into(),
@@ -125,6 +179,23 @@ pub async fn run_command(
}
Ok(Ok(AgentReply::Error(error))) => {
warn!(code = %error.code, "agent command returned error");
if let Err(err) = state
.store
.append_audit_event(AuditEventInput {
actor_type: "admin_api".into(),
actor_id: None,
agent_id: Some(agent_id.clone()),
request_id: Some(request_id_string.clone()),
event_type: "command_result".into(),
outcome: "error".into(),
latency_ms: Some(started.elapsed().as_millis() as u64),
message: error.message.clone(),
metadata: serde_json::json!({ "command": command, "code": error.code }),
})
.await
{
warn!(error = %err, "failed to append audit event for command error result");
}
RelayCommandResponse {
request_id: request_id_string,
status: "error".into(),
@@ -134,6 +205,23 @@ pub async fn run_command(
}
Ok(Err(_)) => {
warn!("agent response channel dropped");
if let Err(err) = state
.store
.append_audit_event(AuditEventInput {
actor_type: "admin_api".into(),
actor_id: None,
agent_id: Some(agent_id.clone()),
request_id: Some(request_id_string.clone()),
event_type: "command_result".into(),
outcome: "response_dropped".into(),
latency_ms: Some(started.elapsed().as_millis() as u64),
message: "agent response channel dropped".into(),
metadata: serde_json::json!({ "command": command }),
})
.await
{
warn!(error = %err, "failed to append audit event for dropped response");
}
return Err(json_error(
StatusCode::BAD_GATEWAY,
"agent_response_dropped",
@@ -143,6 +231,23 @@ pub async fn run_command(
Err(_) => {
state.pending.lock().await.remove(&request_id_string);
warn!(timeout_ms = timeout.as_millis() as u64, "agent command timed out");
if let Err(err) = state
.store
.append_audit_event(AuditEventInput {
actor_type: "admin_api".into(),
actor_id: None,
agent_id: Some(agent_id.clone()),
request_id: Some(request_id_string.clone()),
event_type: "command_result".into(),
outcome: "timeout".into(),
latency_ms: Some(started.elapsed().as_millis() as u64),
message: "agent command timed out".into(),
metadata: serde_json::json!({ "command": command, "timeout_ms": timeout.as_millis() as u64 }),
})
.await
{
warn!(error = %err, "failed to append audit event for timeout");
}
return Err(json_error(
StatusCode::GATEWAY_TIMEOUT,
"agent_timeout",
+102 -4
View File
@@ -7,6 +7,7 @@ use tracing::{info, warn};
use crate::api::json_error;
use crate::runtime::AppState;
use crate::state::AuditEventInput;
#[derive(Debug, Deserialize)]
pub struct EnrollRequest {
@@ -69,6 +70,23 @@ pub async fn enroll(
match state.store.enroll(&req.enroll_token).await {
Ok(issued) => {
info!(agent_id = %issued.agent_id, "agent enrollment accepted");
if let Err(err) = state
.store
.append_audit_event(AuditEventInput {
actor_type: "agent".into(),
actor_id: Some(issued.agent_id.clone()),
agent_id: Some(issued.agent_id.clone()),
request_id: None,
event_type: "agent_enroll".into(),
outcome: "ok".into(),
latency_ms: None,
message: "agent enrollment accepted".into(),
metadata: serde_json::json!({}),
})
.await
{
warn!(error = %err, "failed to append audit event for enroll success");
}
Ok((
StatusCode::OK,
Json(EnrollResponse {
@@ -80,6 +98,23 @@ pub async fn enroll(
}
Err(err) => {
warn!(error = %err, "agent enrollment rejected");
if let Err(audit_err) = state
.store
.append_audit_event(AuditEventInput {
actor_type: "agent".into(),
actor_id: None,
agent_id: None,
request_id: None,
event_type: "agent_enroll".into(),
outcome: "rejected".into(),
latency_ms: None,
message: err.to_string(),
metadata: serde_json::json!({}),
})
.await
{
warn!(error = %audit_err, "failed to append audit event for enroll rejection");
}
Err(json_error(
StatusCode::UNAUTHORIZED,
"enrollment_rejected",
@@ -106,6 +141,26 @@ pub async fn issue_enroll_token(
expires_at_unix = issued.expires_at_unix,
"issued enroll token"
);
if let Err(err) = state
.store
.append_audit_event(AuditEventInput {
actor_type: "admin_api".into(),
actor_id: None,
agent_id: None,
request_id: None,
event_type: "enroll_token_issue".into(),
outcome: "ok".into(),
latency_ms: None,
message: "issued enroll token".into(),
metadata: serde_json::json!({
"ttl_seconds": ttl.as_secs(),
"expires_at_unix": issued.expires_at_unix,
}),
})
.await
{
warn!(error = %err, "failed to append audit event for token issuance");
}
Ok((
StatusCode::OK,
Json(IssueEnrollTokenResponse {
@@ -132,6 +187,26 @@ pub async fn list_enroll_tokens(
let include_expired = query.include_expired.unwrap_or(false);
match state.store.list_enroll_tokens(include_expired).await {
Ok(tokens) => {
if let Err(err) = state
.store
.append_audit_event(AuditEventInput {
actor_type: "admin_api".into(),
actor_id: None,
agent_id: None,
request_id: None,
event_type: "enroll_token_list".into(),
outcome: "ok".into(),
latency_ms: None,
message: "listed enroll tokens".into(),
metadata: serde_json::json!({
"include_expired": include_expired,
"count": tokens.len(),
}),
})
.await
{
warn!(error = %err, "failed to append audit event for token listing");
}
let body = tokens
.into_iter()
.map(|t| EnrollTokenStatus {
@@ -158,10 +233,33 @@ pub async fn revoke_enroll_token(
AxumPath(token): AxumPath<String>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
match state.store.revoke_enroll_token(&token).await {
Ok(revoked) => Ok((
StatusCode::OK,
Json(RevokeEnrollTokenResponse { token, revoked }),
)),
Ok(revoked) => {
if let Err(err) = state
.store
.append_audit_event(AuditEventInput {
actor_type: "admin_api".into(),
actor_id: None,
agent_id: None,
request_id: None,
event_type: "enroll_token_revoke".into(),
outcome: if revoked { "ok".into() } else { "not_found".into() },
latency_ms: None,
message: if revoked {
"revoked enroll token".into()
} else {
"enroll token not found".into()
},
metadata: serde_json::json!({ "token": token }),
})
.await
{
warn!(error = %err, "failed to append audit event for token revoke");
}
Ok((
StatusCode::OK,
Json(RevokeEnrollTokenResponse { token, revoked }),
))
}
Err(err) => {
warn!(error = %err, "failed to revoke enroll token");
Err(json_error(
+2
View File
@@ -3,8 +3,10 @@ use axum::http::StatusCode;
mod commands;
mod control;
mod audit;
pub use commands::{list_agents, run_command};
pub use audit::list_audit_events;
pub use control::{
EnrollTokenStatus, IssueEnrollTokenResponse, RevokeEnrollTokenResponse, StateStatsResponse,
enroll, healthz, issue_enroll_token, list_enroll_tokens, revoke_enroll_token, state_stats,