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.rs143
-rw-r--r--src/nostr/events.rs187
2 files changed, 330 insertions, 0 deletions
diff --git a/src/config.rs b/src/config.rs
index 37b1c1e..5f8cbca 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -195,6 +195,55 @@ impl Default for RepositoryConfig {
195 } 195 }
196} 196}
197 197
198/// Repository blacklist configuration
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct BlacklistConfig {
201 /// Blacklist entries for blocking specific repositories
202 ///
203 /// If empty, no repositories are blacklisted.
204 /// Blacklist takes precedence over both archive and repository whitelists.
205 pub blacklist: Vec<WhitelistEntry>,
206}
207
208impl BlacklistConfig {
209 /// Check if repository blacklist is enabled (non-empty blacklist)
210 pub fn enabled(&self) -> bool {
211 !self.blacklist.is_empty()
212 }
213
214 /// Check if an announcement matches the repository blacklist
215 ///
216 /// Returns Some(reason) if blacklisted, None if not blacklisted.
217 /// The reason indicates what type of match occurred (npub, npub/identifier, or identifier).
218 pub fn check(&self, npub: &str, identifier: &str) -> Option<String> {
219 for entry in &self.blacklist {
220 if entry.matches(npub, identifier) {
221 let reason = match entry {
222 WhitelistEntry::Pubkey(_) => {
223 format!("Repository owner {} is blacklisted", npub)
224 }
225 WhitelistEntry::Repository { .. } => {
226 format!("Repository {}/{} is blacklisted", npub, identifier)
227 }
228 WhitelistEntry::Identifier(_) => {
229 format!("Repository identifier {} is blacklisted", identifier)
230 }
231 };
232 return Some(reason);
233 }
234 }
235 None
236 }
237}
238
239impl Default for BlacklistConfig {
240 fn default() -> Self {
241 Self {
242 blacklist: Vec::new(),
243 }
244 }
245}
246
198/// Database backend type for the relay 247/// Database backend type for the relay
199#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, ValueEnum)] 248#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, ValueEnum)]
200#[serde(rename_all = "lowercase")] 249#[serde(rename_all = "lowercase")]
@@ -373,6 +422,12 @@ pub struct Config {
373 /// When set, only announcements matching the whitelist AND listing the service are accepted 422 /// When set, only announcements matching the whitelist AND listing the service are accepted
374 #[arg(long, env = "NGIT_REPOSITORY_WHITELIST", default_value = "")] 423 #[arg(long, env = "NGIT_REPOSITORY_WHITELIST", default_value = "")]
375 pub repository_whitelist: String, 424 pub repository_whitelist: String,
425
426 /// Repository blacklist: comma-separated list of npub/identifier/npub/identifier entries to reject
427 /// Formats: "npub1...", "npub1.../identifier", "identifier"
428 /// Blacklist takes precedence over all whitelists (archive and repository)
429 #[arg(long, env = "NGIT_REPOSITORY_BLACKLIST", default_value = "")]
430 pub repository_blacklist: String,
376} 431}
377 432
378impl Config { 433impl Config {
@@ -549,6 +604,14 @@ impl Config {
549 RepositoryConfig { whitelist } 604 RepositoryConfig { whitelist }
550 } 605 }
551 606
607 /// Get parsed repository blacklist configuration
608 ///
609 /// This method assumes config has been validated - call Config::validate() first!
610 pub fn blacklist_config(&self) -> BlacklistConfig {
611 let blacklist = WhitelistEntry::parse_whitelist(&self.repository_blacklist);
612 BlacklistConfig { blacklist }
613 }
614
552 /// Create config for testing 615 /// Create config for testing
553 #[cfg(test)] 616 #[cfg(test)]
554 pub fn for_testing() -> Self { 617 pub fn for_testing() -> Self {
@@ -583,6 +646,7 @@ impl Config {
583 archive_whitelist: String::new(), 646 archive_whitelist: String::new(),
584 archive_read_only: None, 647 archive_read_only: None,
585 repository_whitelist: String::new(), 648 repository_whitelist: String::new(),
649 repository_blacklist: String::new(),
586 } 650 }
587 } 651 }
588} 652}
@@ -1105,4 +1169,83 @@ mod tests {
1105 .to_string() 1169 .to_string()
1106 .contains("relay_owner_nsec not set")); 1170 .contains("relay_owner_nsec not set"));
1107 } 1171 }
1172
1173 #[test]
1174 fn test_blacklist_config_parsing() {
1175 let keys = Keys::generate();
1176 let test_npub = keys.public_key().to_bech32().unwrap();
1177 let config = Config {
1178 repository_blacklist: format!("{},bitcoin-core", test_npub),
1179 ..Config::for_testing()
1180 };
1181 let blacklist_config = config.blacklist_config();
1182 assert_eq!(blacklist_config.blacklist.len(), 2);
1183 assert!(blacklist_config.enabled());
1184 }
1185
1186 #[test]
1187 fn test_blacklist_config_empty() {
1188 let config = Config::for_testing();
1189 let blacklist_config = config.blacklist_config();
1190 assert!(blacklist_config.blacklist.is_empty());
1191 assert!(!blacklist_config.enabled());
1192 }
1193
1194 #[test]
1195 fn test_blacklist_check_npub() {
1196 let keys = Keys::generate();
1197 let test_npub = keys.public_key().to_bech32().unwrap();
1198 let config = BlacklistConfig {
1199 blacklist: vec![WhitelistEntry::Pubkey(test_npub.clone())],
1200 };
1201
1202 let result = config.check(&test_npub, "any-repo");
1203 assert!(result.is_some());
1204 let reason = result.unwrap();
1205 assert!(reason.contains("owner"));
1206 assert!(reason.contains(&test_npub));
1207 }
1208
1209 #[test]
1210 fn test_blacklist_check_identifier() {
1211 let config = BlacklistConfig {
1212 blacklist: vec![WhitelistEntry::Identifier("banned-repo".to_string())],
1213 };
1214
1215 let result = config.check("npub1alice", "banned-repo");
1216 assert!(result.is_some());
1217 let reason = result.unwrap();
1218 assert!(reason.contains("identifier"));
1219 assert!(reason.contains("banned-repo"));
1220 }
1221
1222 #[test]
1223 fn test_blacklist_check_repository() {
1224 let keys = Keys::generate();
1225 let test_npub = keys.public_key().to_bech32().unwrap();
1226 let config = BlacklistConfig {
1227 blacklist: vec![WhitelistEntry::Repository {
1228 npub: test_npub.clone(),
1229 identifier: "specific-repo".to_string(),
1230 }],
1231 };
1232
1233 let result = config.check(&test_npub, "specific-repo");
1234 assert!(result.is_some());
1235 let reason = result.unwrap();
1236 assert!(reason.contains(&test_npub));
1237 assert!(reason.contains("specific-repo"));
1238 }
1239
1240 #[test]
1241 fn test_blacklist_check_not_blacklisted() {
1242 let keys = Keys::generate();
1243 let test_npub = keys.public_key().to_bech32().unwrap();
1244 let config = BlacklistConfig {
1245 blacklist: vec![WhitelistEntry::Identifier("banned-repo".to_string())],
1246 };
1247
1248 let result = config.check(&test_npub, "allowed-repo");
1249 assert!(result.is_none());
1250 }
1108} 1251}
diff --git a/src/nostr/events.rs b/src/nostr/events.rs
index 3b4ef25..39014da 100644
--- a/src/nostr/events.rs
+++ b/src/nostr/events.rs
@@ -366,6 +366,9 @@ impl RepositoryState {
366/// - AcceptArchive: Announcement matches archive config (GRASP-05) 366/// - AcceptArchive: Announcement matches archive config (GRASP-05)
367/// - Reject: Validation failed 367/// - Reject: Validation failed
368/// 368///
369/// Blacklist takes precedence over all whitelists:
370/// - If blacklisted, always reject with specific reason (npub/identifier/npub+identifier)
371///
369/// When archive_read_only is true: 372/// When archive_read_only is true:
370/// - ONLY accept announcements matching archive whitelist/all 373/// - ONLY accept announcements matching archive whitelist/all
371/// - REJECT announcements listing our service but not in whitelist (read-only sync mode) 374/// - REJECT announcements listing our service but not in whitelist (read-only sync mode)
@@ -403,10 +406,16 @@ pub fn validate_announcement(
403 // Get validated configs (config.validate() must be called at startup) 406 // Get validated configs (config.validate() must be called at startup)
404 let archive_config = config.archive_config(); 407 let archive_config = config.archive_config();
405 let repository_config = config.repository_config(); 408 let repository_config = config.repository_config();
409 let blacklist_config = config.blacklist_config();
406 410
407 let npub = announcement.owner_npub(); 411 let npub = announcement.owner_npub();
408 let lists_service = announcement.lists_service(&config.domain); 412 let lists_service = announcement.lists_service(&config.domain);
409 413
414 // Check blacklist FIRST - it overrides everything
415 if let Some(reason) = blacklist_config.check(&npub, &announcement.identifier) {
416 return AnnouncementResult::Reject(reason);
417 }
418
410 // GRASP-01: Normal mode - accept if announcement lists our service AND matches repository whitelist (if enabled) 419 // GRASP-01: Normal mode - accept if announcement lists our service AND matches repository whitelist (if enabled)
411 if lists_service && !archive_config.read_only { 420 if lists_service && !archive_config.read_only {
412 // Check repository whitelist if enabled 421 // Check repository whitelist if enabled
@@ -1309,4 +1318,182 @@ mod tests {
1309 let result = validate_announcement(&event, &config); 1318 let result = validate_announcement(&event, &config);
1310 assert!(matches!(result, AnnouncementResult::Reject(_))); 1319 assert!(matches!(result, AnnouncementResult::Reject(_)));
1311 } 1320 }
1321
1322 #[test]
1323 fn test_blacklist_rejects_npub() {
1324 use crate::config::Config;
1325 use crate::nostr::policy::AnnouncementResult;
1326
1327 let keys = create_test_keys();
1328 let npub = keys.public_key().to_bech32().unwrap();
1329
1330 // Create announcement that lists our service
1331 let event = create_announcement_event(
1332 &keys,
1333 "test-repo",
1334 vec!["https://gitnostr.com/alice/test-repo.git"],
1335 vec!["wss://gitnostr.com"],
1336 );
1337
1338 // Config with blacklist for this npub
1339 let config = Config {
1340 domain: "gitnostr.com".to_string(),
1341 repository_blacklist: npub.clone(),
1342 ..Config::for_testing()
1343 };
1344
1345 let result = validate_announcement(&event, &config);
1346 if let AnnouncementResult::Reject(reason) = result {
1347 assert!(reason.contains("owner"));
1348 assert!(reason.contains(&npub));
1349 } else {
1350 panic!("Expected Reject, got {:?}", result);
1351 }
1352 }
1353
1354 #[test]
1355 fn test_blacklist_rejects_identifier() {
1356 use crate::config::Config;
1357 use crate::nostr::policy::AnnouncementResult;
1358
1359 let keys = create_test_keys();
1360
1361 // Create announcement that lists our service
1362 let event = create_announcement_event(
1363 &keys,
1364 "banned-repo",
1365 vec!["https://gitnostr.com/alice/banned-repo.git"],
1366 vec!["wss://gitnostr.com"],
1367 );
1368
1369 // Config with blacklist for this identifier
1370 let config = Config {
1371 domain: "gitnostr.com".to_string(),
1372 repository_blacklist: "banned-repo".to_string(),
1373 ..Config::for_testing()
1374 };
1375
1376 let result = validate_announcement(&event, &config);
1377 if let AnnouncementResult::Reject(reason) = result {
1378 assert!(reason.contains("identifier"));
1379 assert!(reason.contains("banned-repo"));
1380 } else {
1381 panic!("Expected Reject, got {:?}", result);
1382 }
1383 }
1384
1385 #[test]
1386 fn test_blacklist_rejects_specific_repository() {
1387 use crate::config::Config;
1388 use crate::nostr::policy::AnnouncementResult;
1389
1390 let keys = create_test_keys();
1391 let npub = keys.public_key().to_bech32().unwrap();
1392
1393 // Create announcement that lists our service
1394 let event = create_announcement_event(
1395 &keys,
1396 "specific-repo",
1397 vec!["https://gitnostr.com/alice/specific-repo.git"],
1398 vec!["wss://gitnostr.com"],
1399 );
1400
1401 // Config with blacklist for this specific repo
1402 let config = Config {
1403 domain: "gitnostr.com".to_string(),
1404 repository_blacklist: format!("{}/specific-repo", npub),
1405 ..Config::for_testing()
1406 };
1407
1408 let result = validate_announcement(&event, &config);
1409 if let AnnouncementResult::Reject(reason) = result {
1410 assert!(reason.contains(&npub));
1411 assert!(reason.contains("specific-repo"));
1412 } else {
1413 panic!("Expected Reject, got {:?}", result);
1414 }
1415 }
1416
1417 #[test]
1418 fn test_blacklist_overrides_repository_whitelist() {
1419 use crate::config::Config;
1420 use crate::nostr::policy::AnnouncementResult;
1421
1422 let keys = create_test_keys();
1423 let npub = keys.public_key().to_bech32().unwrap();
1424
1425 // Create announcement that lists our service
1426 let event = create_announcement_event(
1427 &keys,
1428 "test-repo",
1429 vec!["https://gitnostr.com/alice/test-repo.git"],
1430 vec!["wss://gitnostr.com"],
1431 );
1432
1433 // Config with both whitelist and blacklist - blacklist should win
1434 let config = Config {
1435 domain: "gitnostr.com".to_string(),
1436 repository_whitelist: npub.clone(),
1437 repository_blacklist: npub.clone(),
1438 ..Config::for_testing()
1439 };
1440
1441 let result = validate_announcement(&event, &config);
1442 assert!(matches!(result, AnnouncementResult::Reject(_)));
1443 }
1444
1445 #[test]
1446 fn test_blacklist_overrides_archive_whitelist() {
1447 use crate::config::Config;
1448 use crate::nostr::policy::AnnouncementResult;
1449
1450 let keys = create_test_keys();
1451 let npub = keys.public_key().to_bech32().unwrap();
1452
1453 // Create announcement that does NOT list our service
1454 let event = create_announcement_event(
1455 &keys,
1456 "test-repo",
1457 vec!["https://other-service.com/alice/test-repo.git"],
1458 vec!["wss://other-service.com"],
1459 );
1460
1461 // Config with archive whitelist and blacklist - blacklist should win
1462 let config = Config {
1463 domain: "gitnostr.com".to_string(),
1464 archive_whitelist: npub.clone(),
1465 archive_read_only: Some(false),
1466 repository_blacklist: npub.clone(),
1467 ..Config::for_testing()
1468 };
1469
1470 let result = validate_announcement(&event, &config);
1471 assert!(matches!(result, AnnouncementResult::Reject(_)));
1472 }
1473
1474 #[test]
1475 fn test_blacklist_allows_non_blacklisted() {
1476 use crate::config::Config;
1477 use crate::nostr::policy::AnnouncementResult;
1478
1479 let keys = create_test_keys();
1480
1481 // Create announcement that lists our service
1482 let event = create_announcement_event(
1483 &keys,
1484 "allowed-repo",
1485 vec!["https://gitnostr.com/alice/allowed-repo.git"],
1486 vec!["wss://gitnostr.com"],
1487 );
1488
1489 // Config with blacklist for different identifier
1490 let config = Config {
1491 domain: "gitnostr.com".to_string(),
1492 repository_blacklist: "banned-repo".to_string(),
1493 ..Config::for_testing()
1494 };
1495
1496 let result = validate_announcement(&event, &config);
1497 assert!(matches!(result, AnnouncementResult::Accept));
1498 }
1312} 1499}