samael 0.0.21

A SAML2 library for Rust
use crate::schema::{Assertion, EncryptedAssertion, Issuer, Status};
use crate::signature::Signature;
use chrono::prelude::*;
use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
use quick_xml::Writer;
use serde::Deserialize;
use std::io::Cursor;
use std::str::FromStr;

const NAME: &str = "saml2p:Response";
const SCHEMA: (&str, &str) = ("xmlns:saml2p", "urn:oasis:names:tc:SAML:2.0:protocol");

#[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub struct Response {
    #[serde(rename = "@ID")]
    pub id: String,
    #[serde(rename = "@InResponseTo")]
    pub in_response_to: Option<String>,
    #[serde(rename = "@Version")]
    pub version: String,
    #[serde(rename = "@IssueInstant")]
    pub issue_instant: DateTime<Utc>,
    #[serde(rename = "@Destination")]
    pub destination: Option<String>,
    #[serde(rename = "@Consent")]
    pub consent: Option<String>,
    #[serde(rename = "Issuer")]
    pub issuer: Option<Issuer>,
    #[serde(rename = "Signature")]
    pub signature: Option<Signature>,
    #[serde(rename = "Status")]
    pub status: Option<Status>,
    #[serde(rename = "EncryptedAssertion")]
    pub encrypted_assertion: Option<EncryptedAssertion>,
    #[serde(rename = "Assertion")]
    pub assertion: Option<Assertion>,
}

impl FromStr for Response {
    type Err = quick_xml::DeError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        quick_xml::de::from_str(s)
    }
}

impl TryFrom<Response> for Event<'_> {
    type Error = Box<dyn std::error::Error>;

    fn try_from(value: Response) -> Result<Self, Self::Error> {
        (&value).try_into()
    }
}

impl TryFrom<&Response> for Event<'_> {
    type Error = Box<dyn std::error::Error>;

    fn try_from(value: &Response) -> Result<Self, Self::Error> {
        let mut write_buf = Vec::new();
        let mut writer = Writer::new(Cursor::new(&mut write_buf));
        writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;

        let mut root = BytesStart::new(NAME);
        root.push_attribute(SCHEMA);
        root.push_attribute(("ID", value.id.as_ref()));
        if let Some(resp_to) = &value.in_response_to {
            root.push_attribute(("InResponseTo", resp_to.as_ref()));
        }
        root.push_attribute(("Version", value.version.as_ref()));
        root.push_attribute((
            "IssueInstant",
            value
                .issue_instant
                .to_rfc3339_opts(SecondsFormat::Millis, true)
                .as_ref(),
        ));
        if let Some(destination) = &value.destination {
            root.push_attribute(("Destination", destination.as_ref()));
        }
        if let Some(consent) = &value.consent {
            root.push_attribute(("Consent", consent.as_ref()));
        }

        writer.write_event(Event::Start(root))?;

        if let Some(issuer) = &value.issuer {
            let event: Event<'_> = issuer.try_into()?;
            writer.write_event(event)?;
        }
        if let Some(signature) = &value.signature {
            let event: Event<'_> = signature.try_into()?;
            writer.write_event(event)?;
        }
        if let Some(status) = &value.status {
            let event: Event<'_> = status.try_into()?;
            writer.write_event(event)?;
        }

        if let Some(assertion) = &value.assertion {
            let event: Event<'_> = assertion.try_into()?;
            writer.write_event(event)?;
        }

        if let Some(encrypted_assertion) = &value.encrypted_assertion {
            let event: Event<'_> = encrypted_assertion.try_into()?;
            writer.write_event(event)?;
        }

        writer.write_event(Event::End(BytesEnd::new(NAME)))?;
        Ok(Event::Text(BytesText::from_escaped(String::from_utf8(
            write_buf,
        )?)))
    }
}

#[cfg(test)]
mod test {
    use super::Response;
    use crate::traits::ToXml;

    #[test]
    fn test_deserialize_serialize_response() {
        let response_xml = include_str!(concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/test_vectors/response.xml",
        ));
        let expected_response: Response =
            response_xml.parse().expect("failed to parse response.xml");
        let serialized_response = expected_response
            .to_string()
            .expect("failed to convert response to xml");
        let actual_response: Response = serialized_response
            .parse()
            .expect("failed to re-parse response");

        assert_eq!(expected_response, actual_response);
    }

    #[test]
    fn test_deserialize_serialize_response_with_signed_assertion() {
        let response_xml = include_str!(concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/test_vectors/response_signed_assertion.xml",
        ));
        let expected_response: Response = response_xml
            .parse()
            .expect("failed to parse response_signed_assertion.xml");
        let serialized_response = expected_response
            .to_string()
            .expect("failed to convert response to xml");
        let actual_response: Response = serialized_response
            .parse()
            .expect("failed to re-parse response");

        assert_eq!(expected_response, actual_response);
    }

    #[test]
    fn test_deserialize_serialize_signed_response() {
        let response_xml = include_str!(concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/test_vectors/response_signed.xml",
        ));
        let expected_response: Response = response_xml
            .parse()
            .expect("failed to parse response_signed.xml");
        let serialized_response = expected_response
            .to_string()
            .expect("failed to convert response to xml");
        let actual_response: Response = serialized_response
            .parse()
            .expect("failed to re-parse response");

        assert_eq!(expected_response, actual_response);
    }

    #[test]
    fn test_deserialize_serialize_response_encrypted_assertion() {
        let response_xml = include_str!(concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/test_vectors/response_encrypted.xml",
        ));
        let expected_response: Response = response_xml
            .parse()
            .expect("failed to parse response_encrypted.xml");
        let serialized_response = expected_response
            .to_string()
            .expect("failed to convert response to xml");
        let actual_response: Response = serialized_response
            .parse()
            .expect("failed to re-parse response");

        assert_eq!(expected_response, actual_response);
    }
}