1use std::collections::HashMap;
2use std::fmt;
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use crate::client::GuacamoleClient;
8use crate::error::Result;
9use crate::history::HistoryEntry;
10use crate::patch::PatchOperation;
11use crate::validation::validate_username;
12
13#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16#[non_exhaustive]
17pub struct User {
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub username: Option<String>,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub password: Option<String>,
25
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub attributes: Option<HashMap<String, Option<String>>>,
29}
30
31impl fmt::Debug for User {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 f.debug_struct("User")
34 .field("username", &self.username)
35 .field("password", &"<redacted>")
36 .field("attributes", &self.attributes)
37 .finish()
38 }
39}
40
41#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44#[non_exhaustive]
45pub struct PasswordChange {
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub old_password: Option<String>,
49
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub new_password: Option<String>,
53}
54
55impl fmt::Debug for PasswordChange {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 f.debug_struct("PasswordChange")
58 .field("old_password", &"<redacted>")
59 .field("new_password", &"<redacted>")
60 .finish()
61 }
62}
63
64#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69#[non_exhaustive]
70pub struct UserPermissions {
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub connection_permissions: Option<HashMap<String, Vec<String>>>,
74
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub connection_group_permissions: Option<HashMap<String, Vec<String>>>,
78
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub sharing_profile_permissions: Option<HashMap<String, Vec<String>>>,
82
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub system_permissions: Option<Vec<String>>,
86
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub user_permissions: Option<HashMap<String, Vec<String>>>,
90
91 #[serde(flatten)]
93 pub extra: HashMap<String, Value>,
94}
95
96impl GuacamoleClient {
97 pub async fn list_users(
99 &self,
100 data_source: Option<&str>,
101 ) -> Result<HashMap<String, User>> {
102 let ds = self.resolve_data_source(data_source)?;
103 let url = self.url(&format!("/api/session/data/{ds}/users"))?;
104 let response = self.http.get(&url).send().await?;
105 Self::parse_response(response, "users").await
106 }
107
108 pub async fn get_user(
110 &self,
111 data_source: Option<&str>,
112 username: &str,
113 ) -> Result<User> {
114 validate_username(username)?;
115 let ds = self.resolve_data_source(data_source)?;
116 let url = self.url(&format!("/api/session/data/{ds}/users/{username}"))?;
117 let response = self.http.get(&url).send().await?;
118 Self::parse_response(response, &format!("user {username}")).await
119 }
120
121 pub async fn get_self(
123 &self,
124 data_source: Option<&str>,
125 ) -> Result<User> {
126 let ds = self.resolve_data_source(data_source)?;
127 let url = self.url(&format!("/api/session/data/{ds}/self"))?;
128 let response = self.http.get(&url).send().await?;
129 Self::parse_response(response, "self").await
130 }
131
132 pub async fn create_user(
134 &self,
135 data_source: Option<&str>,
136 user: &User,
137 ) -> Result<()> {
138 let ds = self.resolve_data_source(data_source)?;
139 let url = self.url(&format!("/api/session/data/{ds}/users"))?;
140 let response = self.http.post(&url).json(user).send().await?;
141 Self::handle_error(response, "create user").await?;
142 Ok(())
143 }
144
145 pub async fn update_user(
147 &self,
148 data_source: Option<&str>,
149 username: &str,
150 user: &User,
151 ) -> Result<()> {
152 validate_username(username)?;
153 let ds = self.resolve_data_source(data_source)?;
154 let url = self.url(&format!("/api/session/data/{ds}/users/{username}"))?;
155 let response = self.http.put(&url).json(user).send().await?;
156 Self::handle_error(response, &format!("user {username}")).await?;
157 Ok(())
158 }
159
160 pub async fn delete_user(
162 &self,
163 data_source: Option<&str>,
164 username: &str,
165 ) -> Result<()> {
166 validate_username(username)?;
167 let ds = self.resolve_data_source(data_source)?;
168 let url = self.url(&format!("/api/session/data/{ds}/users/{username}"))?;
169 let response = self.http.delete(&url).send().await?;
170 Self::handle_error(response, &format!("user {username}")).await?;
171 Ok(())
172 }
173
174 pub async fn update_user_password(
176 &self,
177 data_source: Option<&str>,
178 username: &str,
179 password_change: &PasswordChange,
180 ) -> Result<()> {
181 validate_username(username)?;
182 let ds = self.resolve_data_source(data_source)?;
183 let url = self.url(&format!(
184 "/api/session/data/{ds}/users/{username}/password"
185 ))?;
186 let response = self.http.put(&url).json(password_change).send().await?;
187 Self::handle_error(response, &format!("user {username} password")).await?;
188 Ok(())
189 }
190
191 pub async fn get_user_permissions(
193 &self,
194 data_source: Option<&str>,
195 username: &str,
196 ) -> Result<UserPermissions> {
197 validate_username(username)?;
198 let ds = self.resolve_data_source(data_source)?;
199 let url = self.url(&format!(
200 "/api/session/data/{ds}/users/{username}/permissions"
201 ))?;
202 let response = self.http.get(&url).send().await?;
203 Self::parse_response(response, &format!("user {username} permissions")).await
204 }
205
206 pub async fn get_user_effective_permissions(
208 &self,
209 data_source: Option<&str>,
210 username: &str,
211 ) -> Result<UserPermissions> {
212 validate_username(username)?;
213 let ds = self.resolve_data_source(data_source)?;
214 let url = self.url(&format!(
215 "/api/session/data/{ds}/users/{username}/effectivePermissions"
216 ))?;
217 let response = self.http.get(&url).send().await?;
218 Self::parse_response(response, &format!("user {username} effective permissions"))
219 .await
220 }
221
222 pub async fn get_user_groups(
224 &self,
225 data_source: Option<&str>,
226 username: &str,
227 ) -> Result<Vec<String>> {
228 validate_username(username)?;
229 let ds = self.resolve_data_source(data_source)?;
230 let url = self.url(&format!(
231 "/api/session/data/{ds}/users/{username}/userGroups"
232 ))?;
233 let response = self.http.get(&url).send().await?;
234 Self::parse_response(response, &format!("user {username} groups")).await
235 }
236
237 pub async fn get_user_history(
239 &self,
240 data_source: Option<&str>,
241 username: &str,
242 ) -> Result<Vec<HistoryEntry>> {
243 validate_username(username)?;
244 let ds = self.resolve_data_source(data_source)?;
245 let url = self.url(&format!(
246 "/api/session/data/{ds}/users/{username}/history"
247 ))?;
248 let response = self.http.get(&url).send().await?;
249 Self::parse_response(response, &format!("user {username} history")).await
250 }
251
252 pub async fn update_user_groups(
254 &self,
255 data_source: Option<&str>,
256 username: &str,
257 patches: &[PatchOperation],
258 ) -> Result<()> {
259 validate_username(username)?;
260 let ds = self.resolve_data_source(data_source)?;
261 let url = self.url(&format!(
262 "/api/session/data/{ds}/users/{username}/userGroups"
263 ))?;
264 let response = self.http.patch(&url).json(patches).send().await?;
265 Self::handle_error(response, &format!("user {username} groups")).await?;
266 Ok(())
267 }
268
269 pub async fn update_user_permissions(
271 &self,
272 data_source: Option<&str>,
273 username: &str,
274 patches: &[PatchOperation],
275 ) -> Result<()> {
276 validate_username(username)?;
277 let ds = self.resolve_data_source(data_source)?;
278 let url = self.url(&format!(
279 "/api/session/data/{ds}/users/{username}/permissions"
280 ))?;
281 let response = self.http.patch(&url).json(patches).send().await?;
282 Self::handle_error(response, &format!("user {username} permissions")).await?;
283 Ok(())
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn user_serde_roundtrip() {
293 let user = User {
294 username: Some("guacadmin".to_string()),
295 password: Some("secret".to_string()),
296 attributes: Some(HashMap::from([(
297 "disabled".to_string(),
298 Some("false".to_string()),
299 )])),
300 };
301 let json = serde_json::to_string(&user).unwrap();
302 let deserialized: User = serde_json::from_str(&json).unwrap();
303 assert_eq!(user, deserialized);
304 }
305
306 #[test]
307 fn user_debug_redacts_password() {
308 let user = User {
309 username: Some("admin".to_string()),
310 password: Some("hunter2".to_string()),
311 ..Default::default()
312 };
313 let debug = format!("{user:?}");
314 assert!(!debug.contains("hunter2"), "Debug must not leak password");
315 assert!(debug.contains("<redacted>"));
316 assert!(debug.contains("admin"));
317 }
318
319 #[test]
320 fn user_skip_none_fields() {
321 let user = User::default();
322 let json = serde_json::to_value(&user).unwrap();
323 let obj = json.as_object().unwrap();
324 assert!(obj.is_empty());
325 }
326
327 #[test]
328 fn password_change_debug_redacts_both() {
329 let pc = PasswordChange {
330 old_password: Some("old-secret".to_string()),
331 new_password: Some("new-secret".to_string()),
332 };
333 let debug = format!("{pc:?}");
334 assert!(!debug.contains("old-secret"));
335 assert!(!debug.contains("new-secret"));
336 assert!(debug.contains("<redacted>"));
337 }
338
339 #[test]
340 fn password_change_serde_roundtrip() {
341 let pc = PasswordChange {
342 old_password: Some("old".to_string()),
343 new_password: Some("new".to_string()),
344 };
345 let json = serde_json::to_string(&pc).unwrap();
346 let deserialized: PasswordChange = serde_json::from_str(&json).unwrap();
347 assert_eq!(pc, deserialized);
348 }
349
350 #[test]
351 fn password_change_camel_case() {
352 let pc = PasswordChange {
353 old_password: Some("old".to_string()),
354 new_password: Some("new".to_string()),
355 };
356 let json = serde_json::to_value(&pc).unwrap();
357 assert!(json.get("oldPassword").is_some());
358 assert!(json.get("newPassword").is_some());
359 }
360
361 #[test]
362 fn user_permissions_serde_roundtrip() {
363 let json_str = r#"{
364 "connectionPermissions": {"1": ["READ"], "2": ["READ", "UPDATE"]},
365 "connectionGroupPermissions": {},
366 "sharingProfilePermissions": {"3": ["READ"]},
367 "systemPermissions": ["ADMINISTER"],
368 "userPermissions": {}
369 }"#;
370 let perms: UserPermissions = serde_json::from_str(json_str).unwrap();
371 assert_eq!(
372 perms.system_permissions,
373 Some(vec!["ADMINISTER".to_string()])
374 );
375 assert!(perms.connection_permissions.is_some());
376
377 let conn_perms = perms.connection_permissions.as_ref().unwrap();
378 assert_eq!(conn_perms.get("1"), Some(&vec!["READ".to_string()]));
379 assert_eq!(
380 conn_perms.get("2"),
381 Some(&vec!["READ".to_string(), "UPDATE".to_string()])
382 );
383
384 let json_roundtrip = serde_json::to_string(&perms).unwrap();
385 let deserialized: UserPermissions = serde_json::from_str(&json_roundtrip).unwrap();
386 assert_eq!(perms, deserialized);
387 }
388
389 #[test]
390 fn user_permissions_skip_none_fields() {
391 let perms = UserPermissions::default();
392 let json = serde_json::to_value(&perms).unwrap();
393 let obj = json.as_object().unwrap();
394 assert!(obj.is_empty());
395 }
396
397 #[test]
398 fn deserialize_user_from_api_json() {
399 let json = r#"{
400 "username": "student",
401 "attributes": {
402 "disabled": "",
403 "expired": "",
404 "access-window-start": "",
405 "access-window-end": ""
406 }
407 }"#;
408 let user: User = serde_json::from_str(json).unwrap();
409 assert_eq!(user.username.as_deref(), Some("student"));
410 assert!(user.password.is_none());
411 assert!(user.attributes.is_some());
412 }
413
414 #[test]
415 fn deserialize_user_unknown_fields_ignored() {
416 let json = r#"{"username": "test", "unknownField": 42}"#;
417 let user: User = serde_json::from_str(json).unwrap();
418 assert_eq!(user.username.as_deref(), Some("test"));
419 }
420
421 #[test]
422 fn user_permissions_extra_captures_unknown_fields() {
423 let json = r#"{
424 "connectionPermissions": {"1": ["READ"]},
425 "customPermissionType": {"x": ["ADMIN"]}
426 }"#;
427 let perms: UserPermissions = serde_json::from_str(json).unwrap();
428 assert!(perms.extra.contains_key("customPermissionType"));
429 let custom = perms.extra["customPermissionType"].as_object().unwrap();
430 assert_eq!(custom["x"], serde_json::json!(["ADMIN"]));
431 }
432
433 #[test]
434 fn user_permissions_extra_survives_roundtrip() {
435 let json = r#"{
436 "connectionPermissions": {"1": ["READ"]},
437 "futurePermissions": {"a": ["WRITE"]}
438 }"#;
439 let perms: UserPermissions = serde_json::from_str(json).unwrap();
440 let serialized = serde_json::to_string(&perms).unwrap();
441 let deserialized: UserPermissions = serde_json::from_str(&serialized).unwrap();
442 assert_eq!(perms, deserialized);
443 assert!(deserialized.extra.contains_key("futurePermissions"));
444 }
445
446 #[test]
447 fn user_null_attribute_values() {
448 let json = r#"{
449 "username": "admin",
450 "attributes": {
451 "disabled": "false",
452 "expired": null
453 }
454 }"#;
455 let user: User = serde_json::from_str(json).unwrap();
456 let attrs = user.attributes.as_ref().unwrap();
457 assert_eq!(attrs.get("disabled"), Some(&Some("false".to_string())));
458 assert_eq!(attrs.get("expired"), Some(&None));
459 }
460
461 #[test]
462 fn password_change_skip_none_fields() {
463 let pc = PasswordChange::default();
464 let json = serde_json::to_value(&pc).unwrap();
465 let obj = json.as_object().unwrap();
466 assert!(obj.is_empty());
467 }
468
469 #[test]
470 fn password_change_unknown_fields_ignored() {
471 let json = r#"{"oldPassword": "old", "unknownField": true}"#;
472 let pc: PasswordChange = serde_json::from_str(json).unwrap();
473 assert_eq!(pc.old_password.as_deref(), Some("old"));
474 }
475}