Skip to main content

saml_rs/model/
relay.rs

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