diff options
Diffstat (limited to 'src/git/authorization.rs')
| -rw-r--r-- | src/git/authorization.rs | 142 |
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; | |||
| 35 | use tracing::debug; | 35 | use tracing::debug; |
| 36 | 36 | ||
| 37 | use crate::nostr::events::{ | 37 | use 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 | ||
| 819 | pub 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 | ||
| 874 | pub 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)] |
| 809 | mod tests { | 923 | mod 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 | } |