Skip to main content

guacamole_client/
user_group.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::client::GuacamoleClient;
6use crate::error::Result;
7use crate::patch::PatchOperation;
8use crate::validation::validate_user_group_id;
9
10/// A Guacamole user group.
11#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13#[non_exhaustive]
14pub struct UserGroup {
15    /// The group identifier.
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub identifier: Option<String>,
18
19    /// Arbitrary group attributes.
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub attributes: Option<HashMap<String, Option<String>>>,
22}
23
24impl GuacamoleClient {
25    /// Lists all user groups in the given data source.
26    pub async fn list_user_groups(
27        &self,
28        data_source: Option<&str>,
29    ) -> Result<HashMap<String, UserGroup>> {
30        let ds = self.resolve_data_source(data_source)?;
31        let url = self.url(&format!("/api/session/data/{ds}/userGroups"))?;
32        let response = self.http.get(&url).send().await?;
33        Self::parse_response(response, "user groups").await
34    }
35
36    /// Retrieves a single user group by its identifier.
37    pub async fn get_user_group(
38        &self,
39        data_source: Option<&str>,
40        group_id: &str,
41    ) -> Result<UserGroup> {
42        validate_user_group_id(group_id)?;
43        let ds = self.resolve_data_source(data_source)?;
44        let url = self.url(&format!("/api/session/data/{ds}/userGroups/{group_id}"))?;
45        let response = self.http.get(&url).send().await?;
46        Self::parse_response(response, &format!("user group {group_id}")).await
47    }
48
49    /// Creates a new user group. Returns `()` on success (204 No Content).
50    pub async fn create_user_group(
51        &self,
52        data_source: Option<&str>,
53        group: &UserGroup,
54    ) -> Result<()> {
55        let ds = self.resolve_data_source(data_source)?;
56        let url = self.url(&format!("/api/session/data/{ds}/userGroups"))?;
57        let response = self.http.post(&url).json(group).send().await?;
58        Self::handle_error(response, "create user group").await?;
59        Ok(())
60    }
61
62    /// Updates an existing user group. Returns `()` on success (204 No Content).
63    pub async fn update_user_group(
64        &self,
65        data_source: Option<&str>,
66        group_id: &str,
67        group: &UserGroup,
68    ) -> Result<()> {
69        validate_user_group_id(group_id)?;
70        let ds = self.resolve_data_source(data_source)?;
71        let url = self.url(&format!("/api/session/data/{ds}/userGroups/{group_id}"))?;
72        let response = self.http.put(&url).json(group).send().await?;
73        Self::handle_error(response, &format!("user group {group_id}")).await?;
74        Ok(())
75    }
76
77    /// Deletes a user group by its identifier. Returns `()` on success (204 No Content).
78    pub async fn delete_user_group(
79        &self,
80        data_source: Option<&str>,
81        group_id: &str,
82    ) -> Result<()> {
83        validate_user_group_id(group_id)?;
84        let ds = self.resolve_data_source(data_source)?;
85        let url = self.url(&format!("/api/session/data/{ds}/userGroups/{group_id}"))?;
86        let response = self.http.delete(&url).send().await?;
87        Self::handle_error(response, &format!("user group {group_id}")).await?;
88        Ok(())
89    }
90
91    /// Updates the member users of a user group using JSON Patch operations.
92    pub async fn update_user_group_member_users(
93        &self,
94        data_source: Option<&str>,
95        group_id: &str,
96        patches: &[PatchOperation],
97    ) -> Result<()> {
98        validate_user_group_id(group_id)?;
99        let ds = self.resolve_data_source(data_source)?;
100        let url = self.url(&format!(
101            "/api/session/data/{ds}/userGroups/{group_id}/memberUsers"
102        ))?;
103        let response = self.http.patch(&url).json(patches).send().await?;
104        Self::handle_error(response, &format!("user group {group_id} member users")).await?;
105        Ok(())
106    }
107
108    /// Updates the member groups of a user group using JSON Patch operations.
109    pub async fn update_user_group_member_groups(
110        &self,
111        data_source: Option<&str>,
112        group_id: &str,
113        patches: &[PatchOperation],
114    ) -> Result<()> {
115        validate_user_group_id(group_id)?;
116        let ds = self.resolve_data_source(data_source)?;
117        let url = self.url(&format!(
118            "/api/session/data/{ds}/userGroups/{group_id}/memberUserGroups"
119        ))?;
120        let response = self.http.patch(&url).json(patches).send().await?;
121        Self::handle_error(response, &format!("user group {group_id} member groups")).await?;
122        Ok(())
123    }
124
125    /// Updates the parent groups of a user group using JSON Patch operations.
126    pub async fn update_user_group_parent_groups(
127        &self,
128        data_source: Option<&str>,
129        group_id: &str,
130        patches: &[PatchOperation],
131    ) -> Result<()> {
132        validate_user_group_id(group_id)?;
133        let ds = self.resolve_data_source(data_source)?;
134        let url = self.url(&format!(
135            "/api/session/data/{ds}/userGroups/{group_id}/userGroups"
136        ))?;
137        let response = self.http.patch(&url).json(patches).send().await?;
138        Self::handle_error(response, &format!("user group {group_id} parent groups")).await?;
139        Ok(())
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn user_group_serde_roundtrip() {
149        let group = UserGroup {
150            identifier: Some("admins".to_string()),
151            attributes: Some(HashMap::from([(
152                "disabled".to_string(),
153                Some("false".to_string()),
154            )])),
155        };
156        let json = serde_json::to_string(&group).unwrap();
157        let deserialized: UserGroup = serde_json::from_str(&json).unwrap();
158        assert_eq!(group, deserialized);
159    }
160
161    #[test]
162    fn user_group_skip_none_fields() {
163        let group = UserGroup::default();
164        let json = serde_json::to_value(&group).unwrap();
165        let obj = json.as_object().unwrap();
166        assert!(obj.is_empty());
167    }
168
169    #[test]
170    fn deserialize_user_group_from_api_json() {
171        let json = r#"{
172            "identifier": "developers",
173            "attributes": {
174                "disabled": "",
175                "expired": ""
176            }
177        }"#;
178        let group: UserGroup = serde_json::from_str(json).unwrap();
179        assert_eq!(group.identifier.as_deref(), Some("developers"));
180        assert!(group.attributes.is_some());
181    }
182
183    #[test]
184    fn deserialize_user_group_unknown_fields_ignored() {
185        let json = r#"{"identifier": "admins", "unknownField": 42}"#;
186        let group: UserGroup = serde_json::from_str(json).unwrap();
187        assert_eq!(group.identifier.as_deref(), Some("admins"));
188    }
189
190    #[test]
191    fn user_group_null_attribute_values() {
192        let json = r#"{
193            "identifier": "team",
194            "attributes": {
195                "disabled": null,
196                "notes": "hello"
197            }
198        }"#;
199        let group: UserGroup = serde_json::from_str(json).unwrap();
200        let attrs = group.attributes.unwrap();
201        assert_eq!(attrs.get("disabled"), Some(&None));
202        assert_eq!(attrs.get("notes"), Some(&Some("hello".to_string())));
203    }
204}