mirror of
https://github.com/block/buzz.git
synced 2026-11-01 15:01:12 +00:00
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>
73 lines
3.4 KiB
Rust
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,
|
|
}
|