Files
buzz/crates/buzz-audit/src/error.rs
T
+1 14fba21e57 Multi-tenant Buzz relay: community_id as a server-resolved key (comprehensive rewrite) (#1321)
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Quinn <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: Sami <sami@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
2026-06-29 12:39:02 -04:00

109 lines
4.3 KiB
Rust

use thiserror::Error;
/// Errors that can occur during audit log operations.
///
/// These are **operator-internal** diagnostics (logged by the audit worker, or
/// returned to an operator-scoped verification call) — they are never relayed to
/// a client on the wire. Even so, no variant embeds a `community_id` or any
/// cross-community object identifier: a `seq` is per-community and meaningless
/// without its chain, and hashes are opaque. An error raised while verifying
/// community A's chain therefore cannot reveal a fact about community B.
#[derive(Debug, Error)]
pub enum AuditError {
/// A database operation failed.
#[error("database error: {0}")]
Database(#[from] sqlx::Error),
/// The `prev_hash` of an entry does not match the hash of the preceding
/// entry in the same community's chain.
#[error(
"hash chain integrity violation at seq {seq}: prev_hash does not match preceding entry"
)]
ChainViolation {
/// Per-community sequence number of the offending entry.
seq: i64,
},
/// The stored hash of an entry does not match the recomputed hash.
#[error("hash mismatch at seq {seq}: stored hash does not match recomputed hash")]
HashMismatch {
/// Per-community sequence number of the offending entry.
seq: i64,
},
/// An unrecognised action string was found in the database.
#[error("unknown audit action in database")]
UnknownAction,
/// A JSON serialization error occurred (e.g. while canonicalising `detail`).
#[error("serialization error: {0}")]
Serialization(#[from] serde_json::Error),
}
#[cfg(test)]
mod tests {
use super::*;
/// The sanitization obligation for the conformance `audit_log` row: an error
/// raised while verifying or appending to one community's chain must not let
/// its rendered text become a cross-community identifier — no `community_id`,
/// no constraint name. Only `seq` may appear, and `seq` is per-community and
/// meaningless without the chain it indexes.
///
/// This is the *complement* to the structural fence in the variant
/// definitions above: those variants simply have no `community_id` field, so
/// there is no slot to leak one from. This test pins the observable form —
/// if anyone adds a `community_id` to a variant and threads it into the
/// `#[error(...)]` format string, the assertion below reds.
#[test]
fn audit_error_text_carries_no_community_id_or_constraint() {
// A concrete community whose chain is "being verified" when these errors
// fire. If its id leaked into any error text, the error would identify a
// specific tenant.
let community = uuid::Uuid::new_v4();
let community_str = community.to_string();
let community_simple = community.simple().to_string();
// The variants the audit crate constructs itself with chain-derived data.
let domain_errors = [
AuditError::ChainViolation { seq: 7 },
AuditError::HashMismatch { seq: 42 },
AuditError::UnknownAction,
];
for err in &domain_errors {
let text = err.to_string();
// No form of the community id may appear.
assert!(
!text.contains(&community_str) && !text.contains(&community_simple),
"audit error text leaked a community_id: {text:?}"
);
// No Postgres constraint/PK names that would reveal schema shape or
// the existence of a cross-community key.
for needle in [
"community_id",
"audit_log_pkey",
"constraint",
"communities",
] {
assert!(
!text.to_ascii_lowercase().contains(needle),
"audit error text leaked a constraint/identifier '{needle}': {text:?}"
);
}
}
// The two chain-integrity variants must still carry their per-community
// `seq` (the diagnostic is useless without it) — proves the assertion
// above isn't vacuously passing on empty strings.
assert!(AuditError::ChainViolation { seq: 7 }
.to_string()
.contains('7'));
assert!(AuditError::HashMismatch { seq: 42 }
.to_string()
.contains("42"));
}
}