Files
buzz/crates/buzz-audit/src/entry.rs
tlongwell-block fec1834caf feat(audit): widen NewAuditEntry.community_id to CommunityId
Make the provenance fence visible in the type signature, not a
per-call-site convention. `NewAuditEntry.community_id` becomes
`CommunityId` (the server-resolved newtype) instead of a raw `Uuid`,
so a wiring call site can no longer pass an arbitrary UUID off the
event/channel being acted on — the only doors to a `CommunityId` are
host resolution or a server-scoped DB row, never client input.

The DB-row type `AuditEntry` stays `Uuid`: sqlx reads/writes it directly
and `compute_hash` does `.as_bytes()` on it, so the stored hash bytes are
byte-for-byte identical and the already-integrated chain stays valid — no
migration, no re-hash. The `as_uuid()` dereference moves inside
`AuditService::log` at the DB boundary, where the column is written; the
advisory-lock key is unchanged (CommunityId's Display delegates to Uuid).

Drop the now-orphaned `Serialize`/`Deserialize` derive (and the
`#[serde(default)]` on `detail`) from `NewAuditEntry`: it has no
serde consumer — it travels only through the in-process audit sink
(mpsc), never a wire/DB boundary. Keeping it non-deserializable
reinforces the fence: no client blob can mint a NewAuditEntry.

Full package green (13/13, incl. the 6 PG isolation tests and the
community_id_is_part_of_identity fence); clippy -D warnings + fmt clean.
Adversarially verified the fence is non-vacuous: dropping community_id
from compute_hash turns community_id_is_part_of_identity RED, restored.

(cherry picked from commit 284cc699b2b04c9078456ffda849315fb4562763)

Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
2026-06-26 20:36:59 -04:00

73 lines
3.4 KiB
Rust

use buzz_core::CommunityId;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::action::AuditAction;
/// A materialised audit log entry as stored in `audit_log`.
///
/// Rows are keyed `(community_id, seq)`: `seq` is monotonic *within one
/// community*, and `prev_hash` chains to the previous entry *of the same
/// community*. The chain is independent per tenant.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuditEntry {
/// Server-resolved community this entry belongs to. Leads the primary key.
pub community_id: Uuid,
/// Sequence number, monotonic within `community_id` (starts at 1).
pub seq: i64,
/// SHA-256 of this entry's fields including `community_id` and `prev_hash`.
pub hash: Vec<u8>,
/// SHA-256 of the previous entry in *this community's* chain, or `None` for
/// the community's first entry (hashed as [`crate::hash::GENESIS_HASH`]).
pub prev_hash: Option<Vec<u8>>,
/// Action that was performed.
pub action: AuditAction,
/// Raw bytes of the actor's Nostr pubkey, if the action has one.
pub actor_pubkey: Option<Vec<u8>>,
/// Generic identifier of the object acted upon (event id hex, channel UUID,
/// media sha256, …), if any. The relay resolves it under `community_id`;
/// it never names an object in another community.
pub object_id: Option<String>,
/// Arbitrary JSON context. **Included in the hash** (serialized with sorted
/// keys for determinism) so tampering with it is detectable.
pub detail: serde_json::Value,
/// When the entry was recorded.
pub created_at: DateTime<Utc>,
}
/// Input for appending a new audit entry. `seq`, `prev_hash`, `hash`, and
/// `created_at` are assigned by [`crate::service::AuditService::log`].
///
/// `community_id` is the **server-resolved** tenant (from the request's
/// `TenantContext`), never a client-supplied value — the same provenance rule
/// the whole multi-tenant model rests on.
///
/// Not `Serialize`/`Deserialize`: this is an in-process input struct (consumed
/// by `AuditService::log`, threaded through the in-memory audit sink), never
/// crossing a wire or DB boundary as a whole. Keeping it non-deserializable
/// reinforces the fence — there is no path by which a client-supplied blob
/// becomes a `NewAuditEntry` (and thus a `CommunityId`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NewAuditEntry {
/// Server-resolved community this entry belongs to. Typed as [`CommunityId`]
/// (not a raw `Uuid`) so the provenance rule is visible in the signature:
/// the only ways to obtain one are host resolution or a server-scoped DB
/// row — never a value parsed from client input.
pub community_id: CommunityId,
/// Action that was performed.
pub action: AuditAction,
/// Raw bytes of the actor's Nostr pubkey, if the action has one.
pub actor_pubkey: Option<Vec<u8>>,
/// Generic identifier of the object acted upon, if any.
pub object_id: Option<String>,
/// Arbitrary JSON context included in the hash.
///
/// **Never bearer-token material.** This field is opaque to the audit
/// crate and persisted verbatim; callers must not write tokens, passwords,
/// or other secrets here. `AuthSuccess`/`AuthFailure` entries carry only
/// outcome metadata — the token has no slot in this type, and `detail` must
/// not become one.
pub detail: serde_json::Value,
}