fix: explicit SQLx type + debug tracing for HTTP git auth

Fixes potential silent failure where sqlx::query_scalar couldn't infer
the return type at runtime. Also adds step-by-step tracing so the exact
failure point (no header / bad base64 / key not found / db error) is
visible in `docker compose logs server`.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
This commit is contained in:
Claude 2026-03-24 08:33:10 +00:00
parent a2627a3e2f
commit 4504c22af8
No known key found for this signature in database
2 changed files with 62 additions and 14 deletions

View file

@ -92,11 +92,19 @@ pub async fn revoke(
/// Returns the user_id if the raw key matches a stored hash. /// Returns the user_id if the raw key matches a stored hash.
pub async fn verify_key(s: &AppState, raw_key: &str) -> Option<String> { pub async fn verify_key(s: &AppState, raw_key: &str) -> Option<String> {
let hash = hex::encode(Sha256::digest(raw_key.as_bytes())); let hash = hex::encode(Sha256::digest(raw_key.as_bytes()));
sqlx::query_scalar("SELECT user_id FROM api_keys WHERE key_hash = ?") match sqlx::query_scalar::<_, String>(
.bind(&hash) "SELECT user_id FROM api_keys WHERE key_hash = ?",
.fetch_optional(&s.db) )
.await .bind(&hash)
.unwrap_or(None) .fetch_optional(&s.db)
.await
{
Ok(r) => r,
Err(e) => {
tracing::error!("api_key verify db error: {e}");
None
}
}
} }
// ── Tiny CSPRNG using uuid entropy ─────────────────────────────────────────── // ── Tiny CSPRNG using uuid entropy ───────────────────────────────────────────

View file

@ -35,15 +35,55 @@ fn check_token(state: &AppState, headers: &HeaderMap) -> bool {
/// git sends Basic Auth where the password field is the API key. /// git sends Basic Auth where the password field is the API key.
/// Returns the user_id on success. /// Returns the user_id on success.
async fn http_authenticate(s: &AppState, headers: &HeaderMap) -> Option<String> { async fn http_authenticate(s: &AppState, headers: &HeaderMap) -> Option<String> {
let value = headers.get("authorization")?.to_str().ok()?; let value = headers
let encoded = value.strip_prefix("Basic ")?; .get("authorization")
let decoded = base64::engine::general_purpose::STANDARD .and_then(|v| v.to_str().ok())
.decode(encoded) .unwrap_or("");
.ok()?;
let credentials = std::str::from_utf8(&decoded).ok()?; if value.is_empty() {
// credentials is "username:password" — the password IS the API key. tracing::debug!("git http auth: no Authorization header");
let api_key = credentials.splitn(2, ':').nth(1)?; return None;
api_keys::verify_key(s, api_key).await }
let encoded = match value.strip_prefix("Basic ") {
Some(e) => e,
None => {
tracing::debug!("git http auth: not Basic (got: {})", &value[..value.len().min(20)]);
return None;
}
};
let decoded = match base64::engine::general_purpose::STANDARD.decode(encoded) {
Ok(d) => d,
Err(e) => {
tracing::debug!("git http auth: base64 error: {e}");
return None;
}
};
let credentials = match std::str::from_utf8(&decoded) {
Ok(c) => c,
Err(e) => {
tracing::debug!("git http auth: utf8 error: {e}");
return None;
}
};
// credentials = "username:password" — the password IS the API key.
let api_key = match credentials.splitn(2, ':').nth(1) {
Some(k) => k,
None => {
tracing::debug!("git http auth: no colon in credentials");
return None;
}
};
let result = api_keys::verify_key(s, api_key).await;
tracing::debug!(
"git http auth: key lookup → {}",
if result.is_some() { "ok" } else { "not found" }
);
result
} }
fn unauthorized() -> Response<Body> { fn unauthorized() -> Response<Body> {