upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-27 15:23:59 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-27 15:23:59 +0000
commit09025b8435f673779ce109e2fb72ce48a13bf28e (patch)
tree65f1a988db805b3c58c08cb0cf782f3430aa4df4
parent233feae6af4b291e4860a1ddf9df2ccf82e57c2f (diff)
fix(auth): accept state announcements from maintainers
Updated get_maintainers_recursive() to properly handle maintainers listed in accepted repository announcements: 1. Separated 'visited' set (cycle prevention) from 'maintainers' set (result) 2. Maintainers listed in an announcement's 'maintainers' tag are now added to the maintainer set immediately, even without their own announcement 3. Recursively traverse maintainer chains to handle multi-level delegation Also fixed RecursiveMaintainerRepoAndState fixture to publish the maintainer's announcement (which lists the recursive maintainer) before publishing the recursive maintainer's announcement, establishing the proper trust chain: Owner -> Maintainer -> RecursiveMaintainer Test results: 7/7 push authorization tests passing
-rw-r--r--grasp-audit/src/fixtures.rs6
-rw-r--r--src/git/authorization.rs53
2 files changed, 42 insertions, 17 deletions
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs
index 3e21eae..45a413d 100644
--- a/grasp-audit/src/fixtures.rs
+++ b/grasp-audit/src/fixtures.rs
@@ -533,6 +533,12 @@ impl<'a> TestContext<'a> {
533 .ok_or_else(|| anyhow::anyhow!("Missing d tag in owner repo announcement"))? 533 .ok_or_else(|| anyhow::anyhow!("Missing d tag in owner repo announcement"))?
534 .to_string(); 534 .to_string();
535 535
536 // Build and send the maintainer's repo announcement first
537 // This establishes the chain: Owner -> Maintainer -> RecursiveMaintainer
538 // The maintainer's announcement lists the recursive maintainer in its maintainers tag
539 let maintainer_announcement = self.build_maintainer_announcement(&repo_id).await?;
540 self.client.send_event(maintainer_announcement).await?;
541
536 // Build and send the recursive maintainer's repo announcement 542 // Build and send the recursive maintainer's repo announcement
537 let recursive_maintainer_announcement = self.build_recursive_maintainer_announcement(&repo_id).await?; 543 let recursive_maintainer_announcement = self.build_recursive_maintainer_announcement(&repo_id).await?;
538 self.client.send_event(recursive_maintainer_announcement).await?; 544 self.client.send_event(recursive_maintainer_announcement).await?;
diff --git a/src/git/authorization.rs b/src/git/authorization.rs
index 2fe81b0..e9f59c7 100644
--- a/src/git/authorization.rs
+++ b/src/git/authorization.rs
@@ -17,7 +17,7 @@
17use anyhow::{anyhow, Result}; 17use anyhow::{anyhow, Result};
18use nostr_sdk::{Event, Filter, Kind, PublicKey, SingleLetterTag, Timestamp, ToBech32, Alphabet}; 18use nostr_sdk::{Event, Filter, Kind, PublicKey, SingleLetterTag, Timestamp, ToBech32, Alphabet};
19use std::collections::HashSet; 19use std::collections::HashSet;
20use tracing::{debug, warn}; 20use tracing::debug;
21 21
22use crate::nostr::events::{ 22use crate::nostr::events::{
23 RepositoryAnnouncement, RepositoryState, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE, 23 RepositoryAnnouncement, RepositoryState, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE,
@@ -128,40 +128,59 @@ impl AuthorizationContext {
128 /// - getMaintainers(alice) -> [alice, bob, charlie] 128 /// - getMaintainers(alice) -> [alice, bob, charlie]
129 /// - getMaintainers(bob) -> [bob, charlie] (bob doesn't have alice's trust) 129 /// - getMaintainers(bob) -> [bob, charlie] (bob doesn't have alice's trust)
130 pub fn get_maintainers(&self, pubkey: &str, identifier: &str) -> Vec<String> { 130 pub fn get_maintainers(&self, pubkey: &str, identifier: &str) -> Vec<String> {
131 let mut checked: HashSet<String> = HashSet::new(); 131 let mut visited: HashSet<String> = HashSet::new();
132 self.get_maintainers_recursive(pubkey, identifier, &mut checked); 132 let mut maintainers: HashSet<String> = HashSet::new();
133 self.get_maintainers_recursive(pubkey, identifier, &mut visited, &mut maintainers);
133 134
134 checked.into_iter().collect() 135 maintainers.into_iter().collect()
135 } 136 }
136 137
137 /// Recursive helper for get_maintainers 138 /// Recursive helper for get_maintainers
139 ///
140 /// The key insight is that a pubkey is a valid maintainer if:
141 /// 1. They have their own accepted announcement for this repo, OR
142 /// 2. They are listed in the "maintainers" tag of an accepted announcement
143 ///
144 /// This allows maintainers to publish state events without needing their own
145 /// announcement - they're authorized by being listed in the owner's announcement.
146 ///
147 /// We use separate sets:
148 /// - `visited`: Tracks which pubkeys we've already processed (cycle prevention)
149 /// - `maintainers`: The result set of valid maintainers
138 fn get_maintainers_recursive( 150 fn get_maintainers_recursive(
139 &self, 151 &self,
140 pubkey: &str, 152 pubkey: &str,
141 identifier: &str, 153 identifier: &str,
142 checked: &mut HashSet<String>, 154 visited: &mut HashSet<String>,
155 maintainers: &mut HashSet<String>,
143 ) { 156 ) {
144 // Skip if already processed 157 // Skip if already visited (prevents infinite loops)
145 if checked.contains(pubkey) { 158 if visited.contains(pubkey) {
146 return; 159 return;
147 } 160 }
161 visited.insert(pubkey.to_string());
148 162
149 // Find the announcement event for this pubkey 163 // Find the announcement event for this pubkey
150 let announcement = self.find_announcement_by_pubkey(pubkey, identifier); 164 let announcement = self.find_announcement_by_pubkey(pubkey, identifier);
151 if announcement.is_none() {
152 return;
153 }
154 165
155 // Mark this pubkey as checked (they have a valid announcement) 166 if let Some(announcement) = announcement {
156 checked.insert(pubkey.to_string()); 167 // This pubkey has an announcement - they are a valid maintainer
168 maintainers.insert(pubkey.to_string());
157 169
158 let announcement = announcement.unwrap(); 170 // Get maintainers listed in this announcement (maintainers tag)
171 // These are ALSO valid maintainers, even without their own announcement
172 for maintainer_pubkey in &announcement.maintainers {
173 // Add them to the maintainer set immediately - they're authorized
174 // by being listed in an accepted announcement
175 maintainers.insert(maintainer_pubkey.clone());
159 176
160 // Get maintainers listed in this announcement (p tags) 177 // Recursively check if they have their own announcement
161 for maintainer_pubkey in &announcement.maintainers { 178 // to get any maintainers THEY list (recursive maintainer chain)
162 // Recursively process each listed maintainer 179 self.get_maintainers_recursive(maintainer_pubkey, identifier, visited, maintainers);
163 self.get_maintainers_recursive(maintainer_pubkey, identifier, checked); 180 }
164 } 181 }
182 // If no announcement found, they can still be valid if they were
183 // added to maintainers by their parent caller
165 } 184 }
166 185
167 /// Find a repository announcement event by pubkey and identifier 186 /// Find a repository announcement event by pubkey and identifier