upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/docs/explanation/inline-authorization.md
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-04 10:25:53 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-04 10:25:53 +0000
commit52bad9954cdddf55ab749fd0c6387edbc766632f (patch)
treed9dd2078b70a627a71d1adb9555cee83faec5cd0 /docs/explanation/inline-authorization.md
parentdb460efdd4cf34d3b6ac8c19b1b8f89f22bc279f (diff)
docs: use Diátaxis structure
Diffstat (limited to 'docs/explanation/inline-authorization.md')
-rw-r--r--docs/explanation/inline-authorization.md403
1 files changed, 403 insertions, 0 deletions
diff --git a/docs/explanation/inline-authorization.md b/docs/explanation/inline-authorization.md
new file mode 100644
index 0000000..98f6e5a
--- /dev/null
+++ b/docs/explanation/inline-authorization.md
@@ -0,0 +1,403 @@
1# Explanation: Inline Authorization
2
3**Purpose:** Understand why ngit-grasp validates Git pushes inline rather than using Git hooks
4**Audience:** Developers and architects wanting to understand design decisions
5
6---
7
8## The Problem
9
10Git hosting with authorization requires validating pushes before accepting them. The question is: **where** should this validation happen?
11
12Two approaches exist:
13
141. **Git Hooks** (traditional): Use Git's pre-receive hook mechanism
152. **Inline Authorization** (our approach): Validate before spawning Git
16
17This document explains why we chose inline authorization and what benefits it provides.
18
19---
20
21## Background: How Git Hooks Work
22
23Git provides a **pre-receive hook** that runs during `git push`:
24
25```
26Client Server
27 | |
28 |--- git push ----->|
29 | |--- spawn git-receive-pack
30 | |
31 | |--- pre-receive hook runs
32 | | (reads stdin: old new ref)
33 | | (exit 0 = accept, 1 = reject)
34 | |
35 |<--- success ------| (if hook exits 0)
36 |<--- error --------| (if hook exits 1)
37```
38
39**Pros:**
40- Standard Git mechanism
41- Language-agnostic (hook can be any executable)
42- Well-documented
43
44**Cons:**
45- Hook output goes to stderr (client sees as `remote:` messages)
46- Hard to provide structured error messages
47- Requires hook installation and management
48- Difficult to test (needs Git repository setup)
49- Hook runs *after* Git has started processing
50
51---
52
53## Background: How Inline Authorization Works
54
55With inline authorization, we validate **before** spawning Git:
56
57```
58Client Server (ngit-grasp)
59 | |
60 |--- git push ----->|--- HTTP handler receives request
61 | |
62 | |--- Parse ref updates from request
63 | |--- Query Nostr relay for state
64 | |--- Validate push against state
65 | |
66 | |--- If invalid: return HTTP error
67 | |--- If valid: spawn git-receive-pack
68 | |
69 |<--- success ------| (if valid)
70 |<--- HTTP error ---| (if invalid)
71```
72
73**Pros:**
74- Full control over error messages (HTTP response)
75- Can skip spawning Git entirely for invalid pushes
76- Easier testing (pure Rust, no Git setup needed)
77- Shared state between Git and Nostr components
78- Better performance (early rejection)
79
80**Cons:**
81- Requires parsing Git protocol ourselves
82- Less standard than hooks
83- Tighter coupling to Git HTTP protocol
84
85---
86
87## Why Inline Authorization Is Better for GRASP
88
89### 1. Better Error Messages
90
91**With hooks:**
92```
93$ git push
94remote: error: Push rejected - not authorized for ref refs/heads/main
95remote: See https://docs.gitnostr.com/errors/unauthorized
96To https://gitnostr.com/alice/myrepo.git
97 ! [remote rejected] main -> main (pre-receive hook declined)
98```
99
100**With inline authorization:**
101```
102$ git push
103error: RPC failed; HTTP 403 Forbidden
104error: {
105 "error": "unauthorized",
106 "ref": "refs/heads/main",
107 "required_state": "event_id_abc123",
108 "your_pubkey": "npub1alice...",
109 "docs": "https://docs.gitnostr.com/errors/unauthorized"
110}
111```
112
113The inline approach can return **structured JSON** with actionable information.
114
115### 2. Performance Benefits
116
117**With hooks:**
118- Git process spawns
119- Git starts receiving pack data
120- Hook runs (might query Nostr relay)
121- If rejected, Git throws away received data
122
123**With inline authorization:**
124- Parse ref updates from HTTP request
125- Validate against Nostr state (cached)
126- If rejected, return HTTP 403 immediately
127- Never spawn Git for invalid pushes
128
129**Result:** Faster rejection, less resource usage.
130
131### 3. Easier Testing
132
133**With hooks:**
134```bash
135# Test setup
136mkdir -p /tmp/test-repo
137cd /tmp/test-repo
138git init --bare
139cp pre-receive.sh hooks/pre-receive
140chmod +x hooks/pre-receive
141
142# Test execution
143git push /tmp/test-repo main
144
145# Cleanup
146rm -rf /tmp/test-repo
147```
148
149**With inline authorization:**
150```rust
151#[tokio::test]
152async fn test_unauthorized_push() {
153 let state = create_test_state().await;
154 let result = validate_push(&state, "refs/heads/main", alice_pubkey).await;
155 assert!(result.is_err());
156}
157```
158
159**Result:** Pure Rust unit tests, no shell scripts, no Git setup.
160
161### 4. Shared State and Types
162
163**With hooks:**
164- Hook is separate process
165- Must query Nostr relay over WebSocket
166- Can't share in-memory cache
167- Separate error types
168
169**With inline authorization:**
170```rust
171pub struct GitHandler {
172 nostr_relay: Arc<NostrRelay>, // Shared!
173 state_cache: Arc<StateCache>, // Shared!
174}
175
176impl GitHandler {
177 async fn validate_push(&self, refs: &[RefUpdate]) -> Result<()> {
178 // Direct access to Nostr state
179 let state = self.state_cache.get_latest().await?;
180 // Validate using shared types
181 state.validate_refs(refs)?;
182 Ok(())
183 }
184}
185```
186
187**Result:** Better performance, type safety, simpler architecture.
188
189### 5. Simpler Deployment
190
191**With hooks (ngit-relay):**
192```
193Docker container:
194 - nginx (HTTP frontend)
195 - git-http-backend (C binary)
196 - pre-receive hook (Go binary)
197 - Khatru relay (Go binary)
198 - supervisord (process manager)
199
200Setup steps:
201 1. Install all components
202 2. Configure nginx
203 3. Install hook in each repository
204 4. Set up supervisord
205 5. Configure inter-process communication
206```
207
208**With inline authorization (ngit-grasp):**
209```
210Single Rust binary:
211 - HTTP server (actix-web)
212 - Git protocol handler
213 - Nostr relay
214 - Authorization logic
215
216Setup steps:
217 1. Run binary
218 2. Configure environment variables
219```
220
221**Result:** Simpler deployment, fewer moving parts.
222
223---
224
225## Technical Implementation
226
227### How We Parse Ref Updates
228
229The Git HTTP protocol sends ref updates in the request body:
230
231```
232POST /alice/myrepo.git/git-receive-pack HTTP/1.1
233Content-Type: application/x-git-receive-pack-request
234
2350000000000000000000000000000000000000000 abc123... refs/heads/main\0 report-status
236```
237
238We parse this **before** spawning Git:
239
240```rust
241pub async fn git_receive_pack(
242 req: HttpRequest,
243 body: web::Bytes,
244) -> Result<HttpResponse, Error> {
245 // 1. Parse ref updates from request body
246 let ref_updates = parse_ref_updates(&body)?;
247
248 // 2. Validate against Nostr state
249 let state = get_latest_state(&repo).await?;
250 validate_push(&state, &ref_updates).await?;
251
252 // 3. If valid, spawn git-receive-pack
253 spawn_git_receive_pack(req, body).await
254}
255```
256
257### How We Validate
258
259Validation checks:
2601. Does pusher's pubkey have write access?
2612. Are they listed as a maintainer in the latest state event?
2623. Do maintainer sets form a valid chain?
263
264```rust
265async fn validate_push(
266 state: &RepoState,
267 refs: &[RefUpdate],
268) -> Result<()> {
269 for ref_update in refs {
270 // Check if pusher is authorized for this ref
271 if !state.is_authorized(&ref_update.name, pusher_pubkey) {
272 return Err(Error::Unauthorized {
273 ref_name: ref_update.name.clone(),
274 pubkey: pusher_pubkey,
275 });
276 }
277 }
278 Ok(())
279}
280```
281
282---
283
284## Comparison with Reference Implementation
285
286| Aspect | ngit-relay (hooks) | ngit-grasp (inline) |
287|--------|-------------------|---------------------|
288| **Components** | nginx + git-http-backend + hook + Khatru | Single Rust binary |
289| **Validation** | Pre-receive hook (separate process) | Inline HTTP handler |
290| **Error messages** | Hook stderr → `remote:` | HTTP response JSON |
291| **Performance** | Spawns Git first | Validates first |
292| **Testing** | Shell scripts + Go tests | Pure Rust tests |
293| **Deployment** | Docker + supervisord | Single binary |
294| **State sharing** | WebSocket query | Direct memory access |
295
296Both are GRASP-compliant, but inline authorization is simpler and more efficient.
297
298---
299
300## Trade-offs and Limitations
301
302### What We Gain
303- ✅ Better error messages
304- ✅ Better performance
305- ✅ Easier testing
306- ✅ Simpler deployment
307- ✅ Tighter integration
308
309### What We Lose
310- ❌ Non-standard approach (not using Git's hook system)
311- ❌ Tighter coupling to Git HTTP protocol
312- ❌ Must parse protocol ourselves
313
314### Is It Worth It?
315
316**Yes**, because:
3171. The `git-http-backend` crate handles protocol parsing
3182. GRASP is already non-standard (Nostr authorization)
3193. Benefits far outweigh the coupling cost
3204. We can still add hook support later if needed
321
322---
323
324## Alternative Considered: Hybrid Approach
325
326We could use **both** inline validation and hooks:
327
328```rust
329// Inline: Fast path for common cases
330if !quick_validate(pusher).await? {
331 return Err(Error::Unauthorized);
332}
333
334// Hook: Detailed validation
335spawn_git_with_hook().await?;
336```
337
338**Why we didn't choose this:**
339- Added complexity
340- Redundant validation
341- Slower (two validation steps)
342- Harder to maintain
343
344If inline validation is sufficient, why add hooks?
345
346---
347
348## Future Considerations
349
350### If We Need Hooks Later
351
352We can add hook support without removing inline validation:
353
354```rust
355pub struct GitConfig {
356 inline_validation: bool, // Default: true
357 hook_validation: bool, // Default: false
358}
359```
360
361This would allow:
362- Migration path for hook-based systems
363- Extra validation for paranoid deployments
364- Compatibility with other Git tools
365
366### If Git Protocol Changes
367
368The `git-http-backend` crate abstracts protocol details. If the Git protocol changes:
369- Update the crate dependency
370- Adjust our ref parsing if needed
371- Tests will catch any breakage
372
373---
374
375## Conclusion
376
377**Inline authorization is the right choice for ngit-grasp** because:
378
3791. It provides better error messages for users
3802. It's more performant (early rejection)
3813. It's easier to test (pure Rust)
3824. It's simpler to deploy (single binary)
3835. It enables better integration (shared state)
384
385The trade-off (coupling to Git HTTP protocol) is acceptable because:
386- The protocol is stable and well-specified
387- The `git-http-backend` crate abstracts details
388- Benefits far outweigh the cost
389
390This decision aligns with our goal of creating a **developer-friendly, production-ready GRASP implementation**.
391
392---
393
394## Related Documentation
395
396- [Architecture Overview](architecture.md) - Full system design
397- [Design Decisions](decisions.md) - All architectural choices
398- [Comparison with ngit-relay](comparison.md) - Detailed comparison
399- [Git Protocol Reference](../reference/git-protocol.md) - Protocol details
400
401---
402
403*Part of the [ngit-grasp explanation docs](./)*