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:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-24 08:02:12 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-24 11:54:18 +0000
commit70d0197e85ae4ef85202781f6d2dc9e76bd508b3 (patch)
tree45efb6565e81ba755acc5955e68d5b7119d1e122 /src/git/authorization.rs
parentf8c3e3920ed2a1bdaab30be912276993449a5476 (diff)
feat(purgatory): add broken purgatory implementation
Diffstat (limited to 'src/git/authorization.rs')
-rw-r--r--src/git/authorization.rs113
1 files changed, 94 insertions, 19 deletions
diff --git a/src/git/authorization.rs b/src/git/authorization.rs
index 4896fc0..fbeaf9e 100644
--- a/src/git/authorization.rs
+++ b/src/git/authorization.rs
@@ -31,7 +31,7 @@ use anyhow::{anyhow, Result};
31use nostr_relay_builder::prelude::*; 31use nostr_relay_builder::prelude::*;
32use nostr_sdk::{EventId, ToBech32}; 32use nostr_sdk::{EventId, ToBech32};
33use std::collections::{HashMap, HashSet}; 33use std::collections::{HashMap, HashSet};
34use tracing::debug; 34use tracing::{debug, info, warn};
35 35
36use crate::nostr::builder::SharedDatabase; 36use crate::nostr::builder::SharedDatabase;
37use crate::nostr::events::{ 37use crate::nostr::events::{
@@ -325,26 +325,31 @@ pub async fn get_authorization_from_db(
325 325
326/// Get the authorization result for a repository scoped to a specific owner 326/// Get the authorization result for a repository scoped to a specific owner
327/// 327///
328/// Unlike `get_authorization_from_db`, this function scopes the authorization 328/// Push authorization checks ONLY purgatory for state events. The database represents
329/// to a specific owner's announcement. This is the correct approach for Git push 329/// the current git state, while purgatory holds the intended future state that pushes
330/// authorization where the URL path specifies the owner. 330/// should be authorized against.
331/// 331///
332/// A push to `alice/my-repo` should only consider authorization from alice's 332/// A push to `alice/my-repo` should only consider authorization from alice's
333/// announcement, not bob's announcement for the same identifier. 333/// announcement, not bob's announcement for the same identifier.
334/// 334///
335/// It: 335/// It:
336/// 1. Fetches all announcements and states for the identifier 336/// 1. Fetches announcements for the identifier
337/// 2. Collects authorized maintainers from all announcements (grouped by owner) 337/// 2. Collects authorized maintainers from owner's announcement
338/// 3. Looks up the authorized set for the specific owner 338/// 3. Checks purgatory for matching state events from authorized maintainers
339/// 4. Finds the latest state event from an authorized maintainer
340/// 339///
341/// Returns an `AuthorizationResult` that indicates whether a push is authorized. 340/// Returns an `AuthorizationResult` that indicates whether a push is authorized.
342pub async fn get_authorization_for_owner( 341pub async fn get_state_authorization_for_specific_owner_repo(
343 database: &SharedDatabase, 342 database: &SharedDatabase,
344 identifier: &str, 343 identifier: &str,
345 owner_pubkey: &str, 344 owner_pubkey: &str,
345 purgatory: &std::sync::Arc<crate::purgatory::Purgatory>,
346 pushed_refs: &[(String, String, String)],
347 repo_path: &std::path::Path,
346) -> Result<AuthorizationResult> { 348) -> Result<AuthorizationResult> {
347 // Fetch all repository data with a single query 349 use crate::git::list_refs;
350 use crate::purgatory::RefUpdate;
351
352 // Fetch announcements only - we don't need database states
348 let repo_data = fetch_repository_data(database, identifier).await?; 353 let repo_data = fetch_repository_data(database, identifier).await?;
349 354
350 if repo_data.announcements.is_empty() { 355 if repo_data.announcements.is_empty() {
@@ -380,16 +385,82 @@ pub async fn get_authorization_for_owner(
380 owner_pubkey 385 owner_pubkey
381 ); 386 );
382 387
383 // Find the latest authorized state from owner's maintainer set 388 // Check purgatory for matching state events
384 match find_latest_authorized_state(&repo_data.states, &authorized) { 389 // Convert pushed refs to RefUpdate (filter out refs/nostr/* refs)
385 Some(state) => Ok(AuthorizationResult::authorized( 390 let pushed_updates: Vec<RefUpdate> = pushed_refs
386 state.clone(), 391 .iter()
387 authorized.into_iter().collect(), 392 .filter(|(_, _, name)| !name.starts_with("refs/nostr/"))
388 )), 393 .map(|(old_oid, new_oid, ref_name)| RefUpdate {
389 None => Ok(AuthorizationResult::denied( 394 old_oid: old_oid.clone(),
390 "No state event found from authorized publishers", 395 new_oid: new_oid.clone(),
391 )), 396 ref_name: ref_name.clone(),
397 })
398 .collect();
399
400 // Get local refs from repository
401 let local_refs_list = list_refs(repo_path).unwrap_or_default();
402 let local_refs: HashMap<String, String> = local_refs_list.into_iter().collect();
403
404 // Find matching state events in purgatory
405 let matching_events = purgatory.find_matching_states(identifier, &pushed_updates, &local_refs);
406
407 if !matching_events.is_empty() {
408 debug!(
409 "Found {} matching state event(s) in purgatory",
410 matching_events.len()
411 );
412
413 // Filter to authorized events and collect them
414 let authorized_events: Vec<Event> = matching_events
415 .into_iter()
416 .filter(|event| {
417 let author_hex = event.pubkey.to_hex();
418 authorized.contains(&author_hex)
419 })
420 .collect();
421
422 if !authorized_events.is_empty() {
423 // Find the latest event
424 let latest_authorized = authorized_events
425 .iter()
426 .max_by_key(|event| event.created_at)
427 .unwrap(); // Safe because we checked the vec is not empty
428
429 // Parse the event into RepositoryState
430 if let Ok(state) = RepositoryState::from_event(latest_authorized.clone()) {
431 info!(
432 "Authorized by state event {} from purgatory (author: {})",
433 latest_authorized.id,
434 latest_authorized
435 .pubkey
436 .to_bech32()
437 .unwrap_or_else(|_| latest_authorized.pubkey.to_hex())
438 );
439
440 return Ok(AuthorizationResult {
441 authorized: true,
442 reason: "Authorized by state event in purgatory".to_string(),
443 state: Some(state),
444 maintainers: authorized.into_iter().collect(),
445 purgatory_events: vec![latest_authorized.clone()],
446 });
447 } else {
448 warn!(
449 "Failed to parse purgatory event {} as RepositoryState",
450 latest_authorized.id
451 );
452 }
453 } else {
454 debug!("Purgatory events found but none from authorized authors");
455 }
456 } else {
457 debug!("No matching state events found in purgatory");
392 } 458 }
459
460 // No matching state found in purgatory
461 Ok(AuthorizationResult::denied(
462 "No state event found in purgatory from authorized publishers",
463 ))
393} 464}
394 465
395/// Result of authorization check 466/// Result of authorization check
@@ -403,6 +474,8 @@ pub struct AuthorizationResult {
403 pub state: Option<RepositoryState>, 474 pub state: Option<RepositoryState>,
404 /// The set of valid maintainers (authorized publishers) 475 /// The set of valid maintainers (authorized publishers)
405 pub maintainers: Vec<String>, 476 pub maintainers: Vec<String>,
477 /// Events from purgatory that authorized this push (state, PR, PR-update events)
478 pub purgatory_events: Vec<Event>,
406} 479}
407 480
408impl AuthorizationResult { 481impl AuthorizationResult {
@@ -413,6 +486,7 @@ impl AuthorizationResult {
413 reason: "Push matches latest authorized state".to_string(), 486 reason: "Push matches latest authorized state".to_string(),
414 state: Some(state), 487 state: Some(state),
415 maintainers, 488 maintainers,
489 purgatory_events: vec![],
416 } 490 }
417 } 491 }
418 492
@@ -423,6 +497,7 @@ impl AuthorizationResult {
423 reason: reason.into(), 497 reason: reason.into(),
424 state: None, 498 state: None,
425 maintainers: vec![], 499 maintainers: vec![],
500 purgatory_events: vec![],
426 } 501 }
427 } 502 }
428} 503}