feat(audit): live-append event for the dashboard audit view
Follow-up to the audit-log backend + surface. Emits a DashboardEvent on
each agent-initiated privileged action so the audit view live-appends off
/dashboard/stream instead of polling.
- new DashboardEvent::AuditEntryAdded { seq, <flattened AuditEntry> } —
serde tag `audit_entry_added`; the AuditEntry fields flatten to the top
level so the wire shape matches one /api/audit-log `entries` row exactly.
- Coordinator::emit_audit_entry helper (stamps seq like the others).
- audit_log::record now returns the canonical inserted AuditEntry (id + ts
assigned) so the streamed event is the same row that was stored — no
drift. Best-effort unchanged (None on a sqlite blip).
- handle_restart_infra records + emits for every attempt (ok/err/denied),
threading the coordinator through.
Tests: kind_tag round-trip now covers the new variant; added a flatten
test pinning the top-level wire shape (kind/seq/id/…/detail, no nesting).
Pairs with iris's audit view (the /dashboard/stream listener half).
This commit is contained in:
parent
9bd51a7440
commit
629f08a113
4 changed files with 122 additions and 19 deletions
|
|
@ -133,6 +133,13 @@ impl AuditLog {
|
|||
/// but never returned, so a transient blip never fails the underlying
|
||||
/// privileged action (the action already happened — losing its audit
|
||||
/// row is strictly less bad than failing the action retroactively).
|
||||
///
|
||||
/// Returns the inserted [`AuditEntry`] (with its assigned id +
|
||||
/// timestamp) on success, or `None` if the insert failed. The
|
||||
/// returned row is the canonical record — callers that also push a
|
||||
/// live event (e.g. the dashboard stream) emit *this* rather than
|
||||
/// re-deriving the fields, so the stored row and the streamed event
|
||||
/// can't drift.
|
||||
pub fn record(
|
||||
&self,
|
||||
agent: &str,
|
||||
|
|
@ -140,19 +147,31 @@ impl AuditLog {
|
|||
target: &str,
|
||||
outcome: AuditOutcome,
|
||||
detail: Option<&str>,
|
||||
) {
|
||||
) -> Option<AuditEntry> {
|
||||
let now = now_secs();
|
||||
let conn = self.conn.lock().unwrap();
|
||||
if let Err(e) = conn.execute(
|
||||
match conn.execute(
|
||||
"INSERT INTO audit_log (ts_unix, agent, action, target, outcome, detail)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![now, agent, action, target, outcome.as_str(), detail],
|
||||
) {
|
||||
tracing::warn!(
|
||||
%agent, %action, %target,
|
||||
error = ?e,
|
||||
"audit_log: record failed (dropping entry)"
|
||||
);
|
||||
Ok(_) => Some(AuditEntry {
|
||||
id: conn.last_insert_rowid(),
|
||||
ts_unix: now,
|
||||
agent: agent.to_owned(),
|
||||
action: action.to_owned(),
|
||||
target: target.to_owned(),
|
||||
outcome: outcome.as_str().to_owned(),
|
||||
detail: detail.map(str::to_owned),
|
||||
}),
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
%agent, %action, %target,
|
||||
error = ?e,
|
||||
"audit_log: record failed (dropping entry)"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -261,8 +280,15 @@ mod tests {
|
|||
#[test]
|
||||
fn record_and_list_newest_first() {
|
||||
let (_d, db) = tmpdb();
|
||||
db.record("atlas", "restart_infra", "hive-ci", AuditOutcome::Ok, None);
|
||||
db.record(
|
||||
// record() returns the canonical inserted row (id + ts assigned).
|
||||
let entry = db
|
||||
.record("atlas", "restart_infra", "hive-ci", AuditOutcome::Ok, None)
|
||||
.expect("record returns the inserted entry");
|
||||
assert!(entry.id > 0);
|
||||
assert_eq!(entry.target, "hive-ci");
|
||||
assert_eq!(entry.outcome, "ok");
|
||||
assert!(entry.detail.is_none());
|
||||
let _ = db.record(
|
||||
"atlas",
|
||||
"restart_infra",
|
||||
"hive-gateway",
|
||||
|
|
@ -286,7 +312,7 @@ mod tests {
|
|||
#[test]
|
||||
fn list_clamps_to_500() {
|
||||
let (_d, db) = tmpdb();
|
||||
db.record("a", "x", "t", AuditOutcome::Ok, None);
|
||||
let _ = db.record("a", "x", "t", AuditOutcome::Ok, None);
|
||||
let rows = db.list_recent(999_999).expect("list");
|
||||
assert!(rows.len() <= 500);
|
||||
}
|
||||
|
|
@ -294,7 +320,7 @@ mod tests {
|
|||
#[test]
|
||||
fn vacuum_drops_only_old_rows() {
|
||||
let (_d, db) = tmpdb();
|
||||
db.record("a", "restart_infra", "hive-ci", AuditOutcome::Ok, None);
|
||||
let _ = db.record("a", "restart_infra", "hive-ci", AuditOutcome::Ok, None);
|
||||
// Backdate it past the retention window.
|
||||
{
|
||||
let conn = db.conn.lock().unwrap();
|
||||
|
|
@ -304,7 +330,7 @@ mod tests {
|
|||
)
|
||||
.unwrap();
|
||||
}
|
||||
db.record("a", "restart_infra", "hive-forge", AuditOutcome::Ok, None);
|
||||
let _ = db.record("a", "restart_infra", "hive-forge", AuditOutcome::Ok, None);
|
||||
let removed = db.vacuum().expect("vacuum");
|
||||
assert_eq!(removed, 1, "only the backdated row should be reaped");
|
||||
let rows = db.list_recent(10).expect("list");
|
||||
|
|
|
|||
Loading…
Reference in a new issue