╔═══════════════════════════════════════════════════════════════════════════════╗ ║ GIT PUSH AUTHORIZATION FLOW (INLINE) ║ ╚═══════════════════════════════════════════════════════════════════════════════╝ ┌─────────────────────────────────────────────────────────────────────────────┐ │ CLIENT: git push │ └────────────────────────────────┬────────────────────────────────────────────┘ │ │ HTTP POST │ /npub/repo.git/git-receive-pack ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ ACTIX-WEB ROUTER (git-http-backend) │ │ │ │ Route: /{namespace}/{repo}/git-receive-pack │ │ Handler: git_receive_pack() │ └────────────────────────────────┬────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ STEP 1: RESOLVE REPOSITORY PATH │ │ │ │ GitConfig::rewrite("/npub/repo") → /data/git/npub/repo.git │ └────────────────────────────────┬────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ STEP 2: VALIDATE REPOSITORY EXISTS │ │ │ │ ✓ Check HEAD exists │ │ ✓ Check config exists │ │ ✓ Check bare = true │ │ │ │ ❌ If not: Return 400 "Repository not found" │ └────────────────────────────────┬────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ STEP 3: READ REQUEST BODY (MODIFIED) │ │ │ │ • Read full request body into memory │ │ • Decode gzip if Content-Encoding: gzip │ │ • Store in body_data: Vec │ └────────────────────────────────┬────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ STEP 4: PARSE REF UPDATES (NEW!) │ │ │ │ parse_receive_pack_request(&body_data) → Vec │ │ │ │ Git Pack Protocol: │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ 0000000000000000000000000000000000000000 a1b2c3d4e5f6... │ │ │ │ refs/heads/main\0 report-status\n │ │ │ │ │ │ │ │ old_oid: 0000... (new branch) │ │ │ │ new_oid: a1b2c3d4e5f6... │ │ │ │ ref_name: refs/heads/main │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ │ Result: RefUpdate { │ │ old_oid: "0000...", │ │ new_oid: "a1b2c3d4e5f6...", │ │ ref_name: "refs/heads/main" │ │ } │ └────────────────────────────────┬────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ STEP 5: VALIDATE AUTHORIZATION (NEW!) │ │ │ │ validator.validate_push(npub, identifier, &ref_updates).await │ │ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ PushValidator::validate_push() │ │ │ │ │ │ │ │ 1. Get latest state event from Nostr relay │ │ │ │ • Query: kind=30618, d=identifier, author=npub │ │ │ │ • Extract refs from state event │ │ │ │ │ │ │ │ 2. For each ref update: │ │ │ │ • If refs/heads/* or refs/tags/*: │ │ │ │ - Check state event has matching ref │ │ │ │ - Check new_oid matches state event oid │ │ │ │ - ❌ Reject if mismatch │ │ │ │ • If refs/nostr/*: │ │ │ │ - ✅ Always allow (PRs) │ │ │ │ │ │ │ │ 3. Get maintainers (recursive) │ │ │ │ • Extract maintainers from announcement │ │ │ │ • Recursively resolve maintainer sets │ │ │ │ • Check if pusher is in maintainer list │ │ │ │ • ❌ Reject if not maintainer │ │ │ │ │ │ │ │ 4. Return Ok(()) or Err(message) │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ │ ❌ If validation fails: │ │ Return 403 Forbidden │ │ { │ │ "error": "unauthorized", │ │ "message": "Push rejected: refs/heads/main points to ..., state has..."│ │ } │ └────────────────────────────────┬────────────────────────────────────────────┘ │ │ ✅ AUTHORIZED ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ STEP 6: SPAWN GIT RECEIVE-PACK (EXISTING) │ │ │ │ Command::new("git") │ │ .arg("receive-pack") │ │ .arg("--stateless-rpc") │ │ .arg(".") │ │ .current_dir(&repo_path) │ │ .spawn() │ │ │ │ • Write body_data to git stdin │ │ • Stream git stdout back to client │ └────────────────────────────────┬────────────────────────────────────────────┘ │ │ Stream response ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ CLIENT: git push success │ └─────────────────────────────────────────────────────────────────────────────┘ ╔═══════════════════════════════════════════════════════════════════════════════╗ ║ COMPARISON: BEFORE vs AFTER ║ ╚═══════════════════════════════════════════════════════════════════════════════╝ ┌─────────────────────────────────┬─────────────────────────────────────────┐ │ BEFORE (git-http-backend) │ AFTER (our fork) │ ├─────────────────────────────────┼─────────────────────────────────────────┤ │ 1. Resolve path │ 1. Resolve path │ │ 2. Check bare repo │ 2. Check bare repo │ │ 3. Read request body │ 3. Read request body │ │ 4. Spawn git immediately ❌ │ 4. Parse ref updates ← NEW │ │ 5. Stream response │ 5. Validate authorization ← NEW │ │ │ 6. Spawn git (if authorized) │ │ │ 7. Stream response │ └─────────────────────────────────┴─────────────────────────────────────────┘ ┌─────────────────────────────────┬─────────────────────────────────────────┐ │ AUTHORIZATION │ METHOD │ ├─────────────────────────────────┼─────────────────────────────────────────┤ │ ❌ None │ No validation │ │ ⚠️ Git hooks (pre-receive) │ After git accepts push │ │ ✅ Inline (our approach) │ Before git touches repository │ └─────────────────────────────────┴─────────────────────────────────────────┘ ╔═══════════════════════════════════════════════════════════════════════════════╗ ║ KEY MODIFICATIONS NEEDED ║ ╚═══════════════════════════════════════════════════════════════════════════════╝ ┌─────────────────────────────────────────────────────────────────────────────┐ │ FILE: src/actix/git_receive_pack.rs │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ CHANGE 1: Add validator parameter │ │ ──────────────────────────────────────────────────────────────────────── │ │ pub async fn git_receive_pack( │ │ request: HttpRequest, │ │ mut payload: Payload, │ │ service: web::Data, │ │ + validator: web::Data, // ← ADD THIS │ │ ) -> impl Responder { │ │ │ │ CHANGE 2: Parse ref updates after reading body │ │ ──────────────────────────────────────────────────────────────────────── │ │ // Read and decode body (existing) │ │ let body_data = read_and_decode_body(&mut payload, &request).await?; │ │ │ │ + // Parse ref updates (NEW) │ │ + let ref_updates = parse_receive_pack_request(&body_data)?; │ │ │ │ CHANGE 3: Validate before spawning git │ │ ──────────────────────────────────────────────────────────────────────── │ │ + // Extract repo info from path │ │ + let (npub, identifier) = extract_repo_info(&request.uri().path())?; │ │ + │ │ + // Validate authorization │ │ + if let Err(e) = validator.validate_push(&npub, &identifier, │ │ + &ref_updates).await { │ │ + return HttpResponse::Forbidden() │ │ + .json(json!({ │ │ + "error": "unauthorized", │ │ + "message": e.to_string(), │ │ + })); │ │ + } │ │ │ │ // Spawn git (existing, unchanged) │ │ let mut cmd = Command::new("git"); │ │ // ... rest of existing code ... │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────┐ │ NEW FILE: src/git/protocol.rs │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ pub struct RefUpdate { │ │ pub old_oid: String, │ │ pub new_oid: String, │ │ pub ref_name: String, │ │ } │ │ │ │ pub fn parse_receive_pack_request(body: &[u8]) -> Result> { │ │ // Parse git pack protocol │ │ // Extract ref updates from pkt-line format │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────┐ │ NEW FILE: src/git/authorization.rs │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ pub struct PushValidator { │ │ storage: Storage, │ │ } │ │ │ │ impl PushValidator { │ │ pub async fn validate_push( │ │ &self, │ │ npub: &str, │ │ identifier: &str, │ │ updates: &[RefUpdate], │ │ ) -> Result<()> { │ │ // Query Nostr relay for state event │ │ // Validate each ref update │ │ // Check maintainer permissions │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ╔═══════════════════════════════════════════════════════════════════════════════╗ ║ BENEFITS OF INLINE AUTH ║ ╚═══════════════════════════════════════════════════════════════════════════════╝ ✅ BETTER ERROR MESSAGES • Return 403 with JSON error details • Show exactly which ref failed validation • Show expected vs. actual commit • Better developer experience ✅ SIMPLER DEPLOYMENT • No git hooks to manage • No symlinks or hook installation • Single binary handles everything • Easier to test ✅ TIGHTER INTEGRATION • Direct access to Nostr relay state • Shared storage layer • No IPC between components • Atomic validation ✅ EASIER TESTING • Pure Rust unit tests • Mock validator for testing • No subprocess coordination • Deterministic behavior ✅ SECURITY • Validation before git touches repo • Can't bypass by manipulating hooks • Centralized authorization logic • Audit trail in application logs ╔═══════════════════════════════════════════════════════════════════════════════╗ ║ TIMELINE ║ ╚═══════════════════════════════════════════════════════════════════════════════╝ Week 1: Foundation ├─ Day 1-2: Fork git-http-backend, set up integration ├─ Day 3-4: Add git2, implement GitRepository └─ Day 5: Add protocol parsing module Week 2: Authorization ├─ Day 1-2: Implement PushValidator ├─ Day 3-4: Modify git_receive_pack handler └─ Day 5: Integration tests Week 3: Polish ├─ Day 1-2: Add CORS support ├─ Day 3-4: Error handling improvements └─ Day 5: E2E tests with real git Week 4: Compliance ├─ Day 1-3: GRASP-01 compliance testing ├─ Day 4: Performance testing └─ Day 5: Documentation Status: ✅ Analysis complete, ready to implement