use std::fmt;
use std::sync::Arc;
use quick_xml::NsReader;
use quick_xml::events::{BytesRef, BytesStart, Event};
use quick_xml::name::{NamespaceResolver, ResolveResult};
use super::error::InvalidInput;
use ihe_atna::redact::REDACTED;
const SAML2: &str = "urn:oasis:names:tc:SAML:2.0:assertion";
#[derive(Clone)]
pub struct XuaAssertion(Arc<str>);
impl XuaAssertion {
pub fn new(xml: &str) -> Result<Self, InvalidInput> {
let (start, end) = element_span(xml).ok_or(InvalidInput::Assertion)?;
let element = xml.get(start..end).ok_or(InvalidInput::Assertion)?;
Ok(Self(Arc::from(element)))
}
pub(super) fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Debug for XuaAssertion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("XuaAssertion").field(&REDACTED).finish()
}
}
fn element_span(xml: &str) -> Option<(usize, usize)> {
let mut reader = NsReader::from_str(xml);
let mut start = None;
let mut end = None;
let mut depth = 0_usize;
loop {
let before = usize::try_from(reader.buffer_position()).ok()?;
let (namespace, event) = reader.read_resolved_event().ok()?;
if matches!(namespace, ResolveResult::Unknown(_)) {
return None;
}
let saml = matches!(namespace, ResolveResult::Bound(ns) if ns.0 == SAML2);
if let Event::Start(element) | Event::Empty(element) = &event {
if !attributes_declared(reader.resolver(), element) {
return None;
}
if depth == 0 {
start = Some(assertion_start(before, end.is_some(), saml, element)?);
}
}
match event {
Event::Start(_) => depth = depth.checked_add(1)?,
Event::End(_) => {
depth = depth.checked_sub(1)?;
if depth == 0 {
end = Some(usize::try_from(reader.buffer_position()).ok()?);
}
}
Event::Empty(_) if depth == 0 => {
end = Some(usize::try_from(reader.buffer_position()).ok()?);
}
Event::Text(text) if depth == 0 => {
if !text.xml10_content().trim().is_empty() {
return None;
}
}
Event::GeneralRef(reference) => {
if depth == 0 || !reference_resolves(&reference) {
return None;
}
}
Event::Decl(_) if start.is_none() && before == 0 => {}
Event::CData(_) if depth == 0 => return None,
Event::DocType(_) | Event::Decl(_) | Event::PI(_) => return None,
Event::Eof => break,
Event::Empty(_) | Event::Text(_) | Event::CData(_) | Event::Comment(_) => {}
}
}
Some((start?, end?))
}
fn attributes_declared(resolver: &NamespaceResolver, element: &BytesStart<'_>) -> bool {
element.attributes().all(|attribute| {
attribute.is_ok_and(|attribute| {
let (bound, _) = resolver.resolve_attribute(attribute.key);
!matches!(bound, ResolveResult::Unknown(_))
})
})
}
fn assertion_start(
before: usize,
closed: bool,
saml: bool,
element: &BytesStart<'_>,
) -> Option<usize> {
let root = !closed && saml && element.local_name().as_ref() == "Assertion";
root.then_some(before)
}
fn reference_resolves(reference: &BytesRef<'_>) -> bool {
let predefined =
quick_xml::escape::resolve_predefined_entity(&reference.xml10_content()).is_some();
let character = matches!(reference.resolve_char_ref(), Ok(Some(_)));
predefined || character
}
#[cfg(test)]
mod tests {
use super::XuaAssertion;
const ASSERTION: &str = r#"<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_a" Version="2.0"><saml2:Issuer>urn:oid:2.999.7</saml2:Issuer></saml2:Assertion>"#;
#[test]
fn the_element_is_kept_byte_for_byte_and_the_declaration_dropped() {
let written =
format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- issued -->\n{ASSERTION}\n");
let read = XuaAssertion::new(&written).map(|assertion| assertion.as_str().to_owned());
assert_eq!(Ok(ASSERTION.to_owned()), read);
}
#[test]
fn anything_but_one_saml_assertion_is_refused() {
let open = r#"<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">"#;
for written in [
"",
"not xml",
r#"<Assertion xmlns="urn:example"/>"#,
&format!("{ASSERTION}{ASSERTION}"),
&format!(
"{ASSERTION}<wsa:To xmlns:wsa=\"http://www.w3.org/2005/08/addressing\">x</wsa:To>"
),
&format!("<injected/>{ASSERTION}"),
&format!("{ASSERTION}trailing"),
&format!("{ASSERTION}<![CDATA[</wsse:Security>]]>"),
&format!("{ASSERTION}<?pi?>"),
&format!("<!DOCTYPE a>{ASSERTION}"),
open,
&format!("{open}</saml2:Assertion></wsse:Security><wsse:Security>"),
&format!("{open}<soap:Body/></saml2:Assertion>"),
&format!("{open}<saml2:Issuer soap:mustUnderstand=\"1\"/></saml2:Assertion>"),
&format!("{open}&undeclared;</saml2:Assertion>"),
] {
assert!(XuaAssertion::new(written).is_err(), "refused: {written:?}");
}
}
#[test]
fn references_and_nested_declarations_inside_the_element_are_kept() {
let written = r#"<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"><saml2:Subject xmlns:ds="http://www.w3.org/2000/09/xmldsig#" ds:x="a&bA"><![CDATA[c]]></saml2:Subject></saml2:Assertion>"#;
let read = XuaAssertion::new(written).map(|assertion| assertion.as_str().to_owned());
assert_eq!(Ok(written.to_owned()), read);
}
#[test]
fn debug_shows_nothing_of_it() {
let rendered = XuaAssertion::new(ASSERTION).map(|assertion| format!("{assertion:?}"));
assert_eq!(Ok("XuaAssertion(\"***\")".to_owned()), rendered);
}
}