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#[cfg(test)]
71mod tests {
72  use super::{extract_image_groups, extract_session_template_groups};
73  use serde_json::json;
74
75  #[test]
76  fn extract_image_groups_reads_configuration_group_names() {
77    let image = json!({
78      "name": "img-v1",
79      "configuration": "cfg-v1",
80      "configuration_group_names": ["compute", "uan"],
81    });
82    assert_eq!(extract_image_groups(&image), vec!["compute", "uan"]);
83  }
84
85  #[test]
86  fn extract_image_groups_empty_when_field_absent() {
87    let image = json!({ "name": "img-v1", "configuration": "cfg-v1" });
88    assert!(extract_image_groups(&image).is_empty());
89  }
90
91  #[test]
92  fn extract_image_groups_empty_when_field_is_not_array() {
93    let image = json!({
94      "name": "img-v1",
95      "configuration_group_names": "compute",
96    });
97    assert!(extract_image_groups(&image).is_empty());
98  }
99
100  #[test]
101  fn extract_session_template_groups_reads_all_boot_sets() {
102    let template = json!({
103      "name": "st-1",
104      "bos_parameters": {
105        "boot_sets": {
106          "compute": { "node_groups": ["compute", "shared"] },
107          "uan":     { "node_groups": ["uan",     "shared"] },
108        }
109      }
110    });
111    let groups = extract_session_template_groups(&template);
112    assert_eq!(groups, vec!["compute", "shared", "uan"]);
113  }
114
115  #[test]
116  fn extract_session_template_groups_empty_when_bos_parameters_missing() {
117    let template = json!({ "name": "st-1" });
118    assert!(extract_session_template_groups(&template).is_empty());
119  }
120
121  #[test]
122  fn extract_session_template_groups_empty_when_boot_sets_missing() {
123    let template = json!({ "name": "st-1", "bos_parameters": {} });
124    assert!(extract_session_template_groups(&template).is_empty());
125  }
126
127  #[test]
128  fn extract_session_template_groups_skips_boot_sets_without_node_groups() {
129    let template = json!({
130      "name": "st-1",
131      "bos_parameters": {
132        "boot_sets": {
133          "compute": { "node_groups": ["compute"] },
134          "uan":     { "kernel": "linux" }
135        }
136      }
137    });
138    assert_eq!(extract_session_template_groups(&template), vec!["compute"]);
139  }
140}