mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(search): exclude p-gated membership notices from FTS
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
parent
46ba39e45a
commit
a3f407ce96
@@ -6,7 +6,10 @@
|
||||
//! migration into it, exercises a scenario, and drops it. Tests are
|
||||
//! parallel-safe.
|
||||
|
||||
use buzz_core::{kind::AUTHOR_ONLY_KINDS, CommunityId};
|
||||
use buzz_core::{
|
||||
kind::{AUTHOR_ONLY_KINDS, KIND_MEMBER_ADDED_NOTIFICATION, KIND_MEMBER_REMOVED_NOTIFICATION},
|
||||
CommunityId,
|
||||
};
|
||||
use buzz_search::{ChannelScope, SearchQuery, SearchService};
|
||||
use sqlx::{postgres::PgPoolOptions, Executor, PgPool};
|
||||
use uuid::Uuid;
|
||||
@@ -798,13 +801,15 @@ async fn very_long_query_is_bounded_before_pg_parse() {
|
||||
/// - 1059 = `KIND_GIFT_WRAP` (NIP-17 ciphertext)
|
||||
/// - 30300 = `KIND_EVENT_REMINDER` (in `AUTHOR_ONLY_KINDS`)
|
||||
/// - 30622 = `KIND_DM_VISIBILITY` (per-viewer private hide state)
|
||||
/// - 44100 = `KIND_MEMBER_ADDED_NOTIFICATION` (p-gated membership notice)
|
||||
/// - 44101 = `KIND_MEMBER_REMOVED_NOTIFICATION` (p-gated membership notice)
|
||||
///
|
||||
/// All four events are inserted with the same unique token in their content
|
||||
/// All six events are inserted with the same unique token in their content
|
||||
/// so a single search query exercises every kind in one round-trip. Only
|
||||
/// the kind:9 control must surface — the three excluded kinds must not.
|
||||
/// the kind:9 control must surface — the excluded kinds must not.
|
||||
///
|
||||
/// Mutate-bite: drop the `CASE WHEN kind IN (…)` from the generated column
|
||||
/// (revert to `to_tsvector('simple', content)`) → all four events surface →
|
||||
/// (revert to `to_tsvector('simple', content)`) → excluded events surface →
|
||||
/// restore.
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
@@ -866,6 +871,32 @@ async fn excluded_kinds_are_storage_level_unsearchable() {
|
||||
)
|
||||
.await;
|
||||
|
||||
// kind:44100 member-added notification — p-gated and MUST NOT be searchable.
|
||||
insert_event(
|
||||
&pool,
|
||||
c,
|
||||
rand_bytes32(),
|
||||
rand_bytes32(),
|
||||
KIND_MEMBER_ADDED_NOTIFICATION as i32,
|
||||
&format!("member added — {token}"),
|
||||
None,
|
||||
1_700_000_004,
|
||||
)
|
||||
.await;
|
||||
|
||||
// kind:44101 member-removed notification — p-gated and MUST NOT be searchable.
|
||||
insert_event(
|
||||
&pool,
|
||||
c,
|
||||
rand_bytes32(),
|
||||
rand_bytes32(),
|
||||
KIND_MEMBER_REMOVED_NOTIFICATION as i32,
|
||||
&format!("member removed — {token}"),
|
||||
None,
|
||||
1_700_000_005,
|
||||
)
|
||||
.await;
|
||||
|
||||
let svc = SearchService::new(pool.clone());
|
||||
let result = svc
|
||||
.search(&SearchQuery {
|
||||
@@ -891,7 +922,13 @@ async fn excluded_kinds_are_storage_level_unsearchable() {
|
||||
);
|
||||
|
||||
// Negative (load-bearing): each excluded kind MUST NOT surface.
|
||||
for forbidden in [1059, 30300, 30622] {
|
||||
for forbidden in [
|
||||
1059,
|
||||
30300,
|
||||
30622,
|
||||
KIND_MEMBER_ADDED_NOTIFICATION as i32,
|
||||
KIND_MEMBER_REMOVED_NOTIFICATION as i32,
|
||||
] {
|
||||
assert!(
|
||||
!kinds.contains(&forbidden),
|
||||
"kind:{forbidden} MUST NOT be searchable — \
|
||||
|
||||
@@ -209,14 +209,18 @@ CREATE TABLE events (
|
||||
-- 1059 = KIND_GIFT_WRAP (NIP-17 ciphertext)
|
||||
-- 30300 = KIND_EVENT_REMINDER (AUTHOR_ONLY_KINDS — defense in depth)
|
||||
-- 30622 = KIND_DM_VISIBILITY (per-viewer private hide state)
|
||||
-- 44100 = KIND_MEMBER_ADDED_NOTIFICATION (p-gated membership notice)
|
||||
-- 44101 = KIND_MEMBER_REMOVED_NOTIFICATION (p-gated membership notice)
|
||||
-- NULL tsvector never matches `@@`, so excluded rows are storage-level
|
||||
-- unsearchable. Constants kept in `buzz_core::kind` (KIND_GIFT_WRAP,
|
||||
-- KIND_EVENT_REMINDER, KIND_DM_VISIBILITY); inlined here because a sqlx
|
||||
-- KIND_EVENT_REMINDER, KIND_DM_VISIBILITY,
|
||||
-- KIND_MEMBER_ADDED_NOTIFICATION, KIND_MEMBER_REMOVED_NOTIFICATION); inlined
|
||||
-- here because a sqlx
|
||||
-- migration is frozen SQL and cannot import the Rust constant. If a new
|
||||
-- privacy-sensitive kind is added there, update this list and add a
|
||||
-- regression test in `buzz-search/tests/fts_integration.rs`.
|
||||
search_tsv TSVECTOR GENERATED ALWAYS AS (
|
||||
CASE WHEN kind IN (1059, 30300, 30622) THEN NULL::tsvector
|
||||
CASE WHEN kind IN (1059, 30300, 30622, 44100, 44101) THEN NULL::tsvector
|
||||
ELSE to_tsvector('simple', content)
|
||||
END
|
||||
) STORED,
|
||||
|
||||
+4
-3
@@ -203,11 +203,12 @@ CREATE TABLE events (
|
||||
-- community-leading btree filters BitmapAnd-ed with the GIN probe, so the
|
||||
-- GIN index itself stays the minimal `GIN (search_tsv)` (Max's caveat:
|
||||
-- avoid btree_gin unless EXPLAIN proves it buys something).
|
||||
-- Privacy: encrypted/private routing wrappers must never be discoverable
|
||||
-- through NIP-50 full-text search. NULL tsvector never matches `@@`.
|
||||
-- Privacy: encrypted/private routing wrappers and p-gated membership notices
|
||||
-- must never be discoverable through NIP-50 full-text search. NULL tsvector
|
||||
-- never matches `@@`.
|
||||
-- Keep in sync with migrations/0001_initial_schema.sql.
|
||||
search_tsv TSVECTOR GENERATED ALWAYS AS (
|
||||
CASE WHEN kind IN (1059, 30300, 30622) THEN NULL::tsvector
|
||||
CASE WHEN kind IN (1059, 30300, 30622, 44100, 44101) THEN NULL::tsvector
|
||||
ELSE to_tsvector('simple', content)
|
||||
END
|
||||
) STORED,
|
||||
|
||||
Reference in New Issue
Block a user