diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-24 08:02:12 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-24 11:54:18 +0000 |
| commit | 70d0197e85ae4ef85202781f6d2dc9e76bd508b3 (patch) | |
| tree | 45efb6565e81ba755acc5955e68d5b7119d1e122 /src/git/authorization.rs | |
| parent | f8c3e3920ed2a1bdaab30be912276993449a5476 (diff) | |
feat(purgatory): add broken purgatory implementation
Diffstat (limited to 'src/git/authorization.rs')
| -rw-r--r-- | src/git/authorization.rs | 113 |
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}; | |||
| 31 | use nostr_relay_builder::prelude::*; | 31 | use nostr_relay_builder::prelude::*; |
| 32 | use nostr_sdk::{EventId, ToBech32}; | 32 | use nostr_sdk::{EventId, ToBech32}; |
| 33 | use std::collections::{HashMap, HashSet}; | 33 | use std::collections::{HashMap, HashSet}; |
| 34 | use tracing::debug; | 34 | use tracing::{debug, info, warn}; |
| 35 | 35 | ||
| 36 | use crate::nostr::builder::SharedDatabase; | 36 | use crate::nostr::builder::SharedDatabase; |
| 37 | use crate::nostr::events::{ | 37 | use 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. |
| 342 | pub async fn get_authorization_for_owner( | 341 | pub 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 | ||
| 408 | impl AuthorizationResult { | 481 | impl 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 | } |