diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-04 12:34:20 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-04 13:02:59 +0000 |
| commit | d9bc5ed7fddef3a26de8e69a7124e1dbe5b8602f (patch) | |
| tree | c76ffcbf246c8bef7545337316c0afb90433bbf5 /docs/explanation/inline-authorization.md | |
| parent | 40831a9025d05fa354b7d8386eeebd902092ea86 (diff) | |
docs: update based on current implementation
Diffstat (limited to 'docs/explanation/inline-authorization.md')
| -rw-r--r-- | docs/explanation/inline-authorization.md | 126 |
1 files changed, 56 insertions, 70 deletions
diff --git a/docs/explanation/inline-authorization.md b/docs/explanation/inline-authorization.md index 98f6e5a..4538602 100644 --- a/docs/explanation/inline-authorization.md +++ b/docs/explanation/inline-authorization.md | |||
| @@ -150,14 +150,17 @@ rm -rf /tmp/test-repo | |||
| 150 | ```rust | 150 | ```rust |
| 151 | #[tokio::test] | 151 | #[tokio::test] |
| 152 | async fn test_unauthorized_push() { | 152 | async fn test_unauthorized_push() { |
| 153 | let state = create_test_state().await; | 153 | let relay = TestRelay::start().await; |
| 154 | let result = validate_push(&state, "refs/heads/main", alice_pubkey).await; | 154 | let result = validate_push(&state, "refs/heads/main", alice_pubkey).await; |
| 155 | assert!(result.is_err()); | 155 | assert!(result.is_err()); |
| 156 | relay.stop().await; | ||
| 156 | } | 157 | } |
| 157 | ``` | 158 | ``` |
| 158 | 159 | ||
| 159 | **Result:** Pure Rust unit tests, no shell scripts, no Git setup. | 160 | **Result:** Pure Rust unit tests, no shell scripts, no Git setup. |
| 160 | 161 | ||
| 162 | See [`tests/push_authorization.rs`](tests/push_authorization.rs) for actual test examples. | ||
| 163 | |||
| 161 | ### 4. Shared State and Types | 164 | ### 4. Shared State and Types |
| 162 | 165 | ||
| 163 | **With hooks:** | 166 | **With hooks:** |
| @@ -168,19 +171,17 @@ async fn test_unauthorized_push() { | |||
| 168 | 171 | ||
| 169 | **With inline authorization:** | 172 | **With inline authorization:** |
| 170 | ```rust | 173 | ```rust |
| 171 | pub struct GitHandler { | 174 | // From src/git/handlers.rs |
| 172 | nostr_relay: Arc<NostrRelay>, // Shared! | 175 | pub async fn handle_receive_pack( |
| 173 | state_cache: Arc<StateCache>, // Shared! | 176 | repo_path: PathBuf, |
| 174 | } | 177 | body: Bytes, |
| 175 | 178 | database: SharedDatabase, // Shared with Nostr relay! | |
| 176 | impl GitHandler { | 179 | npub: &str, |
| 177 | async fn validate_push(&self, refs: &[RefUpdate]) -> Result<()> { | 180 | identifier: &str, |
| 178 | // Direct access to Nostr state | 181 | ) -> Result<Response<Full<Bytes>>, GitError> { |
| 179 | let state = self.state_cache.get_latest().await?; | 182 | // Direct database access for authorization |
| 180 | // Validate using shared types | 183 | let auth = get_authorization_for_owner(&database, pubkey, identifier).await?; |
| 181 | state.validate_refs(refs)?; | 184 | // ... |
| 182 | Ok(()) | ||
| 183 | } | ||
| 184 | } | 185 | } |
| 185 | ``` | 186 | ``` |
| 186 | 187 | ||
| @@ -208,9 +209,9 @@ Setup steps: | |||
| 208 | **With inline authorization (ngit-grasp):** | 209 | **With inline authorization (ngit-grasp):** |
| 209 | ``` | 210 | ``` |
| 210 | Single Rust binary: | 211 | Single Rust binary: |
| 211 | - HTTP server (actix-web) | 212 | - HTTP server (Hyper) |
| 212 | - Git protocol handler | 213 | - Git protocol handler |
| 213 | - Nostr relay | 214 | - Nostr relay (nostr-relay-builder) |
| 214 | - Authorization logic | 215 | - Authorization logic |
| 215 | 216 | ||
| 216 | Setup steps: | 217 | Setup steps: |
| @@ -235,44 +236,38 @@ Content-Type: application/x-git-receive-pack-request | |||
| 235 | 0000000000000000000000000000000000000000 abc123... refs/heads/main\0 report-status | 236 | 0000000000000000000000000000000000000000 abc123... refs/heads/main\0 report-status |
| 236 | ``` | 237 | ``` |
| 237 | 238 | ||
| 238 | We parse this **before** spawning Git: | 239 | We parse this **before** spawning Git. See [`src/git/authorization.rs`](src/git/authorization.rs) for the implementation: |
| 239 | 240 | ||
| 240 | ```rust | 241 | ```rust |
| 241 | pub async fn git_receive_pack( | 242 | /// Parse ref updates from git-receive-pack request body |
| 242 | req: HttpRequest, | 243 | pub fn parse_pushed_refs(body: &[u8]) -> Result<Vec<PushedRef>, AuthorizationError> { |
| 243 | body: web::Bytes, | 244 | // Parse pkt-line format |
| 244 | ) -> Result<HttpResponse, Error> { | 245 | // Extract ref updates |
| 245 | // 1. Parse ref updates from request body | 246 | // Return structured data |
| 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 | } | 247 | } |
| 255 | ``` | 248 | ``` |
| 256 | 249 | ||
| 257 | ### How We Validate | 250 | ### How We Validate |
| 258 | 251 | ||
| 259 | Validation checks: | 252 | Validation checks (from [`src/git/authorization.rs`](src/git/authorization.rs)): |
| 253 | |||
| 260 | 1. Does pusher's pubkey have write access? | 254 | 1. Does pusher's pubkey have write access? |
| 261 | 2. Are they listed as a maintainer in the latest state event? | 255 | 2. Are they listed as a maintainer in the latest state event? |
| 262 | 3. Do maintainer sets form a valid chain? | 256 | 3. Do the refs match the state event? |
| 263 | 257 | ||
| 264 | ```rust | 258 | ```rust |
| 265 | async fn validate_push( | 259 | /// Validate that pushed refs match the authorized state |
| 266 | state: &RepoState, | 260 | pub fn validate_push_refs( |
| 267 | refs: &[RefUpdate], | 261 | pushed_refs: &[PushedRef], |
| 268 | ) -> Result<()> { | 262 | state: &RepositoryState, |
| 269 | for ref_update in refs { | 263 | ) -> Result<(), AuthorizationError> { |
| 270 | // Check if pusher is authorized for this ref | 264 | for pushed_ref in pushed_refs { |
| 271 | if !state.is_authorized(&ref_update.name, pusher_pubkey) { | 265 | if pushed_ref.ref_name.starts_with("refs/heads/") { |
| 272 | return Err(Error::Unauthorized { | 266 | // Validate branch against state |
| 273 | ref_name: ref_update.name.clone(), | 267 | } else if pushed_ref.ref_name.starts_with("refs/tags/") { |
| 274 | pubkey: pusher_pubkey, | 268 | // Validate tag against state |
| 275 | }); | 269 | } else if pushed_ref.ref_name.starts_with("refs/nostr/") { |
| 270 | // Allow refs/nostr/<event-id> for PRs | ||
| 276 | } | 271 | } |
| 277 | } | 272 | } |
| 278 | Ok(()) | 273 | Ok(()) |
| @@ -291,7 +286,7 @@ async fn validate_push( | |||
| 291 | | **Performance** | Spawns Git first | Validates first | | 286 | | **Performance** | Spawns Git first | Validates first | |
| 292 | | **Testing** | Shell scripts + Go tests | Pure Rust tests | | 287 | | **Testing** | Shell scripts + Go tests | Pure Rust tests | |
| 293 | | **Deployment** | Docker + supervisord | Single binary | | 288 | | **Deployment** | Docker + supervisord | Single binary | |
| 294 | | **State sharing** | WebSocket query | Direct memory access | | 289 | | **State sharing** | WebSocket query | Direct database access | |
| 295 | 290 | ||
| 296 | Both are GRASP-compliant, but inline authorization is simpler and more efficient. | 291 | Both are GRASP-compliant, but inline authorization is simpler and more efficient. |
| 297 | 292 | ||
| @@ -314,34 +309,25 @@ Both are GRASP-compliant, but inline authorization is simpler and more efficient | |||
| 314 | ### Is It Worth It? | 309 | ### Is It Worth It? |
| 315 | 310 | ||
| 316 | **Yes**, because: | 311 | **Yes**, because: |
| 317 | 1. The `git-http-backend` crate handles protocol parsing | 312 | 1. We handle protocol parsing in [`src/git/protocol.rs`](src/git/protocol.rs) |
| 318 | 2. GRASP is already non-standard (Nostr authorization) | 313 | 2. GRASP is already non-standard (Nostr authorization) |
| 319 | 3. Benefits far outweigh the coupling cost | 314 | 3. Benefits far outweigh the coupling cost |
| 320 | 4. We can still add hook support later if needed | 315 | 4. We can still add hook support later if needed |
| 321 | 316 | ||
| 322 | --- | 317 | --- |
| 323 | 318 | ||
| 324 | ## Alternative Considered: Hybrid Approach | 319 | ## Implementation References |
| 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 | 320 | ||
| 338 | **Why we didn't choose this:** | 321 | Key files in the ngit-grasp implementation: |
| 339 | - Added complexity | ||
| 340 | - Redundant validation | ||
| 341 | - Slower (two validation steps) | ||
| 342 | - Harder to maintain | ||
| 343 | 322 | ||
| 344 | If inline validation is sufficient, why add hooks? | 323 | | Component | Location | |
| 324 | |-----------|----------| | ||
| 325 | | HTTP routing | [`src/http/mod.rs`](src/http/mod.rs) | | ||
| 326 | | Git handlers | [`src/git/handlers.rs`](src/git/handlers.rs) | | ||
| 327 | | Push authorization | [`src/git/authorization.rs`](src/git/authorization.rs) | | ||
| 328 | | Git protocol parsing | [`src/git/protocol.rs`](src/git/protocol.rs) | | ||
| 329 | | 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` | | ||
| 345 | 331 | ||
| 346 | --- | 332 | --- |
| 347 | 333 | ||
| @@ -365,9 +351,8 @@ This would allow: | |||
| 365 | 351 | ||
| 366 | ### If Git Protocol Changes | 352 | ### If Git Protocol Changes |
| 367 | 353 | ||
| 368 | The `git-http-backend` crate abstracts protocol details. If the Git protocol changes: | 354 | The protocol parsing is isolated in [`src/git/protocol.rs`](src/git/protocol.rs). If the Git protocol changes: |
| 369 | - Update the crate dependency | 355 | - Update the protocol module |
| 370 | - Adjust our ref parsing if needed | ||
| 371 | - Tests will catch any breakage | 356 | - Tests will catch any breakage |
| 372 | 357 | ||
| 373 | --- | 358 | --- |
| @@ -380,11 +365,11 @@ The `git-http-backend` crate abstracts protocol details. If the Git protocol cha | |||
| 380 | 2. It's more performant (early rejection) | 365 | 2. It's more performant (early rejection) |
| 381 | 3. It's easier to test (pure Rust) | 366 | 3. It's easier to test (pure Rust) |
| 382 | 4. It's simpler to deploy (single binary) | 367 | 4. It's simpler to deploy (single binary) |
| 383 | 5. It enables better integration (shared state) | 368 | 5. It enables better integration (shared database) |
| 384 | 369 | ||
| 385 | The trade-off (coupling to Git HTTP protocol) is acceptable because: | 370 | The trade-off (coupling to Git HTTP protocol) is acceptable because: |
| 386 | - The protocol is stable and well-specified | 371 | - The protocol is stable and well-specified |
| 387 | - The `git-http-backend` crate abstracts details | 372 | - Protocol handling is isolated in one module |
| 388 | - Benefits far outweigh the cost | 373 | - Benefits far outweigh the cost |
| 389 | 374 | ||
| 390 | This decision aligns with our goal of creating a **developer-friendly, production-ready GRASP implementation**. | 375 | This decision aligns with our goal of creating a **developer-friendly, production-ready GRASP implementation**. |
| @@ -397,7 +382,8 @@ This decision aligns with our goal of creating a **developer-friendly, productio | |||
| 397 | - [Design Decisions](decisions.md) - All architectural choices | 382 | - [Design Decisions](decisions.md) - All architectural choices |
| 398 | - [Comparison with ngit-relay](comparison.md) - Detailed comparison | 383 | - [Comparison with ngit-relay](comparison.md) - Detailed comparison |
| 399 | - [Git Protocol Reference](../reference/git-protocol.md) - Protocol details | 384 | - [Git Protocol Reference](../reference/git-protocol.md) - Protocol details |
| 385 | - [Test Strategy](../reference/test-strategy.md) - How we test this | ||
| 400 | 386 | ||
| 401 | --- | 387 | --- |
| 402 | 388 | ||
| 403 | *Part of the [ngit-grasp explanation docs](./)* | 389 | *Part of the [ngit-grasp explanation docs](./)* \ No newline at end of file |