1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5use crate::client::GuacamoleClient;
6use crate::error::Result;
7use crate::validation::{validate_data_source, validate_token};
8
9#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12#[non_exhaustive]
13pub struct AuthResponse {
14 #[serde(skip_serializing_if = "Option::is_none")]
16 pub auth_token: Option<String>,
17
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub username: Option<String>,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub data_source: Option<String>,
25
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub available_data_sources: Option<Vec<String>>,
29}
30
31impl fmt::Debug for AuthResponse {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 f.debug_struct("AuthResponse")
34 .field("auth_token", &"<redacted>")
35 .field("username", &self.username)
36 .field("data_source", &self.data_source)
37 .field("available_data_sources", &self.available_data_sources)
38 .finish()
39 }
40}
41
42impl GuacamoleClient {
43 pub async fn login(&mut self, username: &str, password: &str) -> Result<AuthResponse> {
48 let url = self.url_unauth("/api/tokens");
49 let response = self
50 .http
51 .post(&url)
52 .form(&[("username", username), ("password", password)])
53 .send()
54 .await?;
55
56 let auth: AuthResponse = Self::parse_response(response, "authentication").await?;
57
58 if let Some(ref token) = auth.auth_token {
59 validate_token(token)?;
60 }
61
62 if let Some(ref ds) = auth.data_source {
63 validate_data_source(ds)?;
64 }
65
66 self.auth_token.clone_from(&auth.auth_token);
67 self.data_source.clone_from(&auth.data_source);
68
69 Ok(auth)
70 }
71
72 pub async fn logout(&mut self) -> Result<()> {
81 let token = self.require_token()?.to_owned();
82 let url = self.url_unauth(&format!("/api/tokens/{token}"));
83 let response = self.http.delete(&url).send().await?;
84 Self::handle_error(response, "logout").await?;
85
86 self.auth_token = None;
87 self.data_source = None;
88
89 Ok(())
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn auth_response_serde_roundtrip() {
99 let auth = AuthResponse {
100 auth_token: Some("ABCDEF".to_string()),
101 username: Some("guacadmin".to_string()),
102 data_source: Some("mysql".to_string()),
103 available_data_sources: Some(vec!["mysql".to_string(), "postgresql".to_string()]),
104 };
105 let json = serde_json::to_string(&auth).unwrap();
106 let deserialized: AuthResponse = serde_json::from_str(&json).unwrap();
107 assert_eq!(auth, deserialized);
108 }
109
110 #[test]
111 fn auth_response_camel_case_keys() {
112 let auth = AuthResponse {
113 auth_token: Some("tok".to_string()),
114 data_source: Some("mysql".to_string()),
115 available_data_sources: Some(vec!["mysql".to_string()]),
116 ..Default::default()
117 };
118 let json = serde_json::to_value(&auth).unwrap();
119 assert!(json.get("authToken").is_some());
120 assert!(json.get("dataSource").is_some());
121 assert!(json.get("availableDataSources").is_some());
122 }
123
124 #[test]
125 fn auth_response_debug_redacts_token() {
126 let auth = AuthResponse {
127 auth_token: Some("super-secret-token".to_string()),
128 username: Some("guacadmin".to_string()),
129 ..Default::default()
130 };
131 let debug_output = format!("{auth:?}");
132 assert!(
133 !debug_output.contains("super-secret-token"),
134 "Debug must not contain the auth token"
135 );
136 assert!(debug_output.contains("<redacted>"));
137 assert!(debug_output.contains("guacadmin"));
138 }
139
140 #[test]
141 fn auth_response_skip_none_fields() {
142 let auth = AuthResponse::default();
143 let json = serde_json::to_value(&auth).unwrap();
144 let obj = json.as_object().unwrap();
145 assert!(obj.is_empty());
146 }
147
148 #[test]
149 fn deserialize_from_api_json() {
150 let json = r#"{
151 "authToken": "168F8D0A2D68247F30B7E2E01187AEE2CF82186D",
152 "username": "guacadmin",
153 "dataSource": "mysql",
154 "availableDataSources": ["mysql", "mysql-shared"]
155 }"#;
156 let auth: AuthResponse = serde_json::from_str(json).unwrap();
157 assert_eq!(
158 auth.auth_token.as_deref(),
159 Some("168F8D0A2D68247F30B7E2E01187AEE2CF82186D")
160 );
161 assert_eq!(auth.username.as_deref(), Some("guacadmin"));
162 assert_eq!(auth.data_source.as_deref(), Some("mysql"));
163 assert_eq!(
164 auth.available_data_sources,
165 Some(vec!["mysql".to_string(), "mysql-shared".to_string()])
166 );
167 }
168
169 #[test]
170 fn auth_response_unknown_fields_ignored() {
171 let json = r#"{"authToken": "tok", "unknownField": 42}"#;
172 let auth: AuthResponse = serde_json::from_str(json).unwrap();
173 assert_eq!(auth.auth_token.as_deref(), Some("tok"));
174 }
175}