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:
Diffstat (limited to 'src/git/authorization.rs')
-rw-r--r--src/git/authorization.rs177
1 files changed, 177 insertions, 0 deletions
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 @@
28//! - HEAD updates triggered by state events in builder.rs (event policy) 28//! - HEAD updates triggered by state events in builder.rs (event policy)
29 29
30use anyhow::{anyhow, Result}; 30use anyhow::{anyhow, Result};
31use hyper::body::Bytes;
31use nostr_relay_builder::prelude::*; 32use nostr_relay_builder::prelude::*;
32use nostr_sdk::{EventId, ToBech32}; 33use nostr_sdk::{EventId, ToBech32};
33use std::collections::{HashMap, HashSet}; 34use std::collections::{HashMap, HashSet};
35use std::sync::Arc;
34use tracing::{debug, info, warn}; 36use tracing::{debug, info, warn};
35 37
36use crate::nostr::builder::SharedDatabase; 38use crate::nostr::builder::SharedDatabase;
@@ -38,6 +40,181 @@ use crate::nostr::events::{
38 RepositoryAnnouncement, RepositoryState, KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_ANNOUNCEMENT, 40 RepositoryAnnouncement, RepositoryState, KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_ANNOUNCEMENT,
39 KIND_REPOSITORY_STATE, 41 KIND_REPOSITORY_STATE,
40}; 42};
43use crate::purgatory::Purgatory;
44
45/// Perform GRASP authorization for a push operation
46///
47/// This function queries the database directly (not via WebSocket):
48/// 1. Parses the pushed refs from the git pack protocol
49/// 2. Separates refs/nostr/ refs from normal refs
50/// 3. For normal refs: validates against state events in purgatory
51/// 4. For refs/nostr/ refs: validates event ID format and collects PR/PR-update events from purgatory
52/// 5. Returns all authorizing events (state + PR/PR-update) in the result
53pub async fn authorize_push(
54 database: &SharedDatabase,
55 identifier: &str,
56 owner_pubkey: &str,
57 request_body: &Bytes,
58 purgatory: &Arc<Purgatory>,
59 repo_path: &std::path::Path,
60) -> anyhow::Result<AuthorizationResult> {
61 debug!(
62 "Authorizing push for {} owned by {} via database query",
63 identifier, owner_pubkey
64 );
65
66 // Parse refs from the push request
67 let pushed_refs = parse_pushed_refs(request_body);
68 debug!("Parsed {} refs from push request", pushed_refs.len());
69 for (old_oid, new_oid, ref_name) in &pushed_refs {
70 debug!(" {} {} -> {}", ref_name, old_oid, new_oid);
71 }
72
73 // Separate refs/nostr/ refs from state refs
74 let (nostr_refs, state_refs): (Vec<_>, Vec<_>) = pushed_refs
75 .iter()
76 .partition(|(_, _, ref_name)| ref_name.starts_with("refs/nostr/"));
77
78 // Collect all purgatory events that authorize this push
79 let mut purgatory_events = Vec::new();
80
81 // Handle refs/nostr/ refs - validate and collect PR/PR-update events from purgatory
82 if !nostr_refs.is_empty() {
83 debug!(
84 "Found {} refs/nostr/ refs - validating and collecting from purgatory",
85 nostr_refs.len()
86 );
87
88 for (_, new_oid, ref_name) in &nostr_refs {
89 // Extract event ID from ref name
90 if let Some(event_id_hex) = ref_name.strip_prefix("refs/nostr/") {
91 // Validate event ID format
92 if EventId::parse(event_id_hex).is_err() {
93 warn!("Invalid event ID format in ref: {}", ref_name);
94 return Ok(AuthorizationResult::denied(format!(
95 "Invalid event ID format in ref: {}",
96 ref_name
97 )));
98 }
99
100 // Check purgatory for PR event
101 if let Some(entry) = purgatory.find_pr(event_id_hex) {
102 if let Some(event) = entry.event {
103 // Verify commit matches
104 if entry.commit == *new_oid {
105 debug!(
106 "Found matching PR event {} in purgatory for ref {}",
107 event_id_hex, ref_name
108 );
109 purgatory_events.push(event);
110 } else {
111 warn!(
112 "PR event {} in purgatory has commit mismatch: expected {}, got {}",
113 event_id_hex, entry.commit, new_oid
114 );
115 return Ok(AuthorizationResult::denied(format!(
116 "PR event {} commit mismatch: expected {}, got {}",
117 event_id_hex, entry.commit, new_oid
118 )));
119 }
120 } else {
121 // Placeholder exists - allow push (git-data-first scenario)
122 debug!(
123 "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",
124 event_id_hex
125 );
126 }
127 } else {
128 // No entry in purgatory - check database for existing event
129 let nostr_refs_owned = vec![(String::new(), new_oid.clone(), ref_name.clone())];
130 if let Err(e) = validate_nostr_ref_pushes(database, &nostr_refs_owned).await {
131 warn!("refs/nostr/ validation failed: {}", e);
132 return Ok(AuthorizationResult::denied(format!(
133 "refs/nostr/ validation failed: {}",
134 e
135 )));
136 }
137 debug!(
138 "No purgatory entry for {} - validated against database",
139 event_id_hex
140 );
141 }
142 }
143 }
144 }
145
146 // Handle normal refs - validate against state events
147 if !state_refs.is_empty() {
148 debug!(
149 "Found {} non-refs/nostr/ refs - checking state authorization",
150 state_refs.len()
151 );
152
153 let auth_result = get_state_authorization_for_specific_owner_repo(
154 database,
155 identifier,
156 owner_pubkey,
157 purgatory,
158 &pushed_refs, //it would be better to accept state_refs but thats in different format
159 repo_path,
160 )
161 .await?;
162
163 if !auth_result.authorized {
164 return Ok(auth_result);
165 }
166
167 // Collect state events from purgatory
168 purgatory_events.extend(auth_result.purgatory_events);
169
170 // Validate refs against state
171 let other_refs_owned: Vec<(String, String, String)> = state_refs
172 .into_iter()
173 .map(|(a, b, c)| (a.clone(), b.clone(), c.clone()))
174 .collect();
175
176 if let Some(ref state) = auth_result.state {
177 debug!(
178 "Validating against state with {} branches",
179 state.branches.len()
180 );
181
182 if other_refs_owned.is_empty() && !state.branches.is_empty() {
183 warn!("No refs parsed from push request but state event has branches - rejecting");
184 return Ok(AuthorizationResult::denied(
185 "Failed to parse refs from push request - cannot validate against state",
186 ));
187 }
188
189 if let Err(e) = validate_push_refs(state, &other_refs_owned) {
190 warn!("Ref validation failed: {}", e);
191 return Ok(AuthorizationResult::denied(format!(
192 "Ref validation failed: {}",
193 e
194 )));
195 }
196 debug!("Ref validation passed");
197 }
198
199 // Return result with purgatory events
200 return Ok(AuthorizationResult {
201 authorized: true,
202 reason: auth_result.reason,
203 state: auth_result.state,
204 maintainers: auth_result.maintainers,
205 purgatory_events,
206 });
207 }
208
209 // Only refs/nostr/ refs - return success with collected events
210 Ok(AuthorizationResult {
211 authorized: true,
212 reason: "Push to refs/nostr/ validated".to_string(),
213 state: None,
214 maintainers: vec![],
215 purgatory_events,
216 })
217}
41 218
42/// Repository data fetched from the database 219/// Repository data fetched from the database
43/// 220///