manta_server/service/
template.rs

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