diff --git a/rust/agama-lib/src/users/client.rs b/rust/agama-lib/src/users/client.rs index d3bace6b63..5aa08bc92a 100644 --- a/rust/agama-lib/src/users/client.rs +++ b/rust/agama-lib/src/users/client.rs @@ -20,7 +20,10 @@ //! Implements a client to access Agama's users service. -use super::proxies::{FirstUser as FirstUserFromDBus, RootUser as RootUserFromDBus, Users1Proxy}; +use super::{ + proxies::{FirstUser as FirstUserFromDBus, RootUser as RootUserFromDBus, Users1Proxy}, + FirstUserSettings, +}; use crate::error::ServiceError; use serde::{Deserialize, Serialize}; use zbus::Connection; @@ -51,6 +54,25 @@ impl FirstUser { } } +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")] diff --git a/rust/agama-lib/src/users/settings.rs b/rust/agama-lib/src/users/settings.rs index ea9b3b59be..c925937443 100644 --- a/rust/agama-lib/src/users/settings.rs +++ b/rust/agama-lib/src/users/settings.rs @@ -20,6 +20,8 @@ use serde::{Deserialize, Serialize}; +use super::{FirstUser, RootUser}; + /// User settings /// /// Holds the user settings for the installation. @@ -41,12 +43,62 @@ pub struct UserSettings { 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, - /// First user's password (in clear text) - pub password: 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)] +#[serde(rename_all = "camelCase")] +pub struct UserPassword { + /// User password + pub password: String, /// Whether the password is hashed or is plain text - pub hashed_password: Option, + pub hashed_password: bool, } /// Root user settings @@ -55,13 +107,98 @@ pub struct FirstUserSettings { #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RootUserSettings { - /// Root's password (in clear text) - #[serde(skip_serializing)] - pub password: Option, - /// Whether the password is hashed or is plain text - #[serde(skip_serializing)] - pub hashed_password: Option, + /// 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::{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())); + } +} diff --git a/rust/agama-lib/src/users/store.rs b/rust/agama-lib/src/users/store.rs index 63b85393e0..e8f3769bba 100644 --- a/rust/agama-lib/src/users/store.rs +++ b/rust/agama-lib/src/users/store.rs @@ -19,8 +19,8 @@ // find current contact information at www.suse.com. use super::{ - http_client::UsersHTTPClientError, FirstUser, FirstUserSettings, RootUserSettings, - UserSettings, UsersHTTPClient, + http_client::UsersHTTPClientError, FirstUserSettings, RootUserSettings, UserSettings, + UsersHTTPClient, }; use crate::http::BaseHTTPClient; @@ -50,23 +50,18 @@ impl UsersStore { pub async fn load(&self) -> UsersStoreResult { let first_user = self.users_client.first_user().await?; - let first_user = FirstUserSettings { - user_name: Some(first_user.user_name), - full_name: Some(first_user.full_name), - password: Some(first_user.password), - hashed_password: Some(first_user.hashed_password), - }; - let root_user = self.users_client.root_user().await?; - let root_user = RootUserSettings { - password: root_user.password, - hashed_password: root_user.hashed_password, - ssh_public_key: root_user.ssh_public_key, + let first_user: FirstUserSettings = first_user.into(); + let first_user = if first_user.is_valid() { + Some(first_user) + } else { + None }; - Ok(UserSettings { - first_user: Some(first_user), - root: Some(root_user), - }) + 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<()> { @@ -82,21 +77,13 @@ impl UsersStore { } async fn store_first_user(&self, settings: &FirstUserSettings) -> UsersStoreResult<()> { - let first_user = FirstUser { - user_name: settings.user_name.clone().unwrap_or_default(), - full_name: settings.full_name.clone().unwrap_or_default(), - password: settings.password.clone().unwrap_or_default(), - hashed_password: settings.hashed_password.unwrap_or_default(), - }; - Ok(self.users_client.set_first_user(&first_user).await?) + Ok(self.users_client.set_first_user(&settings.into()).await?) } async fn store_root_user(&self, settings: &RootUserSettings) -> UsersStoreResult<()> { - let hashed_password = settings.hashed_password.unwrap_or_default(); - - if let Some(root_password) = &settings.password { + if let Some(password) = &settings.password { self.users_client - .set_root_password(root_password, hashed_password) + .set_root_password(&password.password, password.hashed_password) .await?; } @@ -112,6 +99,7 @@ impl UsersStore { mod test { use super::*; use crate::http::BaseHTTPClient; + use crate::users::settings::UserPassword; use httpmock::prelude::*; use httpmock::Method::PATCH; use std::error::Error; @@ -160,13 +148,17 @@ mod test { let first_user = FirstUserSettings { full_name: Some("Tux".to_owned()), user_name: Some("tux".to_owned()), - password: Some("fish".to_owned()), - hashed_password: Some(false), + 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("nots3cr3t".to_owned()), - hashed_password: Some(false), + password: Some(UserPassword { + password: "nots3cr3t".to_owned(), + hashed_password: false, + }), ssh_public_key: Some("keykeykey".to_owned()), }; let expected = UserSettings { @@ -216,12 +208,16 @@ mod test { let first_user = FirstUserSettings { full_name: Some("Tux".to_owned()), user_name: Some("tux".to_owned()), - password: Some("fish".to_owned()), - hashed_password: Some(false), + password: Some(UserPassword { + password: "fish".to_owned(), + hashed_password: false, + }), }; let root_user = RootUserSettings { - password: Some("1234".to_owned()), - hashed_password: Some(false), + password: Some(UserPassword { + password: "1234".to_owned(), + hashed_password: false, + }), ssh_public_key: Some("keykeykey".to_owned()), }; let settings = UserSettings { diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 77fb99dfd9..f7d5a1d370 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,12 @@ +------------------------------------------------------------------- +Tue Jun 10 13:33:09 UTC 2025 - Imobach Gonzalez Sosa + +- Expose the user and the root password when exporting the configuration + (bsc#1235602). +- Do not export the "user" section unless a first user is defined. +- Do not export the "root" section unless an authentication mechanism + is defined. + ------------------------------------------------------------------- Tue Jun 10 09:25:39 UTC 2025 - Martin Vidner