use std::fmt;
pub const AUTH_REASON_HEADER: &str = "vgi-auth-reason";
pub const AUTH_PROXY_REQUIRED_HEADER: &str = "vgi-auth-proxy-required";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AuthReason {
MissingCredential,
InvalidCredential,
ExpiredCredential,
InsufficientScope,
ProxyRequired,
Unauthorized,
}
impl AuthReason {
pub fn as_str(&self) -> &'static str {
match self {
Self::MissingCredential => "missing_credential",
Self::InvalidCredential => "invalid_credential",
Self::ExpiredCredential => "expired_credential",
Self::InsufficientScope => "insufficient_scope",
Self::ProxyRequired => "proxy_required",
Self::Unauthorized => "unauthorized",
}
}
}
impl fmt::Display for AuthReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(feature = "http")]
pub(crate) fn proxy_hint(headers: &[String]) -> String {
format!(
"This service only accepts requests that arrive through its configured \
reverse proxy, which must set the {} header(s). A rejection here is at \
least as likely to be a proxy misconfiguration as a bad credential — \
check that the proxy is forwarding them before re-issuing credentials.",
headers.join(", ")
)
}
#[cfg(feature = "http")]
pub(crate) fn envelope(reason: AuthReason, detail: &str, hint: Option<&str>) -> String {
let mut out = String::from("{\"error\":\"unauthorized\",\"reason\":\"");
out.push_str(reason.as_str());
out.push_str("\",\"detail\":");
out.push_str(&json_string(detail));
if let Some(hint) = hint {
out.push_str(",\"proxy_hint\":");
out.push_str(&json_string(hint));
}
out.push('}');
out
}
#[cfg(feature = "http")]
fn json_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
#[cfg(all(test, feature = "http"))]
mod tests {
use super::*;
#[test]
fn envelope_omits_the_hint_when_it_does_not_apply() {
let body = envelope(AuthReason::InvalidCredential, "nope", None);
assert!(!body.contains("proxy_hint"), "{body}");
assert!(body.contains("\"reason\":\"invalid_credential\""), "{body}");
assert!(body.contains("\"error\":\"unauthorized\""), "{body}");
}
#[test]
fn envelope_carries_the_hint_when_it_applies() {
let hint = proxy_hint(&["vgi-proxy-proof".to_string()]);
let body = envelope(AuthReason::ProxyRequired, "", Some(&hint));
assert!(body.contains("proxy_hint"), "{body}");
assert!(body.contains("vgi-proxy-proof"), "{body}");
}
#[test]
fn detail_is_escaped() {
let body = envelope(AuthReason::Unauthorized, "a \"quoted\"\nline", None);
assert!(body.contains("\\\"quoted\\\""), "{body}");
assert!(body.contains("\\n"), "{body}");
}
}