Skip to main content

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}