1use super::extract::{
2 optional_endpoint, optional_request_id, required_str, session_indexes_from_value,
3};
4use super::identifiers::{MessageId, SamlInstant, SessionIndex};
5use super::subject::NameId;
6use super::EndpointUrl;
7use crate::config::EntityId;
8use crate::constants::status_code;
9use crate::error::SamlError;
10use crate::raw::FlowResult;
11use crate::xml::parse_saml_utc_date_time;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct LogoutSubject {
16 name_id: NameId,
17 session_indexes: Vec<SessionIndex>,
18}
19
20impl LogoutSubject {
21 pub fn new(name_id: NameId, session_indexes: Vec<SessionIndex>) -> Self {
23 Self {
24 name_id,
25 session_indexes,
26 }
27 }
28
29 pub fn from_name_id(name_id: NameId) -> Self {
31 Self::new(name_id, Vec::new())
32 }
33
34 pub fn with_session_index(name_id: NameId, session_index: SessionIndex) -> Self {
36 Self::new(name_id, vec![session_index])
37 }
38
39 pub fn name_id(&self) -> &NameId {
41 &self.name_id
42 }
43
44 pub fn session_index(&self) -> Option<&SessionIndex> {
46 self.session_indexes.first()
47 }
48
49 pub fn session_indexes(&self) -> &[SessionIndex] {
51 &self.session_indexes
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct LogoutRequest {
58 id: MessageId,
59 issue_instant: SamlInstant,
60 not_on_or_after: Option<SamlInstant>,
61 issuer: EntityId,
62 name_id: Option<NameId>,
63 session_indexes: Vec<SessionIndex>,
64 destination: Option<EndpointUrl>,
65 raw_flow: FlowResult,
66}
67
68impl LogoutRequest {
69 pub fn id(&self) -> &MessageId {
71 &self.id
72 }
73
74 pub fn issue_instant(&self) -> &SamlInstant {
76 &self.issue_instant
77 }
78
79 pub fn not_on_or_after(&self) -> Option<&SamlInstant> {
81 self.not_on_or_after.as_ref()
82 }
83
84 pub fn issuer(&self) -> &EntityId {
86 &self.issuer
87 }
88
89 pub fn name_id(&self) -> Option<&NameId> {
91 self.name_id.as_ref()
92 }
93
94 pub fn session_indexes(&self) -> &[SessionIndex] {
96 &self.session_indexes
97 }
98
99 pub fn destination(&self) -> Option<&EndpointUrl> {
101 self.destination.as_ref()
102 }
103
104 pub fn raw_flow(&self) -> &FlowResult {
106 &self.raw_flow
107 }
108}
109
110impl TryFrom<FlowResult> for LogoutRequest {
111 type Error = SamlError;
112
113 fn try_from(raw_flow: FlowResult) -> Result<Self, Self::Error> {
114 let id = MessageId::try_new(required_str(&raw_flow.extract, "request.id")?)?;
115 let issue_instant = logout_request_instant_from_extract(
116 &raw_flow.extract,
117 "issueInstant",
118 "IssueInstant",
119 true,
120 )?
121 .ok_or_else(|| {
122 SamlError::ProtocolProfile(
123 "LogoutRequest is missing required unqualified attribute IssueInstant".into(),
124 )
125 })?;
126 let not_on_or_after = logout_request_instant_from_extract(
127 &raw_flow.extract,
128 "notOnOrAfter",
129 "NotOnOrAfter",
130 false,
131 )?;
132 let issuer = EntityId::try_new(required_str(&raw_flow.extract, "issuer")?)?;
133 let name_id = raw_flow
134 .extract
135 .get_str("nameID")
136 .map(|value| NameId::new(value, None));
137 let session_indexes = session_indexes_from_value(raw_flow.extract.get("sessionIndex"))?;
138 let destination = optional_endpoint(&raw_flow.extract, "request.destination")?;
139 Ok(Self {
140 id,
141 issue_instant,
142 not_on_or_after,
143 issuer,
144 name_id,
145 session_indexes,
146 destination,
147 raw_flow,
148 })
149 }
150}
151
152fn logout_request_instant_from_extract(
153 extract: &crate::util::Value,
154 extract_field: &str,
155 attribute: &str,
156 required: bool,
157) -> Result<Option<SamlInstant>, SamlError> {
158 let path = format!("request.{extract_field}");
159 let Some(value) = extract.get_str(&path) else {
160 if required {
161 return Err(SamlError::ProtocolProfile(format!(
162 "LogoutRequest is missing required unqualified attribute {attribute}"
163 )));
164 }
165 return Ok(None);
166 };
167 let normalized = parse_saml_utc_date_time(value).ok_or_else(|| {
168 SamlError::ProtocolProfile(format!(
169 "LogoutRequest {attribute} must use the SAML-conformant UTC xs:dateTime form ending in Z"
170 ))
171 })?;
172 SamlInstant::try_new(normalized).map(Some)
173}
174
175#[derive(Debug, Clone)]
177pub struct LogoutResponse {
178 id: MessageId,
179 issuer: EntityId,
180 issue_instant: SamlInstant,
181 in_response_to: Option<MessageId>,
182 destination: Option<EndpointUrl>,
183 raw_flow: FlowResult,
184}
185
186impl LogoutResponse {
187 pub fn id(&self) -> &MessageId {
189 &self.id
190 }
191
192 pub fn issuer(&self) -> &EntityId {
194 &self.issuer
195 }
196
197 pub fn issue_instant(&self) -> &SamlInstant {
199 &self.issue_instant
200 }
201
202 pub fn in_response_to(&self) -> Option<&MessageId> {
204 self.in_response_to.as_ref()
205 }
206
207 pub fn destination(&self) -> Option<&EndpointUrl> {
209 self.destination.as_ref()
210 }
211
212 pub fn raw_flow(&self) -> &FlowResult {
214 &self.raw_flow
215 }
216}
217
218impl TryFrom<FlowResult> for LogoutResponse {
219 type Error = SamlError;
220
221 fn try_from(raw_flow: FlowResult) -> Result<Self, Self::Error> {
222 let id = MessageId::try_new(required_str(&raw_flow.extract, "response.id")?)?;
223 let issue_instant = issue_instant_from_extract(&raw_flow.extract)?;
224 let issuer = EntityId::try_new(required_str(&raw_flow.extract, "issuer")?)?;
225 let in_response_to = optional_request_id(&raw_flow.extract, "response.inResponseTo")?;
226 let destination = optional_endpoint(&raw_flow.extract, "response.destination")?;
227 Ok(Self {
228 id,
229 issuer,
230 issue_instant,
231 in_response_to,
232 destination,
233 raw_flow,
234 })
235 }
236}
237
238fn issue_instant_from_extract(extract: &crate::util::Value) -> Result<SamlInstant, SamlError> {
239 let issue_instant = extract.get_str("response.issueInstant").ok_or_else(|| {
240 SamlError::ProtocolProfile(
241 "LogoutResponse is missing required unqualified attribute IssueInstant".into(),
242 )
243 })?;
244 let issue_instant = parse_saml_utc_date_time(issue_instant).ok_or_else(|| {
245 SamlError::ProtocolProfile(
246 "LogoutResponse IssueInstant must use the SAML-conformant UTC xs:dateTime form ending in Z"
247 .into(),
248 )
249 })?;
250 SamlInstant::try_new(issue_instant)
251}
252
253#[derive(Debug, Clone)]
255pub struct LogoutCompleted {
256 peer_entity_id: EntityId,
257 response: Option<LogoutResponse>,
258}
259
260impl PartialEq for LogoutCompleted {
261 fn eq(&self, other: &Self) -> bool {
262 self.peer_entity_id == other.peer_entity_id
263 }
264}
265
266impl Eq for LogoutCompleted {}
267
268impl LogoutCompleted {
269 pub fn new(peer_entity_id: EntityId) -> Self {
271 Self {
272 peer_entity_id,
273 response: None,
274 }
275 }
276
277 pub fn from_response(peer_entity_id: EntityId, response: LogoutResponse) -> Self {
279 Self {
280 peer_entity_id,
281 response: Some(response),
282 }
283 }
284
285 pub fn peer_entity_id(&self) -> &EntityId {
287 &self.peer_entity_id
288 }
289
290 pub fn response(&self) -> Option<&LogoutResponse> {
292 self.response.as_ref()
293 }
294
295 pub fn status(&self) -> Option<&str> {
297 self.response.as_ref().map(|_| status_code::SUCCESS)
298 }
299
300 pub fn raw_flow(&self) -> Option<&FlowResult> {
302 self.response.as_ref().map(LogoutResponse::raw_flow)
303 }
304}