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::redfish::manager_account::ManagerAccount;
49use crate::schema::redfish::manager_account_collection::ManagerAccountCollection;
50use crate::schema::redfish::resource::ResourceCollection;
51use crate::Error;
52use crate::NvBmc;
53use nv_redfish_core::Bmc;
54use nv_redfish_core::EntityTypeRef as _;
55use nv_redfish_core::NavProperty;
56use nv_redfish_core::ODataId;
57use std::sync::Arc;
58
59/// Configuration for slot-defined user accounts.
60///
61/// In slot-defined mode, accounts are pre-provisioned as numeric-id "slots".
62/// Creation reuses the first eligible disabled slot (respecting `min_slot`),
63/// listing may hide disabled slots, and deletion can disable instead of remove.
64#[derive(Clone)]
65pub struct SlotDefinedConfig {
66 /// Minimum slot number (the slot is identified by an `Id`
67 /// containing a numeric string).
68 pub min_slot: Option<u32>,
69 /// Hide disabled accounts when listing all accounts.
70 pub hide_disabled: bool,
71 /// Disable the account instead of deleting it.
72 pub disable_account_on_delete: bool,
73}
74
75/// Configuration for account collection behavior.
76///
77/// Combines per-account settings and optional slot-defined mode that changes
78/// how accounts are created, listed, and deleted.
79#[derive(Clone)]
80pub struct Config {
81 /// Configuration of `Account` objects.
82 pub account: AccountConfig,
83 /// Configuration for slot-defined user accounts.
84 pub slot_defined_user_accounts: Option<SlotDefinedConfig>,
85}
86
87/// Account collection.
88///
89/// Provides functions to access collection members.
90pub struct AccountCollection<B: Bmc> {
91 config: Config,
92 bmc: NvBmc<B>,
93 collection: Arc<ManagerAccountCollection>,
94}
95
96impl<B: Bmc> CollectionWithPatch<ManagerAccountCollection, ManagerAccount, B>
97 for AccountCollection<B>
98{
99 fn convert_patched(
100 base: ResourceCollection,
101 members: Vec<NavProperty<ManagerAccount>>,
102 ) -> ManagerAccountCollection {
103 ManagerAccountCollection { base, members }
104 }
105}
106
107impl<B: Bmc> CreateWithPatch<ManagerAccountCollection, ManagerAccount, ManagerAccountCreate, B>
108 for AccountCollection<B>
109{
110 fn entity_ref(&self) -> &ManagerAccountCollection {
111 self.collection.as_ref()
112 }
113 fn patch(&self) -> Option<&ReadPatchFn> {
114 self.config.account.read_patch_fn.as_ref()
115 }
116 fn bmc(&self) -> &B {
117 self.bmc.as_ref()
118 }
119}
120
121impl<B: Bmc> AccountCollection<B> {
122 pub(crate) async fn new(
123 bmc: NvBmc<B>,
124 collection_ref: &NavProperty<ManagerAccountCollection>,
125 config: Config,
126 ) -> Result<Self, Error<B>> {
127 let collection =
128 Self::expand_collection(&bmc, collection_ref, config.account.read_patch_fn.as_ref())
129 .await?;
130 Ok(Self {
131 config,
132 bmc,
133 collection,
134 })
135 }
136
137 /// `OData` identifier of the account collection in Redfish.
138 ///
139 /// Typically `/redfish/v1/AccountService/Accounts`.
140 #[must_use]
141 pub fn odata_id(&self) -> &ODataId {
142 self.collection.as_ref().id()
143 }
144
145 /// Create a new account.
146 ///
147 /// # Errors
148 ///
149 /// Returns an error if creating a new account fails.
150 pub async fn create_account(
151 &self,
152 create: ManagerAccountCreate,
153 ) -> Result<Account<B>, Error<B>> {
154 if let Some(cfg) = &self.config.slot_defined_user_accounts {
155 // For slot-defined configuration, find the first account
156 // that is disabled (and whose id is >= `min_slot`, if defined)
157 // and apply an update to it.
158 for nav in &self.collection.members {
159 let Ok(account) = Account::new(&self.bmc, nav, &self.config.account).await else {
160 continue;
161 };
162 if let Some(min) = cfg.min_slot {
163 // If the minimum id is configured and this slot id is below
164 // the threshold, look for another slot.
165 let Ok(id) = account.raw().base.id.parse::<u32>() else {
166 continue;
167 };
168 if id < min {
169 continue;
170 }
171 }
172 if account.is_enabled() {
173 // Slot is already explicitly enabled. Find another slot.
174 continue;
175 }
176 // Build an update based on the create request:
177 let update = ManagerAccountUpdate {
178 base: None,
179 user_name: Some(create.user_name),
180 password: Some(create.password),
181 role_id: Some(create.role_id),
182 enabled: Some(true),
183 account_expiration: create.account_expiration,
184 account_types: create.account_types,
185 email_address: create.email_address,
186 locked: create.locked,
187 oem_account_types: create.oem_account_types,
188 one_time_passcode_delivery_address: create.one_time_passcode_delivery_address,
189 password_change_required: create.password_change_required,
190 password_expiration: create.password_expiration,
191 phone_number: create.phone_number,
192 snmp: create.snmp,
193 strict_account_types: create.strict_account_types,
194 mfa_bypass: create.mfa_bypass,
195 };
196
197 return account.update(&update).await;
198 }
199 // No available slot found
200 Err(Error::AccountSlotNotAvailable)
201 } else {
202 let account = self.create_with_patch(&create).await?;
203 Ok(Account::from_data(
204 self.bmc.clone(),
205 account,
206 self.config.account.clone(),
207 ))
208 }
209 }
210
211 /// Retrieve account data.
212 ///
213 /// This method does not update the collection itself. It only
214 /// retrieves all account data (if not already retrieved).
215 ///
216 /// # Errors
217 ///
218 /// Returns an error if retrieving account data fails. This can
219 /// occur if the account collection was not expanded.
220 pub async fn all_accounts_data(&self) -> Result<Vec<Account<B>>, Error<B>> {
221 let mut result = Vec::with_capacity(self.collection.members.len());
222 if let Some(cfg) = &self.config.slot_defined_user_accounts {
223 // For slot-defined account configuration, disabled accounts may be hidden
224 // to make it appear as if they were not created. This behavior is
225 // controlled by the `hide_disabled` configuration parameter.
226 for m in &self.collection.members {
227 let account = Account::new(&self.bmc, m, &self.config.account).await?;
228 if !cfg.hide_disabled || account.is_enabled() {
229 result.push(account);
230 }
231 }
232 } else {
233 for m in &self.collection.members {
234 result.push(Account::new(&self.bmc, m, &self.config.account).await?);
235 }
236 }
237 Ok(result)
238 }
239}