diff options
| -rw-r--r-- | .env.example | 25 | ||||
| -rw-r--r-- | README.md | 100 | ||||
| -rw-r--r-- | docs/reference/configuration.md | 92 | ||||
| -rw-r--r-- | nix/module.nix | 14 | ||||
| -rw-r--r-- | src/config.rs | 110 | ||||
| -rw-r--r-- | src/nostr/builder.rs | 32 | ||||
| -rw-r--r-- | src/nostr/policy/mod.rs | 4 |
7 files changed, 374 insertions, 3 deletions
diff --git a/.env.example b/.env.example index 993399a..fb954c0 100644 --- a/.env.example +++ b/.env.example | |||
| @@ -254,4 +254,27 @@ | |||
| 254 | # NGIT_REPOSITORY_BLACKLIST=npub1spam... | 254 | # NGIT_REPOSITORY_BLACKLIST=npub1spam... |
| 255 | # NGIT_REPOSITORY_BLACKLIST=npub1alice.../bad-repo | 255 | # NGIT_REPOSITORY_BLACKLIST=npub1alice.../bad-repo |
| 256 | # NGIT_REPOSITORY_BLACKLIST=malware-repo,spam-repo | 256 | # NGIT_REPOSITORY_BLACKLIST=malware-repo,spam-repo |
| 257 | # NGIT_REPOSITORY_BLACKLIST= \ No newline at end of file | 257 | # NGIT_REPOSITORY_BLACKLIST= |
| 258 | |||
| 259 | # ============================================================================ | ||
| 260 | # EVENT BLACKLIST | ||
| 261 | # ============================================================================ | ||
| 262 | |||
| 263 | # Blacklist events from specific authors (npubs) | ||
| 264 | # Comma-separated list of npubs whose events are rejected | ||
| 265 | # ALL events from these authors are blocked from both relay storage and purgatory | ||
| 266 | # | ||
| 267 | # Event blacklist takes precedence over ALL other validation: | ||
| 268 | # - Blacklisted events are rejected before any other policy checks | ||
| 269 | # - Applies to announcements, state events, PRs, and all other event types | ||
| 270 | # - Events never reach purgatory (rejected immediately) | ||
| 271 | # | ||
| 272 | # Rejection reason: | ||
| 273 | # - "Event author <npub> is blacklisted" | ||
| 274 | # | ||
| 275 | # CLI: --event-blacklist <list> | ||
| 276 | # Default: (empty - no events are blacklisted by author) | ||
| 277 | # Examples: | ||
| 278 | # NGIT_EVENT_BLACKLIST=npub1spam... | ||
| 279 | # NGIT_EVENT_BLACKLIST=npub1spam...,npub1abuser... | ||
| 280 | # NGIT_EVENT_BLACKLIST= \ No newline at end of file | ||
| @@ -36,7 +36,7 @@ Unlike the reference implementation ([ngit-relay](https://gitworkshop.dev/npub15 | |||
| 36 | - **Pure Rust Implementation**: Single binary, no external dependencies beyond Git itself | 36 | - **Pure Rust Implementation**: Single binary, no external dependencies beyond Git itself |
| 37 | - **Integrated Authorization**: Push validation happens inline during the Git receive-pack operation | 37 | - **Integrated Authorization**: Push validation happens inline during the Git receive-pack operation |
| 38 | - **GRASP-01 Compliant**: Core service requirements for Git hosting with Nostr authorization | 38 | - **GRASP-01 Compliant**: Core service requirements for Git hosting with Nostr authorization |
| 39 | - **Repository Whitelist/Blacklist**: Optional curation via pubkey/identifier whitelist (GRASP-01 mode) and blacklist (overrides all whitelists) | 39 | - **Flexible Curation & Moderation**: Repository whitelists (GRASP-01 mode), repository blacklists (moderation), and event blacklists (author blocking) |
| 40 | - **GRASP-02 Proactive Sync**: Sophisticated relay-to-relay event and git data synchronization | 40 | - **GRASP-02 Proactive Sync**: Sophisticated relay-to-relay event and git data synchronization |
| 41 | - **NIP-77 Negentropy**: Efficient set reconciliation with automatic fallback to REQ+EOSE | 41 | - **NIP-77 Negentropy**: Efficient set reconciliation with automatic fallback to REQ+EOSE |
| 42 | - **Live & Historic Sync**: Real-time event streaming plus catch-up for past events | 42 | - **Live & Historic Sync**: Real-time event streaming plus catch-up for past events |
| @@ -150,6 +150,93 @@ See [GRASP-02 Proactive Sync](docs/explanation/grasp-02-proactive-sync.md) for f | |||
| 150 | 150 | ||
| 151 | **See**: [GRASP-05 Archive Mode](docs/explanation/grasp-05-archive.md) | 151 | **See**: [GRASP-05 Archive Mode](docs/explanation/grasp-05-archive.md) |
| 152 | 152 | ||
| 153 | ## Curation & Moderation | ||
| 154 | |||
| 155 | ngit-grasp provides flexible tools for both curation (repository selection) and moderation (blocking spam/abuse): | ||
| 156 | |||
| 157 | ### Repository Whitelists (Curation) | ||
| 158 | |||
| 159 | Control which repositories your relay accepts via two independent whitelist modes: | ||
| 160 | |||
| 161 | **Repository Whitelist (GRASP-01 Mode):** | ||
| 162 | - Only accept announcements that **both** list your service AND match the whitelist | ||
| 163 | - Three formats: `<npub>`, `<npub>/<identifier>`, `<identifier>` | ||
| 164 | - Environment: `NGIT_REPOSITORY_WHITELIST=npub1alice...,bitcoin-core` | ||
| 165 | - Use case: Curated relay accepting specific projects/developers | ||
| 166 | |||
| 167 | **Archive Whitelist (GRASP-05 Mode):** | ||
| 168 | - Accept announcements matching the whitelist **even if they don't list your service** | ||
| 169 | - Same three formats as repository whitelist | ||
| 170 | - Environment: `NGIT_ARCHIVE_WHITELIST=npub1satoshi...,linux` | ||
| 171 | - Use case: Backup/mirror relay for critical projects | ||
| 172 | - Default: Read-only mode (`NGIT_ARCHIVE_READ_ONLY=true`) | ||
| 173 | |||
| 174 | Both whitelists support flexible matching: | ||
| 175 | ```bash | ||
| 176 | # Accept all repos from specific developer | ||
| 177 | NGIT_REPOSITORY_WHITELIST=npub1alice... | ||
| 178 | |||
| 179 | # Accept specific repository | ||
| 180 | NGIT_REPOSITORY_WHITELIST=npub1alice.../my-project | ||
| 181 | |||
| 182 | # Accept repos with specific identifier (any author) | ||
| 183 | NGIT_REPOSITORY_WHITELIST=bitcoin-core | ||
| 184 | ``` | ||
| 185 | |||
| 186 | ### Blacklists (Moderation) | ||
| 187 | |||
| 188 | Block unwanted content without affecting your curation policy: | ||
| 189 | |||
| 190 | **Repository Blacklist:** | ||
| 191 | - Block specific repositories/developers/identifiers | ||
| 192 | - **Takes precedence over ALL whitelists** (checked first) | ||
| 193 | - Three formats: `<npub>`, `<npub>/<identifier>`, `<identifier>` | ||
| 194 | - Environment: `NGIT_REPOSITORY_BLACKLIST=npub1spam...,malware-repo` | ||
| 195 | - Use case: Block spam/malware repos while maintaining whitelist curation | ||
| 196 | |||
| 197 | **Event Blacklist:** | ||
| 198 | - Block **ALL events** from specific authors (npubs) | ||
| 199 | - **Takes precedence over ALL other validation** (checked first) | ||
| 200 | - Applies to all event types: announcements, state events, PRs, comments, etc. | ||
| 201 | - Events never reach relay storage or purgatory | ||
| 202 | - Environment: `NGIT_EVENT_BLACKLIST=npub1spammer...,npub1abuser...` | ||
| 203 | - Use case: Block abusive users completely | ||
| 204 | |||
| 205 | ### Precedence & Interaction | ||
| 206 | |||
| 207 | Validation order (from first to last): | ||
| 208 | |||
| 209 | 1. **Event Blacklist** → Reject if author is blacklisted (ALL event types) | ||
| 210 | 2. **Repository Blacklist** → Reject if repository/npub/identifier is blacklisted (announcements only) | ||
| 211 | 3. **Repository Whitelist** → Accept if announcement lists service AND matches whitelist | ||
| 212 | 4. **Archive Whitelist** → Accept if announcement matches whitelist (even without listing service) | ||
| 213 | 5. **Default GRASP-01** → Accept if announcement lists service (no whitelist configured) | ||
| 214 | |||
| 215 | Examples: | ||
| 216 | ```bash | ||
| 217 | # Curated relay blocking spam | ||
| 218 | NGIT_REPOSITORY_WHITELIST=npub1alice...,npub1bob... | ||
| 219 | NGIT_REPOSITORY_BLACKLIST=npub1alice.../spam-repo | ||
| 220 | NGIT_EVENT_BLACKLIST=npub1spammer... | ||
| 221 | # Result: Accept Alice & Bob's repos EXCEPT Alice's spam-repo, block all events from spammer | ||
| 222 | |||
| 223 | # Archive relay with moderation | ||
| 224 | NGIT_ARCHIVE_WHITELIST=bitcoin-core,linux | ||
| 225 | NGIT_EVENT_BLACKLIST=npub1abuser... | ||
| 226 | # Result: Mirror bitcoin-core and linux projects, block all events from abuser | ||
| 227 | |||
| 228 | # Public relay with spam protection | ||
| 229 | NGIT_EVENT_BLACKLIST=npub1spam1...,npub1spam2... | ||
| 230 | # Result: Accept all GRASP-01 repos, block all events from spammers | ||
| 231 | ``` | ||
| 232 | |||
| 233 | **Privacy & Transparency:** | ||
| 234 | - Blacklists are **not advertised** in NIP-11 metadata (operational, not curation policy) | ||
| 235 | - Rejected events receive specific error messages for operator debugging | ||
| 236 | - No client-visible indication that blacklists are in use | ||
| 237 | |||
| 238 | **See**: [Configuration Reference](docs/reference/configuration.md) for complete details | ||
| 239 | |||
| 153 | ## Roadmap | 240 | ## Roadmap |
| 154 | 241 | ||
| 155 | ### GRASP-02 Enhancements | 242 | ### GRASP-02 Enhancements |
| @@ -326,6 +413,17 @@ NGIT_RELAY_OWNER_NSEC=nsec1... ngit-grasp --domain relay.example.com | |||
| 326 | | Disable negentropy | `--sync-disable-negentropy` | `NGIT_SYNC_DISABLE_NEGENTROPY` | `false` | | 413 | | Disable negentropy | `--sync-disable-negentropy` | `NGIT_SYNC_DISABLE_NEGENTROPY` | `false` | |
| 327 | | Batch window | N/A | `NGIT_SYNC_BATCH_WINDOW_MS` | `5000` ms | | 414 | | Batch window | N/A | `NGIT_SYNC_BATCH_WINDOW_MS` | `5000` ms | |
| 328 | 415 | ||
| 416 | #### Curation & Moderation Settings | ||
| 417 | |||
| 418 | | Option | CLI Flag | Environment Variable | Default | | ||
| 419 | | -------------------- | --------------------------- | ------------------------------ | --------- | | ||
| 420 | | Repository whitelist | `--repository-whitelist` | `NGIT_REPOSITORY_WHITELIST` | (empty) | | ||
| 421 | | Archive whitelist | `--archive-whitelist` | `NGIT_ARCHIVE_WHITELIST` | (empty) | | ||
| 422 | | Archive all | `--archive-all` | `NGIT_ARCHIVE_ALL` | `false` | | ||
| 423 | | Archive read-only | `--archive-read-only` | `NGIT_ARCHIVE_READ_ONLY` | (auto) | | ||
| 424 | | Repository blacklist | `--repository-blacklist` | `NGIT_REPOSITORY_BLACKLIST` | (empty) | | ||
| 425 | | Event blacklist | `--event-blacklist` | `NGIT_EVENT_BLACKLIST` | (empty) | | ||
| 426 | |||
| 329 | **Sync Notes:** | 427 | **Sync Notes:** |
| 330 | 428 | ||
| 331 | - **Bootstrap relay**: Optional starting point for relay discovery. System automatically discovers additional relays from repository announcements. URL scheme is optional - if not provided, `wss://` is assumed (e.g., `git.shakespeare.diy` → `wss://git.shakespeare.diy`). | 429 | - **Bootstrap relay**: Optional starting point for relay discovery. System automatically discovers additional relays from repository announcements. URL scheme is optional - if not provided, `wss://` is assumed (e.g., `git.shakespeare.diy` → `wss://git.shakespeare.diy`). |
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index b90686e..66f39f1 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md | |||
| @@ -833,6 +833,98 @@ Blacklist does **not** affect NIP-11 metadata: | |||
| 833 | 833 | ||
| 834 | --- | 834 | --- |
| 835 | 835 | ||
| 836 | ### Event Blacklist | ||
| 837 | |||
| 838 | #### `NGIT_EVENT_BLACKLIST` | ||
| 839 | |||
| 840 | **Description:** Blacklist events from specific authors (npubs) | ||
| 841 | **Type:** Comma-separated list of npubs | ||
| 842 | **Default:** Empty (no events are blacklisted by author) | ||
| 843 | **Required:** No | ||
| 844 | |||
| 845 | **Format:** | ||
| 846 | - `npub1...` - Block all events from this author | ||
| 847 | |||
| 848 | **Precedence:** Event blacklist takes precedence over **ALL** other validation: | ||
| 849 | - Blacklisted events are rejected **before** any other policy checks | ||
| 850 | - Applies to all event types (announcements, state events, PRs, etc.) | ||
| 851 | - Events never reach purgatory (rejected immediately) | ||
| 852 | - Overrides repository blacklist, whitelists, and all other policies | ||
| 853 | |||
| 854 | **Examples:** | ||
| 855 | |||
| 856 | ```bash | ||
| 857 | # Block all events from specific author | ||
| 858 | NGIT_EVENT_BLACKLIST=npub1spam... | ||
| 859 | |||
| 860 | # Block events from multiple authors | ||
| 861 | NGIT_EVENT_BLACKLIST=npub1spam...,npub1abuser...,npub1troll... | ||
| 862 | ``` | ||
| 863 | |||
| 864 | **Rejection Reason:** | ||
| 865 | |||
| 866 | The event blacklist provides a specific rejection reason: | ||
| 867 | - **Format:** `"Event author <npub> is blacklisted"` | ||
| 868 | |||
| 869 | This reason helps operators understand why an event was rejected without needing to flag it in metadata. | ||
| 870 | |||
| 871 | **Behavior:** | ||
| 872 | |||
| 873 | Event blacklist is checked **first** before all other validation: | ||
| 874 | 1. Check event blacklist → Reject if author is blacklisted | ||
| 875 | 2. Check repository blacklist (for announcements) → Reject if matched | ||
| 876 | 3. Check event-type specific policies → Accept/Reject based on policy | ||
| 877 | 4. Process event normally | ||
| 878 | |||
| 879 | **Use Cases:** | ||
| 880 | |||
| 881 | ```bash | ||
| 882 | # Block spam/abusive users | ||
| 883 | NGIT_EVENT_BLACKLIST=npub1spammer...,npub1abuser... | ||
| 884 | |||
| 885 | # Block malicious actors | ||
| 886 | NGIT_EVENT_BLACKLIST=npub1malware...,npub1phisher... | ||
| 887 | |||
| 888 | # Temporary block for investigation | ||
| 889 | NGIT_EVENT_BLACKLIST=npub1suspicious... | ||
| 890 | ``` | ||
| 891 | |||
| 892 | **Comparison with Repository Blacklist:** | ||
| 893 | |||
| 894 | | Configuration | Scope | Checked When | Applies To | | ||
| 895 | |---------------|-------|--------------|------------| | ||
| 896 | | Event Blacklist | Author-based | **First** (before all policies) | **All events** from author | | ||
| 897 | | Repository Blacklist | Repo-based | Second (announcements only) | Specific repositories | | ||
| 898 | |||
| 899 | **Event Blacklist vs Repository Blacklist:** | ||
| 900 | |||
| 901 | ```bash | ||
| 902 | # Scenario: npub1alice is event-blacklisted | ||
| 903 | NGIT_EVENT_BLACKLIST=npub1alice... | ||
| 904 | |||
| 905 | # Result: | ||
| 906 | # - ALL events from npub1alice are rejected (announcements, PRs, etc.) | ||
| 907 | # - Events never reach relay or purgatory | ||
| 908 | # - Rejection: "Event author npub1alice... is blacklisted" | ||
| 909 | |||
| 910 | # Scenario: npub1alice/repo is repository-blacklisted | ||
| 911 | NGIT_REPOSITORY_BLACKLIST=npub1alice.../malware | ||
| 912 | |||
| 913 | # Result: | ||
| 914 | # - Only announcements for npub1alice.../malware are rejected | ||
| 915 | # - Other events from npub1alice are still processed normally | ||
| 916 | # - PRs/state events for different repos from npub1alice are accepted | ||
| 917 | ``` | ||
| 918 | |||
| 919 | **NIP-11 Impact:** | ||
| 920 | |||
| 921 | Event blacklist does **not** affect NIP-11 metadata: | ||
| 922 | - No `curation` field changes (blacklist is operational, not policy) | ||
| 923 | - Blacklist is transparent to clients (rejected with specific reason) | ||
| 924 | - Operators can use blacklist without advertising moderation | ||
| 925 | |||
| 926 | --- | ||
| 927 | |||
| 836 | ### Logging Configuration | 928 | ### Logging Configuration |
| 837 | 929 | ||
| 838 | #### `RUST_LOG` | 930 | #### `RUST_LOG` |
diff --git a/nix/module.nix b/nix/module.nix index cfac0fc..799ae2d 100644 --- a/nix/module.nix +++ b/nix/module.nix | |||
| @@ -237,6 +237,19 @@ let | |||
| 237 | ''; | 237 | ''; |
| 238 | }; | 238 | }; |
| 239 | 239 | ||
| 240 | eventBlacklist = mkOption { | ||
| 241 | type = types.listOf types.str; | ||
| 242 | default = [ ]; | ||
| 243 | example = [ "npub1spam..." "npub1abuser..." ]; | ||
| 244 | description = '' | ||
| 245 | Event blacklist for blocking all events from specific authors (npubs). | ||
| 246 | Takes precedence over ALL other validation (checked first). | ||
| 247 | ALL events from these authors are rejected from relay storage and purgatory. | ||
| 248 | Applies to announcements, state events, PRs, and all other event types. | ||
| 249 | Does not affect NIP-11 metadata (operational, not curation policy). | ||
| 250 | ''; | ||
| 251 | }; | ||
| 252 | |||
| 240 | user = mkOption { | 253 | user = mkOption { |
| 241 | type = types.str; | 254 | type = types.str; |
| 242 | default = "ngit-grasp-${name}"; | 255 | default = "ngit-grasp-${name}"; |
| @@ -281,6 +294,7 @@ let | |||
| 281 | NGIT_ARCHIVE_WHITELIST = concatStringsSep "," cfg.archiveWhitelist; | 294 | NGIT_ARCHIVE_WHITELIST = concatStringsSep "," cfg.archiveWhitelist; |
| 282 | NGIT_REPOSITORY_WHITELIST = concatStringsSep "," cfg.repositoryWhitelist; | 295 | NGIT_REPOSITORY_WHITELIST = concatStringsSep "," cfg.repositoryWhitelist; |
| 283 | NGIT_REPOSITORY_BLACKLIST = concatStringsSep "," cfg.repositoryBlacklist; | 296 | NGIT_REPOSITORY_BLACKLIST = concatStringsSep "," cfg.repositoryBlacklist; |
| 297 | NGIT_EVENT_BLACKLIST = concatStringsSep "," cfg.eventBlacklist; | ||
| 284 | RUST_LOG = cfg.logLevel; | 298 | RUST_LOG = cfg.logLevel; |
| 285 | } // optionalAttrs (cfg.relayName != null) { | 299 | } // optionalAttrs (cfg.relayName != null) { |
| 286 | NGIT_RELAY_NAME = cfg.relayName; | 300 | NGIT_RELAY_NAME = cfg.relayName; |
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)] | ||
| 249 | pub 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 | |||
| 257 | impl 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 | |||
| 275 | impl 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 | ||
| 433 | impl Config { | 474 | impl 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 | ||
| 37 | impl PolicyContext { | 39 | impl 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 | ||