1use axum::{
4 Json,
5 extract::{Path, Query},
6 http::StatusCode,
7 response::IntoResponse,
8};
9use manta_backend_dispatcher::interfaces::hsm::group::GroupTrait;
10use serde::{Deserialize, Serialize};
11use utoipa::{IntoParams, ToSchema};
12
13use super::{ErrorResponse, RequestCtx, SiteHeader, to_handler_error};
14use crate::service;
15
16#[derive(Deserialize, IntoParams)]
22pub struct GroupQuery {
23 pub name: Option<String>,
25}
26
27#[utoipa::path(get, path = "/groups/available", tag = "groups",
32 params(SiteHeader),
33 security(("bearerAuth" = [])),
34 responses(
35 (status = 200, description = "List of accessible group names", body = Vec<String>),
36 (status = 401, description = "Unauthorized", body = ErrorResponse),
37 (status = 500, description = "Internal error", body = ErrorResponse),
38 )
39)]
40#[tracing::instrument(skip_all)]
41pub async fn get_available_groups(
42 ctx: RequestCtx,
43) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
44 let infra = ctx.infra();
45 let names = service::group::get_available_group_names(&infra, &ctx.token)
46 .await
47 .map_err(to_handler_error)?;
48 Ok(Json(names))
49}
50
51#[utoipa::path(get, path = "/groups/all", tag = "groups",
56 params(SiteHeader),
57 security(("bearerAuth" = [])),
58 responses(
59 (status = 200, description = "List of all groups", body = serde_json::Value),
60 (status = 401, description = "Unauthorized", body = ErrorResponse),
61 (status = 500, description = "Internal error", body = ErrorResponse),
62 )
63)]
64#[tracing::instrument(skip_all)]
65pub async fn get_all_groups(
66 ctx: RequestCtx,
67) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
68 let infra = ctx.infra();
69 let groups = service::group::get_all_groups(&infra, &ctx.token)
70 .await
71 .map_err(to_handler_error)?;
72 Ok(Json(groups))
73}
74
75#[utoipa::path(get, path = "/groups", tag = "groups",
77 params(GroupQuery, SiteHeader),
78 security(("bearerAuth" = [])),
79 responses(
80 (status = 200, description = "List of groups", body = serde_json::Value),
81 (status = 401, description = "Unauthorized", body = ErrorResponse),
82 (status = 500, description = "Internal error", body = ErrorResponse),
83 )
84)]
85#[tracing::instrument(skip_all)]
86pub async fn get_groups(
87 ctx: RequestCtx,
88 Query(q): Query<GroupQuery>,
89) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
90 let infra = ctx.infra();
91
92 let params = service::group::GetGroupParams {
93 group_name: q.name,
94 settings_hsm_group_name: None,
95 };
96
97 let groups = service::group::get_groups(&infra, &ctx.token, ¶ms)
98 .await
99 .map_err(to_handler_error)?;
100
101 Ok(Json(groups))
102}
103
104#[derive(Deserialize, IntoParams)]
110pub struct DeleteGroupQuery {
111 #[serde(default)]
113 pub force: bool,
114}
115
116#[utoipa::path(delete, path = "/groups/{label}", tag = "groups",
118 params(("label" = String, Path, description = "Group label"), DeleteGroupQuery, SiteHeader),
119 security(("bearerAuth" = [])),
120 responses(
121 (status = 204, description = "Group removed"),
122 (status = 401, description = "Unauthorized", body = ErrorResponse),
123 (status = 404, description = "Not found", body = ErrorResponse),
124 (status = 500, description = "Internal error", body = ErrorResponse),
125 )
126)]
127#[tracing::instrument(skip_all)]
128pub async fn delete_group(
129 ctx: RequestCtx,
130 Path(label): Path<String>,
131 Query(q): Query<DeleteGroupQuery>,
132) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
133 tracing::info!("delete_group label={} force={}", label, q.force);
134 let infra = ctx.infra();
135
136 service::group::delete_group(&infra, &ctx.token, &label, q.force)
137 .await
138 .map_err(to_handler_error)?;
139
140 Ok(StatusCode::NO_CONTENT)
141}
142
143#[utoipa::path(post, path = "/groups", tag = "groups",
149 params(SiteHeader),
150 request_body = manta_backend_dispatcher::types::Group,
151 security(("bearerAuth" = [])),
152 responses(
153 (status = 201, description = "Group created", body = serde_json::Value),
154 (status = 401, description = "Unauthorized", body = ErrorResponse),
155 (status = 409, description = "Conflict", body = ErrorResponse),
156 (status = 500, description = "Internal error", body = ErrorResponse),
157 )
158)]
159#[tracing::instrument(skip_all)]
160pub async fn create_group(
161 ctx: RequestCtx,
162 Json(group): Json<::manta_backend_dispatcher::types::Group>,
163) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
164 tracing::info!("create_group");
165 let infra = ctx.infra();
166
167 service::group::create_group(&infra, &ctx.token, group)
168 .await
169 .map_err(to_handler_error)?;
170
171 Ok((
172 StatusCode::CREATED,
173 Json(serde_json::json!({ "created": true })),
174 ))
175}
176
177#[derive(Deserialize, ToSchema)]
183pub struct AddNodesToGroupRequest {
184 pub hosts_expression: String,
187}
188
189#[derive(Serialize, ToSchema)]
191pub struct AddNodesToGroupResponse {
192 pub added: Vec<String>,
194 pub removed: Vec<String>,
196}
197
198#[utoipa::path(post, path = "/groups/{name}/members", tag = "groups",
200 params(("name" = String, Path, description = "Group name"), SiteHeader),
201 request_body = AddNodesToGroupRequest,
202 security(("bearerAuth" = [])),
203 responses(
204 (status = 200, description = "Members updated", body = AddNodesToGroupResponse),
205 (status = 400, description = "Bad request", body = ErrorResponse),
206 (status = 401, description = "Unauthorized", body = ErrorResponse),
207 (status = 500, description = "Internal error", body = ErrorResponse),
208 )
209)]
210#[tracing::instrument(skip_all)]
211pub async fn add_nodes_to_group(
212 ctx: RequestCtx,
213 Path(name): Path<String>,
214 Json(body): Json<AddNodesToGroupRequest>,
215) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
216 tracing::info!(
217 "add_nodes_to_group group={} hosts={}",
218 name,
219 body.hosts_expression
220 );
221 let infra = ctx.infra();
222
223 let (added, removed) = service::group::add_nodes_to_group(
224 &infra,
225 &ctx.token,
226 &name,
227 &body.hosts_expression,
228 )
229 .await
230 .map_err(to_handler_error)?;
231
232 Ok(Json(AddNodesToGroupResponse { added, removed }))
233}
234
235#[derive(Deserialize, ToSchema)]
241pub struct DeleteGroupMembersRequest {
242 pub xnames_expression: String,
244 #[serde(default)]
246 pub dry_run: bool,
247}
248
249#[utoipa::path(delete, path = "/groups/{name}/members", tag = "groups",
251 params(("name" = String, Path, description = "Group name"), SiteHeader),
252 request_body = DeleteGroupMembersRequest,
253 security(("bearerAuth" = [])),
254 responses(
255 (status = 204, description = "Members removed"),
256 (status = 400, description = "Bad request", body = ErrorResponse),
257 (status = 401, description = "Unauthorized", body = ErrorResponse),
258 (status = 500, description = "Internal error", body = ErrorResponse),
259 )
260)]
261#[tracing::instrument(skip_all)]
262pub async fn delete_group_members(
263 ctx: RequestCtx,
264 Path(name): Path<String>,
265 Json(body): Json<DeleteGroupMembersRequest>,
266) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
267 tracing::info!(
268 "delete_group_members group={} xnames_expression={} dry_run={}",
269 name,
270 body.xnames_expression,
271 body.dry_run
272 );
273 let infra = ctx.infra();
274
275 let xnames = crate::server::common::node_ops::resolve_hosts_expression(
276 infra.backend,
277 &ctx.token,
278 &body.xnames_expression,
279 false,
280 )
281 .await
282 .map_err(to_handler_error)?;
283
284 if !body.dry_run {
285 for xname in &xnames {
286 infra
287 .backend
288 .delete_member_from_group(&ctx.token, &name, xname)
289 .await
290 .map_err(to_handler_error)?;
291 }
292 }
293
294 Ok(StatusCode::NO_CONTENT)
295}