Skip to main content

couchbase_core/mgmtx/
mgmt_user.rs

1/*
2 *
3 *  * Copyright (c) 2025 Couchbase, Inc.
4 *  *
5 *  * Licensed under the Apache License, Version 2.0 (the "License");
6 *  * you may not use this file except in compliance with the License.
7 *  * You may obtain a copy of the License at
8 *  *
9 *  *    http://www.apache.org/licenses/LICENSE-2.0
10 *  *
11 *  * Unless required by applicable law or agreed to in writing, software
12 *  * distributed under the License is distributed on an "AS IS" BASIS,
13 *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  * See the License for the specific language governing permissions and
15 *  * limitations under the License.
16 *
17 */
18
19use crate::httpx::client::Client;
20use crate::mgmtx::error;
21use crate::mgmtx::mgmt::{parse_response_json, Management};
22use crate::mgmtx::options::{
23    ChangePasswordOptions, DeleteGroupOptions, DeleteUserOptions, GetAllGroupsOptions,
24    GetAllUsersOptions, GetGroupOptions, GetRolesOptions, GetUserOptions, UpsertGroupOptions,
25    UpsertUserOptions,
26};
27use crate::mgmtx::user::{Group, Role, RoleAndDescription, UserAndMetadata};
28use crate::mgmtx::user_json::{GroupJson, RoleAndDescriptionJson, UserAndMetadataJson};
29use crate::tracingcomponent::{BeginDispatchFields, EndDispatchFields};
30use crate::util::get_host_port_tuple_from_uri;
31use bytes::Bytes;
32use http::Method;
33
34impl<C: Client> Management<C> {
35    pub async fn get_user(&self, opts: &GetUserOptions<'_>) -> error::Result<UserAndMetadata> {
36        let method = Method::GET;
37        let path = format!(
38            "settings/rbac/users/{}/{}",
39            urlencoding::encode(opts.auth_domain),
40            urlencoding::encode(opts.username)
41        )
42        .to_string();
43
44        let peer_addr = get_host_port_tuple_from_uri(&self.endpoint).unwrap_or_default();
45        let canonical_addr =
46            get_host_port_tuple_from_uri(&self.canonical_endpoint).unwrap_or_default();
47        let resp = self
48            .tracing
49            .orchestrate_dispatch_span(
50                BeginDispatchFields::new(
51                    (&peer_addr.0, &peer_addr.1),
52                    (&canonical_addr.0, &canonical_addr.1),
53                    None,
54                ),
55                self.execute(
56                    method.clone(),
57                    &path,
58                    "",
59                    opts.on_behalf_of_info.cloned(),
60                    None,
61                    None,
62                ),
63                |_| EndDispatchFields::new(None, None),
64            )
65            .await?;
66
67        if resp.status() != 200 {
68            return Err(Self::decode_common_error(method, path, "get_user", resp).await);
69        }
70
71        let user_json: UserAndMetadataJson = parse_response_json(resp).await?;
72
73        user_json.try_into()
74    }
75
76    pub async fn get_all_users(
77        &self,
78        opts: &GetAllUsersOptions<'_>,
79    ) -> error::Result<Vec<UserAndMetadata>> {
80        let method = Method::GET;
81        let path = format!(
82            "settings/rbac/users/{}",
83            urlencoding::encode(opts.auth_domain),
84        )
85        .to_string();
86
87        let peer_addr = get_host_port_tuple_from_uri(&self.endpoint).unwrap_or_default();
88        let canonical_addr =
89            get_host_port_tuple_from_uri(&self.canonical_endpoint).unwrap_or_default();
90        let resp = self
91            .tracing
92            .orchestrate_dispatch_span(
93                BeginDispatchFields::new(
94                    (&peer_addr.0, &peer_addr.1),
95                    (&canonical_addr.0, &canonical_addr.1),
96                    None,
97                ),
98                self.execute(
99                    method.clone(),
100                    &path,
101                    "",
102                    opts.on_behalf_of_info.cloned(),
103                    None,
104                    None,
105                ),
106                |_| EndDispatchFields::new(None, None),
107            )
108            .await?;
109
110        if resp.status() != 200 {
111            return Err(Self::decode_common_error(method, path, "get_all_users", resp).await);
112        }
113
114        let users_json: Vec<UserAndMetadataJson> = parse_response_json(resp).await?;
115
116        users_json
117            .into_iter()
118            .map(UserAndMetadata::try_from)
119            .collect()
120    }
121
122    pub async fn upsert_user(&self, opts: &UpsertUserOptions<'_>) -> error::Result<()> {
123        let body = {
124            let mut form = url::form_urlencoded::Serializer::new(String::new());
125            form.append_pair("name", opts.user.display_name.as_str())
126                .append_pair(
127                    "roles",
128                    &opts
129                        .user
130                        .roles
131                        .iter()
132                        .map(Self::build_role)
133                        .collect::<Vec<String>>()
134                        .join(","),
135                );
136
137            if let Some(password) = &opts.user.password {
138                form.append_pair("password", password.as_str());
139            }
140
141            if !opts.user.groups.is_empty() {
142                form.append_pair("groups", &opts.user.groups.join(","));
143            }
144
145            Bytes::from(form.finish())
146        };
147
148        let method = Method::PUT;
149        let path = format!(
150            "settings/rbac/users/{}/{}",
151            urlencoding::encode(opts.auth_domain),
152            urlencoding::encode(&opts.user.username),
153        );
154
155        let peer_addr = get_host_port_tuple_from_uri(&self.endpoint).unwrap_or_default();
156        let canonical_addr =
157            get_host_port_tuple_from_uri(&self.canonical_endpoint).unwrap_or_default();
158        let resp = self
159            .tracing
160            .orchestrate_dispatch_span(
161                BeginDispatchFields::new(
162                    (&peer_addr.0, &peer_addr.1),
163                    (&canonical_addr.0, &canonical_addr.1),
164                    None,
165                ),
166                self.execute(
167                    method.clone(),
168                    &path,
169                    "application/x-www-form-urlencoded",
170                    opts.on_behalf_of_info.cloned(),
171                    None,
172                    Some(body),
173                ),
174                |_| EndDispatchFields::new(None, None),
175            )
176            .await?;
177
178        if resp.status().as_u16() < 200 || resp.status().as_u16() >= 300 {
179            return Err(Self::decode_common_error(method, path, "upsert_user", resp).await);
180        }
181
182        Ok(())
183    }
184
185    pub async fn delete_user(&self, opts: &DeleteUserOptions<'_>) -> error::Result<()> {
186        let method = Method::DELETE;
187        let path = format!(
188            "settings/rbac/users/{}/{}",
189            urlencoding::encode(opts.auth_domain),
190            urlencoding::encode(opts.username),
191        )
192        .to_string();
193
194        let peer_addr = get_host_port_tuple_from_uri(&self.endpoint).unwrap_or_default();
195        let canonical_addr =
196            get_host_port_tuple_from_uri(&self.canonical_endpoint).unwrap_or_default();
197        let resp = self
198            .tracing
199            .orchestrate_dispatch_span(
200                BeginDispatchFields::new(
201                    (&peer_addr.0, &peer_addr.1),
202                    (&canonical_addr.0, &canonical_addr.1),
203                    None,
204                ),
205                self.execute(
206                    method.clone(),
207                    &path,
208                    "",
209                    opts.on_behalf_of_info.cloned(),
210                    None,
211                    None,
212                ),
213                |_| EndDispatchFields::new(None, None),
214            )
215            .await?;
216
217        if resp.status() != 200 {
218            return Err(Self::decode_common_error(method, path, "delete_user", resp).await);
219        }
220
221        Ok(())
222    }
223
224    pub async fn get_roles(
225        &self,
226        opts: &GetRolesOptions<'_>,
227    ) -> error::Result<Vec<RoleAndDescription>> {
228        let method = Method::GET;
229
230        let path = if let Some(p) = opts.permission {
231            format!("settings/rbac/roles?permission={}", urlencoding::encode(p))
232        } else {
233            "settings/rbac/roles".to_string()
234        };
235
236        let peer_addr = get_host_port_tuple_from_uri(&self.endpoint).unwrap_or_default();
237        let canonical_addr =
238            get_host_port_tuple_from_uri(&self.canonical_endpoint).unwrap_or_default();
239        let resp = self
240            .tracing
241            .orchestrate_dispatch_span(
242                BeginDispatchFields::new(
243                    (&peer_addr.0, &peer_addr.1),
244                    (&canonical_addr.0, &canonical_addr.1),
245                    None,
246                ),
247                self.execute(
248                    method.clone(),
249                    &path,
250                    "",
251                    opts.on_behalf_of_info.cloned(),
252                    None,
253                    None,
254                ),
255                |_| EndDispatchFields::new(None, None),
256            )
257            .await?;
258
259        if resp.status() != 200 {
260            return Err(Self::decode_common_error(method, path, "get_roles", resp).await);
261        }
262
263        let roles_json: Vec<RoleAndDescriptionJson> = parse_response_json(resp).await?;
264
265        Ok(roles_json
266            .into_iter()
267            .map(RoleAndDescription::from)
268            .collect())
269    }
270
271    pub async fn get_group(&self, opts: &GetGroupOptions<'_>) -> error::Result<Group> {
272        let method = Method::GET;
273        let path = format!("settings/rbac/groups/{}", opts.group_name).to_string();
274
275        let peer_addr = get_host_port_tuple_from_uri(&self.endpoint).unwrap_or_default();
276        let canonical_addr =
277            get_host_port_tuple_from_uri(&self.canonical_endpoint).unwrap_or_default();
278        let resp = self
279            .tracing
280            .orchestrate_dispatch_span(
281                BeginDispatchFields::new(
282                    (&peer_addr.0, &peer_addr.1),
283                    (&canonical_addr.0, &canonical_addr.1),
284                    None,
285                ),
286                self.execute(
287                    method.clone(),
288                    &path,
289                    "",
290                    opts.on_behalf_of_info.cloned(),
291                    None,
292                    None,
293                ),
294                |_| EndDispatchFields::new(None, None),
295            )
296            .await?;
297
298        if resp.status() != 200 {
299            return Err(Self::decode_common_error(method, path, "get_group", resp).await);
300        }
301
302        let group_json: GroupJson = parse_response_json(resp).await?;
303
304        Ok(group_json.into())
305    }
306
307    pub async fn get_all_groups(
308        &self,
309        opts: &GetAllGroupsOptions<'_>,
310    ) -> error::Result<Vec<Group>> {
311        let method = Method::GET;
312        let path = "settings/rbac/groups".to_string();
313
314        let peer_addr = get_host_port_tuple_from_uri(&self.endpoint).unwrap_or_default();
315        let canonical_addr =
316            get_host_port_tuple_from_uri(&self.canonical_endpoint).unwrap_or_default();
317        let resp = self
318            .tracing
319            .orchestrate_dispatch_span(
320                BeginDispatchFields::new(
321                    (&peer_addr.0, &peer_addr.1),
322                    (&canonical_addr.0, &canonical_addr.1),
323                    None,
324                ),
325                self.execute(
326                    method.clone(),
327                    &path,
328                    "",
329                    opts.on_behalf_of_info.cloned(),
330                    None,
331                    None,
332                ),
333                |_| EndDispatchFields::new(None, None),
334            )
335            .await?;
336
337        if resp.status() != 200 {
338            return Err(Self::decode_common_error(method, path, "get_all_groups", resp).await);
339        }
340
341        let groups_json: Vec<GroupJson> = parse_response_json(resp).await?;
342
343        Ok(groups_json.into_iter().map(Group::from).collect())
344    }
345
346    pub async fn upsert_group(&self, opts: &UpsertGroupOptions<'_>) -> error::Result<()> {
347        let method = Method::PUT;
348        let path = format!(
349            "settings/rbac/groups/{}",
350            urlencoding::encode(&opts.group.name),
351        )
352        .to_string();
353
354        let body = {
355            let mut form = url::form_urlencoded::Serializer::new(String::new());
356            form.append_pair(
357                "roles",
358                &opts
359                    .group
360                    .roles
361                    .iter()
362                    .map(Self::build_role)
363                    .collect::<Vec<String>>()
364                    .join(","),
365            );
366
367            if let Some(desc) = &opts.group.description {
368                form.append_pair("description", desc.as_str());
369            }
370
371            if let Some(group_ref) = &opts.group.ldap_group_reference {
372                form.append_pair("ldap_group_ref", group_ref.as_str());
373            }
374
375            Bytes::from(form.finish())
376        };
377
378        let peer_addr = get_host_port_tuple_from_uri(&self.endpoint).unwrap_or_default();
379        let canonical_addr =
380            get_host_port_tuple_from_uri(&self.canonical_endpoint).unwrap_or_default();
381        let resp = self
382            .tracing
383            .orchestrate_dispatch_span(
384                BeginDispatchFields::new(
385                    (&peer_addr.0, &peer_addr.1),
386                    (&canonical_addr.0, &canonical_addr.1),
387                    None,
388                ),
389                self.execute(
390                    method.clone(),
391                    &path,
392                    "application/x-www-form-urlencoded",
393                    opts.on_behalf_of_info.cloned(),
394                    None,
395                    Some(body),
396                ),
397                |_| EndDispatchFields::new(None, None),
398            )
399            .await?;
400
401        if resp.status().as_u16() < 200 || resp.status().as_u16() >= 300 {
402            return Err(Self::decode_common_error(method, path, "upsert_group", resp).await);
403        }
404
405        Ok(())
406    }
407
408    pub async fn delete_group(&self, opts: &DeleteGroupOptions<'_>) -> error::Result<()> {
409        let method = Method::DELETE;
410        let path = format!(
411            "settings/rbac/groups/{}",
412            urlencoding::encode(opts.group_name),
413        )
414        .to_string();
415
416        let peer_addr = get_host_port_tuple_from_uri(&self.endpoint).unwrap_or_default();
417        let canonical_addr =
418            get_host_port_tuple_from_uri(&self.canonical_endpoint).unwrap_or_default();
419        let resp = self
420            .tracing
421            .orchestrate_dispatch_span(
422                BeginDispatchFields::new(
423                    (&peer_addr.0, &peer_addr.1),
424                    (&canonical_addr.0, &canonical_addr.1),
425                    None,
426                ),
427                self.execute(
428                    method.clone(),
429                    &path,
430                    "",
431                    opts.on_behalf_of_info.cloned(),
432                    None,
433                    None,
434                ),
435                |_| EndDispatchFields::new(None, None),
436            )
437            .await?;
438
439        if resp.status() != 200 {
440            return Err(Self::decode_common_error(method, path, "delete_group", resp).await);
441        }
442
443        Ok(())
444    }
445
446    pub async fn change_password(&self, opts: &ChangePasswordOptions<'_>) -> error::Result<()> {
447        let method = Method::POST;
448        let path = "controller/changePassword".to_string();
449
450        let body = {
451            let mut form = url::form_urlencoded::Serializer::new(String::new());
452            form.append_pair("password", opts.new_password);
453
454            Bytes::from(form.finish())
455        };
456
457        let peer_addr = get_host_port_tuple_from_uri(&self.endpoint).unwrap_or_default();
458        let canonical_addr =
459            get_host_port_tuple_from_uri(&self.canonical_endpoint).unwrap_or_default();
460        let resp = self
461            .tracing
462            .orchestrate_dispatch_span(
463                BeginDispatchFields::new(
464                    (&peer_addr.0, &peer_addr.1),
465                    (&canonical_addr.0, &canonical_addr.1),
466                    None,
467                ),
468                self.execute(
469                    method.clone(),
470                    &path,
471                    "application/x-www-form-urlencoded",
472                    opts.on_behalf_of_info.cloned(),
473                    None,
474                    Some(body),
475                ),
476                |_| EndDispatchFields::new(None, None),
477            )
478            .await?;
479
480        if resp.status() != 200 {
481            return Err(Self::decode_common_error(method, path, "change_password", resp).await);
482        }
483
484        Ok(())
485    }
486
487    fn build_role(role: &Role) -> String {
488        let mut role_str = role.name.clone();
489
490        if let Some(bucket) = &role.bucket {
491            role_str = format!("{role_str}[{bucket}");
492
493            if let Some(scope) = &role.scope {
494                role_str = format!("{role_str}:{scope}");
495            }
496            if let Some(collection) = &role.collection {
497                role_str = format!("{role_str}:{collection}");
498            }
499
500            role_str = format!("{role_str}]");
501        }
502
503        role_str
504    }
505}