Skip to main content

saml_rs/metadata/
sp.rs

1//! Service Provider metadata.
2
3use super::{as_object_list, Metadata};
4use crate::constants::Binding;
5use crate::error::SamlError;
6use crate::xml::{ExtractorField, XmlLimits};
7use std::ops::Deref;
8
9/// Parsed AssertionConsumerService endpoint from SP metadata.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub(crate) struct AcsMetadataEndpoint {
12    /// Protocol binding.
13    pub(crate) binding: Binding,
14    /// Endpoint location.
15    pub(crate) location: String,
16    /// Metadata index, when declared.
17    pub(crate) index: Option<u16>,
18    /// Whether the endpoint is marked as default.
19    pub(crate) is_default: bool,
20}
21
22/// Parsed SP metadata. Derefs to [`Metadata`] for the shared accessors.
23#[derive(Debug, Clone)]
24pub struct SpMetadata {
25    inner: Metadata,
26}
27
28impl SpMetadata {
29    /// Parse SP metadata XML.
30    ///
31    /// # Errors
32    ///
33    /// Returns [`SamlError`] when XML parsing, parser resource limits, or
34    /// SP-specific metadata extraction fails.
35    pub fn from_xml(xml: &str) -> Result<Self, SamlError> {
36        Self::from_xml_with_limits(xml, XmlLimits::default())
37    }
38
39    /// Parse SP metadata XML with explicit XML parser resource limits.
40    ///
41    /// # Errors
42    ///
43    /// Returns [`SamlError`] when XML parsing, parser resource limits, or
44    /// SP-specific metadata extraction fails.
45    pub fn from_xml_with_limits(xml: &str, limits: XmlLimits) -> Result<Self, SamlError> {
46        let extra = vec![
47            ExtractorField::new("spSSODescriptor", &["EntityDescriptor", "SPSSODescriptor"])
48                .attrs(&["WantAssertionsSigned", "AuthnRequestsSigned"]),
49            ExtractorField::new(
50                "assertionConsumerService",
51                &[
52                    "EntityDescriptor",
53                    "SPSSODescriptor",
54                    "AssertionConsumerService",
55                ],
56            )
57            .attrs(&["Binding", "Location", "isDefault", "index"]),
58        ];
59        Ok(Self {
60            inner: Metadata::parse_with_limits(xml, extra, limits)?,
61        })
62    }
63
64    /// `WantAssertionsSigned` flag.
65    pub fn is_want_assertions_signed(&self) -> bool {
66        self.inner
67            .meta
68            .get_str("spSSODescriptor.wantAssertionsSigned")
69            == Some("true")
70    }
71
72    /// `AuthnRequestsSigned` flag.
73    pub fn is_authn_request_signed(&self) -> bool {
74        self.inner
75            .meta
76            .get_str("spSSODescriptor.authnRequestsSigned")
77            == Some("true")
78    }
79
80    /// `AssertionConsumerService` location for `binding`.
81    pub fn get_assertion_consumer_service(&self, binding: Binding) -> Option<String> {
82        self.get_assertion_consumer_service_endpoint(binding)
83            .map(|endpoint| endpoint.location)
84    }
85
86    /// First `AssertionConsumerService` endpoint for `binding`.
87    pub(crate) fn get_assertion_consumer_service_endpoint(
88        &self,
89        binding: Binding,
90    ) -> Option<AcsMetadataEndpoint> {
91        self.assertion_consumer_service_endpoints()
92            .into_iter()
93            .find(|endpoint| endpoint.binding == binding)
94    }
95
96    /// `AssertionConsumerService` endpoint with the declared metadata index.
97    pub(crate) fn get_assertion_consumer_service_by_index(
98        &self,
99        index: u16,
100    ) -> Result<Option<AcsMetadataEndpoint>, SamlError> {
101        let mut matches = self
102            .assertion_consumer_service_endpoints()
103            .into_iter()
104            .filter(|endpoint| endpoint.index == Some(index));
105        let first = matches.next();
106        if matches.next().is_some() {
107            return Err(SamlError::Invalid(format!(
108                "duplicate AssertionConsumerService index {index}"
109            )));
110        }
111        Ok(first)
112    }
113
114    /// Whether metadata contains `location` for `binding`.
115    pub(crate) fn has_assertion_consumer_service(&self, binding: Binding, location: &str) -> bool {
116        self.assertion_consumer_service_endpoints()
117            .iter()
118            .any(|endpoint| endpoint.binding == binding && endpoint.location == location)
119    }
120
121    fn assertion_consumer_service_endpoints(&self) -> Vec<AcsMetadataEndpoint> {
122        let Some(acs) = self.inner.meta.get("assertionConsumerService") else {
123            return Vec::new();
124        };
125        as_object_list(acs)
126            .into_iter()
127            .filter_map(acs_metadata_endpoint_from_value)
128            .collect()
129    }
130}
131
132fn acs_metadata_endpoint_from_value(value: &crate::util::Value) -> Option<AcsMetadataEndpoint> {
133    let binding = Binding::from_urn(value.get_str("binding")?)?;
134    let location = value.get_str("location")?.to_string();
135    let index = value
136        .get_str("index")
137        .and_then(|index| index.parse::<u16>().ok());
138    let is_default = value.get_str("isDefault") == Some("true");
139    Some(AcsMetadataEndpoint {
140        binding,
141        location,
142        index,
143        is_default,
144    })
145}
146
147impl Deref for SpMetadata {
148    type Target = Metadata;
149    fn deref(&self) -> &Metadata {
150        &self.inner
151    }
152}