upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/git/authorization.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/git/authorization.rs')
-rw-r--r--src/git/authorization.rs142
1 files changed, 128 insertions, 14 deletions
diff --git a/src/git/authorization.rs b/src/git/authorization.rs
index bb3bd01..3b0e759 100644
--- a/src/git/authorization.rs
+++ b/src/git/authorization.rs
@@ -35,7 +35,8 @@ use std::sync::Arc;
35use tracing::debug; 35use tracing::debug;
36 36
37use crate::nostr::events::{ 37use crate::nostr::events::{
38 RepositoryAnnouncement, RepositoryState, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE, 38 RepositoryAnnouncement, RepositoryState, KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_ANNOUNCEMENT,
39 KIND_REPOSITORY_STATE,
39}; 40};
40 41
41/// Repository data fetched from the database 42/// Repository data fetched from the database
@@ -172,9 +173,9 @@ fn get_maintainers_recursive(
172 checked.insert(pubkey.to_string()); // Mark as checked 173 checked.insert(pubkey.to_string()); // Mark as checked
173 174
174 // Find the announcement event for this pubkey+identifier 175 // Find the announcement event for this pubkey+identifier
175 let announcement = announcements.iter().find(|a| { 176 let announcement = announcements
176 a.event.pubkey.to_hex() == pubkey && a.identifier == identifier 177 .iter()
177 }); 178 .find(|a| a.event.pubkey.to_hex() == pubkey && a.identifier == identifier);
178 179
179 let Some(announcement) = announcement else { 180 let Some(announcement) = announcement else {
180 return; // No announcement found for this pubkey 181 return; // No announcement found for this pubkey
@@ -195,19 +196,19 @@ pub fn collect_all_authorized_maintainers(
195) -> HashSet<String> { 196) -> HashSet<String> {
196 let by_owner = collect_authorized_maintainers(announcements); 197 let by_owner = collect_authorized_maintainers(announcements);
197 let mut all_authorized = HashSet::new(); 198 let mut all_authorized = HashSet::new();
198 199
199 for maintainers in by_owner.values() { 200 for maintainers in by_owner.values() {
200 for maintainer in maintainers { 201 for maintainer in maintainers {
201 all_authorized.insert(maintainer.clone()); 202 all_authorized.insert(maintainer.clone());
202 } 203 }
203 } 204 }
204 205
205 debug!( 206 debug!(
206 "Collected {} total authorized maintainers from {} owners", 207 "Collected {} total authorized maintainers from {} owners",
207 all_authorized.len(), 208 all_authorized.len(),
208 by_owner.len() 209 by_owner.len()
209 ); 210 );
210 211
211 all_authorized 212 all_authorized
212} 213}
213 214
@@ -601,10 +602,7 @@ pub fn validate_push_refs(
601 pushed_refs: &[(String, String, String)], // (old_oid, new_oid, ref_name) 602 pushed_refs: &[(String, String, String)], // (old_oid, new_oid, ref_name)
602) -> Result<()> { 603) -> Result<()> {
603 for (old_oid, new_oid, ref_name) in pushed_refs { 604 for (old_oid, new_oid, ref_name) in pushed_refs {
604 debug!( 605 debug!("Validating push: {} {} -> {}", ref_name, old_oid, new_oid);
605 "Validating push: {} {} -> {}",
606 ref_name, old_oid, new_oid
607 );
608 606
609 // Handle branch updates 607 // Handle branch updates
610 if let Some(branch_name) = ref_name.strip_prefix("refs/heads/") { 608 if let Some(branch_name) = ref_name.strip_prefix("refs/heads/") {
@@ -657,7 +655,10 @@ pub fn validate_push_refs(
657 )); 655 ));
658 } 656 }
659 // Valid EventId format - allow push (skip state event check) 657 // Valid EventId format - allow push (skip state event check)
660 debug!("refs/nostr/{} push authorized (valid EventId)", event_id_str); 658 debug!(
659 "refs/nostr/{} push authorized (valid EventId)",
660 event_id_str
661 );
661 continue; // Skip the rest of ref validation for this ref 662 continue; // Skip the rest of ref validation for this ref
662 } else { 663 } else {
663 return Err(anyhow!("Invalid refs/nostr/ format: {}", ref_name)); 664 return Err(anyhow!("Invalid refs/nostr/ format: {}", ref_name));
@@ -805,6 +806,119 @@ pub fn npub_to_pubkey(npub: &str) -> Result<String> {
805 Ok(pk.to_hex()) 806 Ok(pk.to_hex())
806} 807}
807 808
809/// Fetch an event by ID from the database and extract the `c` tag commit hash
810///
811/// This is used for validating pushes to refs/nostr/<event-id>. Per GRASP-01,
812/// if a PR or PR Update event with this ID exists in the database, the pushed
813/// commit must match the commit in the event's `c` tag.
814///
815/// # Returns
816/// - `Ok(Some(commit))` if the event exists and has a valid `c` tag
817/// - `Ok(None)` if the event doesn't exist (push should be allowed)
818/// - `Err(_)` on database errors
819pub async fn get_event_commit_tag(
820 database: &Arc<MemoryDatabase>,
821 event_id: &EventId,
822) -> Result<Option<String>> {
823 // Query for PR (1618) and PR Update (1619) events with this ID
824 let filter = Filter::new()
825 .ids([*event_id])
826 .kinds([Kind::from(KIND_PR), Kind::from(KIND_PR_UPDATE)]);
827
828 let events: Vec<Event> = database
829 .query(filter)
830 .await
831 .map_err(|e| anyhow!("Database query failed: {}", e))?
832 .into_iter()
833 .collect();
834
835 if events.is_empty() {
836 debug!("No PR/PR Update event found with ID {}", event_id);
837 return Ok(None);
838 }
839
840 // Get the first (should be only) event
841 let event = &events[0];
842
843 // Extract the `c` tag (commit hash)
844 // Per NIP-34, PR events have a `c` tag with the head commit
845 let commit = event
846 .tags
847 .iter()
848 .find(|tag| tag.as_slice().first().map(|s| s.as_str()) == Some("c"))
849 .and_then(|tag| tag.as_slice().get(1).map(|s| s.to_string()));
850
851 debug!(
852 "Found PR event {} with commit tag: {:?}",
853 event_id,
854 commit.as_ref()
855 );
856
857 Ok(commit)
858}
859
860/// Validate refs/nostr/ pushes against existing PR/PR Update events
861///
862/// For each ref being pushed to refs/nostr/<event-id>:
863/// 1. Validate the event ID format (error if invalid)
864/// 2. Check if a corresponding event exists in the database
865/// 3. If event exists, verify the pushed commit matches the `c` tag
866///
867/// # Arguments
868/// * `database` - The nostr database to query
869/// * `pushed_refs` - List of (old_oid, new_oid, ref_name) tuples
870///
871/// # Returns
872/// * `Ok(())` if all refs/nostr/ pushes are valid
873/// * `Err(_)` if any ref has invalid event ID format or fails commit validation
874pub async fn validate_nostr_ref_pushes(
875 database: &Arc<MemoryDatabase>,
876 pushed_refs: &[(String, String, String)],
877) -> Result<()> {
878 for (_, new_oid, ref_name) in pushed_refs {
879 // Only check refs/nostr/ refs
880 if let Some(event_id_str) = ref_name.strip_prefix("refs/nostr/") {
881 // Parse the event ID - error on invalid format
882 let event_id = EventId::parse(event_id_str).map_err(|_| {
883 anyhow!(
884 "Invalid event ID format '{}' in ref: {}",
885 event_id_str,
886 ref_name
887 )
888 })?;
889
890 // Check if event exists and get commit tag
891 match get_event_commit_tag(database, &event_id).await? {
892 Some(expected_commit) => {
893 // Event exists - verify commit matches
894 if new_oid != &expected_commit {
895 return Err(anyhow!(
896 "Push to {} rejected: event {} specifies commit {}, but push contains {}",
897 ref_name,
898 event_id_str,
899 expected_commit,
900 new_oid
901 ));
902 }
903 debug!(
904 "Push to {} validated: commit {} matches event's c tag",
905 ref_name, new_oid
906 );
907 }
908 None => {
909 // No event exists yet - allow push
910 debug!(
911 "Push to {} allowed: no PR/PR Update event with ID {} found yet",
912 ref_name, event_id_str
913 );
914 }
915 }
916 }
917 }
918
919 Ok(())
920}
921
808#[cfg(test)] 922#[cfg(test)]
809mod tests { 923mod tests {
810 use super::*; 924 use super::*;
@@ -920,7 +1034,7 @@ mod tests {
920 let eve = create_test_keys(); // Not authorized 1034 let eve = create_test_keys(); // Not authorized
921 let identifier = "test-repo"; 1035 let identifier = "test-repo";
922 1036
923 // Alice lists Bob as maintainer 1037 // Alice lists Bob as maintainer
924 let alice_announcement = create_announcement_event(&alice, identifier, &[&bob]); 1038 let alice_announcement = create_announcement_event(&alice, identifier, &[&bob]);
925 1039
926 let events = vec![alice_announcement]; 1040 let events = vec![alice_announcement];
@@ -1084,4 +1198,4 @@ mod tests {
1084 let back_to_hex = npub_to_pubkey(&npub).unwrap(); 1198 let back_to_hex = npub_to_pubkey(&npub).unwrap();
1085 assert_eq!(hex, back_to_hex); 1199 assert_eq!(hex, back_to_hex);
1086 } 1200 }
1087} \ No newline at end of file 1201}