diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-28 10:31:46 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-28 10:31:46 +0000 |
| commit | 744094c61d6e65892bcdb5a29b90b845ce87559f (patch) | |
| tree | 61c53f0ab93901b2b3d5378f7d13c3ac2b6dea98 /src/git/handlers.rs | |
| parent | 4da51a8adb94f2979c0a911157f26596c1ee2cb5 (diff) | |
fix maintainer recursion
Diffstat (limited to 'src/git/handlers.rs')
| -rw-r--r-- | src/git/handlers.rs | 104 |
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 | ||
| 5 | use std::path::PathBuf; | 5 | use std::path::PathBuf; |
| 6 | use std::sync::Arc; | ||
| 6 | use hyper::{body::Bytes, Response, StatusCode}; | 7 | use hyper::{body::Bytes, Response, StatusCode}; |
| 7 | use http_body_util::Full; | 8 | use http_body_util::Full; |
| 9 | use nostr_relay_builder::prelude::MemoryDatabase; | ||
| 8 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; | 10 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; |
| 9 | use tracing::{debug, error, info, warn}; | 11 | use tracing::{debug, error, info, warn}; |
| 10 | 12 | ||
| 11 | use super::authorization::{ | 13 | use 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 | }; |
| 14 | use super::protocol::{GitService, PktLine}; | 16 | use super::protocol::{GitService, PktLine}; |
| 15 | use super::subprocess::GitSubprocess; | 17 | use super::subprocess::GitSubprocess; |
| 16 | use super::{try_set_head_if_available}; | 18 | use super::try_set_head_if_available; |
| 17 | 19 | ||
| 18 | use crate::nostr::events::RepositoryState; | 20 | use 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)] | ||
| 155 | pub 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 | ||
| 172 | pub async fn handle_receive_pack( | 170 | pub 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 | ||
| 307 | async fn authorize_push( | 307 | async 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(¶ms.relay_url).await?; | ||
| 322 | client.connect().await; | ||
| 323 | |||
| 324 | // Create filter for repository events | ||
| 325 | let filter = AuthorizationContext::create_filter(¶ms.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(¶ms.identifier)?; | ||
| 346 | 320 | ||
| 347 | if !auth_result.authorized { | 321 | if !auth_result.authorized { |
| 348 | return Ok(auth_result); | 322 | return Ok(auth_result); |