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.
pub async fn verify_key(s: &AppState, raw_key: &str) -> Option<String> {
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>(
"SELECT user_id FROM api_keys WHERE key_hash = ?",
)
.bind(&hash)
.fetch_optional(&s.db)
.await
.unwrap_or(None)
{
Ok(r) => r,
Err(e) => {
tracing::error!("api_key verify db error: {e}");
None
}
}
}
// ── 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.
/// Returns the user_id on success.
async fn http_authenticate(s: &AppState, headers: &HeaderMap) -> Option<String> {
let value = headers.get("authorization")?.to_str().ok()?;
let encoded = value.strip_prefix("Basic ")?;
let decoded = base64::engine::general_purpose::STANDARD
.decode(encoded)
.ok()?;
let credentials = std::str::from_utf8(&decoded).ok()?;
// credentials is "username:password" — the password IS the API key.
let api_key = credentials.splitn(2, ':').nth(1)?;
api_keys::verify_key(s, api_key).await
let value = headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if value.is_empty() {
tracing::debug!("git http auth: no Authorization header");
return None;
}
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> {