manta_server/service/
template.rs

1//! BOS session template queries and BOS session creation with
2//! access validation.
3//!
4//! BOS session creation runs a two-step
5//! [`validate_and_prepare_template_session`] + [`create_bos_session`]
6//! flow so authorization (which fans across template targets and the
7//! `limit` argument) is separate from the actual `post_template_session`
8//! call. The split also keeps each side individually unit-testable.
9
10use manta_backend_dispatcher::error::Error;
11use manta_backend_dispatcher::interfaces::bos::{
12  ClusterSessionTrait, ClusterTemplateTrait,
13};
14use manta_backend_dispatcher::interfaces::hsm::group::GroupTrait;
15use manta_backend_dispatcher::types::bos::session::BosSession;
16use manta_backend_dispatcher::types::bos::session::Operation;
17use manta_backend_dispatcher::types::bos::session_template::BosSessionTemplate;
18
19use crate::server::common::app_context::InfraContext;
20use crate::service::authorization::{
21  validate_user_group_members_access, validate_user_group_vec_access,
22};
23use crate::service::node_ops::validate_xname_format;
24pub use manta_shared::types::api::template::{
25  ApplyTemplateParams, GetTemplateParams,
26};
27
28/// List BOS session templates visible to the caller.
29///
30/// When `params.group_name` is unset the lookup spans every HSM
31/// group the token already grants access to. The backend filters
32/// templates whose targets intersect the resolved group set (and
33/// their member xnames), so the response stays scoped to what the
34/// caller could see by other means. Results are sorted by template
35/// name for stable output.
36pub async fn get_templates(
37  infra: &InfraContext<'_>,
38  token: &str,
39  params: &GetTemplateParams,
40) -> Result<Vec<BosSessionTemplate>, Error> {
41  // Get list of target groups the user is asking for
42  let target_group_vec: Vec<String> = if let Some(group) = &params.group_name {
43    vec![group.clone()]
44  } else {
45    infra
46      .backend
47      .get_group_available(token)
48      .await?
49      .iter()
50      .map(|group| group.label.clone())
51      .collect()
52  };
53
54  // Validate groups and get list of groups available
55  validate_user_group_vec_access(infra, token, &target_group_vec).await?;
56
57  let hsm_member_vec = infra
58    .backend
59    .get_member_vec_from_group_name_vec(token, &target_group_vec)
60    .await?;
61
62  let limit_ref = params.limit.as_ref();
63
64  tracing::info!(
65    "Get BOS sessiontemplates for HSM groups: {:?}",
66    target_group_vec
67  );
68
69  let mut bos_sessiontemplate_vec = infra
70    .backend
71    .get_and_filter_templates(
72      token,
73      &target_group_vec,
74      &hsm_member_vec,
75      params.name.as_deref(),
76      limit_ref,
77    )
78    .await?;
79
80  bos_sessiontemplate_vec.sort_by(|a, b| a.name.cmp(&b.name));
81
82  Ok(bos_sessiontemplate_vec)
83}
84
85/// Build the [`BosSession`] that
86/// [`create_bos_session`] will submit, after validating every
87/// xname/group the operation will touch.
88///
89/// Authorization runs in two passes: first against the template's
90/// own targets (group members or explicit xnames), then against each
91/// comma-separated entry of `params.limit`, which may itself be an
92/// xname or a group label. An unrecognised limit value yields
93/// `BadRequest`; a missing template yields `NotFound`. The returned
94/// `Vec<String>` is the split limit list, useful when the caller
95/// wants to display the resolved targets before creation.
96pub async fn validate_and_prepare_template_session(
97  infra: &InfraContext<'_>,
98  token: &str,
99  params: &ApplyTemplateParams,
100) -> Result<(BosSession, Vec<String>), Error> {
101  // Fetch BOS sessiontemplate
102  let bos_sessiontemplate_vec = infra
103    .backend
104    .get_and_filter_templates(
105      token,
106      &[],
107      &[],
108      Some(&params.bos_sessiontemplate_name),
109      None,
110    )
111    .await?;
112
113  let bos_sessiontemplate = if bos_sessiontemplate_vec.is_empty() {
114    return Err(Error::NotFound(format!(
115      "No BOS sessiontemplate '{}' found",
116      params.bos_sessiontemplate_name
117    )));
118  } else {
119    bos_sessiontemplate_vec.first().ok_or_else(|| {
120      Error::NotFound("BOS sessiontemplate list unexpectedly empty".to_string())
121    })?
122  };
123
124  // Validate user has access to the BOS sessiontemplate targets
125  tracing::info!(
126    "Validate user has access to HSM group in BOS sessiontemplate"
127  );
128  let target_hsm_vec = bos_sessiontemplate.get_target_hsm();
129  let target_xname_vec: Vec<String> = if !target_hsm_vec.is_empty() {
130    infra
131      .backend
132      .get_member_vec_from_group_name_vec(token, &target_hsm_vec)
133      .await
134      .unwrap_or_default()
135  } else {
136    bos_sessiontemplate.get_target_xname()
137  };
138
139  validate_user_group_members_access(infra, token, &target_xname_vec).await?;
140
141  // Validate user has access to xnames in `limit` argument
142  tracing::info!("Validate user has access to xnames in BOS sessiontemplate");
143  let limit_vec: Vec<String> =
144    params.limit.split(',').map(str::to_string).collect();
145
146  let mut xnames_to_validate_access_vec = Vec::new();
147
148  for limit_value in &limit_vec {
149    tracing::info!("Check if limit value '{}', is an xname", limit_value);
150    if validate_xname_format(limit_value) {
151      tracing::info!("limit value '{}' is an xname", limit_value);
152      xnames_to_validate_access_vec.push(limit_value.clone());
153    } else {
154      let hsm_members_vec_rslt = infra
155        .backend
156        .get_member_vec_from_group_name_vec(
157          token,
158          std::slice::from_ref(limit_value),
159        )
160        .await;
161
162      if let Ok(mut hsm_members_vec) = hsm_members_vec_rslt {
163        tracing::info!(
164          "Check if limit value '{}', is an HSM group name",
165          limit_value
166        );
167        xnames_to_validate_access_vec.append(&mut hsm_members_vec);
168      } else {
169        return Err(Error::BadRequest(format!(
170          "Value '{limit_value}' in 'limit' argument does not match \
171           an xname or a HSM group name."
172        )));
173      }
174    }
175  }
176
177  tracing::info!("Validate list of xnames translated from 'limit argument'");
178  validate_user_group_members_access(
179    infra,
180    token,
181    &xnames_to_validate_access_vec,
182  )
183  .await?;
184
185  tracing::info!("Access to '{}' granted. Continue.", params.limit);
186
187  // Build BOS session
188  let bos_session = BosSession {
189    name: params.bos_session_name.clone(),
190    tenant: None,
191    operation: Some(
192      Operation::from_str(&params.bos_session_operation).map_err(|_| {
193        Error::BadRequest(format!(
194          "Invalid BOS session operation '{}'",
195          params.bos_session_operation
196        ))
197      })?,
198    ),
199    template_name: params.bos_sessiontemplate_name.clone(),
200    limit: Some(limit_vec.join(",")),
201    stage: Some(false),
202    components: None,
203    include_disabled: Some(params.include_disabled),
204    status: None,
205  };
206
207  Ok((bos_session, limit_vec))
208}
209
210/// Submit a [`BosSession`] previously built by
211/// [`validate_and_prepare_template_session`].
212///
213/// This is a thin wrapper kept so the handler stays a one-liner and
214/// the validate / create steps remain separate testable units.
215pub async fn create_bos_session(
216  infra: &InfraContext<'_>,
217  token: &str,
218  bos_session: BosSession,
219) -> Result<BosSession, Error> {
220  infra
221    .backend
222    .post_template_session(token, bos_session)
223    .await
224}