upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/nostr/builder.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-01-09 17:04:06 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-09 17:04:06 +0000
commit5ecd8d6a434f97da94daef2f59166086fbaf5a6b (patch)
tree54c7d3b953a6b1aedd1db6b9a719e18131659df5 /src/nostr/builder.rs
parent895359aeb6746b98ff82944e4fca503f4a6e5439 (diff)
feat: implement state event authorization per GRASP-01 spec
Add comprehensive authorization checks to ensure state events are only accepted from maintainers of accepted repository announcements. This implements the core GRASP-01 requirement that pushes must match the latest state announcement "respecting the maintainer set." Changes: 1. StatePolicy authorization (src/nostr/policy/state.rs): - Check authorization BEFORE git data validation (fail-fast) - Reject if no announcement exists for repository - Reject if author not in maintainer set - Use existing helpers: fetch_repository_data() and pubkey_authorised_for_repo_owners() - Structured logging for all rejections 2. Purgatory invalidation (src/nostr/builder.rs): - New method: check_purgatory_state_events_for_identifier() - Called when announcements accepted (Accept and AcceptMaintainer) - Re-evaluates state events in purgatory for the identifier - Processes newly-authorized events (releases from purgatory) - Keeps unauthorized events for natural expiry (30 min) - Enables retroactive authorization when announcements arrive late 3. Purgatory sync authorization (src/git/sync.rs): - Check authorization BEFORE processing git data - Remove unauthorized events from purgatory (permanent rejection) - Prevents processing even if git data arrives first - Structured logging for monitoring 4. Rejected events tracking (src/sync/rejected_index.rs): - Add support for tracking rejected state events - New methods: add_state(), contains_state() - Separate metrics for state rejections - Enables sync to avoid re-fetching rejected states 5. Sync metrics (src/sync/metrics.rs, src/sync/mod.rs): - Add state-specific metrics (hot cache, cold index) - Track rejected states separately from announcements - Support monitoring of authorization rejections 6. Comprehensive tests (tests/state_authorization.rs): - test_reject_state_without_announcement - test_reject_state_from_unauthorized_author - test_accept_state_from_announcement_author - test_accept_state_from_maintainer Security Impact: - Before: State events could be published by anyone - After: Only maintainers can publish state events - Defense-in-depth: Authorization checked at 3 points: 1. On arrival (StatePolicy) 2. On announcement acceptance (purgatory re-evaluation) 3. On git data arrival (purgatory sync) All tests pass: - 248 unit tests - 51 NIP-34 announcement tests - 4 new state authorization tests - 9 rejected index tests Closes: State authorization requirement from GRASP-01 spec
Diffstat (limited to 'src/nostr/builder.rs')
-rw-r--r--src/nostr/builder.rs72
1 files changed, 72 insertions, 0 deletions
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs
index 939ccef..acaac71 100644
--- a/src/nostr/builder.rs
+++ b/src/nostr/builder.rs
@@ -102,6 +102,11 @@ impl Nip34WritePolicy {
102 } 102 }
103 103
104 tracing::debug!("Accepted repository announcement: {}", event_id_str); 104 tracing::debug!("Accepted repository announcement: {}", event_id_str);
105
106 // Check purgatory for state events that might now be authorized
107 self.check_purgatory_state_events_for_identifier(&announcement.identifier)
108 .await;
109
105 WritePolicyResult::Accept 110 WritePolicyResult::Accept
106 } 111 }
107 Err(e) => { 112 Err(e) => {
@@ -125,6 +130,11 @@ impl Nip34WritePolicy {
125 announcement.identifier 130 announcement.identifier
126 ); 131 );
127 // Don't create bare repository for external announcements 132 // Don't create bare repository for external announcements
133
134 // Check purgatory for state events that might now be authorized
135 self.check_purgatory_state_events_for_identifier(&announcement.identifier)
136 .await;
137
128 WritePolicyResult::Accept 138 WritePolicyResult::Accept
129 } 139 }
130 Err(e) => { 140 Err(e) => {
@@ -304,6 +314,68 @@ impl Nip34WritePolicy {
304 } 314 }
305 } 315 }
306 316
317 /// Check purgatory for state events that might now be authorized by a new announcement
318 ///
319 /// When an announcement is accepted, state events in purgatory that were previously
320 /// rejected due to missing announcements might now be authorized. This method:
321 /// 1. Finds all state events in purgatory for the identifier
322 /// 2. Re-evaluates authorization for each event
323 /// 3. Processes authorized events (releases from purgatory)
324 /// 4. Keeps unauthorized events in purgatory (will expire naturally)
325 async fn check_purgatory_state_events_for_identifier(&self, identifier: &str) {
326 let state_events = self.ctx.purgatory.find_state(identifier);
327
328 if state_events.is_empty() {
329 return;
330 }
331
332 tracing::debug!(
333 identifier = %identifier,
334 count = state_events.len(),
335 "Checking purgatory state events after announcement acceptance"
336 );
337
338 for entry in state_events {
339 // Re-evaluate authorization with the new announcement
340 match self.state_policy.process_state_event(&entry.event, false).await {
341 Ok(WritePolicyResult::Accept) => {
342 tracing::info!(
343 event_id = %entry.event.id,
344 identifier = %identifier,
345 "State event in purgatory now authorized, will be processed"
346 );
347 // Event will be automatically removed from purgatory by process_state_event
348 // and broadcast to subscribers
349 }
350 Ok(WritePolicyResult::Reject { message, .. }) => {
351 if message.contains("not authorized") {
352 tracing::debug!(
353 event_id = %entry.event.id,
354 identifier = %identifier,
355 "State event in purgatory still not authorized, keeping in purgatory"
356 );
357 // Keep in purgatory - will expire naturally after 30 minutes
358 } else {
359 tracing::debug!(
360 event_id = %entry.event.id,
361 identifier = %identifier,
362 reason = %message,
363 "State event in purgatory rejected for other reason"
364 );
365 }
366 }
367 Err(e) => {
368 tracing::warn!(
369 event_id = %entry.event.id,
370 identifier = %identifier,
371 error = %e,
372 "Error re-evaluating state event in purgatory"
373 );
374 }
375 }
376 }
377 }
378
307 /// Handle events that must reference accepted repositories or events 379 /// Handle events that must reference accepted repositories or events
308 async fn handle_related_event(&self, event: &Event, event_type: &str) -> WritePolicyResult { 380 async fn handle_related_event(&self, event: &Event, event_type: &str) -> WritePolicyResult {
309 let event_id_str = event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex()); 381 let event_id_str = event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex());