diff --git a/CHANGELOG b/CHANGELOG index 4ce3380..edb7869 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,5 @@ # Changelog - ### Unreleased ### 0.0.16 @@ -8,6 +7,7 @@ - Add support for basic `RequestedAuthnContext` de-/serialization in `AuthnRequest` - Add support for Elliptic-curve cryptography - Use enum for signature and digest algorithm +- Add EntityDescriptorType enum to allow parsing of arbitrarily nested EntityDescriptors and EntitiesDescriptors ### 0.0.15 diff --git a/src/metadata/entity_descriptor.rs b/src/metadata/entity_descriptor.rs index 895a910..466db83 100644 --- a/src/metadata/entity_descriptor.rs +++ b/src/metadata/entity_descriptor.rs @@ -4,13 +4,171 @@ use crate::metadata::{ }; use crate::signature::Signature; use chrono::prelude::*; -use quick_xml::events::{BytesEnd, BytesStart, Event}; +use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event}; use quick_xml::Writer; use serde::Deserialize; use std::io::Cursor; use std::str::FromStr; use thiserror::Error; +#[derive(Debug, Error)] +pub enum Error { + #[error("Failed to deserialize SAML response: {:?}", source)] + ParseError { + #[from] + source: quick_xml::DeError, + }, +} + +#[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] +pub enum EntityDescriptorType { + #[serde(rename = "EntitiesDescriptor")] + EntitiesDescriptor(EntitiesDescriptor), + #[serde(rename = "EntityDescriptor")] + EntityDescriptor(EntityDescriptor), +} + +impl EntityDescriptorType { + pub fn take_first(self) -> Option { + match self { + EntityDescriptorType::EntitiesDescriptor(descriptor) => descriptor + .descriptors + .into_iter() + .next() + .and_then(|descriptor_type| match descriptor_type { + EntityDescriptorType::EntitiesDescriptor(_) => None, + EntityDescriptorType::EntityDescriptor(descriptor) => Some(descriptor), + }), + EntityDescriptorType::EntityDescriptor(descriptor) => Some(descriptor), + } + } +} + +impl FromStr for EntityDescriptorType { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(quick_xml::de::from_str(s)?) + } +} + +impl TryFrom for Event<'_> { + type Error = Box; + + fn try_from(value: EntityDescriptorType) -> Result { + (&value).try_into() + } +} + +impl TryFrom<&EntityDescriptorType> for Event<'_> { + type Error = Box; + + fn try_from(value: &EntityDescriptorType) -> Result { + 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 event: Event<'_> = match value { + EntityDescriptorType::EntitiesDescriptor(descriptor) => descriptor.try_into()?, + EntityDescriptorType::EntityDescriptor(descriptor) => descriptor.try_into()?, + }; + writer.write_event(event)?; + + Ok(Event::Text(BytesText::from_escaped(String::from_utf8( + write_buf, + )?))) + } +} + +const ENTITIES_DESCRIPTOR_NAME: &str = "md:EntitiesDescriptor"; + +#[derive(Clone, Debug, Deserialize, Default, Hash, Eq, PartialEq, Ord, PartialOrd)] +#[serde(rename = "md:EntitiesDescriptor")] +pub struct EntitiesDescriptor { + #[serde(rename = "@ID")] + pub id: Option, + #[serde(rename = "@Name")] + pub name: Option, + #[serde(rename = "@validUntil")] + pub valid_until: Option>, + #[serde(rename = "@cacheDuration")] + pub cache_duration: Option, + #[serde(rename = "Signature")] + pub signature: Option, + #[serde(default, rename = "$value")] + pub descriptors: Vec, +} + +impl FromStr for EntitiesDescriptor { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(quick_xml::de::from_str(s)?) + } +} + +impl TryFrom for Event<'_> { + type Error = Box; + + fn try_from(value: EntitiesDescriptor) -> Result { + (&value).try_into() + } +} + +impl TryFrom<&EntitiesDescriptor> for Event<'_> { + type Error = Box; + + fn try_from(value: &EntitiesDescriptor) -> Result { + 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(ENTITIES_DESCRIPTOR_NAME); + root.push_attribute(("xmlns:md", "urn:oasis:names:tc:SAML:2.0:metadata")); + root.push_attribute(( + "xmlns:alg", + "urn:oasis:names:tc:SAML:2.0:metadata:algsupport", + )); + root.push_attribute(("xmlns:mdui", "urn:oasis:names:tc:SAML:metadata:ui")); + root.push_attribute(("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")); + + if let Some(id) = &value.id { + root.push_attribute(("ID", id.as_ref())) + } + + if let Some(name) = &value.name { + root.push_attribute(("Name", name.as_ref())) + } + + if let Some(valid_until) = &value.valid_until { + root.push_attribute(( + "validUntil", + valid_until + .to_rfc3339_opts(SecondsFormat::Secs, true) + .as_ref(), + )) + } + + if let Some(cache_duration) = &value.cache_duration { + root.push_attribute(("cacheDuration", cache_duration.as_ref())); + } + + writer.write_event(Event::Start(root))?; + for descriptor in &value.descriptors { + let event: Event<'_> = descriptor.try_into()?; + writer.write_event(event)?; + } + + writer.write_event(Event::End(BytesEnd::new(ENTITIES_DESCRIPTOR_NAME)))?; + + Ok(Event::Text(BytesText::from_escaped(String::from_utf8( + write_buf, + )?))) + } +} + +const ENTITY_DESCRIPTOR_NAME: &str = "md:EntityDescriptor"; + #[derive(Clone, Debug, Deserialize, Default, Hash, Eq, PartialEq, Ord, PartialOrd)] #[serde(rename = "md:EntityDescriptor")] pub struct EntityDescriptor { @@ -44,15 +202,6 @@ pub struct EntityDescriptor { pub organization: Option, } -#[derive(Debug, Error)] -pub enum Error { - #[error("Failed to deserialize SAML response: {:?}", source)] - ParseError { - #[from] - source: quick_xml::DeError, - }, -} - impl FromStr for EntityDescriptor { type Err = Error; @@ -61,16 +210,38 @@ impl FromStr for EntityDescriptor { } } -impl EntityDescriptor { - pub fn to_xml(&self) -> Result> { +impl TryFrom for Event<'_> { + type Error = Box; + + fn try_from(value: EntityDescriptor) -> Result { + (&value).try_into() + } +} + +impl TryFrom<&EntityDescriptor> for Event<'_> { + type Error = Box; + + fn try_from(value: &EntityDescriptor) -> Result { let mut write_buf = Vec::new(); let mut writer = Writer::new(Cursor::new(&mut write_buf)); - let root_name = "md:EntityDescriptor"; - let mut root = BytesStart::new(root_name); - if let Some(entity_id) = &self.entity_id { + writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?; + + let mut root = BytesStart::new(ENTITY_DESCRIPTOR_NAME); + root.push_attribute(("xmlns:md", "urn:oasis:names:tc:SAML:2.0:metadata")); + root.push_attribute(("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion")); + root.push_attribute(("xmlns:mdrpi", "urn:oasis:names:tc:SAML:metadata:rpi")); + root.push_attribute(("xmlns:mdattr", "urn:oasis:names:tc:SAML:metadata:attribute")); + root.push_attribute(("xmlns:mdui", "urn:oasis:names:tc:SAML:metadata:ui")); + root.push_attribute(("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")); + root.push_attribute(( + "xmlns:idpdisc", + "urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol", + )); + + if let Some(entity_id) = &value.entity_id { root.push_attribute(("entityID", entity_id.as_ref())) } - if let Some(valid_until) = &self.valid_until { + if let Some(valid_until) = &value.valid_until { root.push_attribute(( "validUntil", valid_until @@ -78,51 +249,46 @@ impl EntityDescriptor { .as_ref(), )) } - if let Some(cache_duration) = &self.cache_duration { + if let Some(cache_duration) = &value.cache_duration { root.push_attribute(("cacheDuration", cache_duration.as_ref())); } - root.push_attribute(("xmlns:md", "urn:oasis:names:tc:SAML:2.0:metadata")); - root.push_attribute(("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion")); - root.push_attribute(("xmlns:mdrpi", "urn:oasis:names:tc:SAML:metadata:rpi")); - root.push_attribute(("xmlns:mdattr", "urn:oasis:names:tc:SAML:metadata:attribute")); - root.push_attribute(("xmlns:mdui", "urn:oasis:names:tc:SAML:metadata:ui")); - root.push_attribute(( - "xmlns:idpdisc", - "urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol", - )); - root.push_attribute(("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")); writer.write_event(Event::Start(root))?; - for descriptor in self.sp_sso_descriptors.as_ref().unwrap_or(&vec![]) { + for descriptor in value.sp_sso_descriptors.as_ref().unwrap_or(&vec![]) { let event: Event<'_> = descriptor.try_into()?; writer.write_event(event)?; } - for descriptor in self.idp_sso_descriptors.as_ref().unwrap_or(&vec![]) { + for descriptor in value.idp_sso_descriptors.as_ref().unwrap_or(&vec![]) { let event: Event<'_> = descriptor.try_into()?; writer.write_event(event)?; } - if let Some(organization) = &self.organization { + if let Some(organization) = &value.organization { let event: Event<'_> = organization.try_into()?; writer.write_event(event)?; } - if let Some(contact_persons) = &self.contact_person { + if let Some(contact_persons) = &value.contact_person { for contact_person in contact_persons { let event: Event<'_> = contact_person.try_into()?; writer.write_event(event)?; } } - writer.write_event(Event::End(BytesEnd::new(root_name)))?; - Ok(String::from_utf8(write_buf)?) + writer.write_event(Event::End(BytesEnd::new(ENTITY_DESCRIPTOR_NAME)))?; + + Ok(Event::Text(BytesText::from_escaped(String::from_utf8( + write_buf, + )?))) } } #[cfg(test)] mod test { - use super::EntityDescriptor; + use crate::traits::ToXml; + + use super::{EntitiesDescriptor, EntityDescriptor, EntityDescriptorType}; #[test] fn test_sp_entity_descriptor() { @@ -152,7 +318,7 @@ mod test { )); let entity_descriptor: EntityDescriptor = input_xml .parse() - .expect("Failed to parse sp_metadata.xml into an EntityDescriptor"); + .expect("Failed to parse idp_metadata.xml into an EntityDescriptor"); let output_xml = entity_descriptor .to_xml() .expect("Failed to convert EntityDescriptor to xml"); @@ -162,4 +328,84 @@ mod test { assert_eq!(reparsed_entity_descriptor, entity_descriptor); } + + #[test] + fn test_idp_entities_descriptor() { + let input_xml = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test_vectors/idp_metadata_nested.xml" + )); + let entities_descriptor: EntitiesDescriptor = input_xml + .parse() + .expect("Failed to parse idp_metadata_nested.xml into an EntitiesDescriptor"); + let output_xml = entities_descriptor + .to_xml() + .expect("Failed to convert EntitiesDescriptor to xml"); + let reparsed_entities_descriptor: EntitiesDescriptor = output_xml + .parse() + .expect("Failed to parse EntitiesDescriptor"); + + assert_eq!(reparsed_entities_descriptor, entities_descriptor); + } + + #[test] + fn test_idp_entity_descriptor_type() { + let input_xml = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test_vectors/idp_metadata.xml" + )); + let entity_descriptor_type: EntityDescriptorType = input_xml + .parse() + .expect("Failed to parse idp_metadata.xml into an EntityDescriptorType"); + let output_xml = entity_descriptor_type + .to_xml() + .expect("Failed to convert EntityDescriptorType to xml"); + let reparsed_entity_descriptor_type: EntityDescriptorType = output_xml + .parse() + .expect("Failed to parse EntityDescriptorType"); + + assert_eq!(reparsed_entity_descriptor_type, entity_descriptor_type); + + let expected_entity_descriptor: EntityDescriptor = input_xml + .parse() + .expect("Failed to parse idp_metadata.xml into an EntityDescriptor"); + let entity_descriptor: EntityDescriptor = entity_descriptor_type + .take_first() + .expect("Failed to take first EntityDescriptor from EntityDescriptorType"); + + assert_eq!(expected_entity_descriptor, entity_descriptor); + } + + #[test] + fn test_idp_entity_descriptor_type_nested() { + let input_xml = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test_vectors/idp_metadata_nested.xml" + )); + let entity_descriptor_type: EntityDescriptorType = input_xml + .parse() + .expect("Failed to parse idp_metadata_nested.xml into an EntityDescriptorType"); + let output_xml = entity_descriptor_type + .to_xml() + .expect("Failed to convert EntityDescriptorType to xml"); + let reparsed_entity_descriptor_type: EntityDescriptorType = output_xml + .parse() + .expect("Failed to parse EntityDescriptorType"); + + assert_eq!(reparsed_entity_descriptor_type, entity_descriptor_type); + + let input_xml = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test_vectors/idp_metadata.xml" + )); + let expected_entity_descriptor: EntityDescriptor = input_xml + .parse() + .expect("Failed to parse idp_metadata.xml into an EntityDescriptor"); + let entity_descriptor: EntityDescriptor = entity_descriptor_type + .take_first() + .expect("Failed to take first EntityDescriptor from EntityDescriptorType"); + println!("{entity_descriptor:#?}"); + + assert_eq!(expected_entity_descriptor, entity_descriptor); + } } diff --git a/src/metadata/mod.rs b/src/metadata/mod.rs index 871d1b3..b8e86b6 100644 --- a/src/metadata/mod.rs +++ b/src/metadata/mod.rs @@ -15,7 +15,7 @@ pub use attribute_consuming_service::AttributeConsumingService; pub use contact_person::*; pub use encryption_method::EncryptionMethod; pub use endpoint::*; -pub use entity_descriptor::EntityDescriptor; +pub use entity_descriptor::{EntitiesDescriptor, EntityDescriptor, EntityDescriptorType}; pub use key_descriptor::KeyDescriptor; pub use localized::*; pub use organization::Organization; diff --git a/test_vectors/idp_metadata_nested.xml b/test_vectors/idp_metadata_nested.xml new file mode 100644 index 0000000..b168bfc --- /dev/null +++ b/test_vectors/idp_metadata_nested.xml @@ -0,0 +1,36 @@ + + + + + + + + + MIIEQjCCAqoCCQCrSuOfmFjlRTANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTUExDzANBgNVBAcMBkJvc3RvbjENMAsGA1UECgwEVGVzdDENMAsGA1UECwwEVGVzdDEYMBYGA1UEAwwPaWRwLmV4YW1wbGUuY29tMB4XDTIwMDMwODIzMDM0NVoXDTMwMDMwNjIzMDM0NVowYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1BMQ8wDQYDVQQHDAZCb3N0b24xDTALBgNVBAoMBFRlc3QxDTALBgNVBAsMBFRlc3QxGDAWBgNVBAMMD2lkcC5leGFtcGxlLmNvbTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAL98URjbAoBa7kNxFrIr4WRQ2p82fclLCMWPGV8pgu982jSLePsGuopVCggTRJ9Rd8YdRdkXlK6S8jEa7cZUVaupXlanus48gIm5XxGtbVxr+hkWmLbvs2pZl6UbbCHxOqR4elycsU/NY+9r3R19bHFZxXbcUHWUhdrQanMopWsmT7Jw24ZEyaQjXZ/e9wo6jhbjpW7cRccP/7OmjJsNfDsmnuw6fgk2UFxEAnngUbOfbJ85ksZ0W4Lhs+tyS1sm6vD2vfLx+WYzEqRZDjmeaSEqlg8Atw29lkfXf5ja8GAx+I6lH7qB/Ex4PYU/miBPKUkCv9BkBC6Gklfmutt9kMlwkXDR+xb6Z4jMtUBhqGbsYz/1DzgQbm6B2sq8Q8vm3kkQpnBe3aOUr1KNmNnMQ3HAhG7HpO20UcuvH/AiawOkWA4oepDN03AdMkVSDFg4QhuCk69QAGF0Bwgfvx8BT1kFi6vHuZnhNfDX7PNKLvRceoOwIUa3wqiGsh56wcIjhQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBgQA03335pbzoghD6V4l2Ie1Sj/ffLLCCg6c2prQCX5PiK14sKah0Y8/UY0GattCKYrKPjh4SW1xG0gNFXnA1gyngTXCphlhGCS24lqg040IGIoyQaZNCptdrBRvBgrgONcxH1C9KVc5X+uMjulkW3m5S9nnBHBuU9sEKkF8foCaviY4pFiVsySKgBkfr1pTnXSduohalmfQCAJHKWU4ZZhHAMiJj0Fiy80ba0+40Wt6BTb92XZnyH/3sOmgQ5tazNv3rSoSYepPGLW7Ka6g+xDhl3+pqOS6KyUvA17xFvnakwzV5mLY+rSD2sIuf3qvobPEuq4aNdas7KPZRHDva+DqoMI4wU6woeTagulJV6+vG0YREmdfHmF2QL35yWxTK/vxAJoQzX2QVWk9bOV17Rmf77dDjrBMeLcQUQa9bS2Efg8BAehoDuG+XuqygdHMrAildlU+ZSLdV0YqmVrHsoqTXRrrbuzopEkKeqFblXVii3YBx/E7kpn6/wu84srY+394= + + + + + + + + Example.org Non-Profit Org + Example.org + https://www.example.org/ + + + SAML Technical Support + mailto:technical-support@example.org + + + SAML Support + mailto:support@example.org + + +