nv_redfish/account/collection.rs
1// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Accounts collection utilities.
17//!
18//! Provides `AccountCollection` for working with the Redfish
19//! `ManagerAccountCollection`.
20//!
21//! - List members and fetch full account data without mutating the
22//! collection via `all_accounts_data`.
23//! - Create accounts:
24//! - Default: create a new `ManagerAccount` resource.
25//! - Slot-defined mode: reuse the first available disabled slot,
26//! honoring `min_slot` when configured.
27//!
28//! Configuration:
29//! - `account`: controls read patching via `read_patch_fn`.
30//! - `slot_defined_user_accounts`:
31//! - `min_slot`: minimum numeric slot id considered.
32//! - `hide_disabled`: omit disabled accounts from `all_accounts_data`.
33//! - `disable_account_on_delete`: prefer disabling over deletion.
34//!
35//! Other:
36//! - `odata_id()` returns the collection `@odata.id` (typically
37//! `/redfish/v1/AccountService/Accounts`).
38//! - Collection reads use `$expand` with depth 1 to materialize
39//! members when available.
40
41use crate::account::Account;
42use crate::account::AccountConfig;
43use crate::account::ManagerAccountCreate;
44use crate::account::ManagerAccountUpdate;
45use crate::patch_support::CollectionWithPatch;
46use crate::patch_support::CreateWithPatch;
47use crate::patch_support::ReadPatchFn;
48use crate::schema::manager_account::ManagerAccount;
49use crate::schema::manager_account_collection::ManagerAccountCollection;
50use crate::schema::resource::ResourceCollection;
51use crate::Error;
52use crate::NvBmc;
53use nv_redfish_core::Bmc;
54use nv_redfish_core::EntityTypeRef as _;
55use nv_redfish_core::ModificationResponse;
56use nv_redfish_core::NavProperty;
57use nv_redfish_core::ODataId;
58use std::sync::Arc;
59
60/// Configuration for slot-defined user accounts.
61///
62/// In slot-defined mode, accounts are pre-provisioned as numeric-id "slots".
63/// Creation reuses the first eligible disabled slot (respecting `min_slot`),
64/// listing may hide disabled slots, and deletion can disable instead of remove.
65#[derive(Clone)]
66pub struct SlotDefinedConfig {
67 /// Minimum slot number (the slot is identified by an `Id`
68 /// containing a numeric string).
69 pub min_slot: Option<u32>,
70 /// Hide disabled accounts when listing all accounts.
71 pub hide_disabled: bool,
72 /// Disable the account instead of deleting it.
73 pub disable_account_on_delete: bool,
74}
75
76/// Configuration for account collection behavior.
77///
78/// Combines per-account settings and optional slot-defined mode that changes
79/// how accounts are created, listed, and deleted.
80#[derive(Clone)]
81pub struct Config {
82 /// Configuration of `Account` objects.
83 pub account: AccountConfig,
84 /// Configuration for slot-defined user accounts.
85 pub slot_defined_user_accounts: Option<SlotDefinedConfig>,
86}
87
88/// Account collection.
89///
90/// Provides functions to access collection members.
91pub struct AccountCollection<B: Bmc> {
92 config: Config,
93 bmc: NvBmc<B>,
94 collection: Arc<ManagerAccountCollection>,
95}
96
97impl<B: Bmc> CollectionWithPatch<ManagerAccountCollection, ManagerAccount, B>
98 for AccountCollection<B>
99{
100 fn convert_patched(
101 base: ResourceCollection,
102 members: Vec<NavProperty<ManagerAccount>>,
103 ) -> ManagerAccountCollection {
104 ManagerAccountCollection { base, members }
105 }
106}
107
108impl<B: Bmc> CreateWithPatch<ManagerAccountCollection, ManagerAccount, ManagerAccountCreate, B>
109 for AccountCollection<B>
110{
111 fn entity_ref(&self) -> &ManagerAccountCollection {
112 self.collection.as_ref()
113 }
114 fn patch(&self) -> Option<&ReadPatchFn> {
115 self.config.account.read_patch_fn.as_ref()
116 }
117 fn bmc(&self) -> &B {
118 self.bmc.as_ref()
119 }
120}
121
122impl<B: Bmc> AccountCollection<B> {
123 pub(crate) async fn new(
124 bmc: NvBmc<B>,
125 collection_ref: &NavProperty<ManagerAccountCollection>,
126 config: Config,
127 ) -> Result<Self, Error<B>> {
128 let collection = Self::expand_collection(
129 &bmc,
130 collection_ref,
131 config.account.read_patch_fn.as_ref(),
132 None,
133 )
134 .await?;
135 Ok(Self {
136 config,
137 bmc,
138 collection,
139 })
140 }
141
142 /// `OData` identifier of the account collection in Redfish.
143 ///
144 /// Typically `/redfish/v1/AccountService/Accounts`.
145 #[must_use]
146 pub fn odata_id(&self) -> &ODataId {
147 self.collection.as_ref().odata_id()
148 }
149
150 /// Create a new account.
151 ///
152 /// Returns one of the following modification outcomes:
153 ///
154 /// - `ModificationResponse::Entity` contains the newly created account.
155 /// - `ModificationResponse::Task` identifies an asynchronous operation.
156 /// - `ModificationResponse::Empty` reports synchronous success without a
157 /// response body.
158 ///
159 /// # Errors
160 ///
161 /// Returns an error if creating a new account fails.
162 pub async fn create_account(
163 &self,
164 create: ManagerAccountCreate,
165 ) -> Result<ModificationResponse<Account<B>>, Error<B>> {
166 if let Some(cfg) = &self.config.slot_defined_user_accounts {
167 // For slot-defined configuration, find the first account
168 // that is disabled (and whose id is >= `min_slot`, if defined)
169 // and apply an update to it.
170 for nav in &self.collection.members {
171 let Ok(account) = Account::new(&self.bmc, nav, &self.config.account).await else {
172 continue;
173 };
174 if let Some(min) = cfg.min_slot {
175 // If the minimum id is configured and this slot id is below
176 // the threshold, look for another slot.
177 let Ok(id) = account.raw().base.id.parse::<u32>() else {
178 continue;
179 };
180 if id < min {
181 continue;
182 }
183 }
184 if account.is_enabled() {
185 // Slot is already explicitly enabled. Find another slot.
186 continue;
187 }
188 // Build an update based on the create request:
189 let update = ManagerAccountUpdate {
190 base: None,
191 user_name: Some(create.user_name),
192 password: Some(create.password),
193 role_id: Some(create.role_id),
194 enabled: Some(true),
195 account_expiration: create.account_expiration,
196 account_types: create.account_types,
197 email_address: create.email_address,
198 locked: create.locked,
199 oem_account_types: create.oem_account_types,
200 one_time_passcode_delivery_address: create.one_time_passcode_delivery_address,
201 password_change_required: create.password_change_required,
202 password_expiration: create.password_expiration,
203 phone_number: create.phone_number,
204 snmp: create.snmp,
205 strict_account_types: create.strict_account_types,
206 mfa_bypass: create.mfa_bypass,
207 links: None,
208 };
209
210 return account.update(&update).await;
211 }
212 // No available slot found
213 Err(Error::AccountSlotNotAvailable)
214 } else {
215 Ok(self
216 .create_with_patch(&create)
217 .await?
218 .map_entity(|account| {
219 Account::from_data(self.bmc.clone(), account, self.config.account.clone())
220 }))
221 }
222 }
223
224 /// Retrieve account data.
225 ///
226 /// This method does not update the collection itself. It only
227 /// retrieves all account data (if not already retrieved).
228 ///
229 /// # Errors
230 ///
231 /// Returns an error if retrieving account data fails. This can
232 /// occur if the account collection was not expanded.
233 pub async fn all_accounts_data(&self) -> Result<Vec<Account<B>>, Error<B>> {
234 let mut result = Vec::with_capacity(self.collection.members.len());
235 if let Some(cfg) = &self.config.slot_defined_user_accounts {
236 // For slot-defined account configuration, disabled accounts may be hidden
237 // to make it appear as if they were not created. This behavior is
238 // controlled by the `hide_disabled` configuration parameter.
239 for m in &self.collection.members {
240 let account = Account::new(&self.bmc, m, &self.config.account).await?;
241 if !cfg.hide_disabled || account.is_enabled() {
242 result.push(account);
243 }
244 }
245 } else {
246 for m in &self.collection.members {
247 result.push(Account::new(&self.bmc, m, &self.config.account).await?);
248 }
249 }
250 Ok(result)
251 }
252}