manta_server/service/
redfish.rs

1//! HSM Redfish-endpoint registry queries and CRUD.
2//!
3//! Wraps the `/Inventory/RedfishEndpoints` HSM family. A registration
4//! describes how HSM should talk to a BMC: hostname/FQDN,
5//! credentials, MAC and IP, discovery flags. Every mutation runs
6//! through [`validate_user_group_members_access`] so a caller can
7//! only touch a BMC whose xname they already have group access to.
8//!
9//! Listing is the only special case: admin tokens may broadly list
10//! every endpoint, but non-admin callers MUST scope the query by
11//! `id`, because the full listing would otherwise leak every BMC's
12//! identity and credentials cluster-wide.
13
14use manta_backend_dispatcher::error::Error;
15use manta_backend_dispatcher::interfaces::hsm::redfish_endpoint::RedfishEndpointTrait;
16use manta_backend_dispatcher::types::hsm::inventory::{
17  RedfishEndpoint, RedfishEndpointArray,
18};
19
20use crate::{
21  server::common::app_context::InfraContext,
22  service::authorization::{is_admin, validate_user_group_members_access},
23};
24pub use manta_shared::types::api::redfish_endpoints::{
25  GetRedfishEndpointsParams, UpdateRedfishEndpointParams,
26};
27
28/// Convert a `UpdateRedfishEndpointParams` (CLI/HTTP wire shape) into a
29/// backend [`RedfishEndpoint`] suitable for `add_redfish_endpoint` /
30/// `update_redfish_endpoint`. Pure mapping — no I/O.
31pub(crate) fn params_to_redfish_endpoint(
32  params: UpdateRedfishEndpointParams,
33) -> RedfishEndpoint {
34  RedfishEndpoint {
35    id: params.id,
36    name: params.name,
37    hostname: params.hostname,
38    domain: params.domain,
39    fqdn: params.fqdn,
40    enabled: Some(params.enabled),
41    user: params.user,
42    password: params.password,
43    use_ssdp: Some(params.use_ssdp),
44    mac_required: Some(params.mac_required),
45    mac_addr: params.mac_addr,
46    ip_address: params.ip_address,
47    rediscover_on_update: Some(params.rediscover_on_update),
48    template_id: params.template_id,
49    r#type: None,
50    uuid: None,
51    discovery_info: None,
52  }
53}
54
55/// List Redfish endpoint registrations, applying any caller-supplied
56/// filters (`id` / `fqdn` / `uuid` / `macaddr` / `ipaddress`).
57///
58/// Authorization rules:
59/// - Admin tokens (carrying [`crate::service::authorization::PA_ADMIN`])
60///   may list every endpoint, with or without filters.
61/// - Non-admin callers MUST scope the request by `id`. The xname is
62///   then validated against the caller's accessible groups; without
63///   an `id`, the response could leak every BMC's identity and
64///   credentials. The non-admin broad listing returns `BadRequest`.
65///
66/// # Errors
67///
68/// - [`Error::BadRequest`] when a non-admin caller omits `params.id`,
69///   or names an xname outside their accessible groups.
70/// - [`Error::NetError`] / [`Error::CsmError`] from the backend
71///   `get_redfish_endpoints` call.
72pub async fn get_redfish_endpoints(
73  infra: &InfraContext<'_>,
74  token: &str,
75  params: &GetRedfishEndpointsParams,
76) -> Result<RedfishEndpointArray, Error> {
77  tracing::info!("Get Redfish endpoints");
78
79  if !is_admin(token) {
80    let Some(xname) = params.id.as_deref() else {
81      return Err(Error::BadRequest(
82        "Non-admin callers must scope a Redfish-endpoints query by `id`."
83          .to_string(),
84      ));
85    };
86    validate_user_group_members_access(infra, token, &[xname.to_string()])
87      .await?;
88  }
89
90  infra
91    .backend
92    .get_redfish_endpoints(
93      token,
94      params.id.as_deref(),
95      params.fqdn.as_deref(),
96      None,
97      params.uuid.as_deref(),
98      params.macaddr.as_deref(),
99      params.ipaddress.as_deref(),
100      None,
101    )
102    .await
103}
104
105/// Register a new Redfish endpoint with HSM.
106///
107/// The caller-supplied `UpdateRedfishEndpointParams` is converted to a
108/// single-element `RedfishEndpointArray` before reaching the backend.
109///
110/// # Errors
111///
112/// - [`Error::BadRequest`] when the caller cannot reach `params.id`
113///   through any of their HSM groups.
114/// - [`Error::NetError`] / [`Error::CsmError`] from the backend
115///   `add_redfish_endpoint` call (including conflicts when an
116///   endpoint with that `id` is already registered).
117pub async fn add_redfish_endpoint(
118  infra: &InfraContext<'_>,
119  token: &str,
120  params: UpdateRedfishEndpointParams,
121) -> Result<(), Error> {
122  tracing::info!("Add Redfish endpoint id={}", params.id);
123
124  validate_user_group_members_access(
125    infra,
126    token,
127    std::slice::from_ref(&params.id),
128  )
129  .await?;
130
131  let endpoint = params_to_redfish_endpoint(params);
132  let array = RedfishEndpointArray {
133    redfish_endpoints: Some(vec![endpoint]),
134  };
135  infra.backend.add_redfish_endpoint(token, &array).await
136}
137
138/// Update an existing Redfish endpoint's properties.
139///
140/// All fields on `UpdateRedfishEndpointParams` are written; partial
141/// updates aren't supported by the backend contract.
142///
143/// # Errors
144///
145/// - [`Error::BadRequest`] when the caller cannot reach `params.id`.
146/// - [`Error::NetError`] / [`Error::CsmError`] from the backend
147///   `update_redfish_endpoint` call.
148pub async fn update_redfish_endpoint(
149  infra: &InfraContext<'_>,
150  token: &str,
151  params: UpdateRedfishEndpointParams,
152) -> Result<(), Error> {
153  tracing::info!("Update Redfish endpoint id={}", params.id);
154
155  validate_user_group_members_access(
156    infra,
157    token,
158    std::slice::from_ref(&params.id),
159  )
160  .await?;
161
162  let endpoint = params_to_redfish_endpoint(params);
163  infra
164    .backend
165    .update_redfish_endpoint(token, &endpoint)
166    .await
167}
168
169/// Delete a Redfish endpoint registration by id (BMC xname).
170///
171/// `NotFound` is surfaced by the backend when `id` does not match an
172/// existing registration; the service forwards it unchanged.
173///
174/// # Errors
175///
176/// - [`Error::BadRequest`] when the caller cannot reach `id`.
177/// - [`Error::NotFound`] / other backend errors from the
178///   `delete_redfish_endpoint` call.
179pub async fn delete_redfish_endpoint(
180  infra: &InfraContext<'_>,
181  token: &str,
182  id: &str,
183) -> Result<(), Error> {
184  tracing::info!("Delete Redfish endpoint id={}", id);
185
186  validate_user_group_members_access(infra, token, &[id.to_string()]).await?;
187
188  infra
189    .backend
190    .delete_redfish_endpoint(token, id)
191    .await
192    .map(|_| ())
193}