Files
buzz/crates/buzz-db/src/lib.rs
Brad Seiler 96af488d96 fix(media): revoke private channel attachment reads (#9077)
## Summary

Removed private-channel members can currently download attachments if
they know the URL. With `BUZZ_MEDIA_READ_MODE=enforce`, fresh GET, HEAD
and Range requests check current membership and readable references on
the primary database. Removed members receive 404; remaining members
retain access, and rejoining restores access. Thumbnail aliases use the
parent attachment’s permissions.

Publication checks also prevent copied private URLs from granting new
access. They cover attachments, link previews, profile pictures,
banners, group pictures and emoji. Persona creation and changed-avatar
edits, plus team and individual snapshot imports, transfer animated
posters and animations under the agent’s signer before profile
publication. Failed persona transfers preserve existing local records;
import failures appear in the results.

Authorization uses bounded queries, a two-second statement deadline and
transaction-local JIT suppression. An index on edit targets avoids broad
scans. Publications have a 512-reference limit, and capacity waits up to
500 ms before refusal.

Protection defaults off. Existing downloaded bytes cannot be recalled.
All modes stop creating immutable HTTP cache entries.

### Related issue

Related approaches: #7565, #5185, #8451 and #8444. This change evaluates
live stored references directly.

### Testing

Published head: `3d1aa7431cc495a001a0f13e95e2e422c5a77ab1`, rebased on
main `2cd40badb85de059a6916206e133da7a0a88fb2e`. Push hooks were
**SKIPPED** with explicit author approval. Current-head HTTP, full
package, PostgreSQL migration/media and profile-filter mutation checks
are pending; CI is pending. The earlier complete HTTP journey passed at
`f20b449262` (identical source tree), with its evidence attributed
separately. Earlier failing preview/report-fixture runs are preserved;
they do not count as passes.

The HTTP journey covers upload/publication, fresh GET/HEAD/Range
revocation and rejoin, emoji replacement, edited previews, persona
visibility and non-reference decoys. The migration regression forces an
index timeout, verifies no 0086 history or media tables, then proves a
migrator through the base's 0085 can restart.

Human attachment removal/rejoin testing passed on the pinned
`1f08a8e0fd2edf19dc94321f7258518149fe1bf6` trial. Native animated
persona edits and team/individual import acceptance remain incomplete.
That old-snapshot database stays intact and cannot run the reconciled
binary; a new trial needs a separate fresh database.

### Rollout prerequisites

- Build and verify the reference index before shadow mode. Small
databases can auto-migrate through one atomic 0086 migration within the
configured timeouts. It builds the index before creating the media
tables, so failure records no new migration version. Large deployments
must disable auto-migration and prebuild indexes concurrently.
- **Enforce remains blocked unless the operator can rule out SDK/CLI
attachment edits and the unverified agent-harness edit producer.**
SDK/CLI edits still omit attachment metadata. That behavior is deferred,
not fixed. Removing this blocker requires latest-effective attachment
metadata across successive edits, no restoration of removed files, and
an upload → successive edits → GET/HEAD regression.
- Validate client signer compatibility and split emoji publications
above 512 references before enforce mode.
- Complete the human workflows above in an isolated community with
enforce enabled.
- Follow the [index deployment
guide](https://github.com/block/buzz/blob/honor/private-channel-media/docs/private-media-index-deployment.md)
and [operator
guidance](https://github.com/block/buzz/blob/honor/private-channel-media/docs/operator-private-media.md)
for rollout and rollback.

The [prebuild timeout
finding](https://github.com/block/buzz/pull/9077#discussion_r4486221478)
and [avatar-transfer
finding](https://github.com/block/buzz/pull/9077#discussion_r4486221458)
are implemented and independently reviewed.

### Schema change checklist

- [x] Additive expand and code deployment; no backfill or contract
phase. Prebuild/verify before shadow under either schema deployment
path.
- [x] No SQL function, trigger, procedure, rule or foreign key added.
Signed events are untouched.
- [x] No migration mutates existing rows.
- [x] Migration and desired-state schema match. Catalog parity, target
parsing, future partition inheritance and upgrade refusal are covered.
- [x] Old/new binaries coexist for serving. Suspend community deletion
during expansion and while rolled back to a binary with the old deletion
catalog.
- [x] Keep auto-migration disabled on old binaries after schema
expansion; sqlx rejects unknown versions.
- [x] Migration lock timeout 1 s; statement timeout 5 s. Small populated
databases build inline within those budgets; large deployments use
concurrent child-index prebuild/attachment with auto-migration off.
Missing, invalid or wrong indexes fail verification.

Generated with Codex

---------

Signed-off-by: Brad Seiler <seiler@squareup.com>
Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
2026-11-06 22:35:00 +00:00

137 lines
5.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![deny(unsafe_code)]
#![warn(missing_docs)]
//! buzz-db — Postgres event store for Buzz.
//!
//! ## Design invariants
//! - AUTH events (kind 22242) are never stored — they carry bearer tokens.
//! - Ephemeral events (20000–29999) are never stored — Redis pub/sub only.
//! - Events table is partitioned by month on `created_at`.
//! - No FK references to partitioned tables.
//! - Uses `sqlx::query()` (runtime) not `sqlx::query!()` (compile-time).
//!
//! ## Runtime and store ownership
//! Database runtime infrastructure and domain persistence are physically
//! separated behind this crate-root compatibility facade:
//!
//! - Runtime concerns own pool construction, writer/replica routing,
//! transactions, sessions, metrics, health support, and migrations.
//! - Store concerns own domain-specific SQL, row mapping, locking, mutation
//! rules, indexes, and focused persistence tests.
//!
//! Existing crate-root modules, records, and [`Db`] methods remain the public
//! API. The internal `runtime` and `store` namespaces are not public APIs.
/// Wrap `$retire`, an `UPDATE`/`DELETE` on `events` that retires a
/// coordinate's previous head, so the same statement drops those rows' mention
/// index entries. Mention-index reads (`#p` filters and feeds) never return a
/// retired row, so its mention rows are dead weight they must walk and
/// discard: one per member for every kind:39002 roster republish. `$1` must
/// bind the community.
///
/// The three replacement paths that index mentions (NIP-33 parameterized,
/// addressable, and the relay's member snapshot) go through this, so a head
/// and its mention rows leave together on those paths.
macro_rules! superseded_with_mentions_sql {
($retire:literal) => {
concat!(
"WITH superseded AS (",
$retire,
" RETURNING id) \
DELETE FROM event_mentions mention USING superseded \
WHERE mention.community_id = $1 AND mention.event_id = superseded.id"
)
};
}
mod runtime;
mod store;
/// Concurrency caps whose limit can change at runtime.
pub mod concurrency_gate;
/// Database error types.
pub mod error;
#[cfg(test)]
mod test_support;
pub use runtime::{
insert_mentions, migration, replica_fence, AdmittedTx, ColdStartError, Db, DbConfig,
DbConnectionOutcome, DbConnectionStep, DbPoolRole, DbPoolStats, DbReadinessOutcome,
ReadSession,
};
/// Valid low-cardinality `(pool_role, operation)` pairs for pool-acquisition telemetry.
pub const DB_POOL_ACQUIRE_VALID_PAIRS: [(&str, &str); 11] =
runtime::observability::POOL_ACQUIRE_VALID_PAIRS;
/// Raw Prometheus series ceiling per relay pod for the operation-aware contract.
pub const DB_POOL_ACQUIRE_RAW_SERIES_PER_POD: usize =
runtime::observability::POOL_ACQUIRE_RAW_SERIES_PER_POD;
/// Valid database connection role/step pairs with a start counter.
pub const DB_CONNECTION_STARTED_STEPS: [(DbPoolRole, DbConnectionStep); 4] =
runtime::CONNECTION_STARTED_STEPS;
/// Valid database connection role/step pairs with a duration histogram.
pub const DB_CONNECTION_DURATION_STEPS: [(DbPoolRole, DbConnectionStep); 4] =
runtime::CONNECTION_DURATION_STEPS;
/// Valid database connection role/step/outcome terminal combinations.
pub const DB_CONNECTION_TERMINALS: [(DbPoolRole, DbConnectionStep, DbConnectionOutcome); 15] =
runtime::CONNECTION_TERMINALS;
/// Raw Prometheus series ceiling per pod for connection-step telemetry.
pub const DB_CONNECTION_RAW_SERIES_PER_POD: usize = runtime::CONNECTION_RAW_SERIES_PER_POD;
pub(crate) use runtime::{
begin_community_event_write_transaction,
begin_community_event_write_transaction_with_legacy_metrics, begin_community_sweep_transaction,
begin_community_write_transaction, insert_mentions_in_transaction, observability, route_proof,
sweep_admitted_communities, ReadSessionInner, RouteDecision, RoutePredicate,
};
pub use store::{
admin_moderation, allowlist, api_token, archived_identities, artifact, artifact_sync, channel,
channel_members, community, community_auth, deletion, dm, entity_deletion, event, feed,
git_repo, leader_lock, media, membership_snapshot, moderation, nip_fi_binding,
operator_listener, partition, personal_read, product_feedback, push, reaction, read_state,
relay_admin_actions, relay_invite, relay_members, relay_operators, reminder, replaceable,
storage_accounting, thread, thread_window, user, user_deletion, user_deletion_fences, workflow,
};
pub use allowlist::AllowlistEntry;
pub use api_token::{ApiTokenRecord, TokenSummary};
pub use community::{
ArchivedCommunityRecord, CommunityRecord, CreateCommunityWithOwnerResult,
CreatedCommunityRecord, EnsuredCommunityRecord, OwnedCommunityRecord, UnarchiveCommunityResult,
UnarchivedCommunityRecord,
};
pub use concurrency_gate::{ConcurrencyGate, GatePermit};
pub use error::{DbError, Result};
pub use event::{ChannelHeadPrecondition, ChannelHeadWriteStatus, DeletionTarget};
pub use event::{EventQuery, DEFAULT_MAX_PAGE_LIMIT};
pub use leader_lock::SessionLeader;
pub use nip_fi_binding::{NipFiBindingRecord, NipFiUnbind, RowCapExceeded};
pub use reaction::ReactionEventInsertOutcome;
pub use reminder::DueReminder;
pub use store::{ArtifactSnapshot, SnapshotDenied};
/// A value read from the database, with when its reading connection was
/// checked out of the pool.
///
/// The statement that produced `value` started after `read_at`, so its
/// snapshot is no older than `read_at`. A cache that serves `value` for a
/// bound after `read_at` is never staler than that bound, however long the
/// read waited for a connection.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReadAt<T> {
/// The value as read.
pub value: T,
/// When the connection that read `value` was checked out.
pub read_at: std::time::Instant,
}
use buzz_core::CommunityId;
/// Durable NIP-FI verified-profile projection state.
pub use store::verified_profiles;