Skip to main content

saml_rs/model/
relay.rs

1use crate::error::SamlError;
2
3/// SAML Bindings 2.0 prohibits a RelayState value longer than 80 bytes.
4///
5/// Bindings §3.4.3, §3.5.3, and HTTP-POST-SimpleSign CD-04 §2.3.
6/// Reference: <https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf>.
7pub const MAX_RELAY_STATE_BYTES: usize = 80;
8
9/// RelayState value when a browser message carries the parameter.
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub struct RelayState(String);
12
13impl RelayState {
14    /// Wrap a RelayState value after enforcing the SAML Bindings byte limit.
15    ///
16    /// Explicit empty RelayState is allowed so callers can preserve browser
17    /// parameter presence separately from absence.
18    ///
19    /// # Errors
20    ///
21    /// Returns [`SamlError::Invalid`] when the UTF-8 byte length exceeds
22    /// [`MAX_RELAY_STATE_BYTES`].
23    pub fn try_new(value: impl Into<String>) -> Result<Self, SamlError> {
24        let value = value.into();
25        validate_relay_state_bytes(&value)?;
26        Ok(Self(value))
27    }
28
29    /// Borrow the RelayState string.
30    pub fn as_str(&self) -> &str {
31        &self.0
32    }
33}
34
35/// RelayState represented as absent, present empty, or present with a value.
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub enum RelayStateParam {
38    /// No RelayState parameter was sent.
39    Absent,
40    /// RelayState was sent with an empty value.
41    PresentEmpty,
42    /// RelayState was sent with a non-empty value.
43    PresentValue(RelayState),
44}
45
46impl RelayStateParam {
47    /// Construct an absent RelayState parameter.
48    pub fn absent() -> Self {
49        Self::Absent
50    }
51
52    /// Construct a present RelayState parameter with an empty value.
53    pub fn present_empty() -> Self {
54        Self::PresentEmpty
55    }
56
57    /// Preserve the exact RelayState presence state from optional caller input.
58    ///
59    /// # Errors
60    ///
61    /// Returns [`SamlError::Invalid`] when a present non-empty value exceeds
62    /// the SAML Bindings 80-byte RelayState limit.
63    pub fn try_from_option(value: Option<impl Into<String>>) -> Result<Self, SamlError> {
64        match value {
65            None => Ok(Self::Absent),
66            Some(value) => {
67                let value = value.into();
68                if value.is_empty() {
69                    Ok(Self::PresentEmpty)
70                } else {
71                    Ok(Self::PresentValue(RelayState::try_new(value)?))
72                }
73            }
74        }
75    }
76
77    /// Borrow RelayState as an optional value.
78    pub fn as_deref(&self) -> Option<&str> {
79        match self {
80            Self::Absent => None,
81            Self::PresentEmpty => Some(""),
82            Self::PresentValue(value) => Some(value.as_str()),
83        }
84    }
85
86    pub(crate) fn validate(&self) -> Result<(), SamlError> {
87        if matches!(self, Self::PresentValue(value) if value.as_str().is_empty()) {
88            return Err(SamlError::Invalid(
89                "RelayState PresentValue must not be empty".into(),
90            ));
91        }
92        if let Some(value) = self.as_deref() {
93            validate_relay_state_bytes(value)?;
94        }
95        Ok(())
96    }
97}
98
99impl TryFrom<Option<String>> for RelayStateParam {
100    type Error = SamlError;
101
102    fn try_from(value: Option<String>) -> Result<Self, Self::Error> {
103        Self::try_from_option(value)
104    }
105}
106
107fn validate_relay_state_bytes(value: &str) -> Result<(), SamlError> {
108    if value.len() > MAX_RELAY_STATE_BYTES {
109        return Err(SamlError::Invalid(
110            "RelayState exceeds SAML Bindings 80-byte limit".into(),
111        ));
112    }
113    Ok(())
114}