fix: non-admin users with app grants can now push via HTTP

The user_apps check was silently failing because sqlx::query_scalar
without an explicit type annotation would hit a runtime decoding error,
which .unwrap_or(None) swallowed — always returning None → 403.

All three DB calls in check_push_access now use match + tracing::error!
so failures are visible in logs instead of looking like a missing grant.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
This commit is contained in:
Claude 2026-03-24 10:41:26 +00:00
parent 5bc1948f1a
commit 872efc74ce
No known key found for this signature in database

View file

@ -105,49 +105,65 @@ fn forbidden() -> Response<Body> {
/// Resolves an app name/id and checks whether the user may push to it. /// Resolves an app name/id and checks whether the user may push to it.
/// Returns the app_id on success. /// Returns the app_id on success.
async fn check_push_access(s: &AppState, user_id: &str, app: &str) -> Option<String> { async fn check_push_access(s: &AppState, user_id: &str, app: &str) -> Option<String> {
let app_id: Option<String> = let app_id = match sqlx::query_scalar::<_, String>(
sqlx::query_scalar::<_, String>("SELECT id FROM apps WHERE id = ? OR name = ?") "SELECT id FROM apps WHERE id = ? OR name = ?",
.bind(app) )
.bind(app) .bind(app)
.fetch_optional(&s.db) .bind(app)
.await .fetch_optional(&s.db)
.unwrap_or(None); .await
{
let app_id = match app_id { Ok(Some(id)) => id,
Some(id) => id, Ok(None) => {
None => {
tracing::debug!("check_push_access: no app found for {:?}", app); tracing::debug!("check_push_access: no app found for {:?}", app);
return None; return None;
} }
Err(e) => {
tracing::error!("check_push_access: app lookup error: {e}");
return None;
}
}; };
let is_admin: i64 = sqlx::query_scalar::<_, i64>("SELECT is_admin FROM users WHERE id = ?") let is_admin: i64 = match sqlx::query_scalar::<_, i64>(
.bind(user_id) "SELECT is_admin FROM users WHERE id = ?",
.fetch_optional(&s.db) )
.await .bind(user_id)
.unwrap_or(None) .fetch_optional(&s.db)
.unwrap_or(0); .await
{
Ok(v) => v.unwrap_or(0),
Err(e) => {
tracing::error!("check_push_access: is_admin lookup error: {e}");
0
}
};
if is_admin != 0 { if is_admin != 0 {
tracing::debug!("check_push_access: user {} is admin, access granted", user_id); tracing::debug!("check_push_access: user {} is admin, access granted", user_id);
return Some(app_id); return Some(app_id);
} }
let granted: Option<i64> = match sqlx::query_scalar::<_, i64>(
sqlx::query_scalar::<_, i64>("SELECT 1 FROM user_apps WHERE user_id = ? AND app_id = ?") "SELECT 1 FROM user_apps WHERE user_id = ? AND app_id = ?",
.bind(user_id) )
.bind(&app_id) .bind(user_id)
.fetch_optional(&s.db) .bind(&app_id)
.await .fetch_optional(&s.db)
.unwrap_or(None); .await
{
if granted.is_none() { Ok(Some(_)) => Some(app_id),
tracing::debug!( Ok(None) => {
"check_push_access: user {} has no grant for app {}", tracing::debug!(
user_id, app_id "check_push_access: user {} has no grant for app {}",
); user_id, app_id
);
None
}
Err(e) => {
tracing::error!("check_push_access: user_apps lookup error: {e}");
None
}
} }
granted.map(|_| app_id)
} }
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────