Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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"));
    }
}