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::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 = Self::expand_collection(
128            &bmc,
129            collection_ref,
130            config.account.read_patch_fn.as_ref(),
131            None,
132        )
133        .await?;
134        Ok(Self {
135            config,
136            bmc,
137            collection,
138        })
139    }
140
141    /// `OData` identifier of the account collection in Redfish.
142    ///
143    /// Typically `/redfish/v1/AccountService/Accounts`.
144    #[must_use]
145    pub fn odata_id(&self) -> &ODataId {
146        self.collection.as_ref().odata_id()
147    }
148
149    /// Create a new account.
150    ///
151    /// # Errors
152    ///
153    /// Returns an error if creating a new account fails.
154    pub async fn create_account(
155        &self,
156        create: ManagerAccountCreate,
157    ) -> Result<Option<Account<B>>, Error<B>> {
158        if let Some(cfg) = &self.config.slot_defined_user_accounts {
159            // For slot-defined configuration, find the first account
160            // that is disabled (and whose id is >= `min_slot`, if defined)
161            // and apply an update to it.
162            for nav in &self.collection.members {
163                let Ok(account) = Account::new(&self.bmc, nav, &self.config.account).await else {
164                    continue;
165                };
166                if let Some(min) = cfg.min_slot {
167                    // If the minimum id is configured and this slot id is below
168                    // the threshold, look for another slot.
169                    let Ok(id) = account.raw().base.id.parse::<u32>() else {
170                        continue;
171                    };
172                    if id < min {
173                        continue;
174                    }
175                }
176                if account.is_enabled() {
177                    // Slot is already explicitly enabled. Find another slot.
178                    continue;
179                }
180                // Build an update based on the create request:
181                let update = ManagerAccountUpdate {
182                    base: None,
183                    user_name: Some(create.user_name),
184                    password: Some(create.password),
185                    role_id: Some(create.role_id),
186                    enabled: Some(true),
187                    account_expiration: create.account_expiration,
188                    account_types: create.account_types,
189                    email_address: create.email_address,
190                    locked: create.locked,
191                    oem_account_types: create.oem_account_types,
192                    one_time_passcode_delivery_address: create.one_time_passcode_delivery_address,
193                    password_change_required: create.password_change_required,
194                    password_expiration: create.password_expiration,
195                    phone_number: create.phone_number,
196                    snmp: create.snmp,
197                    strict_account_types: create.strict_account_types,
198                    mfa_bypass: create.mfa_bypass,
199                };
200
201                return account.update(&update).await;
202            }
203            // No available slot found
204            Err(Error::AccountSlotNotAvailable)
205        } else {
206            let outcome = self.create_with_patch(&create).await?;
207            Ok(match outcome {
208                nv_redfish_core::ModificationResponse::Entity(account) => Some(Account::from_data(
209                    self.bmc.clone(),
210                    account,
211                    self.config.account.clone(),
212                )),
213                nv_redfish_core::ModificationResponse::Task(_)
214                | nv_redfish_core::ModificationResponse::Empty => None,
215            })
216        }
217    }
218
219    /// Retrieve account data.
220    ///
221    /// This method does not update the collection itself. It only
222    /// retrieves all account data (if not already retrieved).
223    ///
224    /// # Errors
225    ///
226    /// Returns an error if retrieving account data fails. This can
227    /// occur if the account collection was not expanded.
228    pub async fn all_accounts_data(&self) -> Result<Vec<Account<B>>, Error<B>> {
229        let mut result = Vec::with_capacity(self.collection.members.len());
230        if let Some(cfg) = &self.config.slot_defined_user_accounts {
231            // For slot-defined account configuration, disabled accounts may be hidden
232            // to make it appear as if they were not created. This behavior is
233            // controlled by the `hide_disabled` configuration parameter.
234            for m in &self.collection.members {
235                let account = Account::new(&self.bmc, m, &self.config.account).await?;
236                if !cfg.hide_disabled || account.is_enabled() {
237                    result.push(account);
238                }
239            }
240        } else {
241            for m in &self.collection.members {
242                result.push(Account::new(&self.bmc, m, &self.config.account).await?);
243            }
244        }
245        Ok(result)
246    }
247}