Files
buzz/crates/sprout-relay/src/api/git/mod.rs
tlongwell-block 3899ca566d feat(git): Role-based push permissions with service-bound auth (#441)
Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
2026-05-01 00:25:33 -04:00

61 lines
1.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Git hosting — Smart HTTP transport, permission hooks, and policy engine.
//!
//! # Module structure
//!
//! - `transport` — Smart HTTP protocol (info/refs, upload-pack, receive-pack)
//! - `hook` — Pre-receive hook script and injection
//! - `policy` — Internal policy endpoint (HMAC-authenticated callback from hook)
use std::net::SocketAddr;
use std::sync::Arc;
use axum::{
body::Body,
extract::ConnectInfo,
http::{Request, StatusCode},
middleware::{self, Next},
response::{IntoResponse, Response},
routing::post,
Router,
};
use tower_http::limit::RequestBodyLimitLayer;
use crate::state::AppState;
pub mod hook;
pub mod policy;
pub mod transport;
pub use transport::git_router;
/// Middleware that rejects requests from non-loopback addresses.
///
/// Defense-in-depth: the internal policy endpoint should only be reachable
/// from localhost (the pre-receive hook runs on the same host as the relay).
async fn require_localhost(req: Request<Body>, next: Next) -> Response {
let is_loopback = req
.extensions()
.get::<ConnectInfo<SocketAddr>>()
.map(|ci| ci.0.ip().is_loopback())
.unwrap_or(false);
if !is_loopback {
return (StatusCode::FORBIDDEN, "internal endpoint: localhost only").into_response();
}
next.run(req).await
}
/// Build the internal git policy router.
///
/// Mounted at `/internal/git/policy` — only accessible from localhost.
/// The pre-receive hook calls this to authorize pushes.
/// Body limit: 1 MB (500 refs × ~200 bytes each = ~100 KB typical; 1 MB is generous).
pub fn git_policy_router(state: Arc<AppState>) -> Router {
Router::new()
.route("/internal/git/policy", post(policy::hook_policy_check))
.layer(RequestBodyLimitLayer::new(1024 * 1024)) // 1 MB
.layer(middleware::from_fn(require_localhost))
.with_state(state)
}