crates/nornir-identity-sso/src/egress.rs
//! **The network, as a product reaches an identity provider** (moved from
//! Gunnar's `sso/mod.rs`, A34 M-4). The product hands in its own blocking
//! client; the implementation enforces [`crate::guard::url_allowed_in`] again,
//! checks every address a name resolves to, follows no redirect and reads at
//! most [`crate::MAX_IDP_BODY`].
/// **The network, as a product reaches an IdP.** Blocking (an async product
/// calls it from its blocking pool).
pub trait Egress: Send + Sync {
/// `GET url` → (status, body).
fn get(&self, url: &str) -> Result<(u16, String), String>;
/// `POST url` with a form body and an `Authorization` header →
/// (status, body).
fn post_form(
&self,
url: &str,
authorization: &str,
body: &str,
) -> Result<(u16, String), String>;
/// [`Egress::get`] trusting `ca` (DER certificates: ONE account's IdP CA)
/// beside the public roots. An egress that cannot add a trust anchor
/// refuses a call that names one.
fn get_trusting(&self, url: &str, ca: &[Vec<u8>]) -> Result<(u16, String), String> {
if ca.is_empty() {
return self.get(url);
}
Err("this server's egress cannot trust an identity provider's own CA".to_owned())
}
/// [`Egress::post_form`] trusting `ca`, as [`Egress::get_trusting`].
fn post_form_trusting(
&self,
url: &str,
authorization: &str,
body: &str,
ca: &[Vec<u8>],
) -> Result<(u16, String), String> {
if ca.is_empty() {
return self.post_form(url, authorization, body);
}
Err("this server's egress cannot trust an identity provider's own CA".to_owned())
}
}
/// An answer read as a 2xx body no larger than [`crate::MAX_IDP_BODY`], or why not.
pub fn checked(answer: Result<(u16, String), String>, what: &str) -> Result<String, String> {
let (status, body) = answer.map_err(|e| format!("{what}: {e}"))?;
if body.len() > crate::MAX_IDP_BODY {
return Err(format!(
"{what}: the answer is larger than {} bytes",
crate::MAX_IDP_BODY
));
}
if !(200..300).contains(&status) {
return Err(format!(
"{what}: the identity provider answered HTTP {status}"
));
}
Ok(body)
}
#[cfg(test)]
mod tests {
use super::*;
struct Plain;
impl Egress for Plain {
fn get(&self, _: &str) -> Result<(u16, String), String> {
Ok((200, "ok".into()))
}
fn post_form(&self, _: &str, _: &str, _: &str) -> Result<(u16, String), String> {
Ok((200, "ok".into()))
}
}
#[test]
fn an_egress_without_a_trust_store_refuses_a_ca() {
assert_eq!(
Plain.get_trusting("https://idp.example", &[]).unwrap().1,
"ok"
);
assert!(Plain
.get_trusting("https://idp.example", &[vec![1]])
.is_err());
assert!(Plain
.post_form_trusting("https://idp.example", "", "", &[vec![1]])
.is_err());
}
#[test]
fn answers_are_bounded_and_2xx() {
assert_eq!(checked(Ok((200, "x".into())), "w").unwrap(), "x");
assert!(checked(Ok((404, "x".into())), "w")
.unwrap_err()
.contains("HTTP 404"));
assert!(checked(Ok((200, "x".repeat(crate::MAX_IDP_BODY + 1))), "w").is_err());
assert!(checked(Err("down".into()), "w")
.unwrap_err()
.contains("down"));
}
}