manta_server/service/
configuration.rs

1//! CFS configuration queries, layer-detail lookups, and cascading deletion of
2//! all dependent resources (sessions, BOS templates, IMS images).
3//!
4//! The module is the service-layer counterpart to the
5//! `/configurations` handlers. It wraps three backend trait families:
6//!
7//! - [`CfsTrait`] — list and filter CFS configurations, fetch CFS
8//!   components for the post-list analysis pass.
9//! - [`DeleteConfigurationsAndDataRelatedTrait`] — walk every CFS
10//!   session, BOS session template, and IMS image that depends on a
11//!   set of configurations, then issue the cascading delete.
12//! - [`GroupTrait`] — derive the caller's accessible-group set when no
13//!   explicit `group_name` is supplied (used for both listing and
14//!   deletion scoping).
15//!
16//! All public helpers funnel access checks through
17//! [`crate::service::authorization`] so the response can't leak rows
18//! that belong to a group the caller can't see.
19
20use chrono::NaiveDateTime;
21use manta_backend_dispatcher::error::Error;
22use manta_backend_dispatcher::interfaces::cfs::CfsTrait;
23use manta_backend_dispatcher::interfaces::delete_configurations_and_data_related::DeleteConfigurationsAndDataRelatedTrait;
24use manta_backend_dispatcher::interfaces::hsm::group::GroupTrait;
25use manta_backend_dispatcher::types::cfs::cfs_configuration_response::CfsConfigurationResponse;
26use manta_backend_dispatcher::types::cfs::session::CfsSessionGetResponse;
27
28use crate::server::common::app_context::InfraContext;
29use crate::service::authorization::validate_user_group_access;
30use crate::service::group;
31pub use manta_shared::types::api::configuration::GetConfigurationParams;
32
33/// Data gathered for deletion review and execution.
34#[derive(serde::Serialize)]
35pub struct DeletionCandidates {
36  /// CFS sessions whose desired-config matches a candidate configuration.
37  pub cfs_sessions_to_delete: Vec<CfsSessionGetResponse>,
38  /// BOS session templates to delete: `(name, cfs_config, description)`.
39  pub bos_sessiontemplate_tuples: Vec<(String, String, String)>,
40  /// IMS image IDs to delete (built by the matching sessions).
41  pub image_ids: Vec<String>,
42  /// Names of the configurations selected for deletion.
43  pub configuration_names: Vec<String>,
44  /// CFS sessions summary tuples: `(name, config_name, status)`.
45  pub cfs_session_tuples: Vec<(String, String, String)>,
46  /// Full configuration objects selected for deletion.
47  pub configurations: Vec<CfsConfigurationResponse>,
48}
49
50/// List CFS configurations the caller may see.
51///
52/// When `params.group_name` is set, access to that group is validated
53/// first; otherwise the search is scoped to every group the token
54/// already grants access to. Name / pattern / date filters and the
55/// per-call `limit` are applied by the backend.
56///
57/// # Errors
58///
59/// - [`Error::BadRequest`] when `params.group_name` names a group the
60///   caller cannot reach (raised by the authorization helper).
61/// - [`Error::NetError`] / [`Error::CsmError`] propagated from the
62///   `get_group_available` and `get_and_filter_configuration`
63///   backend calls.
64pub async fn get_configurations(
65  infra: &InfraContext<'_>,
66  token: &str,
67  params: &GetConfigurationParams,
68) -> Result<Vec<CfsConfigurationResponse>, Error> {
69  // Single backend round-trip: resolve accessible groups and validate access.
70  let (_groups, target_group_vec) = group::resolve_target_and_available_groups(
71    infra,
72    token,
73    params.group_name.as_deref(),
74  )
75  .await?;
76
77  let limit_ref = params.limit.as_ref();
78
79  let cfs_configuration_vec = infra
80    .backend
81    .get_and_filter_configuration(
82      token,
83      params.name.as_deref(),
84      params.pattern.as_deref(),
85      &target_group_vec,
86      params.since,
87      params.until,
88      limit_ref,
89    )
90    .await?;
91
92  Ok(cfs_configuration_vec)
93}
94
95/// Like [`get_configurations`] but pairs every row with a
96/// `safe_to_delete` verdict by fetching CFS components and running
97/// the pure [`crate::service::analysis::build_configuration_analysis`] linker.
98///
99/// The verdict is **CFS-components-only**: a configuration is unsafe
100/// if any CFS component lists it as its `desired_config`. The endpoint
101/// does not check whether any BSS-referenced image was built from the
102/// configuration; skipping the BSS and IMS fetches keeps this listing
103/// fast and avoids the upstream-proxy resets that fanning out four
104/// heavy fetches has been prone to.
105///
106/// # Errors
107///
108/// Any error produced by [`get_configurations`] plus
109/// [`Error::NetError`] / [`Error::CsmError`] from the secondary
110/// `get_cfs_components` call used to build the link graph.
111pub async fn get_configurations_with_analysis(
112  infra: &InfraContext<'_>,
113  token: &str,
114  params: &GetConfigurationParams,
115) -> Result<Vec<crate::service::analysis::ConfigurationAnalysis>, Error> {
116  let configs = get_configurations(infra, token, params).await?;
117  let components = infra
118    .backend
119    .get_cfs_components(token, None, None, None)
120    .await?;
121  Ok(crate::service::analysis::build_configuration_analysis(
122    configs,
123    components,
124    vec![],
125    vec![],
126  ))
127}
128
129/// Collect every resource that would be removed by a cascading
130/// configuration delete, without actually deleting anything.
131///
132/// Returns the configurations matching `configuration_name_pattern`
133/// (within `since`/`until` if provided) plus the CFS sessions, BOS
134/// session templates, and IMS images that depend on them. The CLI
135/// shows this set as a confirmation prompt before invoking
136/// [`delete_configurations_and_derivatives`].
137///
138/// When `settings_hsm_group_name_opt` is `Some(name)`, the caller's
139/// access to that group is validated first; when `None`, the
140/// candidate set is scoped to every group the token already grants
141/// access to. The backend's `get_data_to_delete` only walks resources
142/// reachable from the supplied group set, so the candidates returned
143/// here are guaranteed to be reachable through the caller's
144/// accessible-group lens.
145///
146/// # Errors
147///
148/// - [`Error::BadRequest`] when `since > until`, or when
149///   `settings_hsm_group_name_opt` names a group the caller cannot
150///   reach.
151/// - [`Error::NetError`] / [`Error::CsmError`] from
152///   `get_group_name_available` or `get_data_to_delete`.
153pub(crate) async fn get_deletion_candidates(
154  infra: &InfraContext<'_>,
155  token: &str,
156  settings_hsm_group_name_opt: Option<&str>,
157  configuration_name_pattern: Option<&str>,
158  since: Option<NaiveDateTime>,
159  until: Option<NaiveDateTime>,
160) -> Result<DeletionCandidates, Error> {
161  validate_date_range(since, until)?;
162
163  let target_hsm_group_vec =
164    if let Some(settings_hsm_group_name) = settings_hsm_group_name_opt {
165      // Defense-in-depth: today the handler always passes `None`, but
166      // if a future caller (CLI, another handler) routes a user-
167      // supplied group label through here, an unchecked group would
168      // let the caller cascade-delete configurations they don't own.
169      validate_user_group_access(infra, token, settings_hsm_group_name).await?;
170      vec![settings_hsm_group_name.to_string()]
171    } else {
172      infra.backend.get_group_name_available(token).await?
173    };
174
175  let (
176    cfs_sessions_to_delete,
177    bos_sessiontemplate_tuples,
178    image_ids,
179    configuration_names,
180    cfs_session_tuples,
181    configurations,
182  ) = infra
183    .backend
184    .get_data_to_delete(
185      token,
186      &target_hsm_group_vec,
187      configuration_name_pattern,
188      since,
189      until,
190    )
191    .await?;
192  Ok(DeletionCandidates {
193    cfs_sessions_to_delete,
194    bos_sessiontemplate_tuples,
195    image_ids,
196    configuration_names,
197    cfs_session_tuples,
198    configurations,
199  })
200}
201
202/// Validate that a `(since, until)` date range is well-ordered.
203///
204/// Extracted so the HTTP handler and CLI can share the check without
205/// constructing a full backend context.
206///
207/// # Errors
208///
209/// [`Error::BadRequest`] when both bounds are supplied and
210/// `since > until`. Returns `Ok(())` whenever either bound is `None`,
211/// including when they are equal.
212pub fn validate_date_range(
213  since: Option<NaiveDateTime>,
214  until: Option<NaiveDateTime>,
215) -> Result<(), Error> {
216  if let (Some(s), Some(u)) = (since, until)
217    && s > u
218  {
219    return Err(Error::BadRequest(
220      "'since' date can't be after 'until' date".to_string(),
221    ));
222  }
223  Ok(())
224}
225
226/// Apply a cascading delete previously planned by
227/// [`get_deletion_candidates`].
228///
229/// Removes the named configurations together with every dependent
230/// CFS session, BOS session template, and IMS image listed in
231/// `candidates`. The two-step plan/apply split exists so the caller
232/// can show the user exactly what is about to disappear before any
233/// state changes.
234///
235/// # Errors
236///
237/// [`Error::NetError`] / [`Error::CsmError`] from the backend's
238/// `delete` call. The delete is non-transactional — a failure
239/// mid-batch can leave some derivatives removed and others intact.
240pub(crate) async fn delete_configurations_and_derivatives(
241  infra: &InfraContext<'_>,
242  token: &str,
243  candidates: &DeletionCandidates,
244) -> Result<(), Error> {
245  let cfs_session_name_vec: Vec<String> = candidates
246    .cfs_session_tuples
247    .iter()
248    .map(|(session, _, _)| session.clone())
249    .collect();
250
251  let bos_sessiontemplate_name_vec: Vec<String> = candidates
252    .bos_sessiontemplate_tuples
253    .iter()
254    .map(|(sessiontemplate, _, _)| sessiontemplate.clone())
255    .collect();
256
257  infra
258    .backend
259    .delete(
260      token,
261      &candidates.configuration_names,
262      &candidates.image_ids,
263      &cfs_session_name_vec,
264      &bos_sessiontemplate_name_vec,
265    )
266    .await?;
267
268  Ok(())
269}
270
271#[cfg(test)]
272mod tests {
273  use super::*;
274  use chrono::NaiveDateTime;
275
276  fn dt(s: &str) -> NaiveDateTime {
277    NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S").unwrap()
278  }
279
280  #[test]
281  fn validate_date_range_ok_when_since_before_until() {
282    assert!(
283      validate_date_range(
284        Some(dt("2024-01-01T00:00:00")),
285        Some(dt("2024-01-02T00:00:00"))
286      )
287      .is_ok()
288    );
289  }
290
291  #[test]
292  fn validate_date_range_ok_when_equal() {
293    let d = dt("2024-01-01T00:00:00");
294    assert!(validate_date_range(Some(d), Some(d)).is_ok());
295  }
296
297  #[test]
298  fn validate_date_range_ok_when_either_none() {
299    let d = dt("2024-01-01T00:00:00");
300    assert!(validate_date_range(Some(d), None).is_ok());
301    assert!(validate_date_range(None, Some(d)).is_ok());
302    assert!(validate_date_range(None, None).is_ok());
303  }
304
305  #[test]
306  fn validate_date_range_err_when_since_after_until() {
307    let result = validate_date_range(
308      Some(dt("2024-01-02T00:00:00")),
309      Some(dt("2024-01-01T00:00:00")),
310    );
311    assert!(result.is_err());
312    assert!(
313      result
314        .unwrap_err()
315        .to_string()
316        .contains("'since' date can't be after 'until' date")
317    );
318  }
319}