From d9c0878b00ed9220fba3e6dee409fc73317389a7 Mon Sep 17 00:00:00 2001 From: Jean Date: Tue, 14 Jul 2026 23:19:00 +0000 Subject: [PATCH] Log store write failures at the source; share the oper guard The store's map_err(|_| Internal) discarded the real IO error, so a disk-full or permissions failure was invisible; log it once in the log append path where it happens. Add echo_api::require_oper (parameterised by the required privilege) and route the three copied per-service guards through it, keeping each service's own wording and policy. --- api/src/lib.rs | 14 ++++++++++++++ modules/helpserv/src/lib.rs | 6 +----- modules/hostserv/src/lib.rs | 6 +----- modules/reportserv/src/lib.rs | 6 +----- src/engine/db/mod.rs | 7 ++++++- 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/api/src/lib.rs b/api/src/lib.rs index d6ca133..d9d5087 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -1175,6 +1175,20 @@ pub fn help(me: &str, from: &Sender, ctx: &mut ServiceCtx, blurb: &str, topics: } } +// Gate a command on operator privilege. `need` is the privilege required, or +// None for "any operator". Notices the user and returns false when denied, +// otherwise returns true. `what` names the action, e.g. "reviewing reports". +pub fn require_oper(me: &str, from: &Sender, ctx: &mut ServiceCtx, need: Option, what: &str) -> bool { + let ok = match need { + Some(p) => from.privs.has(p), + None => from.privs.any(), + }; + if !ok { + ctx.notice(me, from.uid, format!("Access denied — {what} is for services operators.")); + } + ok +} + // Format a Unix timestamp (seconds) as "YYYY-MM-DD HH:MM:SS UTC", using Howard // Hinnant's civil-from-days algorithm so no date crate is needed. pub fn human_time(ts: u64) -> String { diff --git a/modules/helpserv/src/lib.rs b/modules/helpserv/src/lib.rs index 2e96657..571c686 100644 --- a/modules/helpserv/src/lib.rs +++ b/modules/helpserv/src/lib.rs @@ -87,11 +87,7 @@ impl Service for HelpServ { // Working the help queue is operator-only. fn require_oper(me: &str, from: &Sender, ctx: &mut ServiceCtx) -> bool { - if from.privs.any() { - return true; - } - ctx.notice(me, from.uid, "Access denied — working the help queue is for services operators."); - false + echo_api::require_oper(me, from, ctx, None, "working the help queue") } // Claim ticket `id` for the calling operator (shared by TAKE and NEXT). diff --git a/modules/hostserv/src/lib.rs b/modules/hostserv/src/lib.rs index a8a64aa..85e9256 100644 --- a/modules/hostserv/src/lib.rs +++ b/modules/hostserv/src/lib.rs @@ -103,11 +103,7 @@ impl Service for HostServ { // Operator gate for vhost administration. fn require_oper(me: &str, from: &Sender, ctx: &mut ServiceCtx) -> bool { - if from.privs.has(Priv::Admin) { - return true; - } - ctx.notice(me, from.uid, "Access denied — assigning vhosts is for services operators."); - false + echo_api::require_oper(me, from, ctx, Some(Priv::Admin), "assigning vhosts") } // Canonicalise a vhost the way the ircd will actually display it: the host part diff --git a/modules/reportserv/src/lib.rs b/modules/reportserv/src/lib.rs index 0cb6a22..767a0d5 100644 --- a/modules/reportserv/src/lib.rs +++ b/modules/reportserv/src/lib.rs @@ -66,9 +66,5 @@ impl Service for ReportServ { // Reviewing the queue is for operators only. fn require_oper(me: &str, from: &Sender, ctx: &mut ServiceCtx) -> bool { - if from.privs.any() { - return true; - } - ctx.notice(me, from.uid, "Access denied — reviewing reports is for services operators."); - false + echo_api::require_oper(me, from, ctx, None, "reviewing reports") } diff --git a/src/engine/db/mod.rs b/src/engine/db/mod.rs index d43e022..f3d7a24 100644 --- a/src/engine/db/mod.rs +++ b/src/engine/db/mod.rs @@ -690,7 +690,12 @@ impl EventLog { } else { LogEntry { origin: self.origin.clone(), seq: 0, lamport: 0, event } }; - self.persist(&entry)?; + if let Err(e) = self.persist(&entry) { + // Surface the real cause (disk full, permissions, ...) here at the + // source; callers only see a generic Internal error further up. + tracing::error!(%e, path = %self.path.display(), "failed to write event to the log"); + return Err(e); + } if global { self.versions.insert(entry.origin.clone(), entry.seq); self.notify(&entry);