feat(shipote): audit log persistente + HTTP gateway (fase S)
- Daemon escribe append-only a $XDG_STATE_HOME/shipote/audit.log además del tracing. Single-line: ts=<ms> uid=<peer> action=<verb> <detail>. Rotación simple a .log.1 al pasar 1 MiB. - shipote-gateway: TCP listener 127.0.0.1:7378 default. POST /rpc traduce JSON ↔ postcard contra daemon socket. GET / health text. HTTP parser ad-hoc (~70 LOC), sin dep de hyper/axum. Sin auth — bind a localhost + SHIPOTE_TRUST_ANYONE=1 en prod. E2E: curl --noproxy '*' POST /rpc → "Pong", Health JSON, Capabilities JSON. Audit log persiste mutaciones con uid del peer. 85 tests pasan (features nuevos son binarios, no library mods). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -297,6 +297,44 @@ async fn handle_client(
|
||||
}
|
||||
}
|
||||
|
||||
/// Path canónico del audit log: `$XDG_STATE_HOME/shipote/audit.log` o
|
||||
/// fallback `$HOME/.local/state/shipote/audit.log`.
|
||||
fn default_audit_log_path() -> std::path::PathBuf {
|
||||
if let Ok(state) = std::env::var("XDG_STATE_HOME") {
|
||||
return std::path::PathBuf::from(state).join("shipote/audit.log");
|
||||
}
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
return std::path::PathBuf::from(home).join(".local/state/shipote/audit.log");
|
||||
}
|
||||
std::path::PathBuf::from("/tmp/shipote-audit.log")
|
||||
}
|
||||
|
||||
/// Cap del audit log antes de rotar a `audit.log.1`. 1 MiB.
|
||||
const AUDIT_LOG_MAX_BYTES: u64 = 1 << 20;
|
||||
|
||||
/// Append + rotate (mueve a `.1` si supera el cap). Append-only, sin
|
||||
/// reordenar. Sync: cada line, fsync no — el log es defensive, no
|
||||
/// transactional.
|
||||
fn append_audit_line(path: &std::path::Path, line: &str) -> std::io::Result<()> {
|
||||
use std::io::Write;
|
||||
// Rotar si pasa el cap.
|
||||
if let Ok(meta) = std::fs::metadata(path) {
|
||||
if meta.len() >= AUDIT_LOG_MAX_BYTES {
|
||||
let rotated = path.with_extension("log.1");
|
||||
let _ = std::fs::rename(path, &rotated);
|
||||
}
|
||||
}
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
let mut f = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(path)?;
|
||||
writeln!(f, "{line}")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Loguea cada mutación con target="audit" y el peer uid. Reads (ping,
|
||||
/// list, stats) se omiten para no inundar el log.
|
||||
fn audit_request(peer_uid: u32, req: &Request) {
|
||||
@@ -330,6 +368,17 @@ fn audit_request(peer_uid: u32, req: &Request) {
|
||||
| Request::Capabilities => return,
|
||||
};
|
||||
info!(target: "audit", uid = peer_uid, action, detail = %detail, "audit");
|
||||
// Append a file. Failure no es fatal — sólo se pierde la entry.
|
||||
let ts = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis())
|
||||
.unwrap_or(0);
|
||||
let line = format!("ts={ts} uid={peer_uid} action={action} {detail}");
|
||||
let path = default_audit_log_path();
|
||||
if let Err(e) = append_audit_line(&path, &line) {
|
||||
// Sólo loguear si el filesystem está roto. No reportar al cliente.
|
||||
tracing::debug!(?e, path = %path.display(), "audit file write failed");
|
||||
}
|
||||
}
|
||||
|
||||
async fn dispatch(
|
||||
|
||||
Reference in New Issue
Block a user