diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-26 05:45:47 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-26 07:38:58 +0000 |
| commit | 30411a938d072a59d68815c975735d40366ad874 (patch) | |
| tree | f802d1bf9f9959105d2d18af81c528722fa7a675 /src/git/handlers.rs | |
| parent | a005132ab806b7177d4eb3e3306914841704ffec (diff) | |
feat: push authorization from state event
Diffstat (limited to 'src/git/handlers.rs')
| -rw-r--r-- | src/git/handlers.rs | 147 |
1 files changed, 142 insertions, 5 deletions
diff --git a/src/git/handlers.rs b/src/git/handlers.rs index ac35d14..5b511e3 100644 --- a/src/git/handlers.rs +++ b/src/git/handlers.rs | |||
| @@ -6,8 +6,11 @@ use std::path::PathBuf; | |||
| 6 | use hyper::{body::Bytes, Response, StatusCode}; | 6 | use hyper::{body::Bytes, Response, StatusCode}; |
| 7 | use http_body_util::Full; | 7 | use http_body_util::Full; |
| 8 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; | 8 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; |
| 9 | use tracing::{debug, error, warn}; | 9 | use tracing::{debug, error, info, warn}; |
| 10 | 10 | ||
| 11 | use super::authorization::{ | ||
| 12 | AuthorizationContext, AuthorizationResult, npub_to_pubkey, parse_pushed_refs, validate_push_refs, | ||
| 13 | }; | ||
| 11 | use super::protocol::{GitService, PktLine}; | 14 | use super::protocol::{GitService, PktLine}; |
| 12 | use super::subprocess::GitSubprocess; | 15 | use super::subprocess::GitSubprocess; |
| 13 | 16 | ||
| @@ -144,12 +147,26 @@ pub async fn handle_upload_pack( | |||
| 144 | .unwrap()) | 147 | .unwrap()) |
| 145 | } | 148 | } |
| 146 | 149 | ||
| 150 | /// Authorization parameters for push operations | ||
| 151 | #[derive(Debug, Clone)] | ||
| 152 | pub struct PushAuthParams { | ||
| 153 | /// The relay URL for fetching events (e.g., "ws://localhost:8080") | ||
| 154 | pub relay_url: String, | ||
| 155 | /// The npub of the repository owner | ||
| 156 | pub owner_npub: String, | ||
| 157 | /// The repository identifier (d tag) | ||
| 158 | pub identifier: String, | ||
| 159 | } | ||
| 160 | |||
| 147 | /// Handle POST /git-receive-pack (push) | 161 | /// Handle POST /git-receive-pack (push) |
| 148 | /// | 162 | /// |
| 149 | /// This includes an authorization hook point where GRASP validation will be added. | 163 | /// This includes GRASP authorization validation according to GRASP-01: |
| 164 | /// "MUST accept pushes via this service that match the latest repo state announcement | ||
| 165 | /// on the relay, respecting the recursive maintainer set." | ||
| 150 | pub async fn handle_receive_pack( | 166 | pub async fn handle_receive_pack( |
| 151 | repo_path: PathBuf, | 167 | repo_path: PathBuf, |
| 152 | request_body: Bytes, | 168 | request_body: Bytes, |
| 169 | auth_params: Option<PushAuthParams>, | ||
| 153 | ) -> Result<Response<Full<Bytes>>, GitError> { | 170 | ) -> Result<Response<Full<Bytes>>, GitError> { |
| 154 | debug!("Handling receive-pack for {:?}", repo_path); | 171 | debug!("Handling receive-pack for {:?}", repo_path); |
| 155 | 172 | ||
| @@ -157,9 +174,40 @@ pub async fn handle_receive_pack( | |||
| 157 | return Err(GitError::RepositoryNotFound); | 174 | return Err(GitError::RepositoryNotFound); |
| 158 | } | 175 | } |
| 159 | 176 | ||
| 160 | // TODO: Add GRASP authorization here | 177 | // GRASP Authorization Check |
| 161 | // For now, we'll accept all pushes to enable testing | 178 | if let Some(params) = auth_params { |
| 162 | debug!("Authorization check would go here (currently accepting all pushes)"); | 179 | info!( |
| 180 | "Authorizing push for {}/{} via {}", | ||
| 181 | params.owner_npub, params.identifier, params.relay_url | ||
| 182 | ); | ||
| 183 | |||
| 184 | match authorize_push(¶ms, &request_body).await { | ||
| 185 | Ok(auth_result) => { | ||
| 186 | if !auth_result.authorized { | ||
| 187 | warn!( | ||
| 188 | "Push rejected for {}/{}: {}", | ||
| 189 | params.owner_npub, params.identifier, auth_result.reason | ||
| 190 | ); | ||
| 191 | return Err(GitError::Unauthorized); | ||
| 192 | } | ||
| 193 | info!( | ||
| 194 | "Push authorized for {}/{} - {} maintainers", | ||
| 195 | params.owner_npub, | ||
| 196 | params.identifier, | ||
| 197 | auth_result.maintainers.len() | ||
| 198 | ); | ||
| 199 | } | ||
| 200 | Err(e) => { | ||
| 201 | warn!( | ||
| 202 | "Authorization check failed for {}/{}: {}", | ||
| 203 | params.owner_npub, params.identifier, e | ||
| 204 | ); | ||
| 205 | return Err(GitError::Unauthorized); | ||
| 206 | } | ||
| 207 | } | ||
| 208 | } else { | ||
| 209 | debug!("No authorization parameters provided - accepting push"); | ||
| 210 | } | ||
| 163 | 211 | ||
| 164 | // Spawn git receive-pack | 212 | // Spawn git receive-pack |
| 165 | let mut git = GitSubprocess::spawn(GitService::ReceivePack, &repo_path, false) | 213 | let mut git = GitSubprocess::spawn(GitService::ReceivePack, &repo_path, false) |
| @@ -206,6 +254,95 @@ pub async fn handle_receive_pack( | |||
| 206 | .unwrap()) | 254 | .unwrap()) |
| 207 | } | 255 | } |
| 208 | 256 | ||
| 257 | /// Perform GRASP authorization for a push operation | ||
| 258 | /// | ||
| 259 | /// This function: | ||
| 260 | /// 1. Fetches announcement and state events from the relay | ||
| 261 | /// 2. Calculates the recursive maintainer set | ||
| 262 | /// 3. Gets the latest authorized state | ||
| 263 | /// 4. Validates that pushed refs match the state | ||
| 264 | async fn authorize_push( | ||
| 265 | params: &PushAuthParams, | ||
| 266 | request_body: &Bytes, | ||
| 267 | ) -> anyhow::Result<AuthorizationResult> { | ||
| 268 | use nostr_sdk::ClientBuilder; | ||
| 269 | use std::time::Duration; | ||
| 270 | |||
| 271 | // Convert npub to hex pubkey | ||
| 272 | let owner_pubkey = npub_to_pubkey(¶ms.owner_npub)?; | ||
| 273 | |||
| 274 | debug!( | ||
| 275 | "Fetching events for identifier {} from relay {}", | ||
| 276 | params.identifier, params.relay_url | ||
| 277 | ); | ||
| 278 | |||
| 279 | // Create a Nostr client to fetch events | ||
| 280 | let client = ClientBuilder::default().build(); | ||
| 281 | client.add_relay(¶ms.relay_url).await?; | ||
| 282 | client.connect().await; | ||
| 283 | |||
| 284 | // Create filter for repository events | ||
| 285 | let filter = AuthorizationContext::create_filter(¶ms.identifier); | ||
| 286 | |||
| 287 | // Fetch events with timeout | ||
| 288 | let events = client.fetch_events(filter, Duration::from_secs(5)) | ||
| 289 | .await | ||
| 290 | .map_err(|e| anyhow::anyhow!("Failed to fetch events: {}", e))?; | ||
| 291 | |||
| 292 | let events: Vec<_> = events.into_iter().collect(); | ||
| 293 | debug!("Fetched {} events from relay", events.len()); | ||
| 294 | |||
| 295 | if events.is_empty() { | ||
| 296 | return Ok(AuthorizationResult::denied( | ||
| 297 | "No repository announcement or state events found on relay", | ||
| 298 | )); | ||
| 299 | } | ||
| 300 | |||
| 301 | // Create authorization context | ||
| 302 | let ctx = AuthorizationContext::new(events); | ||
| 303 | |||
| 304 | // Get the authorized state | ||
| 305 | let auth_result = ctx.get_authorized_state(&owner_pubkey, ¶ms.identifier)?; | ||
| 306 | |||
| 307 | if !auth_result.authorized { | ||
| 308 | return Ok(auth_result); | ||
| 309 | } | ||
| 310 | |||
| 311 | // Parse refs from the push request | ||
| 312 | let pushed_refs = parse_pushed_refs(request_body); | ||
| 313 | debug!("Parsed {} refs from push request", pushed_refs.len()); | ||
| 314 | for (old_oid, new_oid, ref_name) in &pushed_refs { | ||
| 315 | debug!(" {} {} -> {}", ref_name, old_oid, new_oid); | ||
| 316 | } | ||
| 317 | |||
| 318 | // Validate refs against state | ||
| 319 | if let Some(ref state) = auth_result.state { | ||
| 320 | debug!("Validating against state with {} branches", state.branches.len()); | ||
| 321 | |||
| 322 | // If we have a state event but couldn't parse any refs, reject the push. | ||
| 323 | // This protects against parsing failures allowing unauthorized pushes. | ||
| 324 | if pushed_refs.is_empty() && !state.branches.is_empty() { | ||
| 325 | warn!("No refs parsed from push request but state event has branches - rejecting"); | ||
| 326 | return Ok(AuthorizationResult::denied( | ||
| 327 | "Failed to parse refs from push request - cannot validate against state" | ||
| 328 | )); | ||
| 329 | } | ||
| 330 | |||
| 331 | if let Err(e) = validate_push_refs(state, &pushed_refs) { | ||
| 332 | warn!("Ref validation failed: {}", e); | ||
| 333 | return Ok(AuthorizationResult::denied(format!( | ||
| 334 | "Ref validation failed: {}", | ||
| 335 | e | ||
| 336 | ))); | ||
| 337 | } | ||
| 338 | debug!("Ref validation passed"); | ||
| 339 | } else { | ||
| 340 | warn!("No state in auth_result - cannot validate refs"); | ||
| 341 | } | ||
| 342 | |||
| 343 | Ok(auth_result) | ||
| 344 | } | ||
| 345 | |||
| 209 | /// Errors that can occur in Git handlers | 346 | /// Errors that can occur in Git handlers |
| 210 | #[derive(Debug)] | 347 | #[derive(Debug)] |
| 211 | pub enum GitError { | 348 | pub enum GitError { |