# Explanation: Inline Authorization **Purpose:** Understand why ngit-grasp validates Git pushes inline rather than using Git hooks **Audience:** Developers and architects wanting to understand design decisions --- ## The Problem Git hosting with authorization requires validating pushes before accepting them. The question is: **where** should this validation happen? Two approaches exist: 1. **Git Hooks** (traditional): Use Git's pre-receive hook mechanism 2. **Inline Authorization** (our approach): Validate before spawning Git This document explains why we chose inline authorization and what benefits it provides. --- ## Background: How Git Hooks Work Git provides a **pre-receive hook** that runs during `git push`: ``` Client Server | | |--- git push ----->| | |--- spawn git-receive-pack | | | |--- pre-receive hook runs | | (reads stdin: old new ref) | | (exit 0 = accept, 1 = reject) | | |<--- success ------| (if hook exits 0) |<--- error --------| (if hook exits 1) ``` **Pros:** - Standard Git mechanism - Language-agnostic (hook can be any executable) - Well-documented **Cons:** - Hook output goes to stderr (client sees as `remote:` messages) - Hard to provide structured error messages - Requires hook installation and management - Difficult to test (needs Git repository setup) - Hook runs _after_ Git has started processing --- ## Background: How Inline Authorization Works With inline authorization, we validate **before** spawning Git: ``` Client Server (ngit-grasp) | | |--- git push ----->|--- HTTP handler receives request | | | |--- Parse ref updates from request | |--- Query database + purgatory for state | |--- Validate push against state | | | |--- If invalid: return HTTP error | |--- If valid: spawn git-receive-pack | | |<--- success ------| (if valid) |<--- HTTP error ---| (if invalid) ``` **Pros:** - Full control over error messages (HTTP response) - Can skip spawning Git entirely for invalid pushes - Easier testing (pure Rust, no Git setup needed) - Shared state between Git and Nostr components - Better performance (early rejection) - Can check both database and purgatory for authorization **Cons:** - Requires parsing Git protocol ourselves - Less standard than hooks - Tighter coupling to Git HTTP protocol --- ## Why Inline Authorization Is Better for GRASP ### 1. Purgatory Integration **Critical advantage:** Inline authorization allows checking **both database and purgatory** during authorization: ```rust // From src/git/authorization.rs pub async fn authorize_push( database: &SharedDatabase, identifier: &str, owner_pubkey: &str, request_body: &Bytes, purgatory: &Arc, // Can check purgatory! repo_path: &std::path::Path, ) -> anyhow::Result ``` **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: 1. State event arrives → No git data → Goes to **purgatory** (not database) 2. Git push arrives → Hook checks **database only** → No state found → **REJECTED** ❌ With inline authorization: 1. State event arrives → No git data → Goes to purgatory 2. Git push arrives → Checks **database + purgatory** → State found → **AUTHORIZED** ✅ 3. After push succeeds → Save event to database → Remove from purgatory See [`src/git/authorization.rs:342-400`](../../src/git/authorization.rs) for implementation. otherwise we'd need another way of storing purgatory events. ### 2. Better Error Messages **With hooks:** ``` $ git push remote: error: Push rejected - not authorized for ref refs/heads/main remote: See https://docs.gitnostr.com/errors/unauthorized To https://gitnostr.com/alice/myrepo.git ! [remote rejected] main -> main (pre-receive hook declined) ``` **With inline authorization:** ``` $ git push error: RPC failed; HTTP 403 Forbidden error: Push rejected: No state event found in purgatory from authorized publishers ``` The inline approach provides clear, actionable error messages directly in the HTTP response. ### 3. Performance Benefits **With hooks:** - Git process spawns - Git starts receiving pack data - Hook runs (might query Nostr relay) - If rejected, Git throws away received data **With inline authorization:** - Parse ref updates from HTTP request (pkt-line format) - Validate against database + purgatory state - If rejected, return HTTP error immediately - Never spawn Git for invalid pushes **Result:** Faster rejection, less resource usage, no wasted pack data transfer. ### 4. Easier Testing **With hooks:** ```bash # Test setup mkdir -p /tmp/test-repo cd /tmp/test-repo git init --bare cp pre-receive.sh hooks/pre-receive chmod +x hooks/pre-receive # Test execution git push /tmp/test-repo main # Cleanup rm -rf /tmp/test-repo ``` **With inline authorization:** ```rust #[tokio::test] async fn test_unauthorized_push() { let relay = TestRelay::start().await; let result = validate_push(&state, "refs/heads/main", alice_pubkey).await; assert!(result.is_err()); relay.stop().await; } ``` **Result:** Pure Rust unit tests, no shell scripts, no Git setup. See [`tests/push_authorization.rs`](tests/push_authorization.rs) for actual test examples. ### 5. Shared State and Types **With hooks:** - Hook is separate process - Must query Nostr relay over WebSocket - Can't share in-memory cache - Can't access purgatory - Separate error types **With inline authorization:** ```rust // From src/git/handlers.rs pub async fn handle_receive_pack( repo_path: PathBuf, body: Bytes, database: Option, // Shared with Nostr relay! purgatory: Option>, // Shared purgatory access! npub: &str, identifier: &str, ) -> Result>, GitError> { // Direct database + purgatory access for authorization let auth = authorize_push( &database, identifier, owner_pubkey, &body, &purgatory, // Can check purgatory! &repo_path ).await?; // ... } ``` **Result:** Better performance, type safety, simpler architecture, purgatory integration. ### 6. Simpler Deployment **With hooks (ngit-relay):** ``` Docker container: - nginx (HTTP frontend) - git-http-backend (C binary) - pre-receive hook (Go binary) - Khatru relay (Go binary) - supervisord (process manager) Setup steps: 1. Install all components 2. Configure nginx 3. Install hook in each repository 4. Set up supervisord 5. Configure inter-process communication ``` **With inline authorization (ngit-grasp):** ``` Single Rust binary: - HTTP server (Hyper) - Git protocol handler - Nostr relay (nostr-relay-builder) - Authorization logic Setup steps: 1. Run binary 2. Configure environment variables ``` **Result:** Simpler deployment, fewer moving parts. --- ## Technical Implementation ### How We Parse Ref Updates The Git HTTP protocol sends ref updates in pkt-line format: ``` POST /alice/myrepo.git/git-receive-pack HTTP/1.1 Content-Type: application/x-git-receive-pack-request 00a5 0000...0000 abc123...def456 refs/heads/main\0 report-status\n 0000 PACK... ``` We parse this **before** spawning Git. See [`src/git/authorization.rs:695-778`](../../src/git/authorization.rs) for the implementation: ```rust /// Parse the refs being updated from a Git pack /// /// The receive-pack protocol sends ref updates in pkt-line format: /// - 4-byte hex length prefix (e.g., "00a5") /// - Payload: ` \0\n` /// - Flush packet "0000" terminates the list pub fn parse_pushed_refs(data: &[u8]) -> Vec<(String, String, String)> { // Handles both pkt-line format (real Git clients) // and simple text format (for unit tests) } ``` ### How We Validate The authorization flow (from [`src/git/authorization.rs:51-162`](../../src/git/authorization.rs)): ```rust pub async fn authorize_push( database: &SharedDatabase, identifier: &str, owner_pubkey: &str, request_body: &Bytes, purgatory: &Arc, repo_path: &std::path::Path, ) -> anyhow::Result { // 1. Parse refs from push request let pushed_refs = parse_pushed_refs(request_body); // 2. Separate refs/nostr/ refs from state refs let (nostr_refs, state_refs) = partition_refs(&pushed_refs); // 3. Handle refs/nostr/ refs (PR events) // - Validate event ID format // - Check purgatory for PR event // - Create placeholder if git-data-first scenario // 4. Handle normal refs (state events) // - Check database + purgatory for state events // - Collect authorized maintainers // - Find latest authorized state // - Validate refs match state // 5. Return authorization result with purgatory events } ``` **Key validation checks:** 1. **For state refs** (`refs/heads/*`, `refs/tags/*`): - Query database for announcements → collect authorized maintainers - Check **purgatory** for matching state events (critical for purgatory flow!) - Filter to events from authorized maintainers - Find latest state event - Validate pushed refs match state event refs 2. **For PR refs** (`refs/nostr/`): - Validate event ID format - Check purgatory for PR event with matching commit - If no event found, create placeholder (git-data-first scenario) - Collect PR events from purgatory for post-push processing ## State Event Authorization State events (kind 30618) undergo authorization checks at three points (defense-in-depth): ### 1. On Arrival (StatePolicy) When a state event arrives via WebSocket or sync: ```rust // src/nostr/policy/state.rs impl StatePolicy { async fn admit_event(&self, event: &Event) -> Result { // Check 1: Does announcement exist for this repository? let announcements = query_announcements(pubkey, identifier); if announcements.is_empty() { return Reject("No announcement exists for repository"); } // Check 2: Is author in maintainer set? let maintainers = build_maintainer_set(announcements); if !maintainers.contains(&event.author) { return Reject("Author not in maintainer set"); } // If git data doesn't exist yet, goes to purgatory // Otherwise, accepted to database } } ``` ### 2. On Announcement Acceptance (Purgatory Re-evaluation) When a repository announcement is accepted, waiting state events are re-evaluated: ```rust // After announcement is saved to database for state_event in purgatory.get_state_events(identifier) { // Re-check authorization now that announcement exists if author_in_maintainer_set(state_event.author, identifier) { // If git data now exists, save to database // Otherwise, keep in purgatory } else { // Remove from purgatory - not authorized } } ``` ### 3. On Git Data Arrival (Purgatory Sync) When git data is pushed, purgatory state events are validated before saving: ```rust // src/git/handlers.rs - after successful git push for state_event in purgatory.get_matching_state_events(identifier) { // Final authorization check before database save if author_in_maintainer_set(state_event.author, identifier) { database.save(state_event); purgatory.remove(state_event); } else { purgatory.remove(state_event); // Not authorized } } ``` ### Why Three Checkpoints? **Defense-in-depth** ensures authorization is always validated: 1. **On arrival**: Prevents unauthorized events from entering the system 2. **On announcement acceptance**: Handles race condition where state arrives before announcement 3. **On git data arrival**: Final check before committing to database This prevents scenarios where: - Unauthorized state events are saved after maintainer changes - Race conditions bypass authorization - Purgatory holds events that will never be authorized ### Rejection Tracking State events rejected during authorization are tracked in the rejected events index: - **Reason: MaintainerNotYetValid** - Author not in maintainer set (may become valid later) - **Reason: Other** - Other validation failures When a repository announcement is accepted, rejected state events for that repository are: 1. **Invalidated** from cold index (removed from negentropy exclusion) 2. **Retrieved** from hot cache (if still available within 2 minutes) 3. **Re-processed** immediately with new maintainer set This enables rapid recovery from race conditions where state events arrive before maintainer announcements. See [work/rejected-events-index-summary.md](../../work/rejected-events-index-summary.md) for complete details on rejection tracking and re-processing. --- ## Comparison with Reference Implementation | Aspect | ngit-relay (hooks) | ngit-grasp (inline) | | ------------------ | ---------------------------------------- | ---------------------- | | **Components** | nginx + git-http-backend + hook + Khatru | Single Rust binary | | **Validation** | Pre-receive hook (separate process) | Inline HTTP handler | | **Error messages** | Hook stderr → `remote:` | HTTP response JSON | | **Performance** | Spawns Git first | Validates first | | **Testing** | Shell scripts + Go tests | Pure Rust tests | | **Deployment** | Docker + supervisord | Single binary | | **State sharing** | WebSocket query | Direct database access | Both are GRASP-compliant, but inline authorization is simpler and more efficient. --- ## Trade-offs and Limitations ### What We Gain - ✅ **Purgatory integration** - Can check database + purgatory during authorization - ✅ **Prevents deadlock** - State events in purgatory can authorize pushes - ✅ Better error messages - ✅ Better performance (early rejection) - ✅ Easier testing (pure Rust) - ✅ Simpler deployment (single binary) - ✅ Tighter integration (shared state) ### What We Lose - ❌ Non-standard approach (not using Git's hook system) - ❌ Tighter coupling to Git HTTP protocol - ❌ Must parse pkt-line protocol ourselves ### Is It Worth It? **Absolutely**, because: 1. **Purgatory integration is essential** - Without it, we'd have a deadlock where state events in purgatory can't authorize pushes 2. Protocol parsing is isolated in [`src/git/authorization.rs`](../../src/git/authorization.rs) 3. GRASP is already non-standard (Nostr authorization) 4. Benefits far outweigh the coupling cost 5. We can still add hook support later if needed (but purgatory checking would still need inline access) --- ## Implementation References Key files in the ngit-grasp implementation: | Component | Location | | ----------------------- | ------------------------------------------------------------------------- | | HTTP routing | [`src/http/mod.rs`](../../src/http/mod.rs) | | Git handlers | [`src/git/handlers.rs`](../../src/git/handlers.rs) | | Push authorization | [`src/git/authorization.rs`](../../src/git/authorization.rs) | | Pkt-line parsing | [`src/git/authorization.rs:695-778`](../../src/git/authorization.rs) | | Subprocess management | [`src/git/subprocess.rs`](../../src/git/subprocess.rs) | | Purgatory integration | [`src/purgatory/mod.rs`](../../src/purgatory/mod.rs) | | Event acceptance policy | [`src/nostr/builder.rs`](../../src/nostr/builder.rs) - `Nip34WritePolicy` | --- ## Future Considerations ### If We Need Hooks Later We can add hook support without removing inline validation: ```rust pub struct GitConfig { inline_validation: bool, // Default: true hook_validation: bool, // Default: false } ``` This would allow: - Migration path for hook-based systems - Extra validation for paranoid deployments - Compatibility with other Git tools ### If Git Protocol Changes The protocol parsing is isolated in [`src/git/protocol.rs`](src/git/protocol.rs). If the Git protocol changes: - Update the protocol module - Tests will catch any breakage --- ## Conclusion **Inline authorization is the right choice for ngit-grasp** because: 1. **Purgatory integration** - Without inline authorization, state events in purgatory couldn't authorize pushes, creating a deadlock 2. **Better error messages** - Direct HTTP responses with clear rejection reasons 3. **Better performance** - Early rejection before spawning Git 4. **Easier testing** - Pure Rust unit tests, no Git setup needed 5. **Simpler deployment** - Single binary with shared state 6. **Shared database + purgatory** - Both authorization sources accessible during validation The trade-off (coupling to Git HTTP protocol) is acceptable because: - The pkt-line protocol is stable and well-specified - Protocol parsing is isolated in [`src/git/authorization.rs`](../../src/git/authorization.rs) - Purgatory integration requires inline access anyway - Benefits far outweigh the cost This 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. --- ## Related Documentation - [Architecture Overview](architecture.md) - Full system design - [Design Decisions](decisions.md) - All architectural choices - [Comparison with ngit-relay](comparison.md) - Detailed comparison - [Git Protocol Reference](../reference/git-protocol.md) - Protocol details - [Test Strategy](../reference/test-strategy.md) - How we test this --- _Part of the [ngit-grasp explanation docs](./)_