Skip to main content

saml_rs/model/
status.rs

1use crate::constants::status_code;
2use crate::error::SamlError;
3
4/// Top-level `StatusCode` value.
5///
6/// Core §3.2.2.2 allows only these four URIs. For logout, Core §3.7.3.2 says
7/// it reports only the session authority's own session.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum TopLevelStatusCode {
10    /// `urn:oasis:names:tc:SAML:2.0:status:Success`.
11    Success,
12    /// `urn:oasis:names:tc:SAML:2.0:status:Requester`.
13    Requester,
14    /// `urn:oasis:names:tc:SAML:2.0:status:Responder`.
15    Responder,
16    /// `urn:oasis:names:tc:SAML:2.0:status:VersionMismatch`.
17    VersionMismatch,
18}
19
20impl TopLevelStatusCode {
21    /// Absolute URI carried in `StatusCode/@Value`.
22    pub fn as_uri(self) -> &'static str {
23        match self {
24            Self::Success => status_code::SUCCESS,
25            Self::Requester => status_code::REQUESTER,
26            Self::Responder => status_code::RESPONDER,
27            Self::VersionMismatch => status_code::VERSION_MISMATCH,
28        }
29    }
30}
31
32/// Optional second-level `StatusCode`.
33///
34/// [`Self::partial_logout`] is Core §3.7.3.2 `PartialLogout`. Other absolute
35/// URIs go through [`Self::try_new`].
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub struct SubordinateStatusCode {
38    uri: String,
39}
40
41impl SubordinateStatusCode {
42    /// `urn:oasis:names:tc:SAML:2.0:status:PartialLogout`.
43    pub fn partial_logout() -> Self {
44        Self {
45            uri: status_code::PARTIAL_LOGOUT.to_string(),
46        }
47    }
48
49    /// Wrap an absolute status-code URI.
50    ///
51    /// # Errors
52    ///
53    /// Returns [`SamlError::Invalid`] when `uri` is empty, has whitespace, is
54    /// not absolute, or contains `&`, `"`, `'`, `<`, or `>`.
55    pub fn try_new(uri: impl Into<String>) -> Result<Self, SamlError> {
56        let uri = uri.into();
57        if uri.is_empty()
58            || uri.chars().any(char::is_whitespace)
59            || uri
60                .chars()
61                .any(|character| matches!(character, '&' | '"' | '\'' | '<' | '>'))
62            || url::Url::parse(&uri).is_err()
63        {
64            return Err(SamlError::Invalid(
65                "subordinate status code must be a non-empty absolute URI without whitespace or XML attribute markup"
66                    .into(),
67            ));
68        }
69        Ok(Self { uri })
70    }
71
72    /// Absolute URI carried in the nested `StatusCode/@Value`.
73    pub fn as_uri(&self) -> &str {
74        &self.uri
75    }
76}
77
78/// Top-level `StatusCode` and an optional subordinate code.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct Status {
81    top_level: TopLevelStatusCode,
82    subordinate: Option<SubordinateStatusCode>,
83}
84
85impl Status {
86    /// Top-level `Success` and no subordinate code.
87    pub fn success() -> Self {
88        Self::from_top_level(TopLevelStatusCode::Success)
89    }
90
91    /// Top-level `Requester` and no subordinate code.
92    pub fn requester() -> Self {
93        Self::from_top_level(TopLevelStatusCode::Requester)
94    }
95
96    /// Top-level `Responder` and no subordinate code.
97    pub fn responder() -> Self {
98        Self::from_top_level(TopLevelStatusCode::Responder)
99    }
100
101    /// Top-level `VersionMismatch` and no subordinate code.
102    pub fn version_mismatch() -> Self {
103        Self::from_top_level(TopLevelStatusCode::VersionMismatch)
104    }
105
106    fn from_top_level(top_level: TopLevelStatusCode) -> Self {
107        Self {
108            top_level,
109            subordinate: None,
110        }
111    }
112
113    /// Nest a subordinate `StatusCode` under the top-level code.
114    ///
115    /// A later call replaces the previous subordinate code.
116    #[must_use]
117    pub fn with_subordinate(mut self, subordinate: SubordinateStatusCode) -> Self {
118        self.subordinate = Some(subordinate);
119        self
120    }
121
122    /// Top-level status code.
123    pub fn top_level(&self) -> TopLevelStatusCode {
124        self.top_level
125    }
126
127    /// Subordinate status code, when one was supplied.
128    pub fn subordinate(&self) -> Option<&SubordinateStatusCode> {
129        self.subordinate.as_ref()
130    }
131}