upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/nostr/policy/state.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/policy/state.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/policy/state.rs')
-rw-r--r--src/nostr/policy/state.rs40
1 files changed, 40 insertions, 0 deletions
diff --git a/src/nostr/policy/state.rs b/src/nostr/policy/state.rs
index acb76a3..d26b5ec 100644
--- a/src/nostr/policy/state.rs
+++ b/src/nostr/policy/state.rs
@@ -78,6 +78,46 @@ impl StatePolicy {
78 // Get all repositories and state events from db with identifier 78 // Get all repositories and state events from db with identifier
79 let db_repo_data = fetch_repository_data(&self.ctx.database, &state.identifier).await?; 79 let db_repo_data = fetch_repository_data(&self.ctx.database, &state.identifier).await?;
80 80
81 // CRITICAL: Check if author is authorized via maintainer set
82 // State events MUST be rejected if author is not in maintainer set of any accepted announcement
83 if db_repo_data.announcements.is_empty() {
84 tracing::warn!(
85 event_id = %event.id,
86 identifier = %state.identifier,
87 author = %event.pubkey.to_hex(),
88 "Rejecting state event: no announcement exists for this repository"
89 );
90 return Ok(WritePolicyResult::Reject {
91 status: false,
92 message: "invalid: no announcement exists for this repository".into(),
93 });
94 }
95
96 let authorized_owners =
97 crate::git::authorization::pubkey_authorised_for_repo_owners(&event.pubkey, &db_repo_data);
98
99 if authorized_owners.is_empty() {
100 tracing::warn!(
101 event_id = %event.id,
102 identifier = %state.identifier,
103 author = %event.pubkey.to_hex(),
104 announcements_count = db_repo_data.announcements.len(),
105 "Rejecting state event: author not in maintainer set of any announcement"
106 );
107 return Ok(WritePolicyResult::Reject {
108 status: false,
109 message: "invalid: author not authorized for this repository".into(),
110 });
111 }
112
113 tracing::debug!(
114 event_id = %event.id,
115 identifier = %state.identifier,
116 author = %event.pubkey.to_hex(),
117 authorized_for_owners = ?authorized_owners,
118 "State event author authorized via maintainer set"
119 );
120
81 // Duplicate check in db 121 // Duplicate check in db
82 if db_repo_data.states.iter().any(|e| e.event.id.eq(&event.id)) { 122 if db_repo_data.states.iter().any(|e| e.event.id.eq(&event.id)) {
83 tracing::debug!("processed state event duplicate (in db): {}", event.id); 123 tracing::debug!("processed state event duplicate (in db): {}", event.id);