manta_server/service/
group.rs

1//! HSM group CRUD operations and membership management.
2
3use manta_backend_dispatcher::error::Error;
4use manta_backend_dispatcher::interfaces::hsm::group::GroupTrait;
5use manta_backend_dispatcher::types::Group;
6
7use crate::server::common;
8use crate::server::common::app_context::InfraContext;
9use crate::server::common::authorization::get_groups_names_available;
10pub use manta_shared::shared::params::group::GetGroupParams;
11
12/// Return the list of HSM group names this token can access.
13///
14/// Thin wrapper around `backend.get_group_name_available` — backs the
15/// `GET /api/v1/groups/available` endpoint.
16pub async fn get_available_group_names(
17  infra: &InfraContext<'_>,
18  token: &str,
19) -> Result<Vec<String>, Error> {
20  infra.backend.get_group_name_available(token).await
21}
22
23/// Return every HSM group in the system, regardless of access.
24///
25/// Used by CLI commands that need to display the full set of group
26/// names (e.g. when prompting the operator to pick one to set as
27/// default). Backs the `GET /api/v1/groups/all` endpoint.
28pub async fn get_all_groups(
29  infra: &InfraContext<'_>,
30  token: &str,
31) -> Result<Vec<Group>, Error> {
32  infra.backend.get_all_groups(token).await
33}
34
35/// Validate that `group_name` is in the set this token can access.
36///
37/// Used by handlers that perform privileged HSM-group operations and
38/// need a server-side authorization check before delegating to the
39/// service layer. Returns `Error::BadRequest` with a usable error
40/// message when the group is not accessible.
41pub async fn validate_hsm_group_access(
42  infra: &InfraContext<'_>,
43  token: &str,
44  group_name: &str,
45) -> Result<(), Error> {
46  let accessible = infra.backend.get_group_name_available(token).await?;
47  if !accessible.iter().any(|name| name == group_name) {
48    let mut accessible = accessible;
49    accessible.sort();
50    return Err(Error::BadRequest(format!(
51      "Can't access HSM group '{}'.\nPlease choose one from the list below:\n{}",
52      group_name,
53      accessible.join(", ")
54    )));
55  }
56  Ok(())
57}
58
59/// Fetch HSM groups from the backend.
60pub async fn get_groups(
61  infra: &InfraContext<'_>,
62  token: &str,
63  params: &GetGroupParams,
64) -> Result<Vec<Group>, Error> {
65  let target_hsm_group_vec = get_groups_names_available(
66    infra.backend,
67    token,
68    params.group_name.as_deref(),
69    params.settings_hsm_group_name.as_deref(),
70  )
71  .await?;
72
73  infra
74    .backend
75    .get_groups(token, Some(&target_hsm_group_vec))
76    .await
77}
78
79/// Validate that deleting a group will not orphan any nodes.
80pub async fn validate_group_deletion(
81  infra: &InfraContext<'_>,
82  token: &str,
83  label: &str,
84) -> Result<(), Error> {
85  let xname_vec = infra
86    .backend
87    .get_member_vec_from_group_name_vec(token, &[label.to_string()])
88    .await?;
89
90  let xname_vec: Vec<&str> = xname_vec.iter().map(String::as_str).collect();
91
92  let mut xname_map = infra
93    .backend
94    .get_group_map_and_filter_by_group_vec(token, &xname_vec)
95    .await?;
96
97  xname_map.retain(|_xname, group_name_vec| {
98    group_name_vec.len() == 1
99      && group_name_vec.first().is_some_and(|name| name == label)
100  });
101
102  let mut members_orphan_if_group_deleted: Vec<String> =
103    xname_map.into_keys().collect();
104  members_orphan_if_group_deleted.sort();
105
106  if !members_orphan_if_group_deleted.is_empty() {
107    return Err(Error::Conflict(format!(
108      "The hosts below will become orphan if group '{}' gets deleted: {}",
109      label,
110      members_orphan_if_group_deleted.join(", ")
111    )));
112  }
113
114  Ok(())
115}
116
117/// Delete an HSM group by label.
118pub async fn delete_group(
119  infra: &InfraContext<'_>,
120  token: &str,
121  label: &str,
122  force: bool,
123) -> Result<(), Error> {
124  if !force {
125    validate_group_deletion(infra, token, label).await?;
126  }
127  infra.backend.delete_group(token, label).await.map(|_| ())
128}
129
130/// Create an HSM group via the backend.
131pub async fn create_group(
132  infra: &InfraContext<'_>,
133  token: &str,
134  group: Group,
135) -> Result<(), Error> {
136  infra.backend.add_group(token, group).await.map(|_| ())
137}
138
139/// Resolve hosts expression, validate target group exists,
140/// and add nodes to the HSM group.
141///
142/// Returns `(xnames_resolved, updated_member_list)`.
143pub async fn add_nodes_to_group(
144  infra: &InfraContext<'_>,
145  token: &str,
146  target_hsm_name: &str,
147  hosts_expression: &str,
148) -> Result<(Vec<String>, Vec<String>), Error> {
149  let xname_to_move_vec = common::node_ops::resolve_hosts_expression(
150    infra.backend,
151    token,
152    hosts_expression,
153    false,
154  )
155  .await?;
156
157  if xname_to_move_vec.is_empty() {
158    return Err(Error::BadRequest(
159      "The list of nodes to move is empty. Nothing to do".to_string(),
160    ));
161  }
162
163  if infra
164    .backend
165    .get_group(token, target_hsm_name)
166    .await
167    .is_err()
168  {
169    return Err(Error::NotFound(format!(
170      "Target HSM group '{target_hsm_name}' does not exist"
171    )));
172  }
173
174  let xnames_to_move: Vec<&str> =
175    xname_to_move_vec.iter().map(String::as_str).collect();
176
177  let mut updated_members = infra
178    .backend
179    .add_members_to_group(token, target_hsm_name, &xnames_to_move)
180    .await?;
181
182  updated_members.sort();
183
184  Ok((xname_to_move_vec, updated_members))
185}