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-12-04 12:34:20 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-04 13:02:59 +0000
commitd9bc5ed7fddef3a26de8e69a7124e1dbe5b8602f (patch)
treec76ffcbf246c8bef7545337316c0afb90433bbf5 /docs/explanation/inline-authorization.md
parent40831a9025d05fa354b7d8386eeebd902092ea86 (diff)
docs: update based on current implementation
Diffstat (limited to 'docs/explanation/inline-authorization.md')
-rw-r--r--docs/explanation/inline-authorization.md126
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]
152async fn test_unauthorized_push() { 152async 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
162See [`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
171pub struct GitHandler { 174// From src/git/handlers.rs
172 nostr_relay: Arc<NostrRelay>, // Shared! 175pub 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!
176impl 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```
210Single Rust binary: 211Single 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
216Setup steps: 217Setup steps:
@@ -235,44 +236,38 @@ Content-Type: application/x-git-receive-pack-request
2350000000000000000000000000000000000000000 abc123... refs/heads/main\0 report-status 2360000000000000000000000000000000000000000 abc123... refs/heads/main\0 report-status
236``` 237```
237 238
238We parse this **before** spawning Git: 239We parse this **before** spawning Git. See [`src/git/authorization.rs`](src/git/authorization.rs) for the implementation:
239 240
240```rust 241```rust
241pub async fn git_receive_pack( 242/// Parse ref updates from git-receive-pack request body
242 req: HttpRequest, 243pub 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
259Validation checks: 252Validation checks (from [`src/git/authorization.rs`](src/git/authorization.rs)):
253
2601. Does pusher's pubkey have write access? 2541. Does pusher's pubkey have write access?
2612. Are they listed as a maintainer in the latest state event? 2552. Are they listed as a maintainer in the latest state event?
2623. Do maintainer sets form a valid chain? 2563. Do the refs match the state event?
263 257
264```rust 258```rust
265async fn validate_push( 259/// Validate that pushed refs match the authorized state
266 state: &RepoState, 260pub 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
296Both are GRASP-compliant, but inline authorization is simpler and more efficient. 291Both 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:
3171. The `git-http-backend` crate handles protocol parsing 3121. We handle protocol parsing in [`src/git/protocol.rs`](src/git/protocol.rs)
3182. GRASP is already non-standard (Nostr authorization) 3132. GRASP is already non-standard (Nostr authorization)
3193. Benefits far outweigh the coupling cost 3143. Benefits far outweigh the coupling cost
3204. We can still add hook support later if needed 3154. We can still add hook support later if needed
321 316
322--- 317---
323 318
324## Alternative Considered: Hybrid Approach 319## Implementation References
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 320
338**Why we didn't choose this:** 321Key files in the ngit-grasp implementation:
339- Added complexity
340- Redundant validation
341- Slower (two validation steps)
342- Harder to maintain
343 322
344If 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
368The `git-http-backend` crate abstracts protocol details. If the Git protocol changes: 354The 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
3802. It's more performant (early rejection) 3652. It's more performant (early rejection)
3813. It's easier to test (pure Rust) 3663. It's easier to test (pure Rust)
3824. It's simpler to deploy (single binary) 3674. It's simpler to deploy (single binary)
3835. It enables better integration (shared state) 3685. It enables better integration (shared database)
384 369
385The trade-off (coupling to Git HTTP protocol) is acceptable because: 370The 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
390This decision aligns with our goal of creating a **developer-friendly, production-ready GRASP implementation**. 375This 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