diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-12 21:32:38 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-12 21:33:15 +0000 |
| commit | 70c577f10bbe150b6b13bec545dc8720ad005a64 (patch) | |
| tree | 4f390cd523248db007ecb4335a61598b930ccad9 /src/nostr | |
| parent | 1948312d40f34fca868d1ef6d6d94e165c09738c (diff) | |
feat(config): add repository blacklist to block specific repos/npubs/identifiers
Adds NGIT_REPOSITORY_BLACKLIST option for blocking repositories, taking precedence
over all whitelists (archive and repository) to enable moderation without affecting
curation policy.
Key features:
- Three blacklist formats: <npub>, <npub>/<identifier>, <identifier>
- Blacklist checked first before any other validation
- Overrides archive whitelist and repository whitelist
- Specific rejection reasons based on match type (npub/identifier/both)
- Not flagged in NIP-11 curation (operational, not policy)
Implementation:
- Add BlacklistConfig struct with check() method returning detailed reasons
- Add NGIT_REPOSITORY_BLACKLIST config option and blacklist_config() method
- Update validate_announcement() to check blacklist first with specific reasons
- 12 new unit tests covering all blacklist behavior and precedence
Configuration synced across all four sources:
- src/config.rs: Core implementation with BlacklistConfig
- .env.example: Comprehensive documentation with examples
- docs/reference/configuration.md: Complete reference documentation
- nix/module.nix: NixOS module option with environment mapping
Testing:
- 12 new tests for blacklist functionality (config + validation)
- All 332 library tests passing
- All 38 integration tests passing
Use cases:
- Block spam/malware repos by identifier
- Block abusive users by npub
- Block specific problematic repos by npub/identifier
- Temporary blocks for investigation
Diffstat (limited to 'src/nostr')
| -rw-r--r-- | src/nostr/events.rs | 187 |
1 files changed, 187 insertions, 0 deletions
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 | } |