1use std::collections::HashMap;
4
5use serde_json::Value;
6
7use crate::error::MqRestError;
8use crate::transport::{MqRestTransport, TransportResponse};
9
10pub const LTPA_COOKIE_NAME: &str = "LtpaToken2";
12const LTPA_LOGIN_PATH: &str = "/login";
13const ERROR_LTPA_LOGIN_FAILED: &str = "LTPA login failed.";
14const ERROR_LTPA_TOKEN_MISSING: &str =
15 "LTPA login succeeded but no LtpaToken2 cookie was returned.";
16
17#[derive(Debug, Clone)]
19pub enum Credentials {
20 Basic {
22 username: String,
24 password: String,
26 },
27 Ltpa {
29 username: String,
31 password: String,
33 },
34 Certificate {
36 cert_path: String,
38 key_path: Option<String>,
40 },
41}
42
43pub(crate) fn perform_ltpa_login(
48 transport: &dyn MqRestTransport,
49 rest_base_url: &str,
50 username: &str,
51 password: &str,
52 csrf_token: Option<&str>,
53 timeout_seconds: Option<f64>,
54 verify_tls: bool,
55) -> crate::error::Result<(String, String)> {
56 let login_url = format!("{rest_base_url}{LTPA_LOGIN_PATH}");
57 let mut headers = HashMap::new();
58 headers.insert("Accept".into(), "application/json".into());
59 if let Some(token) = csrf_token {
60 headers.insert("ibm-mq-rest-csrf-token".into(), token.into());
61 }
62 let mut payload = HashMap::new();
63 payload.insert("username".into(), Value::String(username.into()));
64 payload.insert("password".into(), Value::String(password.into()));
65 let response =
66 transport.post_json(&login_url, &payload, &headers, timeout_seconds, verify_tls)?;
67 if response.status_code >= 400 {
68 return Err(MqRestError::Auth {
69 url: login_url,
70 status_code: Some(response.status_code),
71 message: ERROR_LTPA_LOGIN_FAILED.into(),
72 });
73 }
74 match extract_ltpa_token(&response) {
75 Some(result) => Ok(result),
76 None => Err(MqRestError::Auth {
77 url: login_url,
78 status_code: Some(response.status_code),
79 message: ERROR_LTPA_TOKEN_MISSING.into(),
80 }),
81 }
82}
83
84fn extract_ltpa_token(response: &TransportResponse) -> Option<(String, String)> {
92 let set_cookie = response
93 .headers
94 .get("Set-Cookie")
95 .or_else(|| response.headers.get("set-cookie"))?;
96 for cookie_part in set_cookie.split(';') {
98 let cookie_part = cookie_part.trim();
99 if cookie_part.starts_with(LTPA_COOKIE_NAME)
100 && let Some(eq_index) = cookie_part.find('=')
101 {
102 let name = &cookie_part[..eq_index];
103 let value = &cookie_part[eq_index + 1..];
104 return Some((name.to_owned(), value.to_owned()));
105 }
106 }
107 for cookie_entry in set_cookie.split(',') {
109 for cookie_part in cookie_entry.split(';') {
110 let cookie_part = cookie_part.trim();
111 if cookie_part.starts_with(LTPA_COOKIE_NAME)
112 && let Some(eq_index) = cookie_part.find('=')
113 {
114 let name = &cookie_part[..eq_index];
115 let value = &cookie_part[eq_index + 1..];
116 return Some((name.to_owned(), value.to_owned()));
117 }
118 }
119 }
120 None
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::test_helpers::MockTransport;
127
128 fn login_response_with_cookie(cookie_header: &str, cookie_value: &str) -> TransportResponse {
129 let mut headers = HashMap::new();
130 headers.insert(cookie_header.into(), cookie_value.into());
131 TransportResponse {
132 status_code: 200,
133 text: "{}".into(),
134 headers,
135 }
136 }
137
138 #[test]
139 fn ltpa_login_success() {
140 let transport = MockTransport::new(vec![login_response_with_cookie(
141 "Set-Cookie",
142 "LtpaToken2=abc123; Path=/",
143 )]);
144 let result = perform_ltpa_login(
145 &transport,
146 "https://host/ibmmq/rest/v2",
147 "user",
148 "pass",
149 Some("csrf"),
150 Some(10.0),
151 true,
152 );
153 let (name, value) = result.unwrap();
154 assert_eq!(name, "LtpaToken2");
155 assert_eq!(value, "abc123");
156 }
157
158 #[test]
159 fn ltpa_login_success_with_suffixed_cookie() {
160 let transport = MockTransport::new(vec![login_response_with_cookie(
161 "Set-Cookie",
162 "LtpaToken2_abcdef=suffixed_tok; Path=/",
163 )]);
164 let result = perform_ltpa_login(
165 &transport,
166 "https://host/ibmmq/rest/v2",
167 "user",
168 "pass",
169 None,
170 None,
171 false,
172 );
173 let (name, value) = result.unwrap();
174 assert_eq!(name, "LtpaToken2_abcdef");
175 assert_eq!(value, "suffixed_tok");
176 }
177
178 #[test]
179 fn ltpa_login_case_insensitive_header() {
180 let transport = MockTransport::new(vec![login_response_with_cookie(
181 "set-cookie",
182 "LtpaToken2=token456; Path=/",
183 )]);
184 let result = perform_ltpa_login(&transport, "https://h", "u", "p", None, None, false);
185 let (name, value) = result.unwrap();
186 assert_eq!(name, "LtpaToken2");
187 assert_eq!(value, "token456");
188 }
189
190 #[test]
191 fn ltpa_login_comma_separated_cookies() {
192 let transport = MockTransport::new(vec![login_response_with_cookie(
193 "Set-Cookie",
194 "other=x, LtpaToken2=fromcomma; Path=/",
195 )]);
196 let result = perform_ltpa_login(&transport, "https://h", "u", "p", None, None, false);
197 let (name, value) = result.unwrap();
198 assert_eq!(name, "LtpaToken2");
199 assert_eq!(value, "fromcomma");
200 }
201
202 #[test]
203 fn ltpa_login_http_401() {
204 let transport = MockTransport::new(vec![TransportResponse {
205 status_code: 401,
206 text: "Unauthorized".into(),
207 headers: HashMap::new(),
208 }]);
209 let result = perform_ltpa_login(&transport, "https://h", "u", "p", None, None, false);
210 assert!(format!("{:?}", result.unwrap_err()).starts_with("Auth"));
211 }
212
213 #[test]
214 fn ltpa_login_missing_token() {
215 let transport = MockTransport::new(vec![TransportResponse {
216 status_code: 200,
217 text: "{}".into(),
218 headers: HashMap::new(),
219 }]);
220 let result = perform_ltpa_login(&transport, "https://h", "u", "p", None, None, false);
221 assert!(format!("{:?}", result.unwrap_err()).starts_with("Auth"));
222 }
223
224 #[test]
225 fn ltpa_login_csrf_token_present_in_request() {
226 let transport = MockTransport::new(vec![login_response_with_cookie(
227 "Set-Cookie",
228 "LtpaToken2=tok; Path=/",
229 )]);
230 perform_ltpa_login(
231 &transport,
232 "https://h",
233 "u",
234 "p",
235 Some("mytoken"),
236 None,
237 false,
238 )
239 .unwrap();
240 let requests = transport.requests();
241 assert_eq!(
242 requests[0].headers.get("ibm-mq-rest-csrf-token").unwrap(),
243 "mytoken"
244 );
245 }
246
247 #[test]
248 fn ltpa_login_csrf_token_absent() {
249 let transport = MockTransport::new(vec![login_response_with_cookie(
250 "Set-Cookie",
251 "LtpaToken2=tok; Path=/",
252 )]);
253 perform_ltpa_login(&transport, "https://h", "u", "p", None, None, false).unwrap();
254 let requests = transport.requests();
255 assert!(!requests[0].headers.contains_key("ibm-mq-rest-csrf-token"));
256 }
257
258 #[test]
259 fn ltpa_login_cookie_present_but_no_ltpa_token() {
260 let transport = MockTransport::new(vec![login_response_with_cookie(
261 "Set-Cookie",
262 "SomeOtherCookie=value; Path=/",
263 )]);
264 let result = perform_ltpa_login(&transport, "https://h", "u", "p", None, None, false);
265 assert!(format!("{:?}", result.unwrap_err()).starts_with("Auth"));
266 }
267
268 #[test]
269 fn ltpa_login_transport_error() {
270 let transport = MockTransport::new(vec![]);
271 let result = perform_ltpa_login(&transport, "https://h", "u", "p", None, None, false);
272 assert!(result.is_err());
273 }
274}