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-26 05:45:47 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-26 07:38:58 +0000
commit30411a938d072a59d68815c975735d40366ad874 (patch)
treef802d1bf9f9959105d2d18af81c528722fa7a675 /src/git/handlers.rs
parenta005132ab806b7177d4eb3e3306914841704ffec (diff)
feat: push authorization from state event
Diffstat (limited to 'src/git/handlers.rs')
-rw-r--r--src/git/handlers.rs147
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;
6use hyper::{body::Bytes, Response, StatusCode}; 6use hyper::{body::Bytes, Response, StatusCode};
7use http_body_util::Full; 7use http_body_util::Full;
8use tokio::io::{AsyncReadExt, AsyncWriteExt}; 8use tokio::io::{AsyncReadExt, AsyncWriteExt};
9use tracing::{debug, error, warn}; 9use tracing::{debug, error, info, warn};
10 10
11use super::authorization::{
12 AuthorizationContext, AuthorizationResult, npub_to_pubkey, parse_pushed_refs, validate_push_refs,
13};
11use super::protocol::{GitService, PktLine}; 14use super::protocol::{GitService, PktLine};
12use super::subprocess::GitSubprocess; 15use 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)]
152pub 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."
150pub async fn handle_receive_pack( 166pub 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(&params, &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
264async 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(&params.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(&params.relay_url).await?;
282 client.connect().await;
283
284 // Create filter for repository events
285 let filter = AuthorizationContext::create_filter(&params.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, &params.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)]
211pub enum GitError { 348pub enum GitError {