Skip to main content

manta_server/server/handlers/
configuration.rs

1//! CFS-configuration handlers.
2//!
3//! - `GET    /v2/configurations` → [`get_configurations`] —
4//!   wraps `service::configuration::get_configurations_with_analysis`;
5//!   each row carries a `safe_to_delete` verdict derived from CFS
6//!   components only.
7//! - `DELETE /v2/configurations` → [`delete_configurations`] —
8//!   wraps `service::configuration::{get_deletion_candidates,
9//!   delete_configurations_and_derivatives}`; with `?dry_run=true`,
10//!   returns the candidate set without deleting.
11
12use axum::{Json, extract::Query, http::StatusCode, response::IntoResponse};
13
14use super::{
15  ErrorResponse, RequestCtx, SiteHeader, parse_iso_datetime, serialize_or_500,
16  to_handler_error,
17};
18use crate::server::audit::AuditNote;
19use crate::service;
20use manta_shared::types::api::configuration_analysis::ConfigurationAnalysis;
21
22// ---------------------------------------------------------------------------
23// GET /v2/configurations
24// ---------------------------------------------------------------------------
25
26pub use manta_shared::types::api::queries::{
27  ConfigurationQuery, DeleteConfigurationsQuery,
28};
29
30/// GET /configurations — list CFS configurations with optional
31/// name/pattern/group filters. Every row carries the full
32/// `CfsConfigurationResponse` plus a `safe_to_delete` verdict
33/// derived from CFS components only (a configuration is unsafe iff
34/// some component lists it as `desired_config`).
35#[utoipa::path(get, path = "/configurations", tag = "configurations",
36  params(ConfigurationQuery, SiteHeader),
37  security(("bearerAuth" = [])),
38  responses(
39    (status = 200, description = "Configurations with components-only safe_to_delete verdict",
40     body = Vec<ConfigurationAnalysis>),
41    (status = 401, description = "Unauthorized",           body = ErrorResponse),
42    (status = 500, description = "Internal error",         body = ErrorResponse),
43  )
44)]
45#[tracing::instrument(skip_all)]
46pub async fn get_configurations(
47  ctx: RequestCtx,
48  Query(q): Query<ConfigurationQuery>,
49) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
50  let infra = ctx.infra();
51
52  let params = service::configuration::GetConfigurationParams {
53    name: q.name,
54    pattern: q.pattern,
55    group_name: q.hsm_group,
56    settings_hsm_group_name: None,
57    since: None,
58    until: None,
59    limit: q.limit,
60  };
61
62  let rows = service::configuration::get_configurations_with_analysis(
63    &infra, &ctx.token, &params,
64  )
65  .await
66  .map_err(to_handler_error)?;
67
68  Ok(Json(rows))
69}
70
71// ---------------------------------------------------------------------------
72// DELETE /v2/configurations — with ?pattern=...&since=...&until=...&dry_run=true
73// ---------------------------------------------------------------------------
74
75/// `DELETE /v2/configurations` — delete CFS configurations and all derived artifacts.
76#[utoipa::path(delete, path = "/configurations", tag = "configurations",
77  params(DeleteConfigurationsQuery, SiteHeader),
78  security(("bearerAuth" = [])),
79  responses(
80    // dry_run/real result union — kept as Value until the union shape is formalised
81    (status = 200, description = "Configurations deleted or preview", body = serde_json::Value),
82    (status = 400, description = "Bad request",                       body = ErrorResponse),
83    (status = 401, description = "Unauthorized",                      body = ErrorResponse),
84    (status = 500, description = "Internal error",                    body = ErrorResponse),
85  )
86)]
87#[tracing::instrument(skip_all)]
88pub async fn delete_configurations(
89  ctx: RequestCtx,
90  note: AuditNote,
91  Query(q): Query<DeleteConfigurationsQuery>,
92) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
93  note
94    .action("configuration.delete")
95    .detail("pattern", &q.pattern)
96    .detail("since", &q.since)
97    .detail("until", &q.until)
98    .dry_run(q.dry_run);
99
100  tracing::info!("delete_configurations dry_run={}", q.dry_run);
101  let infra = ctx.infra();
102
103  let since = q
104    .since
105    .as_deref()
106    .map(|s| parse_iso_datetime("since", s))
107    .transpose()?;
108  let until = q
109    .until
110    .as_deref()
111    .map(|s| parse_iso_datetime("until", s))
112    .transpose()?;
113
114  let candidates = service::configuration::get_deletion_candidates(
115    &infra,
116    &ctx.token,
117    None,
118    q.pattern.as_deref(),
119    since,
120    until,
121  )
122  .await
123  .map_err(to_handler_error)?;
124  // The caller gave a pattern / date range; record what it matched,
125  // so a search for a configuration (or a derived image) finds this.
126  note
127    .targets("configuration", &candidates.configuration_names)
128    .targets(
129      "session",
130      candidates.cfs_sessions_to_delete.iter().map(|s| &s.name),
131    )
132    .targets(
133      "template",
134      candidates.bos_sessiontemplate_tuples.iter().map(|t| &t.0),
135    )
136    .targets("image", &candidates.image_ids);
137
138  if q.dry_run {
139    return Ok((StatusCode::OK, Json(serialize_or_500(&candidates)?)));
140  }
141
142  service::configuration::delete_configurations_and_derivatives(
143    &infra,
144    &ctx.token,
145    &candidates,
146  )
147  .await
148  .map_err(to_handler_error)?;
149
150  Ok((
151    StatusCode::OK,
152    Json(serde_json::json!({
153      "deleted_configurations": candidates.configuration_names,
154      "deleted_images": candidates.image_ids,
155    })),
156  ))
157}
158
159// ===========================================================================
160// BATCH A — MEDIUM-COMPLEXITY WRITE ENDPOINTS
161// ===========================================================================