1use axum::{
13 Json,
14 extract::{Path, Query},
15 http::StatusCode,
16 response::IntoResponse,
17};
18
19use super::{ErrorResponse, RequestCtx, SiteHeader, to_handler_error};
20use crate::server::audit::AuditNote;
21use crate::service;
22
23pub use manta_shared::types::api::queries::{DeleteGroupQuery, GroupQuery};
28
29#[utoipa::path(get, path = "/groups/available", tag = "groups",
31 params(SiteHeader),
32 security(("bearerAuth" = [])),
33 responses(
34 (status = 200, description = "List of accessible group names", body = Vec<String>),
35 (status = 401, description = "Unauthorized", body = ErrorResponse),
36 (status = 500, description = "Internal error", body = ErrorResponse),
37 )
38)]
39#[tracing::instrument(skip_all)]
40pub async fn get_available_groups(
41 ctx: RequestCtx,
42) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
43 let infra = ctx.infra();
44 let names = service::group::get_available_groups(&infra, &ctx.token)
45 .await
46 .map_err(to_handler_error)?;
47 Ok(Json(names))
48}
49
50#[utoipa::path(get, path = "/groups", tag = "groups",
52 params(GroupQuery, SiteHeader),
53 security(("bearerAuth" = [])),
54 responses(
55 (status = 200, description = "List of groups", body = Vec<manta_backend_dispatcher::types::Group>),
56 (status = 401, description = "Unauthorized", body = ErrorResponse),
57 (status = 500, description = "Internal error", body = ErrorResponse),
58 )
59)]
60#[tracing::instrument(skip_all)]
61pub async fn get_groups(
62 ctx: RequestCtx,
63 Query(q): Query<GroupQuery>,
64) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
65 let infra = ctx.infra();
66
67 let params = service::group::GetGroupParams {
68 group_name: q.name,
69 settings_group_name: None,
70 };
71
72 let groups = service::group::get_groups(&infra, &ctx.token, ¶ms)
73 .await
74 .map_err(to_handler_error)?;
75
76 Ok(Json(groups))
77}
78
79#[utoipa::path(delete, path = "/groups/{label}", tag = "groups",
85 params(("label" = String, Path, description = "Group label"), DeleteGroupQuery, SiteHeader),
86 security(("bearerAuth" = [])),
87 responses(
88 (status = 204, description = "Group removed"),
89 (status = 401, description = "Unauthorized", body = ErrorResponse),
90 (status = 404, description = "Not found", body = ErrorResponse),
91 (status = 500, description = "Internal error", body = ErrorResponse),
92 )
93)]
94#[tracing::instrument(skip_all)]
95pub async fn delete_group(
96 ctx: RequestCtx,
97 note: AuditNote,
98 Path(label): Path<String>,
99 Query(q): Query<DeleteGroupQuery>,
100) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
101 note
102 .action("group.delete")
103 .target("group", &label)
104 .detail("force", q.force);
105
106 tracing::info!("delete_group label={} force={}", label, q.force);
107 let infra = ctx.infra();
108
109 service::authorization::validate_user_group_access(
111 &infra, &ctx.token, &label,
112 )
113 .await
114 .map_err(to_handler_error)?;
115
116 service::group::delete_group(&infra, &ctx.token, &label, q.force)
117 .await
118 .map_err(to_handler_error)?;
119
120 Ok(StatusCode::NO_CONTENT)
121}
122
123#[utoipa::path(post, path = "/groups", tag = "groups",
129 params(SiteHeader),
130 request_body = manta_backend_dispatcher::types::Group,
131 security(("bearerAuth" = [])),
132 responses(
133 (status = 201, description = "Group created", body = manta_shared::types::api::responses::CreatedResponse),
134 (status = 401, description = "Unauthorized", body = ErrorResponse),
135 (status = 409, description = "Conflict", body = ErrorResponse),
136 (status = 500, description = "Internal error", body = ErrorResponse),
137 )
138)]
139#[tracing::instrument(skip_all)]
140pub async fn create_group(
141 ctx: RequestCtx,
142 note: AuditNote,
143 Json(group): Json<::manta_backend_dispatcher::types::Group>,
144) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
145 note.action("group.create").target("group", &group.label);
146
147 tracing::info!("create_group");
148 let infra = ctx.infra();
149
150 service::authorization::require_admin(&ctx.token)
155 .map_err(to_handler_error)?;
156
157 service::group::create_group(&infra, &ctx.token, group)
158 .await
159 .map_err(to_handler_error)?;
160
161 Ok((
162 StatusCode::CREATED,
163 Json(serde_json::json!({ "created": true })),
164 ))
165}
166
167pub use manta_shared::types::api::group::{
172 AddNodesToGroupRequest, AddNodesToGroupResponse, DeleteGroupMembersRequest,
173};
174
175#[utoipa::path(post, path = "/groups/{name}/members", tag = "groups",
177 params(("name" = String, Path, description = "Group name"), SiteHeader),
178 request_body = AddNodesToGroupRequest,
179 security(("bearerAuth" = [])),
180 responses(
181 (status = 200, description = "Members updated", body = AddNodesToGroupResponse),
182 (status = 400, description = "Bad request", body = ErrorResponse),
183 (status = 401, description = "Unauthorized", body = ErrorResponse),
184 (status = 500, description = "Internal error", body = ErrorResponse),
185 )
186)]
187#[tracing::instrument(skip_all)]
188pub async fn add_nodes_to_group(
189 ctx: RequestCtx,
190 note: AuditNote,
191 Path(name): Path<String>,
192 Json(body): Json<AddNodesToGroupRequest>,
193) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
194 note
195 .action("group.members.add")
196 .target("group", &name)
197 .target("hosts", &body.hosts_expression);
198
199 tracing::info!(
200 "add_nodes_to_group group={} hosts={}",
201 name,
202 body.hosts_expression
203 );
204 let infra = ctx.infra();
205
206 service::authorization::validate_user_group_access(&infra, &ctx.token, &name)
208 .await
209 .map_err(to_handler_error)?;
210
211 let (added, removed) = service::group::add_nodes_to_group(
212 &infra,
213 &ctx.token,
214 &name,
215 &body.hosts_expression,
216 )
217 .await
218 .map_err(to_handler_error)?;
219
220 Ok(Json(AddNodesToGroupResponse {
224 added,
225 final_members: removed.clone(),
226 removed,
227 }))
228}
229
230#[utoipa::path(delete, path = "/groups/{name}/members", tag = "groups",
236 params(("name" = String, Path, description = "Group name"), SiteHeader),
237 request_body = DeleteGroupMembersRequest,
238 security(("bearerAuth" = [])),
239 responses(
240 (status = 204, description = "Members removed"),
241 (status = 400, description = "Bad request", body = ErrorResponse),
242 (status = 401, description = "Unauthorized", body = ErrorResponse),
243 (status = 500, description = "Internal error", body = ErrorResponse),
244 )
245)]
246#[tracing::instrument(skip_all)]
247pub async fn delete_group_members(
248 ctx: RequestCtx,
249 note: AuditNote,
250 Path(name): Path<String>,
251 Json(body): Json<DeleteGroupMembersRequest>,
252) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
253 note
254 .action("group.members.delete")
255 .target("group", &name)
256 .target("hosts", &body.xnames_expression)
257 .dry_run(body.dry_run);
258
259 tracing::info!(
260 "delete_group_members group={} xnames_expression={} dry_run={}",
261 name,
262 body.xnames_expression,
263 body.dry_run
264 );
265 let infra = ctx.infra();
266
267 service::authorization::validate_user_group_access(&infra, &ctx.token, &name)
269 .await
270 .map_err(to_handler_error)?;
271
272 service::group::delete_group_members(
273 &infra,
274 &ctx.token,
275 &name,
276 &body.xnames_expression,
277 body.dry_run,
278 )
279 .await
280 .map_err(to_handler_error)?;
281
282 Ok(StatusCode::NO_CONTENT)
283}