diff --git a/rust/agama-lib/src/install_settings.rs b/rust/agama-lib/src/install_settings.rs index e0f7a3add8..570d6f8b61 100644 --- a/rust/agama-lib/src/install_settings.rs +++ b/rust/agama-lib/src/install_settings.rs @@ -26,7 +26,7 @@ use crate::context::InstallationContext; use crate::hostname::model::HostnameSettings; use crate::security::settings::SecuritySettings; use crate::storage::settings::zfcp::ZFCPConfig; -use crate::{network::NetworkSettings, storage::settings::dasd::DASDConfig, users::UserSettings}; +use crate::{network::NetworkSettings, storage::settings::dasd::DASDConfig}; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; use std::default::Default; @@ -43,7 +43,7 @@ pub enum InstallSettingsError { /// Installation settings /// /// This struct represents installation settings. It serves as an entry point and it is composed of -/// other structs which hold the settings for each area ("users", "software", etc.). +/// other structs which hold the settings for each area ("software", etc.). #[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct InstallSettings { @@ -56,8 +56,6 @@ pub struct InstallSettings { #[serde(skip_serializing_if = "Option::is_none")] #[schema(value_type = Object)] pub iscsi: Option>, - #[serde(flatten)] - pub user: Option, #[serde(skip_serializing_if = "Option::is_none")] pub security: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/rust/agama-lib/src/lib.rs b/rust/agama-lib/src/lib.rs index 8684f2c6f7..1bd0770cc9 100644 --- a/rust/agama-lib/src/lib.rs +++ b/rust/agama-lib/src/lib.rs @@ -63,7 +63,6 @@ pub mod questions; pub mod security; pub mod storage; mod store; -pub mod users; pub use store::Store; pub mod utils; pub use agama_utils::{dbus, openapi}; diff --git a/rust/agama-lib/src/store.rs b/rust/agama-lib/src/store.rs index 01052cfb7d..ed6419df55 100644 --- a/rust/agama-lib/src/store.rs +++ b/rust/agama-lib/src/store.rs @@ -39,7 +39,6 @@ use crate::{ }, StorageStore, StorageStoreError, }, - users::{UsersStore, UsersStoreError}, }; #[derive(Debug, thiserror::Error)] @@ -51,8 +50,6 @@ pub enum StoreError { #[error(transparent)] Hostname(#[from] HostnameStoreError), #[error(transparent)] - Users(#[from] UsersStoreError), - #[error(transparent)] Network(#[from] NetworkStoreError), #[error(transparent)] Security(#[from] SecurityStoreError), @@ -76,7 +73,6 @@ pub struct Store { bootloader: BootloaderStore, dasd: DASDStore, hostname: HostnameStore, - users: UsersStore, network: NetworkStore, security: SecurityStore, storage: StorageStore, @@ -91,7 +87,6 @@ impl Store { bootloader: BootloaderStore::new(http_client.clone()), dasd: DASDStore::new(http_client.clone()), hostname: HostnameStore::new(http_client.clone()), - users: UsersStore::new(http_client.clone()), network: NetworkStore::new(http_client.clone()), security: SecurityStore::new(http_client.clone()), storage: StorageStore::new(http_client.clone()), @@ -109,7 +104,6 @@ impl Store { hostname: Some(self.hostname.load().await?), network: Some(self.network.load().await?), security: self.security.load().await?.to_option(), - user: Some(self.users.load().await?), zfcp: self.zfcp.load().await?, ..Default::default() }; @@ -138,9 +132,6 @@ impl Store { if let Some(security) = &settings.security { self.security.store(security).await?; } - if let Some(user) = &settings.user { - self.users.store(user).await?; - } let mut dirty_flag_set = false; // iscsi has to be done before storage if let Some(iscsi) = &settings.iscsi { diff --git a/rust/agama-lib/src/users.rs b/rust/agama-lib/src/users.rs deleted file mode 100644 index fb2aa9ff72..0000000000 --- a/rust/agama-lib/src/users.rs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) [2024] SUSE LLC -// -// All Rights Reserved. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 2 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, contact SUSE LLC. -// -// To contact SUSE LLC about this file by physical or electronic mail, you may -// find current contact information at www.suse.com. - -//! Implements support for handling the users settings - -mod client; -mod http_client; -pub mod model; -pub mod proxies; -mod settings; -mod store; - -pub use client::{FirstUser, RootUser, UsersClient}; -pub use http_client::UsersHTTPClient; -pub use settings::{FirstUserSettings, RootUserSettings, UserPassword, UserSettings}; -pub use store::{UsersStore, UsersStoreError}; diff --git a/rust/agama-lib/src/users/client.rs b/rust/agama-lib/src/users/client.rs deleted file mode 100644 index 5aa08bc92a..0000000000 --- a/rust/agama-lib/src/users/client.rs +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (c) [2024] SUSE LLC -// -// All Rights Reserved. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 2 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, contact SUSE LLC. -// -// To contact SUSE LLC about this file by physical or electronic mail, you may -// find current contact information at www.suse.com. - -//! Implements a client to access Agama's users service. - -use super::{ - proxies::{FirstUser as FirstUserFromDBus, RootUser as RootUserFromDBus, Users1Proxy}, - FirstUserSettings, -}; -use crate::error::ServiceError; -use serde::{Deserialize, Serialize}; -use zbus::Connection; - -/// Represents the settings for the first user -#[derive(Serialize, Deserialize, Clone, Debug, Default, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct FirstUser { - /// First user's full name - pub full_name: String, - /// First user's username - pub user_name: String, - /// First user's password (in clear text) - pub password: String, - /// Whether the password is hashed (true) or is plain text (false) - pub hashed_password: bool, -} - -impl FirstUser { - pub fn from_dbus(dbus_data: zbus::Result) -> zbus::Result { - let data = dbus_data?; - Ok(Self { - full_name: data.0, - user_name: data.1, - password: data.2, - hashed_password: data.3, - }) - } -} - -impl From<&FirstUserSettings> for FirstUser { - fn from(value: &FirstUserSettings) -> Self { - FirstUser { - user_name: value.user_name.clone().unwrap_or_default(), - full_name: value.full_name.clone().unwrap_or_default(), - password: value - .password - .as_ref() - .map(|p| p.password.clone()) - .unwrap_or_default(), - hashed_password: value - .password - .as_ref() - .map(|p| p.hashed_password) - .unwrap_or_default(), - } - } -} - -/// Represents the settings for the first user -#[derive(Serialize, Deserialize, Clone, Debug, Default, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct RootUser { - /// Root user password - #[serde(skip_serializing_if = "Option::is_none")] - pub password: Option, - /// Whether the password is hashed (true) or is plain text (false or None) - pub hashed_password: Option, - /// SSH public key - #[serde(skip_serializing_if = "Option::is_none")] - pub ssh_public_key: Option, -} - -impl RootUser { - pub fn from_dbus(dbus_data: RootUserFromDBus) -> zbus::Result { - let password = if dbus_data.0.is_empty() { - None - } else { - Some(dbus_data.0) - }; - - let ssh_public_key = if dbus_data.2.is_empty() { - None - } else { - Some(dbus_data.2) - }; - - Ok(Self { - password, - hashed_password: Some(dbus_data.1), - ssh_public_key, - }) - } -} - -/// D-Bus client for the users service -#[derive(Clone)] -pub struct UsersClient<'a> { - users_proxy: Users1Proxy<'a>, -} - -impl<'a> UsersClient<'a> { - pub async fn new(connection: Connection) -> zbus::Result> { - Ok(Self { - users_proxy: Users1Proxy::new(&connection).await?, - }) - } - - /// Returns the settings for first non admin user - pub async fn first_user(&self) -> zbus::Result { - FirstUser::from_dbus(self.users_proxy.first_user().await) - } - - pub async fn root_user(&self) -> zbus::Result { - RootUser::from_dbus(self.users_proxy.root_user().await?) - } - - /// SetRootPassword method - pub async fn set_root_password(&self, value: &str, hashed: bool) -> Result { - Ok(self.users_proxy.set_root_password(value, hashed).await?) - } - - pub async fn remove_root_password(&self) -> Result { - Ok(self.users_proxy.remove_root_password().await?) - } - - /// SetRootSSHKey method - pub async fn set_root_sshkey(&self, value: &str) -> Result { - Ok(self.users_proxy.set_root_sshkey(value).await?) - } - - /// Set the configuration for the first user - pub async fn set_first_user( - &self, - first_user: &FirstUser, - ) -> zbus::Result<(bool, Vec)> { - self.users_proxy - .set_first_user( - &first_user.full_name, - &first_user.user_name, - &first_user.password, - first_user.hashed_password, - std::collections::HashMap::new(), - ) - .await - } - - pub async fn remove_first_user(&self) -> zbus::Result { - Ok(self.users_proxy.remove_first_user().await? == 0) - } -} diff --git a/rust/agama-lib/src/users/http_client.rs b/rust/agama-lib/src/users/http_client.rs deleted file mode 100644 index 52808739ad..0000000000 --- a/rust/agama-lib/src/users/http_client.rs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) [2024] SUSE LLC -// -// All Rights Reserved. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 2 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, contact SUSE LLC. -// -// To contact SUSE LLC about this file by physical or electronic mail, you may -// find current contact information at www.suse.com. - -use super::client::{FirstUser, RootUser}; -use crate::http::{BaseHTTPClient, BaseHTTPClientError}; -use crate::users::model::RootPatchSettings; - -#[derive(Debug, thiserror::Error)] -pub enum UsersHTTPClientError { - #[error(transparent)] - HTTP(#[from] BaseHTTPClientError), - #[error("Wrong user parameters: '{0:?}'")] - WrongUser(Vec), - #[error("Could not parse user issues: {0}")] - InvalidUserIssues(#[from] serde_json::Error), -} - -pub struct UsersHTTPClient { - client: BaseHTTPClient, -} - -impl UsersHTTPClient { - pub fn new(client: BaseHTTPClient) -> Self { - Self { client } - } - - /// Returns the settings for first non admin user - pub async fn first_user(&self) -> Result { - Ok(self.client.get("/users/first").await?) - } - - /// Set the configuration for the first user - pub async fn set_first_user(&self, first_user: &FirstUser) -> Result<(), UsersHTTPClientError> { - let result = self.client.put_void("/users/first", first_user).await; - - if let Err(BaseHTTPClientError::BackendError(422, ref issues_s)) = result { - return match serde_json::from_str::>(issues_s) { - Ok(issues) => Err(UsersHTTPClientError::WrongUser(issues)), - Err(e) => Err(UsersHTTPClientError::InvalidUserIssues(e)), - }; - } - - Ok(result?) - } - - pub async fn root_user(&self) -> Result { - Ok(self.client.get("/users/root").await?) - } - - /// SetRootPassword method. - /// Returns 0 if successful (always, for current backend) - pub async fn set_root_password( - &self, - value: &str, - hashed: bool, - ) -> Result { - let rps = RootPatchSettings { - ssh_public_key: None, - password: Some(value.to_owned()), - hashed_password: Some(hashed), - }; - let ret = self.client.patch("/users/root", &rps).await?; - Ok(ret) - } - - /// SetRootSSHKey method. - /// Returns 0 if successful (always, for current backend) - pub async fn set_root_sshkey(&self, value: &str) -> Result { - let rps = RootPatchSettings { - ssh_public_key: Some(value.to_owned()), - password: None, - hashed_password: None, - }; - let ret = self.client.patch("/users/root", &rps).await?; - Ok(ret) - } -} diff --git a/rust/agama-lib/src/users/model.rs b/rust/agama-lib/src/users/model.rs deleted file mode 100644 index ce71c9d003..0000000000 --- a/rust/agama-lib/src/users/model.rs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) [2024] SUSE LLC -// -// All Rights Reserved. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 2 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, contact SUSE LLC. -// -// To contact SUSE LLC about this file by physical or electronic mail, you may -// find current contact information at www.suse.com. - -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct RootPatchSettings { - /// empty string here means remove ssh key for root - pub ssh_public_key: Option, - /// empty string here means remove password for root - pub password: Option, - /// specify if patched password is provided in plain text (default) or hashed - pub hashed_password: Option, -} diff --git a/rust/agama-lib/src/users/proxies.rs b/rust/agama-lib/src/users/proxies.rs deleted file mode 100644 index ecfc1c246c..0000000000 --- a/rust/agama-lib/src/users/proxies.rs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) [2024-2025] SUSE LLC -// -// All Rights Reserved. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 2 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, contact SUSE LLC. -// -// To contact SUSE LLC about this file by physical or electronic mail, you may -// find current contact information at www.suse.com. - -//! # D-Bus interface proxy for: `org.opensuse.Agama.Users1` -//! -//! This code was generated by `zbus-xmlgen` `5.0.0` from D-Bus introspection data. -//! Source: `org.opensuse.Agama.Users1.bus.xml`. -//! -//! You may prefer to adapt it, instead of using it verbatim. -//! -//! More information can be found in the [Writing a client proxy] section of the zbus -//! documentation. -//! -//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the -//! following zbus API can be used: -//! -//! * [`zbus::fdo::PropertiesProxy`] -//! * [`zbus::fdo::IntrospectableProxy`] -//! -//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. -//! -//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html -//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, -use zbus::proxy; - -/// First user as it comes from D-Bus. -/// -/// It is composed of: -/// -/// * full name -/// * user name -/// * password -/// * hashed_password (true = hashed, false = plain text) -/// * some optional and additional data -// NOTE: Manually added to this file. -pub type FirstUser = ( - String, - String, - String, - bool, - std::collections::HashMap, -); - -/// Root user as it comes from D-Bus. -/// -/// It is composed of: -/// -/// * password (an empty string if it is not set) -/// * hashed_password (true = hashed, false = plain text) -/// * SSH public key -pub type RootUser = (String, bool, String); - -#[proxy( - default_service = "org.opensuse.Agama.Manager1", - default_path = "/org/opensuse/Agama/Users1", - interface = "org.opensuse.Agama.Users1", - assume_defaults = true -)] -pub trait Users1 { - /// RemoveFirstUser method - fn remove_first_user(&self) -> zbus::Result; - - /// RemoveRootPassword method - fn remove_root_password(&self) -> zbus::Result; - - /// SetFirstUser method - fn set_first_user( - &self, - full_name: &str, - user_name: &str, - password: &str, - hashed_password: bool, - data: std::collections::HashMap<&str, &zbus::zvariant::Value<'_>>, - ) -> zbus::Result<(bool, Vec)>; - - /// SetRootPassword method - fn set_root_password(&self, value: &str, hashed: bool) -> zbus::Result; - - /// SetRootSSHKey method - #[zbus(name = "SetRootSSHKey")] - fn set_root_sshkey(&self, value: &str) -> zbus::Result; - - /// Write method - fn write(&self) -> zbus::Result; - - /// FirstUser property - #[zbus(property)] - fn first_user(&self) -> zbus::Result; - - /// RootUser property - #[zbus(property)] - fn root_user(&self) -> zbus::Result; -} diff --git a/rust/agama-lib/src/users/settings.rs b/rust/agama-lib/src/users/settings.rs deleted file mode 100644 index 3e4805124b..0000000000 --- a/rust/agama-lib/src/users/settings.rs +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright (c) [2024] SUSE LLC -// -// All Rights Reserved. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 2 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, contact SUSE LLC. -// -// To contact SUSE LLC about this file by physical or electronic mail, you may -// find current contact information at www.suse.com. - -use serde::{Deserialize, Serialize}; - -use super::{FirstUser, RootUser}; - -/// User settings -/// -/// Holds the user settings for the installation. -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct UserSettings { - #[serde(rename = "user")] - #[serde(skip_serializing_if = "Option::is_none")] - pub first_user: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub root: Option, -} - -/// First user settings -/// -/// Holds the settings for the first user. -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct FirstUserSettings { - /// First user's full name - pub full_name: Option, - /// First user password - #[serde(flatten)] - #[serde(skip_serializing_if = "Option::is_none")] - pub password: Option, - /// First user's username - pub user_name: Option, -} - -impl FirstUserSettings { - /// Whether it is a valid user. - pub fn is_valid(&self) -> bool { - self.user_name.is_some() - } -} - -impl From for FirstUserSettings { - fn from(value: FirstUser) -> Self { - let user_name = if value.user_name.is_empty() { - None - } else { - Some(value.user_name.clone()) - }; - - let password = if value.password.is_empty() { - None - } else { - Some(UserPassword { - password: value.password, - hashed_password: value.hashed_password, - }) - }; - - let full_name = if value.full_name.is_empty() { - None - } else { - Some(value.full_name) - }; - - Self { - user_name, - password, - full_name, - } - } -} - -/// Represents a user password. -/// -/// It holds the password and whether it is a hashed or a plain text password. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct UserPassword { - /// User password - pub password: String, - /// Whether the password is hashed or is plain text - #[serde(default)] - pub hashed_password: bool, -} - -/// Root user settings -/// -/// Holds the settings for the root user. -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct RootUserSettings { - /// Root user password - #[serde(flatten)] - #[serde(skip_serializing_if = "Option::is_none")] - pub password: Option, - /// Root SSH public key - #[serde(skip_serializing_if = "Option::is_none")] - pub ssh_public_key: Option, -} - -impl RootUserSettings { - pub fn is_empty(&self) -> bool { - self.password.is_none() && self.ssh_public_key.is_none() - } -} - -impl From for RootUserSettings { - fn from(value: RootUser) -> Self { - let password = value - .password - .filter(|password| !password.is_empty()) - .map(|password| UserPassword { - password, - hashed_password: value.hashed_password.unwrap_or_default(), - }); - let ssh_public_key = value.ssh_public_key.filter(|key| !key.is_empty()); - Self { - password, - ssh_public_key, - } - } -} - -#[cfg(test)] -mod test { - use crate::users::{settings::UserPassword, FirstUser, RootUser}; - - use super::{FirstUserSettings, RootUserSettings}; - - #[test] - fn test_user_settings_from_first_user() { - let empty = FirstUser { - full_name: "".to_string(), - user_name: "".to_string(), - password: "".to_string(), - hashed_password: false, - }; - let settings: FirstUserSettings = empty.into(); - assert_eq!(settings.full_name, None); - assert_eq!(settings.user_name, None); - assert_eq!(settings.password, None); - - let user = FirstUser { - full_name: "SUSE".to_string(), - user_name: "suse".to_string(), - password: "nots3cr3t".to_string(), - hashed_password: false, - }; - let settings: FirstUserSettings = user.into(); - assert_eq!(settings.full_name, Some("SUSE".to_string())); - assert_eq!(settings.user_name, Some("suse".to_string())); - let password = settings.password.unwrap(); - assert_eq!(password.password, "nots3cr3t".to_string()); - assert_eq!(password.hashed_password, false); - } - - #[test] - fn test_root_settings_from_root_user() { - let empty = RootUser { - password: None, - hashed_password: None, - ssh_public_key: None, - }; - - let settings: RootUserSettings = empty.into(); - assert_eq!(settings.password, None); - assert_eq!(settings.ssh_public_key, None); - - let with_password = RootUser { - password: Some("nots3cr3t".to_string()), - hashed_password: Some(false), - ..Default::default() - }; - let settings: RootUserSettings = with_password.into(); - let password = settings.password.unwrap(); - assert_eq!(password.password, "nots3cr3t".to_string()); - assert_eq!(password.hashed_password, false); - - let with_ssh_public_key = RootUser { - ssh_public_key: Some("ssh-rsa ...".to_string()), - ..Default::default() - }; - let settings: RootUserSettings = with_ssh_public_key.into(); - assert_eq!(settings.ssh_public_key, Some("ssh-rsa ...".to_string())); - } - - #[test] - fn test_parse_user_password() { - let password_str = r#"{ "password": "$a$b123", "hashedPassword": true }"#; - let password: UserPassword = serde_json::from_str(&password_str).unwrap(); - assert_eq!(&password.password, "$a$b123"); - assert_eq!(password.hashed_password, true); - - let password_str = r#"{ "password": "$a$b123" }"#; - let password: UserPassword = serde_json::from_str(&password_str).unwrap(); - assert_eq!(&password.password, "$a$b123"); - assert_eq!(password.hashed_password, false); - } -} diff --git a/rust/agama-lib/src/users/store.rs b/rust/agama-lib/src/users/store.rs deleted file mode 100644 index e8f3769bba..0000000000 --- a/rust/agama-lib/src/users/store.rs +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright (c) [2024-2025] SUSE LLC -// -// All Rights Reserved. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 2 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, contact SUSE LLC. -// -// To contact SUSE LLC about this file by physical or electronic mail, you may -// find current contact information at www.suse.com. - -use super::{ - http_client::UsersHTTPClientError, FirstUserSettings, RootUserSettings, UserSettings, - UsersHTTPClient, -}; -use crate::http::BaseHTTPClient; - -#[derive(Debug, thiserror::Error)] -#[error("Error processing users options: {0}")] -pub struct UsersStoreError(#[from] UsersHTTPClientError); - -type UsersStoreResult = Result; - -/// Loads and stores the users settings from/to the D-Bus service. -pub struct UsersStore { - users_client: UsersHTTPClient, -} - -impl UsersStore { - pub fn new(client: BaseHTTPClient) -> Self { - Self { - users_client: UsersHTTPClient::new(client), - } - } - - pub fn new_with_client(client: UsersHTTPClient) -> UsersStoreResult { - Ok(Self { - users_client: client, - }) - } - - pub async fn load(&self) -> UsersStoreResult { - let first_user = self.users_client.first_user().await?; - let first_user: FirstUserSettings = first_user.into(); - let first_user = if first_user.is_valid() { - Some(first_user) - } else { - None - }; - - let root = self.users_client.root_user().await?; - let root: RootUserSettings = root.into(); - let root = if root.is_empty() { None } else { Some(root) }; - - Ok(UserSettings { first_user, root }) - } - - pub async fn store(&self, settings: &UserSettings) -> UsersStoreResult<()> { - // fixme: improve - if let Some(settings) = &settings.first_user { - self.store_first_user(settings).await?; - } - - if let Some(settings) = &settings.root { - self.store_root_user(settings).await?; - } - Ok(()) - } - - async fn store_first_user(&self, settings: &FirstUserSettings) -> UsersStoreResult<()> { - Ok(self.users_client.set_first_user(&settings.into()).await?) - } - - async fn store_root_user(&self, settings: &RootUserSettings) -> UsersStoreResult<()> { - if let Some(password) = &settings.password { - self.users_client - .set_root_password(&password.password, password.hashed_password) - .await?; - } - - if let Some(ssh_public_key) = &settings.ssh_public_key { - self.users_client.set_root_sshkey(ssh_public_key).await?; - } - - Ok(()) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::http::BaseHTTPClient; - use crate::users::settings::UserPassword; - use httpmock::prelude::*; - use httpmock::Method::PATCH; - use std::error::Error; - use tokio::test; // without this, "error: async functions cannot be used for tests" - - fn users_store(mock_server_url: String) -> UsersStoreResult { - let bhc = - BaseHTTPClient::new(mock_server_url).map_err(|e| UsersHTTPClientError::HTTP(e))?; - let client = UsersHTTPClient::new(bhc.clone()); - UsersStore::new_with_client(client) - } - - #[test] - async fn test_getting_users() -> Result<(), Box> { - let server = MockServer::start(); - let user_mock = server.mock(|when, then| { - when.method(GET).path("/api/users/first"); - then.status(200) - .header("content-type", "application/json") - .body( - r#"{ - "fullName": "Tux", - "userName": "tux", - "password": "fish", - "hashedPassword": false - }"#, - ); - }); - let root_mock = server.mock(|when, then| { - when.method(GET).path("/api/users/root"); - then.status(200) - .header("content-type", "application/json") - .body( - r#"{ - "sshPublicKey": "keykeykey", - "password": "nots3cr3t", - "hashedPassword": false - }"#, - ); - }); - let url = server.url("/api"); - - let store = users_store(url)?; - let settings = store.load().await?; - - let first_user = FirstUserSettings { - full_name: Some("Tux".to_owned()), - user_name: Some("tux".to_owned()), - password: Some(UserPassword { - password: "fish".to_owned(), - hashed_password: false, - }), - }; - let root_user = RootUserSettings { - // FIXME this is weird: no matter what HTTP reports, we end up with None - password: Some(UserPassword { - password: "nots3cr3t".to_owned(), - hashed_password: false, - }), - ssh_public_key: Some("keykeykey".to_owned()), - }; - let expected = UserSettings { - first_user: Some(first_user), - root: Some(root_user), - }; - - // main assertion - assert_eq!(settings, expected); - - // Ensure the specified mock was called exactly one time (or fail with a detailed error description). - user_mock.assert(); - root_mock.assert(); - - Ok(()) - } - - #[test] - async fn test_setting_users() -> Result<(), Box> { - let server = MockServer::start(); - let user_mock = server.mock(|when, then| { - when.method(PUT) - .path("/api/users/first") - .header("content-type", "application/json") - .body(r#"{"fullName":"Tux","userName":"tux","password":"fish","hashedPassword":false}"#); - then.status(200); - }); - // note that we use 2 requests for root - let root_mock = server.mock(|when, then| { - when.method(PATCH) - .path("/api/users/root") - .header("content-type", "application/json") - .body(r#"{"sshPublicKey":null,"password":"1234","hashedPassword":false}"#); - then.status(200).body("0"); - }); - let root_mock2 = server.mock(|when, then| { - when.method(PATCH) - .path("/api/users/root") - .header("content-type", "application/json") - .body(r#"{"sshPublicKey":"keykeykey","password":null,"hashedPassword":null}"#); - then.status(200).body("0"); - }); - let url = server.url("/api"); - - let store = users_store(url)?; - - let first_user = FirstUserSettings { - full_name: Some("Tux".to_owned()), - user_name: Some("tux".to_owned()), - password: Some(UserPassword { - password: "fish".to_owned(), - hashed_password: false, - }), - }; - let root_user = RootUserSettings { - password: Some(UserPassword { - password: "1234".to_owned(), - hashed_password: false, - }), - ssh_public_key: Some("keykeykey".to_owned()), - }; - let settings = UserSettings { - first_user: Some(first_user), - root: Some(root_user), - }; - let result = store.store(&settings).await; - - // main assertion - result?; - - // Ensure the specified mock was called exactly one time (or fail with a detailed error description). - user_mock.assert(); - root_mock.assert(); - root_mock2.assert(); - Ok(()) - } -} diff --git a/rust/agama-server/src/users.rs b/rust/agama-server/src/users.rs index a63e0359f2..519f8945db 100644 --- a/rust/agama-server/src/users.rs +++ b/rust/agama-server/src/users.rs @@ -19,5 +19,3 @@ // find current contact information at www.suse.com. pub(crate) mod password; -pub mod web; -pub use web::users_service; diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs deleted file mode 100644 index 0c86e1c5c1..0000000000 --- a/rust/agama-server/src/users/web.rs +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (c) [2024] SUSE LLC -// -// All Rights Reserved. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 2 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, contact SUSE LLC. -// -// To contact SUSE LLC about this file by physical or electronic mail, you may -// find current contact information at www.suse.com. - -//! -//! The module offers two public functions: -//! -//! * `users_service` which returns the Axum service. -//! * `users_stream` which offers an stream that emits the users events coming from D-Bus. - -use crate::{error::Error, users::password::PasswordChecker}; -use agama_lib::{ - error::ServiceError, - users::{model::RootPatchSettings, FirstUser, RootUser, UsersClient}, -}; -use axum::{ - extract::State, - http::StatusCode, - response::IntoResponse, - routing::{get, post}, - Json, Router, -}; -use serde::Deserialize; - -use super::password::PasswordCheckResult; - -#[derive(Clone)] -struct UsersState<'a> { - users: UsersClient<'a>, -} - -/// Sets up and returns the axum service for the users module. -pub async fn users_service(dbus: zbus::Connection) -> Result { - let users = UsersClient::new(dbus.clone()).await?; - let state = UsersState { users }; - // FIXME: use anyhow temporarily until we adapt all these methods to return - // the crate::error::Error instead of ServiceError. - let router = Router::new() - .route( - "/first", - get(get_user_config) - .put(set_first_user) - .delete(remove_first_user), - ) - .route("/root", get(get_root_config).patch(patch_root)) - .route("/password_check", post(check_password)) - .with_state(state); - Ok(router) -} - -/// Removes the first user settings -#[utoipa::path( - delete, - path = "/first", - context_path = "/api/users", - responses( - (status = 200, description = "Removes the first user"), - (status = 400, description = "The D-Bus service could not perform the action"), - ) -)] -async fn remove_first_user(State(state): State>) -> Result<(), Error> { - state.users.remove_first_user().await?; - Ok(()) -} - -#[utoipa::path( - put, - path = "/first", - context_path = "/api/users", - responses( - (status = 200, description = "Sets the first user"), - (status = 400, description = "The D-Bus service could not perform the action"), - (status = 422, description = "Invalid first user. Details are in body", body = Vec), - ) -)] -async fn set_first_user( - State(state): State>, - Json(config): Json, -) -> Result { - // issues: for example, trying to use a system user id; empty password - // success: simply issues.is_empty() - let (_success, issues) = state.users.set_first_user(&config).await?; - let status = if issues.is_empty() { - StatusCode::OK - } else { - StatusCode::UNPROCESSABLE_ENTITY - }; - - Ok((status, Json(issues).into_response())) -} - -#[utoipa::path( - get, - path = "/first", - context_path = "/api/users", - responses( - (status = 200, description = "Configuration for the first user", body = FirstUser), - (status = 400, description = "The D-Bus service could not perform the action"), - ) -)] -async fn get_user_config(State(state): State>) -> Result, Error> { - Ok(Json(state.users.first_user().await?)) -} - -#[utoipa::path( - patch, - path = "/root", - context_path = "/api/users", - responses( - (status = 200, description = "Root configuration is modified", body = RootPatchSettings), - (status = 400, description = "The D-Bus service could not perform the action"), - ) -)] -async fn patch_root( - State(state): State>, - Json(config): Json, -) -> Result { - let mut retcode1 = 0; - if let Some(key) = config.ssh_public_key { - retcode1 = state.users.set_root_sshkey(&key).await?; - } - - let mut retcode2 = 0; - if let Some(password) = config.password { - retcode2 = if password.is_empty() { - state.users.remove_root_password().await? - } else { - state - .users - .set_root_password(&password, config.hashed_password == Some(true)) - .await? - } - } - - let retcode: u32 = if retcode1 != 0 { retcode1 } else { retcode2 }; - - Ok(Json(retcode)) -} - -#[utoipa::path( - get, - path = "/root", - context_path = "/api/users", - responses( - (status = 200, description = "Configuration for the root user", body = RootUser), - (status = 400, description = "The D-Bus service could not perform the action"), - ) -)] -async fn get_root_config(State(state): State>) -> Result, Error> { - Ok(Json(state.users.root_user().await?)) -} - -#[derive(Deserialize, utoipa::ToSchema)] -pub struct PasswordParams { - password: String, -} - -#[utoipa::path( - post, - path = "/password_check", - context_path = "/api/users", - description = "Performs a quality check on a given password", - responses( - (status = 200, description = "The password was checked", body = String), - (status = 400, description = "Could not check the password") - ) -)] -async fn check_password( - Json(password): Json, -) -> Result, Error> { - let checker = PasswordChecker::default(); - let result = checker.check(&password.password); - Ok(Json(result?)) -} diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index 363ea101a5..01c46eb75b 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -26,7 +26,7 @@ use crate::{ bootloader::web::bootloader_service, profile::web::profile_service, security::security_service, - server::server_service, users::web::users_service, + server::server_service, }; use agama_utils::api::event; use axum::Router; @@ -63,7 +63,6 @@ where .add_service("/v2", server_service(events, dbus.clone()).await?) .add_service("/security", security_service(dbus.clone()).await?) .add_service("/bootloader", bootloader_service(dbus.clone()).await?) - .add_service("/users", users_service(dbus.clone()).await?) .add_service("/profile", profile_service().await?) .with_config(config) .build(); diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index 4c09af8593..4a892b2b6d 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -26,8 +26,6 @@ mod bootloader; pub use bootloader::BootloaderApiDocBuilder; mod profile; pub use profile::ProfileApiDocBuilder; -mod users; -pub use users::UsersApiDocBuilder; mod misc; pub use misc::MiscApiDocBuilder; diff --git a/rust/agama-server/src/web/docs/config.rs b/rust/agama-server/src/web/docs/config.rs index b7327a6844..664fcb6f11 100644 --- a/rust/agama-server/src/web/docs/config.rs +++ b/rust/agama-server/src/web/docs/config.rs @@ -116,10 +116,6 @@ impl ApiDocBuilder for ConfigApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() .schema_from::() .schema_from::() .schema_from::() diff --git a/rust/agama-server/src/web/docs/users.rs b/rust/agama-server/src/web/docs/users.rs deleted file mode 100644 index 57fcd71df9..0000000000 --- a/rust/agama-server/src/web/docs/users.rs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) [2024] SUSE LLC -// -// All Rights Reserved. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 2 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, contact SUSE LLC. -// -// To contact SUSE LLC about this file by physical or electronic mail, you may -// find current contact information at www.suse.com. - -use utoipa::openapi::{ComponentsBuilder, Paths, PathsBuilder}; - -use super::ApiDocBuilder; - -pub struct UsersApiDocBuilder; - -impl ApiDocBuilder for UsersApiDocBuilder { - fn title(&self) -> String { - "Users HTTP API".to_string() - } - - fn paths(&self) -> Paths { - PathsBuilder::new() - .path_from::() - .path_from::() - .path_from::() - .path_from::() - .path_from::() - .path_from::() - .build() - } - - fn components(&self) -> utoipa::openapi::Components { - ComponentsBuilder::new() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema( - "zbus.zvariant.OwnedValue", - utoipa::openapi::ObjectBuilder::new() - .description(Some("Additional user information (unused)".to_string())) - .build(), - ) - .build() - } -} diff --git a/rust/agama-users/src/lib.rs b/rust/agama-users/src/lib.rs index a91e231186..3dddf00f45 100644 --- a/rust/agama-users/src/lib.rs +++ b/rust/agama-users/src/lib.rs @@ -24,7 +24,7 @@ pub use service::{Service, Starter}; pub mod message; mod model; -pub use model::{Model, ModelAdapter}; +pub use model::{ChrootCommand, Model, ModelAdapter}; #[cfg(test)] mod tests { diff --git a/rust/agama-users/src/model.rs b/rust/agama-users/src/model.rs index 2fbbf950d1..26b09a5830 100644 --- a/rust/agama-users/src/model.rs +++ b/rust/agama-users/src/model.rs @@ -24,6 +24,7 @@ use agama_utils::api::users::Config; use std::fs; use std::fs::{OpenOptions, Permissions}; use std::io::Write; +use std::ops::{Deref, DerefMut}; use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; @@ -37,6 +38,62 @@ pub trait ModelAdapter: Send + 'static { } } +/// A wrapper for common std::process::Command for use in chroot +/// +/// It basically creates Command for chroot and command to be run +/// in chrooted environment is passed as an argument. +/// +/// Example use: +/// ``` +/// # use agama_users::ChrootCommand; +/// # use agama_users::service; +/// # fn main() -> Result<(), service::Error> { +/// let cmd = ChrootCommand::new("/tmp".into())? +/// .cmd("echo") +/// .args(["Hello world!"]); +/// # Ok(()) +/// # } +/// ``` +pub struct ChrootCommand { + command: Command, +} + +impl ChrootCommand { + pub fn new(chroot: PathBuf) -> Result { + if !chroot.is_dir() { + return Err(service::Error::CommandFailed(String::from( + "Failed to chroot", + ))); + } + + let mut cmd = Command::new("chroot"); + + cmd.arg(chroot); + + Ok(Self { command: cmd }) + } + + pub fn cmd(mut self, command: &str) -> Self { + self.command.arg(command); + + self + } +} + +impl Deref for ChrootCommand { + type Target = Command; + + fn deref(&self) -> &Self::Target { + &self.command + } +} + +impl DerefMut for ChrootCommand { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.command + } +} + /// [ModelAdapter] implementation for systemd-based systems. pub struct Model { install_dir: PathBuf, @@ -49,15 +106,6 @@ impl Model { } } - /// Wrapper for creating Command which works in installation chroot - fn chroot_command(&self) -> Command { - let mut cmd = Command::new("chroot"); - - cmd.arg(&self.install_dir); - - cmd - } - /// Reads first user's data from given config and updates its setup accordingly fn add_first_user(&self, user: &FirstUserConfig) -> Result<(), service::Error> { let Some(ref user_name) = user.user_name else { @@ -67,9 +115,9 @@ impl Model { return Err(service::Error::MissingUserData); }; - let useradd = self - .chroot_command() - .args(["useradd", "-G", "wheel", &user_name]) + let useradd = ChrootCommand::new(self.install_dir.clone())? + .cmd("useradd") + .args(["-G", "wheel", &user_name]) .output()?; if !useradd.status.success() { @@ -113,8 +161,7 @@ impl Model { user_name: &str, user_password: &UserPassword, ) -> Result<(), service::Error> { - let mut passwd_cmd = self.chroot_command(); - passwd_cmd.arg("chpasswd"); + let mut passwd_cmd = ChrootCommand::new(self.install_dir.clone())?.cmd("chpasswd"); if user_password.hashed_password { passwd_cmd.arg("-e"); @@ -163,9 +210,9 @@ impl Model { /// Enables sshd service in the target system fn enable_sshd_service(&self) -> Result<(), service::Error> { - let systemctl = self - .chroot_command() - .args(["systemctl", "enable", "sshd.service"]) + let systemctl = ChrootCommand::new(self.install_dir.clone())? + .cmd("systemctl") + .args(["enable", "sshd.service"]) .output()?; if !systemctl.status.success() { @@ -183,9 +230,9 @@ impl Model { /// Opens the SSH port in firewall in the target system fn open_ssh_port(&self) -> Result<(), service::Error> { - let firewall_cmd = self - .chroot_command() - .args(["firewall-offline-cmd", "--add-service=ssh"]) + let firewall_cmd = ChrootCommand::new(self.install_dir.clone())? + .cmd("firewall-offline-cmd") + .args(["-add-service=ssh"]) .output()?; // ignore error if the firewall is not installed, in that case we do need to open the port, @@ -220,9 +267,9 @@ impl Model { return Ok(()); }; - let chfn = self - .chroot_command() - .args(["chfn", "-f", &full_name, &user_name]) + let chfn = ChrootCommand::new(self.install_dir.clone())? + .cmd("chfn") + .args(["-f", &full_name, &user_name]) .output()?; if !chfn.status.success() { diff --git a/rust/xtask/src/main.rs b/rust/xtask/src/main.rs index 0f7e5f784c..a6c318d892 100644 --- a/rust/xtask/src/main.rs +++ b/rust/xtask/src/main.rs @@ -6,7 +6,6 @@ mod tasks { use agama_cli::Cli; use agama_server::web::docs::{ ApiDocBuilder, ConfigApiDocBuilder, MiscApiDocBuilder, ProfileApiDocBuilder, - UsersApiDocBuilder, }; use clap::CommandFactory; use clap_complete::aot; @@ -66,7 +65,6 @@ mod tasks { write_openapi(ConfigApiDocBuilder {}, out_dir.join("config.json"))?; write_openapi(MiscApiDocBuilder {}, out_dir.join("misc.json"))?; write_openapi(ProfileApiDocBuilder {}, out_dir.join("profile.json"))?; - write_openapi(UsersApiDocBuilder {}, out_dir.join("users.json"))?; println!( "Generate the OpenAPI specification at {}.", out_dir.display()