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>2026-01-08 00:26:51 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-08 00:26:51 +0000
commit543d9e66dd44b70ed467c61635e6c8056fef8555 (patch)
tree99783725680e3f1d4c88699777746bc3ea9fa806 /docs/explanation/inline-authorization.md
parentc67ebe6f33bfa191f17eb0df24d3ee18092c74e1 (diff)
docs: update docs with sync and purgatory and git data sync
Diffstat (limited to 'docs/explanation/inline-authorization.md')
-rw-r--r--docs/explanation/inline-authorization.md270
1 files changed, 180 insertions, 90 deletions
diff --git a/docs/explanation/inline-authorization.md b/docs/explanation/inline-authorization.md
index 4538602..a71a217 100644
--- a/docs/explanation/inline-authorization.md
+++ b/docs/explanation/inline-authorization.md
@@ -37,16 +37,18 @@ Client Server
37``` 37```
38 38
39**Pros:** 39**Pros:**
40
40- Standard Git mechanism 41- Standard Git mechanism
41- Language-agnostic (hook can be any executable) 42- Language-agnostic (hook can be any executable)
42- Well-documented 43- Well-documented
43 44
44**Cons:** 45**Cons:**
46
45- Hook output goes to stderr (client sees as `remote:` messages) 47- Hook output goes to stderr (client sees as `remote:` messages)
46- Hard to provide structured error messages 48- Hard to provide structured error messages
47- Requires hook installation and management 49- Requires hook installation and management
48- Difficult to test (needs Git repository setup) 50- Difficult to test (needs Git repository setup)
49- Hook runs *after* Git has started processing 51- Hook runs _after_ Git has started processing
50 52
51--- 53---
52 54
@@ -60,7 +62,7 @@ Client Server (ngit-grasp)
60 |--- git push ----->|--- HTTP handler receives request 62 |--- git push ----->|--- HTTP handler receives request
61 | | 63 | |
62 | |--- Parse ref updates from request 64 | |--- Parse ref updates from request
63 | |--- Query Nostr relay for state 65 | |--- Query database + purgatory for state
64 | |--- Validate push against state 66 | |--- Validate push against state
65 | | 67 | |
66 | |--- If invalid: return HTTP error 68 | |--- If invalid: return HTTP error
@@ -71,13 +73,16 @@ Client Server (ngit-grasp)
71``` 73```
72 74
73**Pros:** 75**Pros:**
76
74- Full control over error messages (HTTP response) 77- Full control over error messages (HTTP response)
75- Can skip spawning Git entirely for invalid pushes 78- Can skip spawning Git entirely for invalid pushes
76- Easier testing (pure Rust, no Git setup needed) 79- Easier testing (pure Rust, no Git setup needed)
77- Shared state between Git and Nostr components 80- Shared state between Git and Nostr components
78- Better performance (early rejection) 81- Better performance (early rejection)
82- Can check both database and purgatory for authorization
79 83
80**Cons:** 84**Cons:**
85
81- Requires parsing Git protocol ourselves 86- Requires parsing Git protocol ourselves
82- Less standard than hooks 87- Less standard than hooks
83- Tighter coupling to Git HTTP protocol 88- Tighter coupling to Git HTTP protocol
@@ -86,9 +91,41 @@ Client Server (ngit-grasp)
86 91
87## Why Inline Authorization Is Better for GRASP 92## Why Inline Authorization Is Better for GRASP
88 93
89### 1. Better Error Messages 94### 1. Purgatory Integration
95
96**Critical advantage:** Inline authorization allows checking **both database and purgatory** during authorization:
97
98```rust
99// From src/git/authorization.rs
100pub async fn authorize_push(
101 database: &SharedDatabase,
102 identifier: &str,
103 owner_pubkey: &str,
104 request_body: &Bytes,
105 purgatory: &Arc<Purgatory>, // Can check purgatory!
106 repo_path: &std::path::Path,
107) -> anyhow::Result<AuthorizationResult>
108```
109
110**Why this matters:** State events go to purgatory when git data doesn't exist yet. Without inline authorization checking purgatory, we'd have a deadlock:
111
1121. State event arrives → No git data → Goes to **purgatory** (not database)
1132. Git push arrives → Hook checks **database only** → No state found → **REJECTED** ❌
114
115With inline authorization:
116
1171. State event arrives → No git data → Goes to purgatory
1182. Git push arrives → Checks **database + purgatory** → State found → **AUTHORIZED** ✅
1193. After push succeeds → Save event to database → Remove from purgatory
120
121See [`src/git/authorization.rs:342-400`](../../src/git/authorization.rs) for implementation.
122
123otherwise we'd need another way of storing purgatory events.
124
125### 2. Better Error Messages
90 126
91**With hooks:** 127**With hooks:**
128
92``` 129```
93$ git push 130$ git push
94remote: error: Push rejected - not authorized for ref refs/heads/main 131remote: error: Push rejected - not authorized for ref refs/heads/main
@@ -98,39 +135,37 @@ To https://gitnostr.com/alice/myrepo.git
98``` 135```
99 136
100**With inline authorization:** 137**With inline authorization:**
138
101``` 139```
102$ git push 140$ git push
103error: RPC failed; HTTP 403 Forbidden 141error: RPC failed; HTTP 403 Forbidden
104error: { 142error: Push rejected: No state event found in purgatory from authorized publishers
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``` 143```
112 144
113The inline approach can return **structured JSON** with actionable information. 145The inline approach provides clear, actionable error messages directly in the HTTP response.
114 146
115### 2. Performance Benefits 147### 3. Performance Benefits
116 148
117**With hooks:** 149**With hooks:**
150
118- Git process spawns 151- Git process spawns
119- Git starts receiving pack data 152- Git starts receiving pack data
120- Hook runs (might query Nostr relay) 153- Hook runs (might query Nostr relay)
121- If rejected, Git throws away received data 154- If rejected, Git throws away received data
122 155
123**With inline authorization:** 156**With inline authorization:**
124- Parse ref updates from HTTP request 157
125- Validate against Nostr state (cached) 158- Parse ref updates from HTTP request (pkt-line format)
126- If rejected, return HTTP 403 immediately 159- Validate against database + purgatory state
160- If rejected, return HTTP error immediately
127- Never spawn Git for invalid pushes 161- Never spawn Git for invalid pushes
128 162
129**Result:** Faster rejection, less resource usage. 163**Result:** Faster rejection, less resource usage, no wasted pack data transfer.
130 164
131### 3. Easier Testing 165### 4. Easier Testing
132 166
133**With hooks:** 167**With hooks:**
168
134```bash 169```bash
135# Test setup 170# Test setup
136mkdir -p /tmp/test-repo 171mkdir -p /tmp/test-repo
@@ -147,6 +182,7 @@ rm -rf /tmp/test-repo
147``` 182```
148 183
149**With inline authorization:** 184**With inline authorization:**
185
150```rust 186```rust
151#[tokio::test] 187#[tokio::test]
152async fn test_unauthorized_push() { 188async fn test_unauthorized_push() {
@@ -161,43 +197,55 @@ async fn test_unauthorized_push() {
161 197
162See [`tests/push_authorization.rs`](tests/push_authorization.rs) for actual test examples. 198See [`tests/push_authorization.rs`](tests/push_authorization.rs) for actual test examples.
163 199
164### 4. Shared State and Types 200### 5. Shared State and Types
165 201
166**With hooks:** 202**With hooks:**
203
167- Hook is separate process 204- Hook is separate process
168- Must query Nostr relay over WebSocket 205- Must query Nostr relay over WebSocket
169- Can't share in-memory cache 206- Can't share in-memory cache
207- Can't access purgatory
170- Separate error types 208- Separate error types
171 209
172**With inline authorization:** 210**With inline authorization:**
211
173```rust 212```rust
174// From src/git/handlers.rs 213// From src/git/handlers.rs
175pub async fn handle_receive_pack( 214pub async fn handle_receive_pack(
176 repo_path: PathBuf, 215 repo_path: PathBuf,
177 body: Bytes, 216 body: Bytes,
178 database: SharedDatabase, // Shared with Nostr relay! 217 database: Option<SharedDatabase>, // Shared with Nostr relay!
218 purgatory: Option<Arc<Purgatory>>, // Shared purgatory access!
179 npub: &str, 219 npub: &str,
180 identifier: &str, 220 identifier: &str,
181) -> Result<Response<Full<Bytes>>, GitError> { 221) -> Result<Response<Full<Bytes>>, GitError> {
182 // Direct database access for authorization 222 // Direct database + purgatory access for authorization
183 let auth = get_authorization_for_owner(&database, pubkey, identifier).await?; 223 let auth = authorize_push(
224 &database,
225 identifier,
226 owner_pubkey,
227 &body,
228 &purgatory, // Can check purgatory!
229 &repo_path
230 ).await?;
184 // ... 231 // ...
185} 232}
186``` 233```
187 234
188**Result:** Better performance, type safety, simpler architecture. 235**Result:** Better performance, type safety, simpler architecture, purgatory integration.
189 236
190### 5. Simpler Deployment 237### 6. Simpler Deployment
191 238
192**With hooks (ngit-relay):** 239**With hooks (ngit-relay):**
240
193``` 241```
194Docker container: 242Docker container:
195 - nginx (HTTP frontend) 243 - nginx (HTTP frontend)
196 - git-http-backend (C binary) 244 - git-http-backend (C binary)
197 - pre-receive hook (Go binary) 245 - pre-receive hook (Go binary)
198 - Khatru relay (Go binary) 246 - Khatru relay (Go binary)
199 - supervisord (process manager) 247 - supervisord (process manager)
200 248
201Setup steps: 249Setup steps:
202 1. Install all components 250 1. Install all components
203 2. Configure nginx 251 2. Configure nginx
@@ -207,13 +255,14 @@ Setup steps:
207``` 255```
208 256
209**With inline authorization (ngit-grasp):** 257**With inline authorization (ngit-grasp):**
258
210``` 259```
211Single Rust binary: 260Single Rust binary:
212 - HTTP server (Hyper) 261 - HTTP server (Hyper)
213 - Git protocol handler 262 - Git protocol handler
214 - Nostr relay (nostr-relay-builder) 263 - Nostr relay (nostr-relay-builder)
215 - Authorization logic 264 - Authorization logic
216 265
217Setup steps: 266Setup steps:
218 1. Run binary 267 1. Run binary
219 2. Configure environment variables 268 2. Configure environment variables
@@ -227,66 +276,95 @@ Setup steps:
227 276
228### How We Parse Ref Updates 277### How We Parse Ref Updates
229 278
230The Git HTTP protocol sends ref updates in the request body: 279The Git HTTP protocol sends ref updates in pkt-line format:
231 280
232``` 281```
233POST /alice/myrepo.git/git-receive-pack HTTP/1.1 282POST /alice/myrepo.git/git-receive-pack HTTP/1.1
234Content-Type: application/x-git-receive-pack-request 283Content-Type: application/x-git-receive-pack-request
235 284
2360000000000000000000000000000000000000000 abc123... refs/heads/main\0 report-status 28500a5 0000...0000 abc123...def456 refs/heads/main\0 report-status\n
2860000
287PACK...
237``` 288```
238 289
239We parse this **before** spawning Git. See [`src/git/authorization.rs`](src/git/authorization.rs) for the implementation: 290We parse this **before** spawning Git. See [`src/git/authorization.rs:695-778`](../../src/git/authorization.rs) for the implementation:
240 291
241```rust 292```rust
242/// Parse ref updates from git-receive-pack request body 293/// Parse the refs being updated from a Git pack
243pub fn parse_pushed_refs(body: &[u8]) -> Result<Vec<PushedRef>, AuthorizationError> { 294///
244 // Parse pkt-line format 295/// The receive-pack protocol sends ref updates in pkt-line format:
245 // Extract ref updates 296/// - 4-byte hex length prefix (e.g., "00a5")
246 // Return structured data 297/// - Payload: `<old-oid> <new-oid> <ref-name>\0<capabilities>\n`
298/// - Flush packet "0000" terminates the list
299pub fn parse_pushed_refs(data: &[u8]) -> Vec<(String, String, String)> {
300 // Handles both pkt-line format (real Git clients)
301 // and simple text format (for unit tests)
247} 302}
248``` 303```
249 304
250### How We Validate 305### How We Validate
251 306
252Validation checks (from [`src/git/authorization.rs`](src/git/authorization.rs)): 307The authorization flow (from [`src/git/authorization.rs:51-162`](../../src/git/authorization.rs)):
253
2541. Does pusher's pubkey have write access?
2552. Are they listed as a maintainer in the latest state event?
2563. Do the refs match the state event?
257 308
258```rust 309```rust
259/// Validate that pushed refs match the authorized state 310pub async fn authorize_push(
260pub fn validate_push_refs( 311 database: &SharedDatabase,
261 pushed_refs: &[PushedRef], 312 identifier: &str,
262 state: &RepositoryState, 313 owner_pubkey: &str,
263) -> Result<(), AuthorizationError> { 314 request_body: &Bytes,
264 for pushed_ref in pushed_refs { 315 purgatory: &Arc<Purgatory>,
265 if pushed_ref.ref_name.starts_with("refs/heads/") { 316 repo_path: &std::path::Path,
266 // Validate branch against state 317) -> anyhow::Result<AuthorizationResult> {
267 } else if pushed_ref.ref_name.starts_with("refs/tags/") { 318 // 1. Parse refs from push request
268 // Validate tag against state 319 let pushed_refs = parse_pushed_refs(request_body);
269 } else if pushed_ref.ref_name.starts_with("refs/nostr/") { 320
270 // Allow refs/nostr/<event-id> for PRs 321 // 2. Separate refs/nostr/ refs from state refs
271 } 322 let (nostr_refs, state_refs) = partition_refs(&pushed_refs);
272 } 323
273 Ok(()) 324 // 3. Handle refs/nostr/ refs (PR events)
325 // - Validate event ID format
326 // - Check purgatory for PR event
327 // - Create placeholder if git-data-first scenario
328
329 // 4. Handle normal refs (state events)
330 // - Check database + purgatory for state events
331 // - Collect authorized maintainers
332 // - Find latest authorized state
333 // - Validate refs match state
334
335 // 5. Return authorization result with purgatory events
274} 336}
275``` 337```
276 338
339**Key validation checks:**
340
3411. **For state refs** (`refs/heads/*`, `refs/tags/*`):
342
343 - Query database for announcements → collect authorized maintainers
344 - Check **purgatory** for matching state events (critical for purgatory flow!)
345 - Filter to events from authorized maintainers
346 - Find latest state event
347 - Validate pushed refs match state event refs
348
3492. **For PR refs** (`refs/nostr/<event-id>`):
350 - Validate event ID format
351 - Check purgatory for PR event with matching commit
352 - If no event found, create placeholder (git-data-first scenario)
353 - Collect PR events from purgatory for post-push processing
354
277--- 355---
278 356
279## Comparison with Reference Implementation 357## Comparison with Reference Implementation
280 358
281| Aspect | ngit-relay (hooks) | ngit-grasp (inline) | 359| Aspect | ngit-relay (hooks) | ngit-grasp (inline) |
282|--------|-------------------|---------------------| 360| ------------------ | ---------------------------------------- | ---------------------- |
283| **Components** | nginx + git-http-backend + hook + Khatru | Single Rust binary | 361| **Components** | nginx + git-http-backend + hook + Khatru | Single Rust binary |
284| **Validation** | Pre-receive hook (separate process) | Inline HTTP handler | 362| **Validation** | Pre-receive hook (separate process) | Inline HTTP handler |
285| **Error messages** | Hook stderr → `remote:` | HTTP response JSON | 363| **Error messages** | Hook stderr → `remote:` | HTTP response JSON |
286| **Performance** | Spawns Git first | Validates first | 364| **Performance** | Spawns Git first | Validates first |
287| **Testing** | Shell scripts + Go tests | Pure Rust tests | 365| **Testing** | Shell scripts + Go tests | Pure Rust tests |
288| **Deployment** | Docker + supervisord | Single binary | 366| **Deployment** | Docker + supervisord | Single binary |
289| **State sharing** | WebSocket query | Direct database access | 367| **State sharing** | WebSocket query | Direct database access |
290 368
291Both are GRASP-compliant, but inline authorization is simpler and more efficient. 369Both are GRASP-compliant, but inline authorization is simpler and more efficient.
292 370
@@ -295,24 +373,30 @@ Both are GRASP-compliant, but inline authorization is simpler and more efficient
295## Trade-offs and Limitations 373## Trade-offs and Limitations
296 374
297### What We Gain 375### What We Gain
376
377- ✅ **Purgatory integration** - Can check database + purgatory during authorization
378- ✅ **Prevents deadlock** - State events in purgatory can authorize pushes
298- ✅ Better error messages 379- ✅ Better error messages
299- ✅ Better performance 380- ✅ Better performance (early rejection)
300- ✅ Easier testing 381- ✅ Easier testing (pure Rust)
301- ✅ Simpler deployment 382- ✅ Simpler deployment (single binary)
302- ✅ Tighter integration 383- ✅ Tighter integration (shared state)
303 384
304### What We Lose 385### What We Lose
386
305- ❌ Non-standard approach (not using Git's hook system) 387- ❌ Non-standard approach (not using Git's hook system)
306- ❌ Tighter coupling to Git HTTP protocol 388- ❌ Tighter coupling to Git HTTP protocol
307- ❌ Must parse protocol ourselves 389- ❌ Must parse pkt-line protocol ourselves
308 390
309### Is It Worth It? 391### Is It Worth It?
310 392
311**Yes**, because: 393**Absolutely**, because:
3121. We handle protocol parsing in [`src/git/protocol.rs`](src/git/protocol.rs) 394
3132. GRASP is already non-standard (Nostr authorization) 3951. **Purgatory integration is essential** - Without it, we'd have a deadlock where state events in purgatory can't authorize pushes
3143. Benefits far outweigh the coupling cost 3962. Protocol parsing is isolated in [`src/git/authorization.rs`](../../src/git/authorization.rs)
3154. We can still add hook support later if needed 3973. GRASP is already non-standard (Nostr authorization)
3984. Benefits far outweigh the coupling cost
3995. We can still add hook support later if needed (but purgatory checking would still need inline access)
316 400
317--- 401---
318 402
@@ -320,14 +404,15 @@ Both are GRASP-compliant, but inline authorization is simpler and more efficient
320 404
321Key files in the ngit-grasp implementation: 405Key files in the ngit-grasp implementation:
322 406
323| Component | Location | 407| Component | Location |
324|-----------|----------| 408| ----------------------- | ------------------------------------------------------------------------- |
325| HTTP routing | [`src/http/mod.rs`](src/http/mod.rs) | 409| HTTP routing | [`src/http/mod.rs`](../../src/http/mod.rs) |
326| Git handlers | [`src/git/handlers.rs`](src/git/handlers.rs) | 410| Git handlers | [`src/git/handlers.rs`](../../src/git/handlers.rs) |
327| Push authorization | [`src/git/authorization.rs`](src/git/authorization.rs) | 411| Push authorization | [`src/git/authorization.rs`](../../src/git/authorization.rs) |
328| Git protocol parsing | [`src/git/protocol.rs`](src/git/protocol.rs) | 412| Pkt-line parsing | [`src/git/authorization.rs:695-778`](../../src/git/authorization.rs) |
329| Subprocess management | [`src/git/subprocess.rs`](src/git/subprocess.rs) | 413| Subprocess management | [`src/git/subprocess.rs`](../../src/git/subprocess.rs) |
330| Event acceptance policy | [`src/nostr/builder.rs:51`](src/nostr/builder.rs:51) - `Nip34WritePolicy` | 414| Purgatory integration | [`src/purgatory/mod.rs`](../../src/purgatory/mod.rs) |
415| Event acceptance policy | [`src/nostr/builder.rs`](../../src/nostr/builder.rs) - `Nip34WritePolicy` |
331 416
332--- 417---
333 418
@@ -345,6 +430,7 @@ pub struct GitConfig {
345``` 430```
346 431
347This would allow: 432This would allow:
433
348- Migration path for hook-based systems 434- Migration path for hook-based systems
349- Extra validation for paranoid deployments 435- Extra validation for paranoid deployments
350- Compatibility with other Git tools 436- Compatibility with other Git tools
@@ -352,6 +438,7 @@ This would allow:
352### If Git Protocol Changes 438### If Git Protocol Changes
353 439
354The protocol parsing is isolated in [`src/git/protocol.rs`](src/git/protocol.rs). If the Git protocol changes: 440The protocol parsing is isolated in [`src/git/protocol.rs`](src/git/protocol.rs). If the Git protocol changes:
441
355- Update the protocol module 442- Update the protocol module
356- Tests will catch any breakage 443- Tests will catch any breakage
357 444
@@ -361,18 +448,21 @@ The protocol parsing is isolated in [`src/git/protocol.rs`](src/git/protocol.rs)
361 448
362**Inline authorization is the right choice for ngit-grasp** because: 449**Inline authorization is the right choice for ngit-grasp** because:
363 450
3641. It provides better error messages for users 4511. **Purgatory integration** - Without inline authorization, state events in purgatory couldn't authorize pushes, creating a deadlock
3652. It's more performant (early rejection) 4522. **Better error messages** - Direct HTTP responses with clear rejection reasons
3663. It's easier to test (pure Rust) 4533. **Better performance** - Early rejection before spawning Git
3674. It's simpler to deploy (single binary) 4544. **Easier testing** - Pure Rust unit tests, no Git setup needed
3685. It enables better integration (shared database) 4555. **Simpler deployment** - Single binary with shared state
4566. **Shared database + purgatory** - Both authorization sources accessible during validation
369 457
370The trade-off (coupling to Git HTTP protocol) is acceptable because: 458The trade-off (coupling to Git HTTP protocol) is acceptable because:
371- The protocol is stable and well-specified 459
372- Protocol handling is isolated in one module 460- The pkt-line protocol is stable and well-specified
461- Protocol parsing is isolated in [`src/git/authorization.rs`](../../src/git/authorization.rs)
462- Purgatory integration requires inline access anyway
373- Benefits far outweigh the cost 463- Benefits far outweigh the cost
374 464
375This decision aligns with our goal of creating a **developer-friendly, production-ready GRASP implementation**. 465This decision aligns with our goal of creating a **developer-friendly, production-ready GRASP implementation** that properly handles the event-git-data ordering problem via purgatory.
376 466
377--- 467---
378 468
@@ -386,4 +476,4 @@ This decision aligns with our goal of creating a **developer-friendly, productio
386 476
387--- 477---
388 478
389*Part of the [ngit-grasp explanation docs](./)* \ No newline at end of file 479_Part of the [ngit-grasp explanation docs](./)_