1use axum::{
4 Json,
5 extract::{Path, Query},
6 http::StatusCode,
7 response::IntoResponse,
8};
9use manta_backend_dispatcher::interfaces::hsm::group::GroupTrait;
10
11use super::{ErrorResponse, RequestCtx, SiteHeader, to_handler_error};
12use crate::service;
13
14pub use manta_shared::types::api::queries::{DeleteGroupQuery, GroupQuery};
19
20#[utoipa::path(get, path = "/groups/available", tag = "groups",
25 params(SiteHeader),
26 security(("bearerAuth" = [])),
27 responses(
28 (status = 200, description = "List of accessible group names", body = Vec<String>),
29 (status = 401, description = "Unauthorized", body = ErrorResponse),
30 (status = 500, description = "Internal error", body = ErrorResponse),
31 )
32)]
33#[tracing::instrument(skip_all)]
34pub async fn get_available_groups(
35 ctx: RequestCtx,
36) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
37 let infra = ctx.infra();
38 let names = infra
39 .backend
40 .get_group_name_available(&ctx.token)
41 .await
42 .map_err(to_handler_error)?;
43 Ok(Json(names))
44}
45
46#[utoipa::path(get, path = "/groups", tag = "groups",
48 params(GroupQuery, SiteHeader),
49 security(("bearerAuth" = [])),
50 responses(
51 (status = 200, description = "List of groups", body = Vec<manta_backend_dispatcher::types::Group>),
52 (status = 401, description = "Unauthorized", body = ErrorResponse),
53 (status = 500, description = "Internal error", body = ErrorResponse),
54 )
55)]
56#[tracing::instrument(skip_all)]
57pub async fn get_groups(
58 ctx: RequestCtx,
59 Query(q): Query<GroupQuery>,
60) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
61 let infra = ctx.infra();
62
63 let params = service::group::GetGroupParams {
64 group_name: q.name,
65 settings_group_name: None,
66 };
67
68 let groups = service::group::get_groups(&infra, &ctx.token, ¶ms)
69 .await
70 .map_err(to_handler_error)?;
71
72 Ok(Json(groups))
73}
74
75#[utoipa::path(delete, path = "/groups/{label}", tag = "groups",
81 params(("label" = String, Path, description = "Group label"), DeleteGroupQuery, SiteHeader),
82 security(("bearerAuth" = [])),
83 responses(
84 (status = 204, description = "Group removed"),
85 (status = 401, description = "Unauthorized", body = ErrorResponse),
86 (status = 404, description = "Not found", body = ErrorResponse),
87 (status = 500, description = "Internal error", body = ErrorResponse),
88 )
89)]
90#[tracing::instrument(skip_all)]
91pub async fn delete_group(
92 ctx: RequestCtx,
93 Path(label): Path<String>,
94 Query(q): Query<DeleteGroupQuery>,
95) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
96 tracing::info!("delete_group label={} force={}", label, q.force);
97 let infra = ctx.infra();
98
99 service::authorization::validate_user_group_access(
101 &infra, &ctx.token, &label,
102 )
103 .await
104 .map_err(to_handler_error)?;
105
106 service::group::delete_group(&infra, &ctx.token, &label, q.force)
107 .await
108 .map_err(to_handler_error)?;
109
110 Ok(StatusCode::NO_CONTENT)
111}
112
113#[utoipa::path(post, path = "/groups", tag = "groups",
119 params(SiteHeader),
120 request_body = manta_backend_dispatcher::types::Group,
121 security(("bearerAuth" = [])),
122 responses(
123 (status = 201, description = "Group created", body = manta_shared::types::api::responses::CreatedResponse),
124 (status = 401, description = "Unauthorized", body = ErrorResponse),
125 (status = 409, description = "Conflict", body = ErrorResponse),
126 (status = 500, description = "Internal error", body = ErrorResponse),
127 )
128)]
129#[tracing::instrument(skip_all)]
130pub async fn create_group(
131 ctx: RequestCtx,
132 Json(group): Json<::manta_backend_dispatcher::types::Group>,
133) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
134 tracing::info!("create_group");
135 let infra = ctx.infra();
136
137 if !crate::server::common::jwt_ops::is_user_admin(&ctx.token) {
142 return Err(to_handler_error(
143 manta_backend_dispatcher::error::Error::BadRequest(
144 "group creation requires admin privileges".to_string(),
145 ),
146 ));
147 }
148
149 service::group::create_group(&infra, &ctx.token, group)
150 .await
151 .map_err(to_handler_error)?;
152
153 Ok((
154 StatusCode::CREATED,
155 Json(serde_json::json!({ "created": true })),
156 ))
157}
158
159pub use manta_shared::types::api::group::{
164 AddNodesToGroupRequest, AddNodesToGroupResponse, DeleteGroupMembersRequest,
165};
166
167#[utoipa::path(post, path = "/groups/{name}/members", tag = "groups",
169 params(("name" = String, Path, description = "Group name"), SiteHeader),
170 request_body = AddNodesToGroupRequest,
171 security(("bearerAuth" = [])),
172 responses(
173 (status = 200, description = "Members updated", body = AddNodesToGroupResponse),
174 (status = 400, description = "Bad request", body = ErrorResponse),
175 (status = 401, description = "Unauthorized", body = ErrorResponse),
176 (status = 500, description = "Internal error", body = ErrorResponse),
177 )
178)]
179#[tracing::instrument(skip_all)]
180pub async fn add_nodes_to_group(
181 ctx: RequestCtx,
182 Path(name): Path<String>,
183 Json(body): Json<AddNodesToGroupRequest>,
184) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
185 tracing::info!(
186 "add_nodes_to_group group={} hosts={}",
187 name,
188 body.hosts_expression
189 );
190 let infra = ctx.infra();
191
192 service::authorization::validate_user_group_access(&infra, &ctx.token, &name)
194 .await
195 .map_err(to_handler_error)?;
196
197 let (added, removed) = service::group::add_nodes_to_group(
198 &infra,
199 &ctx.token,
200 &name,
201 &body.hosts_expression,
202 )
203 .await
204 .map_err(to_handler_error)?;
205
206 Ok(Json(AddNodesToGroupResponse {
210 added,
211 final_members: removed.clone(),
212 removed,
213 }))
214}
215
216#[utoipa::path(delete, path = "/groups/{name}/members", tag = "groups",
222 params(("name" = String, Path, description = "Group name"), SiteHeader),
223 request_body = DeleteGroupMembersRequest,
224 security(("bearerAuth" = [])),
225 responses(
226 (status = 204, description = "Members removed"),
227 (status = 400, description = "Bad request", body = ErrorResponse),
228 (status = 401, description = "Unauthorized", body = ErrorResponse),
229 (status = 500, description = "Internal error", body = ErrorResponse),
230 )
231)]
232#[tracing::instrument(skip_all)]
233pub async fn delete_group_members(
234 ctx: RequestCtx,
235 Path(name): Path<String>,
236 Json(body): Json<DeleteGroupMembersRequest>,
237) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
238 tracing::info!(
239 "delete_group_members group={} xnames_expression={} dry_run={}",
240 name,
241 body.xnames_expression,
242 body.dry_run
243 );
244 let infra = ctx.infra();
245
246 service::authorization::validate_user_group_access(&infra, &ctx.token, &name)
248 .await
249 .map_err(to_handler_error)?;
250
251 service::group::delete_group_members(
252 &infra,
253 &ctx.token,
254 &name,
255 &body.xnames_expression,
256 body.dry_run,
257 )
258 .await
259 .map_err(to_handler_error)?;
260
261 Ok(StatusCode::NO_CONTENT)
262}