upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.rs110
-rw-r--r--src/nostr/builder.rs32
-rw-r--r--src/nostr/policy/mod.rs4
3 files changed, 145 insertions, 1 deletions
diff --git a/src/config.rs b/src/config.rs
index 5f8cbca..a5e4344 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -244,6 +244,42 @@ impl Default for BlacklistConfig {
244 } 244 }
245} 245}
246 246
247/// Event blacklist configuration for blocking events by author npub
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct EventBlacklistConfig {
250 /// Blacklisted npubs - events from these authors are rejected
251 ///
252 /// If empty, no events are blacklisted by author.
253 /// Applies to ALL event types, preventing events from reaching both the relay and purgatory.
254 pub blacklisted_npubs: Vec<String>,
255}
256
257impl EventBlacklistConfig {
258 /// Check if event blacklist is enabled (non-empty blacklist)
259 pub fn enabled(&self) -> bool {
260 !self.blacklisted_npubs.is_empty()
261 }
262
263 /// Check if an event author is blacklisted
264 ///
265 /// Returns Some(reason) if blacklisted, None if not blacklisted.
266 pub fn check(&self, npub: &str) -> Option<String> {
267 if self.blacklisted_npubs.contains(&npub.to_string()) {
268 Some(format!("Event author {} is blacklisted", npub))
269 } else {
270 None
271 }
272 }
273}
274
275impl Default for EventBlacklistConfig {
276 fn default() -> Self {
277 Self {
278 blacklisted_npubs: Vec::new(),
279 }
280 }
281}
282
247/// Database backend type for the relay 283/// Database backend type for the relay
248#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, ValueEnum)] 284#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, ValueEnum)]
249#[serde(rename_all = "lowercase")] 285#[serde(rename_all = "lowercase")]
@@ -428,6 +464,11 @@ pub struct Config {
428 /// Blacklist takes precedence over all whitelists (archive and repository) 464 /// Blacklist takes precedence over all whitelists (archive and repository)
429 #[arg(long, env = "NGIT_REPOSITORY_BLACKLIST", default_value = "")] 465 #[arg(long, env = "NGIT_REPOSITORY_BLACKLIST", default_value = "")]
430 pub repository_blacklist: String, 466 pub repository_blacklist: String,
467
468 /// Event blacklist: comma-separated list of npubs whose events are rejected
469 /// All events from these authors are blocked from both relay storage and purgatory
470 #[arg(long, env = "NGIT_EVENT_BLACKLIST", default_value = "")]
471 pub event_blacklist: String,
431} 472}
432 473
433impl Config { 474impl Config {
@@ -612,6 +653,20 @@ impl Config {
612 BlacklistConfig { blacklist } 653 BlacklistConfig { blacklist }
613 } 654 }
614 655
656 /// Get parsed event blacklist configuration
657 ///
658 /// This method assumes config has been validated - call Config::validate() first!
659 pub fn event_blacklist_config(&self) -> EventBlacklistConfig {
660 let blacklisted_npubs: Vec<String> = self
661 .event_blacklist
662 .split(',')
663 .map(|s| s.trim())
664 .filter(|s| !s.is_empty())
665 .map(|s| s.to_string())
666 .collect();
667 EventBlacklistConfig { blacklisted_npubs }
668 }
669
615 /// Create config for testing 670 /// Create config for testing
616 #[cfg(test)] 671 #[cfg(test)]
617 pub fn for_testing() -> Self { 672 pub fn for_testing() -> Self {
@@ -647,6 +702,7 @@ impl Config {
647 archive_read_only: None, 702 archive_read_only: None,
648 repository_whitelist: String::new(), 703 repository_whitelist: String::new(),
649 repository_blacklist: String::new(), 704 repository_blacklist: String::new(),
705 event_blacklist: String::new(),
650 } 706 }
651 } 707 }
652} 708}
@@ -1248,4 +1304,58 @@ mod tests {
1248 let result = config.check(&test_npub, "allowed-repo"); 1304 let result = config.check(&test_npub, "allowed-repo");
1249 assert!(result.is_none()); 1305 assert!(result.is_none());
1250 } 1306 }
1307
1308 #[test]
1309 fn test_event_blacklist_config_parsing() {
1310 let keys1 = Keys::generate();
1311 let keys2 = Keys::generate();
1312 let npub1 = keys1.public_key().to_bech32().unwrap();
1313 let npub2 = keys2.public_key().to_bech32().unwrap();
1314 let config = Config {
1315 event_blacklist: format!("{},{}", npub1, npub2),
1316 ..Config::for_testing()
1317 };
1318 let event_blacklist_config = config.event_blacklist_config();
1319 assert_eq!(event_blacklist_config.blacklisted_npubs.len(), 2);
1320 assert!(event_blacklist_config.enabled());
1321 assert!(event_blacklist_config.blacklisted_npubs.contains(&npub1));
1322 assert!(event_blacklist_config.blacklisted_npubs.contains(&npub2));
1323 }
1324
1325 #[test]
1326 fn test_event_blacklist_config_empty() {
1327 let config = Config::for_testing();
1328 let event_blacklist_config = config.event_blacklist_config();
1329 assert!(event_blacklist_config.blacklisted_npubs.is_empty());
1330 assert!(!event_blacklist_config.enabled());
1331 }
1332
1333 #[test]
1334 fn test_event_blacklist_check_blacklisted() {
1335 let keys = Keys::generate();
1336 let test_npub = keys.public_key().to_bech32().unwrap();
1337 let config = EventBlacklistConfig {
1338 blacklisted_npubs: vec![test_npub.clone()],
1339 };
1340
1341 let result = config.check(&test_npub);
1342 assert!(result.is_some());
1343 let reason = result.unwrap();
1344 assert!(reason.contains("author"));
1345 assert!(reason.contains(&test_npub));
1346 }
1347
1348 #[test]
1349 fn test_event_blacklist_check_not_blacklisted() {
1350 let keys1 = Keys::generate();
1351 let keys2 = Keys::generate();
1352 let banned_npub = keys1.public_key().to_bech32().unwrap();
1353 let allowed_npub = keys2.public_key().to_bech32().unwrap();
1354 let config = EventBlacklistConfig {
1355 blacklisted_npubs: vec![banned_npub],
1356 };
1357
1358 let result = config.check(&allowed_npub);
1359 assert!(result.is_none());
1360 }
1251} 1361}
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs
index 9819e37..c2de1df 100644
--- a/src/nostr/builder.rs
+++ b/src/nostr/builder.rs
@@ -56,7 +56,13 @@ impl Nip34WritePolicy {
56 purgatory: std::sync::Arc<crate::purgatory::Purgatory>, 56 purgatory: std::sync::Arc<crate::purgatory::Purgatory>,
57 config: crate::config::Config, 57 config: crate::config::Config,
58 ) -> Self { 58 ) -> Self {
59 let ctx = PolicyContext::new(&config.domain, database, git_data_path, purgatory); 59 let ctx = PolicyContext::new(
60 &config.domain,
61 database,
62 git_data_path,
63 purgatory,
64 config.clone(),
65 );
60 Self { 66 Self {
61 announcement_policy: AnnouncementPolicy::new(ctx.clone(), config.clone()), 67 announcement_policy: AnnouncementPolicy::new(ctx.clone(), config.clone()),
62 state_policy: StatePolicy::new(ctx.clone()), 68 state_policy: StatePolicy::new(ctx.clone()),
@@ -66,6 +72,19 @@ impl Nip34WritePolicy {
66 } 72 }
67 } 73 }
68 74
75 /// Check if an event author is blacklisted
76 ///
77 /// Returns Some(reason) if blacklisted, None if not blacklisted.
78 fn check_event_blacklist(&self, event: &Event) -> Option<String> {
79 let event_blacklist = self.ctx.config.event_blacklist_config();
80 if !event_blacklist.enabled() {
81 return None;
82 }
83
84 let npub = event.pubkey.to_bech32().ok()?;
85 event_blacklist.check(&npub)
86 }
87
69 /// Get a reference to the purgatory for read-only access 88 /// Get a reference to the purgatory for read-only access
70 pub fn purgatory(&self) -> &std::sync::Arc<crate::purgatory::Purgatory> { 89 pub fn purgatory(&self) -> &std::sync::Arc<crate::purgatory::Purgatory> {
71 &self.ctx.purgatory 90 &self.ctx.purgatory
@@ -474,6 +493,17 @@ impl WritePolicy for Nip34WritePolicy {
474 addr: &'a SocketAddr, 493 addr: &'a SocketAddr,
475 ) -> BoxedFuture<'a, WritePolicyResult> { 494 ) -> BoxedFuture<'a, WritePolicyResult> {
476 Box::pin(async move { 495 Box::pin(async move {
496 // Check event blacklist FIRST - it overrides everything
497 if let Some(reason) = self.check_event_blacklist(event) {
498 tracing::debug!(
499 event_id = %event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex()),
500 author = %event.pubkey.to_hex(),
501 reason = %reason,
502 "Rejected event from blacklisted author"
503 );
504 return WritePolicyResult::reject(reason);
505 }
506
477 // Detect if this is a synced event (from proactive sync) vs user-submitted 507 // Detect if this is a synced event (from proactive sync) vs user-submitted
478 // Sync uses localhost:0 as a dummy address 508 // Sync uses localhost:0 as a dummy address
479 let is_synced = addr.ip().is_loopback() && addr.port() == 0; 509 let is_synced = addr.ip().is_loopback() && addr.port() == 0;
diff --git a/src/nostr/policy/mod.rs b/src/nostr/policy/mod.rs
index dc023a9..1566b6c 100644
--- a/src/nostr/policy/mod.rs
+++ b/src/nostr/policy/mod.rs
@@ -32,6 +32,8 @@ pub struct PolicyContext {
32 pub purgatory: Arc<Purgatory>, 32 pub purgatory: Arc<Purgatory>,
33 /// Local relay for notifying WebSocket subscribers (set after relay creation) 33 /// Local relay for notifying WebSocket subscribers (set after relay creation)
34 pub local_relay: Arc<std::sync::RwLock<Option<LocalRelay>>>, 34 pub local_relay: Arc<std::sync::RwLock<Option<LocalRelay>>>,
35 /// Configuration reference for policy settings (includes blacklists)
36 pub config: crate::config::Config,
35} 37}
36 38
37impl PolicyContext { 39impl PolicyContext {
@@ -40,6 +42,7 @@ impl PolicyContext {
40 database: SharedDatabase, 42 database: SharedDatabase,
41 git_data_path: impl Into<std::path::PathBuf>, 43 git_data_path: impl Into<std::path::PathBuf>,
42 purgatory: Arc<Purgatory>, 44 purgatory: Arc<Purgatory>,
45 config: crate::config::Config,
43 ) -> Self { 46 ) -> Self {
44 Self { 47 Self {
45 domain: domain.into(), 48 domain: domain.into(),
@@ -47,6 +50,7 @@ impl PolicyContext {
47 git_data_path: git_data_path.into(), 50 git_data_path: git_data_path.into(),
48 purgatory, 51 purgatory,
49 local_relay: Arc::new(std::sync::RwLock::new(None)), 52 local_relay: Arc::new(std::sync::RwLock::new(None)),
53 config,
50 } 54 }
51 } 55 }
52 56