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-11-28 10:31:46 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-28 10:31:46 +0000
commit744094c61d6e65892bcdb5a29b90b845ce87559f (patch)
tree61c53f0ab93901b2b3d5378f7d13c3ac2b6dea98 /src/git/handlers.rs
parent4da51a8adb94f2979c0a911157f26596c1ee2cb5 (diff)
fix maintainer recursion
Diffstat (limited to 'src/git/handlers.rs')
-rw-r--r--src/git/handlers.rs104
1 files changed, 39 insertions, 65 deletions
diff --git a/src/git/handlers.rs b/src/git/handlers.rs
index 73f72f3..7974d8a 100644
--- a/src/git/handlers.rs
+++ b/src/git/handlers.rs
@@ -3,17 +3,19 @@
3//! This module implements the HTTP handlers for Git Smart HTTP protocol. 3//! This module implements the HTTP handlers for Git Smart HTTP protocol.
4 4
5use std::path::PathBuf; 5use std::path::PathBuf;
6use std::sync::Arc;
6use hyper::{body::Bytes, Response, StatusCode}; 7use hyper::{body::Bytes, Response, StatusCode};
7use http_body_util::Full; 8use http_body_util::Full;
9use nostr_relay_builder::prelude::MemoryDatabase;
8use tokio::io::{AsyncReadExt, AsyncWriteExt}; 10use tokio::io::{AsyncReadExt, AsyncWriteExt};
9use tracing::{debug, error, info, warn}; 11use tracing::{debug, error, info, warn};
10 12
11use super::authorization::{ 13use super::authorization::{
12 AuthorizationContext, AuthorizationResult, parse_pushed_refs, validate_push_refs, 14 get_authorization_for_owner, parse_pushed_refs, validate_push_refs, AuthorizationResult,
13}; 15};
14use super::protocol::{GitService, PktLine}; 16use super::protocol::{GitService, PktLine};
15use super::subprocess::GitSubprocess; 17use super::subprocess::GitSubprocess;
16use super::{try_set_head_if_available}; 18use super::try_set_head_if_available;
17 19
18use crate::nostr::events::RepositoryState; 20use crate::nostr::events::RepositoryState;
19 21
@@ -150,17 +152,6 @@ pub async fn handle_upload_pack(
150 .unwrap()) 152 .unwrap())
151} 153}
152 154
153/// Authorization parameters for push operations
154#[derive(Debug, Clone)]
155pub struct PushAuthParams {
156 /// The relay URL for fetching events (e.g., "ws://localhost:8080")
157 pub relay_url: String,
158 /// The npub of the repository owner
159 pub owner_npub: String,
160 /// The repository identifier (d tag)
161 pub identifier: String,
162}
163
164/// Handle POST /git-receive-pack (push) 155/// Handle POST /git-receive-pack (push)
165/// 156///
166/// This includes GRASP authorization validation according to GRASP-01: 157/// This includes GRASP authorization validation according to GRASP-01:
@@ -169,10 +160,19 @@ pub struct PushAuthParams {
169/// 160///
170/// Also per GRASP-01: "MUST set repository HEAD per repository state announcement 161/// Also per GRASP-01: "MUST set repository HEAD per repository state announcement
171/// as soon as the git data related to that branch has been received." 162/// as soon as the git data related to that branch has been received."
163///
164/// # Arguments
165/// * `repo_path` - Path to the bare git repository
166/// * `request_body` - The git pack data from the client
167/// * `database` - Optional database reference for authorization queries
168/// * `identifier` - The repository identifier (d tag) for authorization lookup
169/// * `owner_pubkey` - The owner's public key (hex) from the URL path, scoping authorization
172pub async fn handle_receive_pack( 170pub async fn handle_receive_pack(
173 repo_path: PathBuf, 171 repo_path: PathBuf,
174 request_body: Bytes, 172 request_body: Bytes,
175 auth_params: Option<PushAuthParams>, 173 database: Option<Arc<MemoryDatabase>>,
174 identifier: &str,
175 owner_pubkey: &str,
176) -> Result<Response<Full<Bytes>>, GitError> { 176) -> Result<Response<Full<Bytes>>, GitError> {
177 debug!("Handling receive-pack for {:?}", repo_path); 177 debug!("Handling receive-pack for {:?}", repo_path);
178 178
@@ -183,26 +183,25 @@ pub async fn handle_receive_pack(
183 // Keep track of state for HEAD setting after push 183 // Keep track of state for HEAD setting after push
184 let mut authorized_state: Option<RepositoryState> = None; 184 let mut authorized_state: Option<RepositoryState> = None;
185 185
186 // GRASP Authorization Check 186 // GRASP Authorization Check (if database is provided)
187 if let Some(ref params) = auth_params { 187 if let Some(ref db) = database {
188 info!( 188 info!(
189 "Authorizing push for {}/{} via {}", 189 "Authorizing push for {} owned by {} via database query",
190 params.owner_npub, params.identifier, params.relay_url 190 identifier, owner_pubkey
191 ); 191 );
192 192
193 match authorize_push(params, &request_body).await { 193 match authorize_push(db, identifier, owner_pubkey, &request_body).await {
194 Ok(auth_result) => { 194 Ok(auth_result) => {
195 if !auth_result.authorized { 195 if !auth_result.authorized {
196 warn!( 196 warn!(
197 "Push rejected for {}/{}: {}", 197 "Push rejected for {}: {}",
198 params.owner_npub, params.identifier, auth_result.reason 198 identifier, auth_result.reason
199 ); 199 );
200 return Err(GitError::Unauthorized); 200 return Err(GitError::Unauthorized);
201 } 201 }
202 info!( 202 info!(
203 "Push authorized for {}/{} - {} maintainers", 203 "Push authorized for {} - {} maintainers",
204 params.owner_npub, 204 identifier,
205 params.identifier,
206 auth_result.maintainers.len() 205 auth_result.maintainers.len()
207 ); 206 );
208 // Save the state for HEAD setting after push 207 // Save the state for HEAD setting after push
@@ -210,14 +209,14 @@ pub async fn handle_receive_pack(
210 } 209 }
211 Err(e) => { 210 Err(e) => {
212 warn!( 211 warn!(
213 "Authorization check failed for {}/{}: {}", 212 "Authorization check failed for {}: {}",
214 params.owner_npub, params.identifier, e 213 identifier, e
215 ); 214 );
216 return Err(GitError::Unauthorized); 215 return Err(GitError::Unauthorized);
217 } 216 }
218 } 217 }
219 } else { 218 } else {
220 debug!("No authorization parameters provided - accepting push"); 219 debug!("No database provided - accepting push without authorization");
221 } 220 }
222 221
223 // Spawn git receive-pack 222 // Spawn git receive-pack
@@ -299,50 +298,25 @@ pub async fn handle_receive_pack(
299 298
300/// Perform GRASP authorization for a push operation 299/// Perform GRASP authorization for a push operation
301/// 300///
302/// This function: 301/// This function queries the database directly (not via WebSocket):
303/// 1. Fetches announcement and state events from the relay 302/// 1. Fetches announcement and state events for the identifier
304/// 2. Collects all authorized publishers from announcements 303/// 2. Filters to the specific owner's announcement
305/// 3. Gets the latest authorized state 304/// 3. Collects authorized publishers from that announcement (owner + maintainers)
306/// 4. Validates that pushed refs match the state 305/// 4. Gets the latest authorized state from those publishers
306/// 5. Validates that pushed refs match the state
307async fn authorize_push( 307async fn authorize_push(
308 params: &PushAuthParams, 308 database: &Arc<MemoryDatabase>,
309 identifier: &str,
310 owner_pubkey: &str,
309 request_body: &Bytes, 311 request_body: &Bytes,
310) -> anyhow::Result<AuthorizationResult> { 312) -> anyhow::Result<AuthorizationResult> {
311 use nostr_sdk::ClientBuilder;
312 use std::time::Duration;
313
314 debug!( 313 debug!(
315 "Fetching events for identifier {} from relay {}", 314 "Authorizing push for {} owned by {} via database query",
316 params.identifier, params.relay_url 315 identifier, owner_pubkey
317 ); 316 );
318 317
319 // Create a Nostr client to fetch events 318 // Get authorization result from database, scoped to specific owner
320 let client = ClientBuilder::default().build(); 319 let auth_result = get_authorization_for_owner(database, identifier, owner_pubkey).await?;
321 client.add_relay(&params.relay_url).await?;
322 client.connect().await;
323
324 // Create filter for repository events
325 let filter = AuthorizationContext::create_filter(&params.identifier);
326
327 // Fetch events with timeout
328 let events = client.fetch_events(filter, Duration::from_secs(5))
329 .await
330 .map_err(|e| anyhow::anyhow!("Failed to fetch events: {}", e))?;
331
332 let events: Vec<_> = events.into_iter().collect();
333 debug!("Fetched {} events from relay", events.len());
334
335 if events.is_empty() {
336 return Ok(AuthorizationResult::denied(
337 "No repository announcement or state events found on relay",
338 ));
339 }
340
341 // Create authorization context
342 let ctx = AuthorizationContext::new(events);
343
344 // Get the authorized state (no owner_pubkey needed - self-contained check)
345 let auth_result = ctx.get_authorized_state(&params.identifier)?;
346 320
347 if !auth_result.authorized { 321 if !auth_result.authorized {
348 return Ok(auth_result); 322 return Ok(auth_result);