use reqwest::header::HeaderValue;
use serde::Deserialize;
use crate::http::{AUTH_HEADER_NAME, AUTH_QUERY_PARAM};
use crate::types::AuthMethod;
#[derive(Deserialize)]
struct WhoamiProbeResponse {
#[serde(default)]
id: u64,
}
pub(super) enum WhoamiOutcome {
Authenticated(AuthMethod),
NotFound,
AuthRejected,
UnparseableResponse,
NetworkError,
}
pub(super) async fn detect_whoami_auth(
http: &reqwest::Client,
base: &str,
api_key: &str,
key_header: &HeaderValue,
) -> WhoamiOutcome {
let url = format!("{base}/rest/whoami");
let header_req = http.get(&url).header(AUTH_HEADER_NAME, key_header.clone());
let outcome = probe_whoami(header_req, AuthMethod::Header).await;
match outcome {
WhoamiOutcome::AuthRejected | WhoamiOutcome::UnparseableResponse => {} other => return other,
}
let query_req = http.get(&url).query(&[(AUTH_QUERY_PARAM, api_key)]);
probe_whoami(query_req, AuthMethod::QueryParam).await
}
async fn probe_whoami(request: reqwest::RequestBuilder, method: AuthMethod) -> WhoamiOutcome {
let resp = match request.send().await {
Ok(r) => r,
Err(e) => {
if crate::http::is_tls_cert_error(&e) {
tracing::warn!(
"{}",
crate::http::tls_hint(&format!("whoami {method} request failed: {e:#}"), &e,)
);
} else {
tracing::debug!("whoami {method} request failed: {e:#}");
}
return WhoamiOutcome::NetworkError;
}
};
let status = resp.status();
let body = resp.text().await.unwrap_or_else(|e| {
tracing::warn!("failed to read whoami response body: {e}");
String::new()
});
tracing::trace!(probe = "whoami", %method, %status, body, "auth probe response");
if status.is_success() {
if let Ok(parsed) = serde_json::from_str::<WhoamiProbeResponse>(&body) {
if parsed.id > 0 {
return WhoamiOutcome::Authenticated(method);
}
return WhoamiOutcome::AuthRejected;
}
tracing::debug!(%method, "whoami returned 200 but body is not valid whoami JSON");
return WhoamiOutcome::UnparseableResponse;
}
if status == reqwest::StatusCode::NOT_FOUND {
tracing::debug!("rest/whoami not available on this server");
return WhoamiOutcome::NotFound;
}
tracing::debug!(%status, %method, "whoami auth probe failed");
WhoamiOutcome::AuthRejected
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn whoami_response_deserializes_with_id() {
let json = r#"{"id": 42, "name": "user@example.com"}"#;
let resp: WhoamiProbeResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.id, 42);
}
#[test]
fn whoami_response_id_zero_means_unauthenticated() {
let json = r#"{"id": 0}"#;
let resp: WhoamiProbeResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.id, 0);
}
#[test]
fn whoami_response_missing_id_defaults_zero() {
let json = r#"{"name": "test"}"#;
let resp: WhoamiProbeResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.id, 0);
}
}