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