diff options
Diffstat (limited to 'src/git/authorization.rs')
| -rw-r--r-- | src/git/authorization.rs | 102 |
1 files changed, 100 insertions, 2 deletions
diff --git a/src/git/authorization.rs b/src/git/authorization.rs index 69a0751..df780bb 100644 --- a/src/git/authorization.rs +++ b/src/git/authorization.rs | |||
| @@ -609,6 +609,28 @@ pub async fn get_state_authorization_for_specific_owner_repo( | |||
| 609 | owner_pubkey | 609 | owner_pubkey |
| 610 | ); | 610 | ); |
| 611 | 611 | ||
| 612 | // Accept pushes where all refs are already at the desired state (old_oid == new_oid) | ||
| 613 | // This handles race conditions where state events are applied between fetch and push | ||
| 614 | if !pushed_refs.is_empty() { | ||
| 615 | let all_refs_unchanged = pushed_refs | ||
| 616 | .iter() | ||
| 617 | .all(|(old_oid, new_oid, _)| old_oid == new_oid); | ||
| 618 | |||
| 619 | if all_refs_unchanged { | ||
| 620 | debug!( | ||
| 621 | "All pushed refs unchanged (old_oid == new_oid) for {} owned by {}, accepting without purgatory check", | ||
| 622 | identifier, owner_pubkey | ||
| 623 | ); | ||
| 624 | return Ok(AuthorizationResult { | ||
| 625 | authorized: true, | ||
| 626 | reason: "Push accepted: all refs already at desired state (no-op)".to_string(), | ||
| 627 | state: None, | ||
| 628 | maintainers: authorized.into_iter().collect(), | ||
| 629 | purgatory_events: vec![], | ||
| 630 | }); | ||
| 631 | } | ||
| 632 | } | ||
| 633 | |||
| 612 | // Check purgatory for matching state events | 634 | // Check purgatory for matching state events |
| 613 | // Convert pushed refs to RefUpdate (filter out refs/nostr/* refs) | 635 | // Convert pushed refs to RefUpdate (filter out refs/nostr/* refs) |
| 614 | let pushed_updates: Vec<RefUpdate> = pushed_refs | 636 | let pushed_updates: Vec<RefUpdate> = pushed_refs |
| @@ -699,12 +721,88 @@ pub async fn get_state_authorization_for_specific_owner_repo( | |||
| 699 | debug!("Purgatory events found but none from authorized authors"); | 721 | debug!("Purgatory events found but none from authorized authors"); |
| 700 | } | 722 | } |
| 701 | } else { | 723 | } else { |
| 702 | debug!("No matching state events found in purgatory"); | 724 | // Check if there are ANY state events in purgatory for this identifier |
| 725 | let all_purgatory_states = purgatory.find_state(identifier); | ||
| 726 | |||
| 727 | if !all_purgatory_states.is_empty() { | ||
| 728 | // There are state events but none match the push - diagnose why | ||
| 729 | debug!( | ||
| 730 | "Found {} state event(s) in purgatory for {} but none match the push", | ||
| 731 | all_purgatory_states.len(), | ||
| 732 | identifier | ||
| 733 | ); | ||
| 734 | |||
| 735 | // Count authorized state events and collect diagnostic info | ||
| 736 | let mut authorized_count = 0; | ||
| 737 | let mut diagnostic_reasons = Vec::new(); | ||
| 738 | |||
| 739 | // Diagnose why each authorized state event doesn't match | ||
| 740 | for entry in all_purgatory_states.iter() { | ||
| 741 | let author_hex = entry.event.pubkey.to_hex(); | ||
| 742 | if authorized.contains(&author_hex) { | ||
| 743 | authorized_count += 1; | ||
| 744 | if let Some(reason) = crate::purgatory::diagnose_state_mismatch( | ||
| 745 | &entry.event, | ||
| 746 | &pushed_updates, | ||
| 747 | &local_refs, | ||
| 748 | ) { | ||
| 749 | debug!( | ||
| 750 | "State event {} from authorized author {} doesn't match push: {}", | ||
| 751 | entry.event.id, | ||
| 752 | entry | ||
| 753 | .event | ||
| 754 | .pubkey | ||
| 755 | .to_bech32() | ||
| 756 | .unwrap_or_else(|_| author_hex.clone()), | ||
| 757 | reason | ||
| 758 | ); | ||
| 759 | diagnostic_reasons.push(reason); | ||
| 760 | } | ||
| 761 | } | ||
| 762 | } | ||
| 763 | |||
| 764 | // Create concise WARN message summarizing the rejection | ||
| 765 | let summary = if authorized_count > 0 { | ||
| 766 | let reason_summary = if !diagnostic_reasons.is_empty() { | ||
| 767 | // Take the first diagnostic reason as representative | ||
| 768 | format!(" ({})", diagnostic_reasons[0]) | ||
| 769 | } else { | ||
| 770 | String::new() | ||
| 771 | }; | ||
| 772 | format!( | ||
| 773 | "{} state event{} in purgatory from authorized publisher{} but doesn't match push{}", | ||
| 774 | authorized_count, | ||
| 775 | if authorized_count == 1 { "" } else { "s" }, | ||
| 776 | if authorized_count == 1 { "" } else { "s" }, | ||
| 777 | reason_summary | ||
| 778 | ) | ||
| 779 | } else { | ||
| 780 | format!( | ||
| 781 | "{} state event{} in purgatory but none from authorized publishers", | ||
| 782 | all_purgatory_states.len(), | ||
| 783 | if all_purgatory_states.len() == 1 { | ||
| 784 | "" | ||
| 785 | } else { | ||
| 786 | "s" | ||
| 787 | } | ||
| 788 | ) | ||
| 789 | }; | ||
| 790 | |||
| 791 | warn!("Push rejected for {}: {}", identifier, summary); | ||
| 792 | return Ok(AuthorizationResult::denied(summary)); | ||
| 793 | } else { | ||
| 794 | debug!("No state events found in purgatory for {}", identifier); | ||
| 795 | warn!( | ||
| 796 | "Push rejected for {}: No state events in purgatory", | ||
| 797 | identifier | ||
| 798 | ); | ||
| 799 | return Ok(AuthorizationResult::denied("No state events in purgatory")); | ||
| 800 | } | ||
| 703 | } | 801 | } |
| 704 | 802 | ||
| 705 | // No matching state found in purgatory | 803 | // No matching state found in purgatory |
| 706 | Ok(AuthorizationResult::denied( | 804 | Ok(AuthorizationResult::denied( |
| 707 | "No state event found in purgatory from authorized publishers", | 805 | "No matching state event found in purgatory from authorized publishers", |
| 708 | )) | 806 | )) |
| 709 | } | 807 | } |
| 710 | 808 | ||