1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
|
╔═══════════════════════════════════════════════════════════════════════════════╗
║ 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<u8> │
└────────────────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ STEP 4: PARSE REF UPDATES (NEW!) │
│ │
│ parse_receive_pack_request(&body_data) → Vec<RefUpdate> │
│ │
│ 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<impl GitConfig>, │
│ + validator: web::Data<PushValidator>, // ← 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<Vec<RefUpdate>> { │
│ // 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
|