diff options
Diffstat (limited to 'docs/explanation/inline-authorization.md')
| -rw-r--r-- | docs/explanation/inline-authorization.md | 403 |
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 | |||
| 10 | Git hosting with authorization requires validating pushes before accepting them. The question is: **where** should this validation happen? | ||
| 11 | |||
| 12 | Two approaches exist: | ||
| 13 | |||
| 14 | 1. **Git Hooks** (traditional): Use Git's pre-receive hook mechanism | ||
| 15 | 2. **Inline Authorization** (our approach): Validate before spawning Git | ||
| 16 | |||
| 17 | This document explains why we chose inline authorization and what benefits it provides. | ||
| 18 | |||
| 19 | --- | ||
| 20 | |||
| 21 | ## Background: How Git Hooks Work | ||
| 22 | |||
| 23 | Git provides a **pre-receive hook** that runs during `git push`: | ||
| 24 | |||
| 25 | ``` | ||
| 26 | Client 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 | |||
| 55 | With inline authorization, we validate **before** spawning Git: | ||
| 56 | |||
| 57 | ``` | ||
| 58 | Client 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 | ||
| 94 | remote: error: Push rejected - not authorized for ref refs/heads/main | ||
| 95 | remote: See https://docs.gitnostr.com/errors/unauthorized | ||
| 96 | To 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 | ||
| 103 | error: RPC failed; HTTP 403 Forbidden | ||
| 104 | error: { | ||
| 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 | |||
| 113 | The 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 | ||
| 136 | mkdir -p /tmp/test-repo | ||
| 137 | cd /tmp/test-repo | ||
| 138 | git init --bare | ||
| 139 | cp pre-receive.sh hooks/pre-receive | ||
| 140 | chmod +x hooks/pre-receive | ||
| 141 | |||
| 142 | # Test execution | ||
| 143 | git push /tmp/test-repo main | ||
| 144 | |||
| 145 | # Cleanup | ||
| 146 | rm -rf /tmp/test-repo | ||
| 147 | ``` | ||
| 148 | |||
| 149 | **With inline authorization:** | ||
| 150 | ```rust | ||
| 151 | #[tokio::test] | ||
| 152 | async 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 | ||
| 171 | pub struct GitHandler { | ||
| 172 | nostr_relay: Arc<NostrRelay>, // Shared! | ||
| 173 | state_cache: Arc<StateCache>, // Shared! | ||
| 174 | } | ||
| 175 | |||
| 176 | impl 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 | ``` | ||
| 193 | Docker 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 | |||
| 200 | Setup 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 | ``` | ||
| 210 | Single Rust binary: | ||
| 211 | - HTTP server (actix-web) | ||
| 212 | - Git protocol handler | ||
| 213 | - Nostr relay | ||
| 214 | - Authorization logic | ||
| 215 | |||
| 216 | Setup 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 | |||
| 229 | The Git HTTP protocol sends ref updates in the request body: | ||
| 230 | |||
| 231 | ``` | ||
| 232 | POST /alice/myrepo.git/git-receive-pack HTTP/1.1 | ||
| 233 | Content-Type: application/x-git-receive-pack-request | ||
| 234 | |||
| 235 | 0000000000000000000000000000000000000000 abc123... refs/heads/main\0 report-status | ||
| 236 | ``` | ||
| 237 | |||
| 238 | We parse this **before** spawning Git: | ||
| 239 | |||
| 240 | ```rust | ||
| 241 | pub 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 | |||
| 259 | Validation checks: | ||
| 260 | 1. Does pusher's pubkey have write access? | ||
| 261 | 2. Are they listed as a maintainer in the latest state event? | ||
| 262 | 3. Do maintainer sets form a valid chain? | ||
| 263 | |||
| 264 | ```rust | ||
| 265 | async 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 | |||
| 296 | Both 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: | ||
| 317 | 1. The `git-http-backend` crate handles protocol parsing | ||
| 318 | 2. GRASP is already non-standard (Nostr authorization) | ||
| 319 | 3. Benefits far outweigh the coupling cost | ||
| 320 | 4. We can still add hook support later if needed | ||
| 321 | |||
| 322 | --- | ||
| 323 | |||
| 324 | ## Alternative Considered: Hybrid Approach | ||
| 325 | |||
| 326 | We could use **both** inline validation and hooks: | ||
| 327 | |||
| 328 | ```rust | ||
| 329 | // Inline: Fast path for common cases | ||
| 330 | if !quick_validate(pusher).await? { | ||
| 331 | return Err(Error::Unauthorized); | ||
| 332 | } | ||
| 333 | |||
| 334 | // Hook: Detailed validation | ||
| 335 | spawn_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 | |||
| 344 | If inline validation is sufficient, why add hooks? | ||
| 345 | |||
| 346 | --- | ||
| 347 | |||
| 348 | ## Future Considerations | ||
| 349 | |||
| 350 | ### If We Need Hooks Later | ||
| 351 | |||
| 352 | We can add hook support without removing inline validation: | ||
| 353 | |||
| 354 | ```rust | ||
| 355 | pub struct GitConfig { | ||
| 356 | inline_validation: bool, // Default: true | ||
| 357 | hook_validation: bool, // Default: false | ||
| 358 | } | ||
| 359 | ``` | ||
| 360 | |||
| 361 | This 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 | |||
| 368 | The `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 | |||
| 379 | 1. It provides better error messages for users | ||
| 380 | 2. It's more performant (early rejection) | ||
| 381 | 3. It's easier to test (pure Rust) | ||
| 382 | 4. It's simpler to deploy (single binary) | ||
| 383 | 5. It enables better integration (shared state) | ||
| 384 | |||
| 385 | The 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 | |||
| 390 | This 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](./)* | ||