From 08ab20509b9c730d3db98dd6e9deb5e2b548979e Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Tue, 30 Dec 2025 13:20:55 +0000 Subject: purgatory: improve git authorization integetration --- src/git/authorization.rs | 177 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) (limited to 'src/git/authorization.rs') diff --git a/src/git/authorization.rs b/src/git/authorization.rs index fbeaf9e..9bcbdf8 100644 --- a/src/git/authorization.rs +++ b/src/git/authorization.rs @@ -28,9 +28,11 @@ //! - HEAD updates triggered by state events in builder.rs (event policy) use anyhow::{anyhow, Result}; +use hyper::body::Bytes; use nostr_relay_builder::prelude::*; use nostr_sdk::{EventId, ToBech32}; use std::collections::{HashMap, HashSet}; +use std::sync::Arc; use tracing::{debug, info, warn}; use crate::nostr::builder::SharedDatabase; @@ -38,6 +40,181 @@ use crate::nostr::events::{ RepositoryAnnouncement, RepositoryState, KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE, }; +use crate::purgatory::Purgatory; + +/// Perform GRASP authorization for a push operation +/// +/// This function queries the database directly (not via WebSocket): +/// 1. Parses the pushed refs from the git pack protocol +/// 2. Separates refs/nostr/ refs from normal refs +/// 3. For normal refs: validates against state events in purgatory +/// 4. For refs/nostr/ refs: validates event ID format and collects PR/PR-update events from purgatory +/// 5. Returns all authorizing events (state + PR/PR-update) in the result +pub async fn authorize_push( + database: &SharedDatabase, + identifier: &str, + owner_pubkey: &str, + request_body: &Bytes, + purgatory: &Arc, + repo_path: &std::path::Path, +) -> anyhow::Result { + debug!( + "Authorizing push for {} owned by {} via database query", + identifier, owner_pubkey + ); + + // Parse refs from the push request + let pushed_refs = parse_pushed_refs(request_body); + debug!("Parsed {} refs from push request", pushed_refs.len()); + for (old_oid, new_oid, ref_name) in &pushed_refs { + debug!(" {} {} -> {}", ref_name, old_oid, new_oid); + } + + // Separate refs/nostr/ refs from state refs + let (nostr_refs, state_refs): (Vec<_>, Vec<_>) = pushed_refs + .iter() + .partition(|(_, _, ref_name)| ref_name.starts_with("refs/nostr/")); + + // Collect all purgatory events that authorize this push + let mut purgatory_events = Vec::new(); + + // Handle refs/nostr/ refs - validate and collect PR/PR-update events from purgatory + if !nostr_refs.is_empty() { + debug!( + "Found {} refs/nostr/ refs - validating and collecting from purgatory", + nostr_refs.len() + ); + + for (_, new_oid, ref_name) in &nostr_refs { + // Extract event ID from ref name + if let Some(event_id_hex) = ref_name.strip_prefix("refs/nostr/") { + // Validate event ID format + if EventId::parse(event_id_hex).is_err() { + warn!("Invalid event ID format in ref: {}", ref_name); + return Ok(AuthorizationResult::denied(format!( + "Invalid event ID format in ref: {}", + ref_name + ))); + } + + // Check purgatory for PR event + if let Some(entry) = purgatory.find_pr(event_id_hex) { + if let Some(event) = entry.event { + // Verify commit matches + if entry.commit == *new_oid { + debug!( + "Found matching PR event {} in purgatory for ref {}", + event_id_hex, ref_name + ); + purgatory_events.push(event); + } else { + warn!( + "PR event {} in purgatory has commit mismatch: expected {}, got {}", + event_id_hex, entry.commit, new_oid + ); + return Ok(AuthorizationResult::denied(format!( + "PR event {} commit mismatch: expected {}, got {}", + event_id_hex, entry.commit, new_oid + ))); + } + } else { + // Placeholder exists - allow push (git-data-first scenario) + debug!( + "Found placeholder already for PR event {} in purgatory - as we dont have the event and therefore dont know the required commit_id we allow overwriting with a different commit_id", + event_id_hex + ); + } + } else { + // No entry in purgatory - check database for existing event + let nostr_refs_owned = vec![(String::new(), new_oid.clone(), ref_name.clone())]; + if let Err(e) = validate_nostr_ref_pushes(database, &nostr_refs_owned).await { + warn!("refs/nostr/ validation failed: {}", e); + return Ok(AuthorizationResult::denied(format!( + "refs/nostr/ validation failed: {}", + e + ))); + } + debug!( + "No purgatory entry for {} - validated against database", + event_id_hex + ); + } + } + } + } + + // Handle normal refs - validate against state events + if !state_refs.is_empty() { + debug!( + "Found {} non-refs/nostr/ refs - checking state authorization", + state_refs.len() + ); + + let auth_result = get_state_authorization_for_specific_owner_repo( + database, + identifier, + owner_pubkey, + purgatory, + &pushed_refs, //it would be better to accept state_refs but thats in different format + repo_path, + ) + .await?; + + if !auth_result.authorized { + return Ok(auth_result); + } + + // Collect state events from purgatory + purgatory_events.extend(auth_result.purgatory_events); + + // Validate refs against state + let other_refs_owned: Vec<(String, String, String)> = state_refs + .into_iter() + .map(|(a, b, c)| (a.clone(), b.clone(), c.clone())) + .collect(); + + if let Some(ref state) = auth_result.state { + debug!( + "Validating against state with {} branches", + state.branches.len() + ); + + if other_refs_owned.is_empty() && !state.branches.is_empty() { + warn!("No refs parsed from push request but state event has branches - rejecting"); + return Ok(AuthorizationResult::denied( + "Failed to parse refs from push request - cannot validate against state", + )); + } + + if let Err(e) = validate_push_refs(state, &other_refs_owned) { + warn!("Ref validation failed: {}", e); + return Ok(AuthorizationResult::denied(format!( + "Ref validation failed: {}", + e + ))); + } + debug!("Ref validation passed"); + } + + // Return result with purgatory events + return Ok(AuthorizationResult { + authorized: true, + reason: auth_result.reason, + state: auth_result.state, + maintainers: auth_result.maintainers, + purgatory_events, + }); + } + + // Only refs/nostr/ refs - return success with collected events + Ok(AuthorizationResult { + authorized: true, + reason: "Push to refs/nostr/ validated".to_string(), + state: None, + maintainers: vec![], + purgatory_events, + }) +} /// Repository data fetched from the database /// -- cgit v1.2.3