upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/git/handlers.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-30 13:20:55 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-30 13:25:30 +0000
commit08ab20509b9c730d3db98dd6e9deb5e2b548979e (patch)
treec23f9be98f0647e639a64ac6936a24a3e4cf361b /src/git/handlers.rs
parent70d0197e85ae4ef85202781f6d2dc9e76bd508b3 (diff)
purgatory: improve git authorization integetration
Diffstat (limited to 'src/git/handlers.rs')
-rw-r--r--src/git/handlers.rs268
1 files changed, 50 insertions, 218 deletions
diff --git a/src/git/handlers.rs b/src/git/handlers.rs
index df6f0e9..2930852 100644
--- a/src/git/handlers.rs
+++ b/src/git/handlers.rs
@@ -4,22 +4,20 @@
4 4
5use http_body_util::Full; 5use http_body_util::Full;
6use hyper::{body::Bytes, Response, StatusCode}; 6use hyper::{body::Bytes, Response, StatusCode};
7use nostr_relay_builder::LocalRelay;
7use nostr_sdk::prelude::*; 8use nostr_sdk::prelude::*;
8use std::path::PathBuf; 9use std::path::PathBuf;
9use std::sync::Arc; 10use std::sync::Arc;
10use tokio::io::{AsyncReadExt, AsyncWriteExt}; 11use tokio::io::{AsyncReadExt, AsyncWriteExt};
11use tracing::{debug, error, info, warn}; 12use tracing::{debug, error, info, warn};
12 13
13use super::authorization::{
14 get_state_authorization_for_specific_owner_repo, parse_pushed_refs, validate_nostr_ref_pushes,
15 validate_push_refs, AuthorizationResult,
16};
17use super::protocol::{GitService, PktLine}; 14use super::protocol::{GitService, PktLine};
18use super::subprocess::GitSubprocess; 15use super::subprocess::GitSubprocess;
19use super::try_set_head_if_available; 16use super::try_set_head_if_available;
20 17
18use crate::git::authorization::authorize_push;
21use crate::nostr::builder::SharedDatabase; 19use crate::nostr::builder::SharedDatabase;
22use crate::nostr::events::{RepositoryState, KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_STATE}; 20use crate::nostr::events::{KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_STATE};
23use crate::purgatory::Purgatory; 21use crate::purgatory::Purgatory;
24 22
25/// Handle GET /info/refs?service=git-{upload,receive}-pack 23/// Handle GET /info/refs?service=git-{upload,receive}-pack
@@ -186,6 +184,7 @@ pub async fn handle_receive_pack(
186 repo_path: PathBuf, 184 repo_path: PathBuf,
187 request_body: Bytes, 185 request_body: Bytes,
188 database: SharedDatabase, 186 database: SharedDatabase,
187 relay: LocalRelay,
189 identifier: &str, 188 identifier: &str,
190 owner_pubkey: &str, 189 owner_pubkey: &str,
191 purgatory: Arc<Purgatory>, 190 purgatory: Arc<Purgatory>,
@@ -196,17 +195,14 @@ pub async fn handle_receive_pack(
196 return Err(GitError::RepositoryNotFound); 195 return Err(GitError::RepositoryNotFound);
197 } 196 }
198 197
199 // Keep track of state and events for processing after push
200 let mut authorized_state: Option<RepositoryState> = None;
201 let mut authorized_events: Vec<Event> = Vec::new();
202
203 // GRASP Authorization Check 198 // GRASP Authorization Check
204 info!( 199 info!(
205 "Authorizing push for {} owned by {} via database query", 200 "Authorizing push for {} owned by {} via database query",
206 identifier, owner_pubkey 201 identifier, owner_pubkey
207 ); 202 );
208 203
209 match authorize_push( 204 // check push is authorised
205 let auth_result = match authorize_push(
210 &database, 206 &database,
211 identifier, 207 identifier,
212 owner_pubkey, 208 owner_pubkey,
@@ -222,21 +218,19 @@ pub async fn handle_receive_pack(
222 return Err(GitError::Unauthorized); 218 return Err(GitError::Unauthorized);
223 } 219 }
224 info!( 220 info!(
225 "Push authorized for {} - {} maintainers, {} purgatory events", 221 "Push authorized for {} - {} maintainers, {} purgatory events: {}",
226 identifier, 222 identifier,
227 auth_result.maintainers.len(), 223 auth_result.maintainers.len(),
228 auth_result.purgatory_events.len() 224 auth_result.purgatory_events.len(),
225 auth_result.reason
229 ); 226 );
230 // Save the state for HEAD setting after push 227 auth_result
231 authorized_state = auth_result.state.clone();
232 // Save the purgatory events for database saving after push
233 authorized_events = auth_result.purgatory_events;
234 } 228 }
235 Err(e) => { 229 Err(e) => {
236 warn!("Authorization check failed for {}: {}", identifier, e); 230 warn!("Authorization check failed for {}: {}", identifier, e);
237 return Err(GitError::Unauthorized); 231 return Err(GitError::Unauthorized);
238 } 232 }
239 } 233 };
240 234
241 // Spawn git receive-pack 235 // Spawn git receive-pack
242 let mut git = GitSubprocess::spawn(GitService::ReceivePack, &repo_path, false) 236 let mut git = GitSubprocess::spawn(GitService::ReceivePack, &repo_path, false)
@@ -283,7 +277,7 @@ pub async fn handle_receive_pack(
283 // GRASP-01: Set HEAD after git data is received 277 // GRASP-01: Set HEAD after git data is received
284 // "MUST set repository HEAD per repository state announcement 278 // "MUST set repository HEAD per repository state announcement
285 // as soon as the git data related to that branch has been received." 279 // as soon as the git data related to that branch has been received."
286 if let Some(ref state) = authorized_state { 280 if let Some(ref state) = auth_result.state {
287 if let Some(head_ref) = &state.head { 281 if let Some(head_ref) = &state.head {
288 if let Some(branch_name) = state.get_head_branch() { 282 if let Some(branch_name) = state.get_head_branch() {
289 if let Some(commit) = state.get_branch_commit(branch_name) { 283 if let Some(commit) = state.get_branch_commit(branch_name) {
@@ -308,41 +302,53 @@ pub async fn handle_receive_pack(
308 302
309 // Save all events from purgatory that authorized this push and remove them from purgatory 303 // Save all events from purgatory that authorized this push and remove them from purgatory
310 // This includes state events, PR events, and PR-update events 304 // This includes state events, PR events, and PR-update events
311 if !authorized_events.is_empty() { 305 info!(
312 info!( 306 "Saving {} purgatory event(s) to database after successful push",
313 "Saving {} purgatory event(s) to database after successful push", 307 auth_result.purgatory_events.len()
314 authorized_events.len() 308 );
315 );
316 309
317 for event in &authorized_events { 310 for event in &auth_result.purgatory_events {
318 match database.save_event(event).await { 311 match database.save_event(event).await {
319 Ok(_) => { 312 Ok(_) => {
320 info!("Saved purgatory event {} to database", event.id); 313 // Remove from purgatory based on event kind
321 // TODO let broadcast_success = local_relay.notify_event(event.clone()); 314 if event.kind == Kind::from(KIND_REPOSITORY_STATE) {
322 warn!("TODO Here we need to broadcast on open websockets for live listeners. eventid; {}", event.id); 315 info!("Saved purgatory state event {} to database", event.id);
323 // Remove from purgatory based on event kind 316 purgatory.remove_state_event(identifier, &event.id);
324 if event.kind == Kind::from(KIND_REPOSITORY_STATE) { 317 info!("Removed saved state event {} from purgatory", event.id);
325 purgatory.remove_state_event(identifier, &event.id); 318 } else if event.kind == Kind::from(KIND_PR)
326 info!("Removed state event {} from purgatory", event.id); 319 || event.kind == Kind::from(KIND_PR_UPDATE)
327 } else if event.kind == Kind::from(KIND_PR) 320 {
328 || event.kind == Kind::from(KIND_PR_UPDATE) 321 info!("Saved purgatory PR event {} to database", event.id);
329 { 322 // Extract event ID from the event itself (it's the event.id)
330 // Extract event ID from the event itself (it's the event.id) 323 let event_id_hex = event.id.to_hex();
331 let event_id_hex = event.id.to_hex(); 324 purgatory.remove_pr(&event_id_hex);
332 purgatory.remove_pr(&event_id_hex); 325 info!("Removed saved PR event {} from purgatory", event.id);
333 info!("Removed PR event {} from purgatory", event.id);
334 }
335 } 326 }
336 Err(e) => { 327 // Broadcast to WebSocket subscribers
328 if relay.notify_event(event.clone()) {
329 info!(
330 "Broadcast purgatory event {} to websocket listeners",
331 event.id
332 );
333 } else {
337 warn!( 334 warn!(
338 "Failed to save purgatory event {} to database: {}", 335 "Failed to broadcast purgatory event {} to websocket listeners",
339 event.id, e 336 event.id
340 ); 337 );
341 } 338 }
342 } 339 }
340 Err(e) => {
341 warn!(
342 "Failed to save purgatory event {} to database: {}",
343 event.id, e
344 );
345 }
343 } 346 }
344 } 347 }
345 348
349 // TODO figure out what atomic pushes look like in GRASP (we cant accepted differnte state events changing different branches at the same time)
350 // TODO sync git data to other repos that these events authorise.
351
346 Ok(Response::builder() 352 Ok(Response::builder()
347 .status(StatusCode::OK) 353 .status(StatusCode::OK)
348 .header( 354 .header(
@@ -354,180 +360,6 @@ pub async fn handle_receive_pack(
354 .unwrap()) 360 .unwrap())
355} 361}
356 362
357/// Perform GRASP authorization for a push operation
358///
359/// This function queries the database directly (not via WebSocket):
360/// 1. Parses the pushed refs from the git pack protocol
361/// 2. Separates refs/nostr/ refs from normal refs
362/// 3. For normal refs: validates against state events in purgatory
363/// 4. For refs/nostr/ refs: validates event ID format and collects PR/PR-update events from purgatory
364/// 5. Returns all authorizing events (state + PR/PR-update) in the result
365async fn authorize_push(
366 database: &SharedDatabase,
367 identifier: &str,
368 owner_pubkey: &str,
369 request_body: &Bytes,
370 purgatory: &Arc<Purgatory>,
371 repo_path: &std::path::Path,
372) -> anyhow::Result<AuthorizationResult> {
373 debug!(
374 "Authorizing push for {} owned by {} via database query",
375 identifier, owner_pubkey
376 );
377
378 // Parse refs from the push request
379 let pushed_refs = parse_pushed_refs(request_body);
380 debug!("Parsed {} refs from push request", pushed_refs.len());
381 for (old_oid, new_oid, ref_name) in &pushed_refs {
382 debug!(" {} {} -> {}", ref_name, old_oid, new_oid);
383 }
384
385 // Separate refs/nostr/ refs from state refs
386 let (nostr_refs, state_refs): (Vec<_>, Vec<_>) = pushed_refs
387 .iter()
388 .partition(|(_, _, ref_name)| ref_name.starts_with("refs/nostr/"));
389
390 // Collect all purgatory events that authorize this push
391 let mut purgatory_events = Vec::new();
392
393 // Handle refs/nostr/ refs - validate and collect PR/PR-update events from purgatory
394 if !nostr_refs.is_empty() {
395 debug!(
396 "Found {} refs/nostr/ refs - validating and collecting from purgatory",
397 nostr_refs.len()
398 );
399
400 for (_, new_oid, ref_name) in &nostr_refs {
401 // Extract event ID from ref name
402 if let Some(event_id_hex) = ref_name.strip_prefix("refs/nostr/") {
403 // Validate event ID format
404 if EventId::parse(event_id_hex).is_err() {
405 warn!("Invalid event ID format in ref: {}", ref_name);
406 return Ok(AuthorizationResult::denied(format!(
407 "Invalid event ID format in ref: {}",
408 ref_name
409 )));
410 }
411
412 // Check purgatory for PR event
413 if let Some(entry) = purgatory.find_pr(event_id_hex) {
414 if let Some(event) = entry.event {
415 // Verify commit matches
416 if entry.commit == *new_oid {
417 debug!(
418 "Found matching PR event {} in purgatory for ref {}",
419 event_id_hex, ref_name
420 );
421 purgatory_events.push(event);
422 } else {
423 warn!(
424 "PR event {} in purgatory has commit mismatch: expected {}, got {}",
425 event_id_hex, entry.commit, new_oid
426 );
427 return Ok(AuthorizationResult::denied(format!(
428 "PR event {} commit mismatch: expected {}, got {}",
429 event_id_hex, entry.commit, new_oid
430 )));
431 }
432 } else {
433 // Placeholder exists - allow push (git-data-first scenario)
434 debug!(
435 "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",
436 event_id_hex
437 );
438 }
439 } else {
440 // No entry in purgatory - check database for existing event
441 let nostr_refs_owned = vec![(String::new(), new_oid.clone(), ref_name.clone())];
442 if let Err(e) = validate_nostr_ref_pushes(database, &nostr_refs_owned).await {
443 warn!("refs/nostr/ validation failed: {}", e);
444 return Ok(AuthorizationResult::denied(format!(
445 "refs/nostr/ validation failed: {}",
446 e
447 )));
448 }
449 debug!(
450 "No purgatory entry for {} - validated against database",
451 event_id_hex
452 );
453 }
454 }
455 }
456 }
457
458 // Handle normal refs - validate against state events
459 if !state_refs.is_empty() {
460 debug!(
461 "Found {} non-refs/nostr/ refs - checking state authorization",
462 state_refs.len()
463 );
464
465 let auth_result = get_state_authorization_for_specific_owner_repo(
466 database,
467 identifier,
468 owner_pubkey,
469 purgatory,
470 &pushed_refs, //it would be better to accept state_refs but thats in different format
471 repo_path,
472 )
473 .await?;
474
475 if !auth_result.authorized {
476 return Ok(auth_result);
477 }
478
479 // Collect state events from purgatory
480 purgatory_events.extend(auth_result.purgatory_events);
481
482 // Validate refs against state
483 let other_refs_owned: Vec<(String, String, String)> = state_refs
484 .into_iter()
485 .map(|(a, b, c)| (a.clone(), b.clone(), c.clone()))
486 .collect();
487
488 if let Some(ref state) = auth_result.state {
489 debug!(
490 "Validating against state with {} branches",
491 state.branches.len()
492 );
493
494 if other_refs_owned.is_empty() && !state.branches.is_empty() {
495 warn!("No refs parsed from push request but state event has branches - rejecting");
496 return Ok(AuthorizationResult::denied(
497 "Failed to parse refs from push request - cannot validate against state",
498 ));
499 }
500
501 if let Err(e) = validate_push_refs(state, &other_refs_owned) {
502 warn!("Ref validation failed: {}", e);
503 return Ok(AuthorizationResult::denied(format!(
504 "Ref validation failed: {}",
505 e
506 )));
507 }
508 debug!("Ref validation passed");
509 }
510
511 // Return result with purgatory events
512 return Ok(AuthorizationResult {
513 authorized: true,
514 reason: auth_result.reason,
515 state: auth_result.state,
516 maintainers: auth_result.maintainers,
517 purgatory_events,
518 });
519 }
520
521 // Only refs/nostr/ refs - return success with collected events
522 Ok(AuthorizationResult {
523 authorized: true,
524 reason: "Push to refs/nostr/ validated".to_string(),
525 state: None,
526 maintainers: vec![],
527 purgatory_events,
528 })
529}
530
531/// Errors that can occur in Git handlers 363/// Errors that can occur in Git handlers
532#[derive(Debug)] 364#[derive(Debug)]
533pub enum GitError { 365pub enum GitError {