Skip to main content

saml_rs/config/
descriptors.rs

1use core::str::FromStr;
2
3use crate::browser::{AcsEndpoint, SloEndpoint, SsoEndpoint};
4use crate::error::SamlError;
5use crate::metadata::{IdpMetadata, SpMetadata};
6
7use super::algorithms::NameIdFormat;
8use super::metadata_trust::{
9    ensure_expected_entity_id, ensure_metadata_trust, metadata_entity_id, AppliedMetadataTrust,
10    MetadataTrustPolicy,
11};
12
13/// SAML entity ID.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct EntityId(String);
16
17impl EntityId {
18    /// Wrap an entity ID URI or deployment-specific identifier without validation.
19    ///
20    /// Prefer [`Self::try_new`] for caller-provided input. This constructor is
21    /// retained for already-validated compatibility data.
22    pub fn new(value: impl Into<String>) -> Self {
23        Self(value.into())
24    }
25
26    /// Validate and wrap a non-empty entity ID.
27    ///
28    /// # Errors
29    ///
30    /// Returns [`SamlError::Invalid`] when the entity ID is empty.
31    pub fn try_new(value: impl Into<String>) -> Result<Self, SamlError> {
32        let value = value.into();
33        validate_entity_id_text(&value)?;
34        Ok(Self(value))
35    }
36
37    /// Borrow the entity ID string.
38    pub fn as_str(&self) -> &str {
39        &self.0
40    }
41}
42
43impl FromStr for EntityId {
44    type Err = SamlError;
45
46    fn from_str(value: &str) -> Result<Self, Self::Err> {
47        Self::try_new(value)
48    }
49}
50
51impl TryFrom<&str> for EntityId {
52    type Error = SamlError;
53
54    fn try_from(value: &str) -> Result<Self, Self::Error> {
55        Self::try_new(value)
56    }
57}
58
59impl TryFrom<String> for EntityId {
60    type Error = SamlError;
61
62    fn try_from(value: String) -> Result<Self, Self::Error> {
63        Self::try_new(value)
64    }
65}
66
67pub(super) fn validate_entity_id_text(value: &str) -> Result<(), SamlError> {
68    if value.trim().is_empty() {
69        return Err(SamlError::Invalid("entity ID must not be empty".into()));
70    }
71    Ok(())
72}
73
74pub(super) fn validate_entity_id(entity_id: &EntityId) -> Result<(), SamlError> {
75    validate_entity_id_text(entity_id.as_str())
76}
77
78/// Local SP metadata inputs used by typed configuration.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct SpMetadataConfig {
81    /// `<NameIDFormat>` values advertised by the SP.
82    pub name_id_format: Vec<NameIdFormat>,
83    /// `SingleLogoutService` endpoints.
84    pub single_logout_service: Vec<SloEndpoint>,
85    /// `AssertionConsumerService` endpoints.
86    pub assertion_consumer_service: Vec<AcsEndpoint>,
87    /// Element ordering profile for generated metadata.
88    pub elements_order: Option<Vec<String>>,
89}
90
91impl SpMetadataConfig {
92    /// Create SP metadata input with required ACS endpoints visible at the call site.
93    ///
94    /// This constructor does not validate the endpoint list. Use
95    /// [`Self::try_new`] for caller-provided endpoint collections.
96    pub fn new(assertion_consumer_service: Vec<AcsEndpoint>) -> Self {
97        Self {
98            name_id_format: Vec::new(),
99            single_logout_service: Vec::new(),
100            assertion_consumer_service,
101            elements_order: None,
102        }
103    }
104
105    /// Validate and create SP metadata input.
106    ///
107    /// # Errors
108    ///
109    /// Returns [`SamlError::MissingMetadata`] when no ACS endpoint is supplied.
110    pub fn try_new(assertion_consumer_service: Vec<AcsEndpoint>) -> Result<Self, SamlError> {
111        let config = Self::new(assertion_consumer_service);
112        config.validate()?;
113        Ok(config)
114    }
115
116    /// Validate required SP metadata inputs.
117    ///
118    /// # Errors
119    ///
120    /// Returns [`SamlError::MissingMetadata`] when no ACS endpoint is supplied.
121    pub fn validate(&self) -> Result<(), SamlError> {
122        if self.assertion_consumer_service.is_empty() {
123            return Err(SamlError::MissingMetadata(
124                "AssertionConsumerService".into(),
125            ));
126        }
127        Ok(())
128    }
129}
130
131/// Local IdP metadata inputs used by typed configuration.
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct IdpMetadataConfig {
134    /// `<NameIDFormat>` values advertised by the IdP.
135    pub name_id_format: Vec<NameIdFormat>,
136    /// `SingleSignOnService` endpoints.
137    pub single_sign_on_service: Vec<SsoEndpoint>,
138    /// `SingleLogoutService` endpoints.
139    pub single_logout_service: Vec<SloEndpoint>,
140    /// Element ordering profile for generated metadata.
141    pub elements_order: Option<Vec<String>>,
142}
143
144impl IdpMetadataConfig {
145    /// Create IdP metadata input with required SSO endpoints visible at the call site.
146    ///
147    /// This constructor does not validate the endpoint list. Use
148    /// [`Self::try_new`] for caller-provided endpoint collections.
149    pub fn new(single_sign_on_service: Vec<SsoEndpoint>) -> Self {
150        Self {
151            name_id_format: Vec::new(),
152            single_sign_on_service,
153            single_logout_service: Vec::new(),
154            elements_order: None,
155        }
156    }
157
158    /// Validate and create IdP metadata input.
159    ///
160    /// # Errors
161    ///
162    /// Returns [`SamlError::MissingMetadata`] when no SSO endpoint is supplied.
163    pub fn try_new(single_sign_on_service: Vec<SsoEndpoint>) -> Result<Self, SamlError> {
164        let config = Self::new(single_sign_on_service);
165        config.validate()?;
166        Ok(config)
167    }
168
169    /// Validate required IdP metadata inputs.
170    ///
171    /// # Errors
172    ///
173    /// Returns [`SamlError::MissingMetadata`] when no SSO endpoint is supplied.
174    pub fn validate(&self) -> Result<(), SamlError> {
175        if self.single_sign_on_service.is_empty() {
176            return Err(SamlError::MissingMetadata("SingleSignOnService".into()));
177        }
178        Ok(())
179    }
180}
181
182/// Typed IdP peer descriptor imported from metadata.
183#[derive(Debug, Clone)]
184pub struct IdpDescriptor {
185    entity_id: EntityId,
186    metadata_xml: String,
187    metadata: IdpMetadata,
188    trust: AppliedMetadataTrust,
189}
190
191impl IdpDescriptor {
192    /// Parse IdP metadata for an expected entity ID and explicit trust policy.
193    ///
194    /// # Errors
195    ///
196    /// Returns [`SamlError`] when the XML is malformed, the entity ID is absent
197    /// or unexpected, or the requested trust policy cannot be satisfied.
198    pub fn from_metadata_xml_for(
199        expected_entity_id: EntityId,
200        xml: &str,
201        trust: MetadataTrustPolicy<'_>,
202    ) -> Result<Self, SamlError> {
203        validate_entity_id(&expected_entity_id)?;
204        let metadata = IdpMetadata::from_xml(xml)?;
205        let actual_entity_id = metadata_entity_id(&metadata)?;
206        ensure_expected_entity_id(&expected_entity_id, actual_entity_id)?;
207        let trust = ensure_metadata_trust(&metadata, trust)?;
208        Ok(Self {
209            entity_id: expected_entity_id,
210            metadata_xml: xml.to_string(),
211            metadata,
212            trust,
213        })
214    }
215
216    /// Parse IdP metadata when the caller accepts the metadata-declared entity ID.
217    ///
218    /// Prefer [`Self::from_metadata_xml_for`] when the expected entity ID is known.
219    ///
220    /// # Errors
221    ///
222    /// Returns [`SamlError`] when the XML is malformed, the entity ID is absent,
223    /// or the requested trust policy cannot be satisfied.
224    pub fn from_metadata_xml(xml: &str, trust: MetadataTrustPolicy<'_>) -> Result<Self, SamlError> {
225        let metadata = IdpMetadata::from_xml(xml)?;
226        let entity_id = EntityId::try_new(metadata_entity_id(&metadata)?)?;
227        let trust = ensure_metadata_trust(&metadata, trust)?;
228        Ok(Self {
229            entity_id,
230            metadata_xml: xml.to_string(),
231            metadata,
232            trust,
233        })
234    }
235
236    /// Metadata entity ID.
237    pub fn entity_id(&self) -> &EntityId {
238        &self.entity_id
239    }
240
241    /// Original metadata XML.
242    pub fn metadata_xml(&self) -> &str {
243        &self.metadata_xml
244    }
245
246    /// Parsed IdP metadata.
247    pub fn metadata(&self) -> &IdpMetadata {
248        &self.metadata
249    }
250
251    /// Whether this descriptor was verified with pinned signing certificates.
252    pub fn was_verified_with_pinned_certificates(&self) -> bool {
253        matches!(
254            self.trust,
255            AppliedMetadataTrust::SignedByPinnedCertificates { .. }
256        )
257    }
258
259    /// Signed metadata descriptor XML when pinned metadata verification passed.
260    pub fn signed_entity_descriptor_xml(&self) -> Option<&str> {
261        match &self.trust {
262            AppliedMetadataTrust::SignedByPinnedCertificates {
263                signed_entity_descriptor_xml,
264            } => Some(signed_entity_descriptor_xml.as_str()),
265            AppliedMetadataTrust::UnsignedForCompatibility => None,
266        }
267    }
268}
269
270/// Typed SP peer descriptor imported from metadata.
271#[derive(Debug, Clone)]
272pub struct SpDescriptor {
273    entity_id: EntityId,
274    metadata_xml: String,
275    metadata: SpMetadata,
276    trust: AppliedMetadataTrust,
277}
278
279impl SpDescriptor {
280    /// Parse SP metadata for an expected entity ID and explicit trust policy.
281    ///
282    /// # Errors
283    ///
284    /// Returns [`SamlError`] when the XML is malformed, the entity ID is absent
285    /// or unexpected, or the requested trust policy cannot be satisfied.
286    pub fn from_metadata_xml_for(
287        expected_entity_id: EntityId,
288        xml: &str,
289        trust: MetadataTrustPolicy<'_>,
290    ) -> Result<Self, SamlError> {
291        validate_entity_id(&expected_entity_id)?;
292        let metadata = SpMetadata::from_xml(xml)?;
293        let actual_entity_id = metadata_entity_id(&metadata)?;
294        ensure_expected_entity_id(&expected_entity_id, actual_entity_id)?;
295        let trust = ensure_metadata_trust(&metadata, trust)?;
296        Ok(Self {
297            entity_id: expected_entity_id,
298            metadata_xml: xml.to_string(),
299            metadata,
300            trust,
301        })
302    }
303
304    /// Parse SP metadata when the caller accepts the metadata-declared entity ID.
305    ///
306    /// Prefer [`Self::from_metadata_xml_for`] when the expected entity ID is known.
307    ///
308    /// # Errors
309    ///
310    /// Returns [`SamlError`] when the XML is malformed, the entity ID is absent,
311    /// or the requested trust policy cannot be satisfied.
312    pub fn from_metadata_xml(xml: &str, trust: MetadataTrustPolicy<'_>) -> Result<Self, SamlError> {
313        let metadata = SpMetadata::from_xml(xml)?;
314        let entity_id = EntityId::try_new(metadata_entity_id(&metadata)?)?;
315        let trust = ensure_metadata_trust(&metadata, trust)?;
316        Ok(Self {
317            entity_id,
318            metadata_xml: xml.to_string(),
319            metadata,
320            trust,
321        })
322    }
323
324    /// Metadata entity ID.
325    pub fn entity_id(&self) -> &EntityId {
326        &self.entity_id
327    }
328
329    /// Original metadata XML.
330    pub fn metadata_xml(&self) -> &str {
331        &self.metadata_xml
332    }
333
334    /// Parsed SP metadata.
335    pub fn metadata(&self) -> &SpMetadata {
336        &self.metadata
337    }
338
339    /// Whether this descriptor was verified with pinned signing certificates.
340    pub fn was_verified_with_pinned_certificates(&self) -> bool {
341        matches!(
342            self.trust,
343            AppliedMetadataTrust::SignedByPinnedCertificates { .. }
344        )
345    }
346
347    /// Signed metadata descriptor XML when pinned metadata verification passed.
348    pub fn signed_entity_descriptor_xml(&self) -> Option<&str> {
349        match &self.trust {
350            AppliedMetadataTrust::SignedByPinnedCertificates {
351                signed_entity_descriptor_xml,
352            } => Some(signed_entity_descriptor_xml.as_str()),
353            AppliedMetadataTrust::UnsignedForCompatibility => None,
354        }
355    }
356}