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}