manta_server/service/
sat_groups.rs

1//! SAT-entry → HSM group-name extractors.
2//!
3//! Pure helpers that read the HSM-group names a single SAT `images[]`
4//! or `session_templates[]` entry references, so handlers can gate
5//! access at the boundary via
6//! [`crate::service::authorization::validate_user_group_vec_access`]
7//! before delegating to the backend.
8//!
9//! The SAT schema lives in csm-rs and is carried as
10//! `serde_json::Value` end-to-end (see ARCHITECTURE.md). These
11//! functions accept the same `Value` shape the handler receives over
12//! the wire and read out a `Vec<String>` of group names; they make no
13//! mutation, do no I/O, and stay deliberately small so the wire
14//! schema can drift without breaking the helpers.
15//!
16//! The shapes they read mirror the csm-rs read paths exactly:
17//!
18//! - Image entry → `configuration_group_names: Vec<String>`
19//!   (`csm-rs/src/commands/i_apply_sat_file/utils/images.rs` —
20//!   `image_yaml.configuration_group_names`).
21//! - Session-template entry →
22//!   `bos_parameters.boot_sets.<set>.node_groups: Vec<String>`
23//!   collected and deduped across every boot_set
24//!   (`csm-rs/src/commands/i_apply_sat_file/utils/session_templates.rs:54-65`).
25
26use serde_json::Value;
27
28/// Read `configuration_group_names` from a SAT `images[]` entry.
29/// Returns an empty `Vec` when the field is absent or not an array.
30pub fn extract_image_groups(image: &Value) -> Vec<String> {
31  image
32    .get("configuration_group_names")
33    .and_then(Value::as_array)
34    .map(|arr| {
35      arr
36        .iter()
37        .filter_map(Value::as_str)
38        .map(str::to_string)
39        .collect()
40    })
41    .unwrap_or_default()
42}
43
44/// Read `bos_parameters.boot_sets.*.node_groups` from a SAT
45/// `session_templates[]` entry. Collects across every boot_set key
46/// (e.g. `compute`, `uan`) and deduplicates so a group named in
47/// multiple boot_sets is only validated once.
48pub fn extract_session_template_groups(
49  session_template: &Value,
50) -> Vec<String> {
51  let Some(boot_sets) = session_template
52    .get("bos_parameters")
53    .and_then(|p| p.get("boot_sets"))
54    .and_then(Value::as_object)
55  else {
56    return Vec::new();
57  };
58
59  let mut groups: Vec<String> = boot_sets
60    .values()
61    .filter_map(|set| set.get("node_groups"))
62    .filter_map(Value::as_array)
63    .flat_map(|arr| arr.iter().filter_map(Value::as_str).map(str::to_string))
64    .collect();
65  groups.sort();
66  groups.dedup();
67  groups
68}
69
70/// Read every HSM group name referenced anywhere in a SAT file —
71/// across all `images[]` and `session_templates[]` entries —
72/// deduplicated.
73///
74/// Returns an empty `Vec` for a SAT file with no groups (or no
75/// images / session_templates sections at all).
76///
77/// Used by [`crate::server::handlers::post_sat_validate`]
78/// to enforce HSM-group access before delegating to the backend.
79pub fn extract_all_target_groups(sat_file: &Value) -> Vec<String> {
80  let mut groups: Vec<String> = Vec::new();
81
82  if let Some(images) = sat_file.get("images").and_then(Value::as_array) {
83    for image in images {
84      groups.extend(extract_image_groups(image));
85    }
86  }
87
88  if let Some(templates) =
89    sat_file.get("session_templates").and_then(Value::as_array)
90  {
91    for tpl in templates {
92      groups.extend(extract_session_template_groups(tpl));
93    }
94  }
95
96  groups.sort();
97  groups.dedup();
98  groups
99}
100
101#[cfg(test)]
102mod tests {
103  use super::{extract_image_groups, extract_session_template_groups};
104  use serde_json::json;
105
106  #[test]
107  fn extract_image_groups_reads_configuration_group_names() {
108    let image = json!({
109      "name": "img-v1",
110      "configuration": "cfg-v1",
111      "configuration_group_names": ["compute", "uan"],
112    });
113    assert_eq!(extract_image_groups(&image), vec!["compute", "uan"]);
114  }
115
116  #[test]
117  fn extract_image_groups_empty_when_field_absent() {
118    let image = json!({ "name": "img-v1", "configuration": "cfg-v1" });
119    assert!(extract_image_groups(&image).is_empty());
120  }
121
122  #[test]
123  fn extract_image_groups_empty_when_field_is_not_array() {
124    let image = json!({
125      "name": "img-v1",
126      "configuration_group_names": "compute",
127    });
128    assert!(extract_image_groups(&image).is_empty());
129  }
130
131  #[test]
132  fn extract_session_template_groups_reads_all_boot_sets() {
133    let template = json!({
134      "name": "st-1",
135      "bos_parameters": {
136        "boot_sets": {
137          "compute": { "node_groups": ["compute", "shared"] },
138          "uan":     { "node_groups": ["uan",     "shared"] },
139        }
140      }
141    });
142    let groups = extract_session_template_groups(&template);
143    assert_eq!(groups, vec!["compute", "shared", "uan"]);
144  }
145
146  #[test]
147  fn extract_session_template_groups_empty_when_bos_parameters_missing() {
148    let template = json!({ "name": "st-1" });
149    assert!(extract_session_template_groups(&template).is_empty());
150  }
151
152  #[test]
153  fn extract_session_template_groups_empty_when_boot_sets_missing() {
154    let template = json!({ "name": "st-1", "bos_parameters": {} });
155    assert!(extract_session_template_groups(&template).is_empty());
156  }
157
158  #[test]
159  fn extract_session_template_groups_skips_boot_sets_without_node_groups() {
160    let template = json!({
161      "name": "st-1",
162      "bos_parameters": {
163        "boot_sets": {
164          "compute": { "node_groups": ["compute"] },
165          "uan":     { "kernel": "linux" }
166        }
167      }
168    });
169    assert_eq!(extract_session_template_groups(&template), vec!["compute"]);
170  }
171
172  #[test]
173  fn extract_all_target_groups_empty_sat_file_returns_empty() {
174    let sat = json!({});
175    assert!(super::extract_all_target_groups(&sat).is_empty());
176  }
177
178  #[test]
179  fn extract_all_target_groups_collects_from_images_and_templates() {
180    let sat = json!({
181      "images": [
182        { "name": "img-1", "configuration_group_names": ["compute", "uan"] },
183        { "name": "img-2", "configuration_group_names": ["compute"] },
184      ],
185      "session_templates": [
186        {
187          "name": "st-1",
188          "bos_parameters": {
189            "boot_sets": {
190              "compute": { "node_groups": ["compute"] },
191              "uan":     { "node_groups": ["uan", "admin"] },
192            }
193          }
194        }
195      ]
196    });
197    let mut got = super::extract_all_target_groups(&sat);
198    got.sort();
199    assert_eq!(got, vec!["admin", "compute", "uan"]);
200  }
201
202  #[test]
203  fn extract_all_target_groups_handles_missing_sections() {
204    let sat = json!({ "images": [ { "name": "img", "configuration_group_names": ["g1"] } ] });
205    let got = super::extract_all_target_groups(&sat);
206    assert_eq!(got, vec!["g1"]);
207  }
208}