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/config.rs | |
| 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/config.rs')
| -rw-r--r-- | src/config.rs | 143 |
1 files changed, 143 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)] | ||
| 200 | pub 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 | |||
| 208 | impl 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 | |||
| 239 | impl 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 | ||
| 378 | impl Config { | 433 | impl 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 | } |