From 8353c837fcf29f985299690b83b550e2f78ed8c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 10 Jun 2025 14:08:03 +0100 Subject: [PATCH 1/4] feat(rust): export user/root passwords --- rust/agama-lib/src/users/settings.rs | 28 ++++++++---- rust/agama-lib/src/users/store.rs | 64 +++++++++++++++++++--------- 2 files changed, 63 insertions(+), 29 deletions(-) diff --git a/rust/agama-lib/src/users/settings.rs b/rust/agama-lib/src/users/settings.rs index 5aa359958a..4cc3683922 100644 --- a/rust/agama-lib/src/users/settings.rs +++ b/rust/agama-lib/src/users/settings.rs @@ -39,12 +39,24 @@ 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, +} + +/// 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 @@ -53,12 +65,10 @@ 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, diff --git a/rust/agama-lib/src/users/store.rs b/rust/agama-lib/src/users/store.rs index 63b85393e0..0ff5a0681a 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, settings::UserPassword, FirstUser, FirstUserSettings, + RootUserSettings, UserSettings, UsersHTTPClient, }; use crate::http::BaseHTTPClient; @@ -50,16 +50,26 @@ impl UsersStore { pub async fn load(&self) -> UsersStoreResult { let first_user = self.users_client.first_user().await?; + let user_password = UserPassword { + password: first_user.password, + hashed_password: first_user.hashed_password, + }; 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), + password: if user_password.password.is_empty() { + None + } else { + Some(user_password) + }, }; let root_user = self.users_client.root_user().await?; + let root_password = root_user.password.map(|password| UserPassword { + password, + hashed_password: root_user.hashed_password.unwrap_or_default(), + }); let root_user = RootUserSettings { - password: root_user.password, - hashed_password: root_user.hashed_password, + password: root_password, ssh_public_key: root_user.ssh_public_key, }; @@ -85,18 +95,24 @@ impl UsersStore { 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(), + password: settings + .password + .as_ref() + .map(|p| p.password.clone()) + .unwrap_or_default(), + hashed_password: settings + .password + .as_ref() + .map(|p| p.hashed_password) + .unwrap_or_default(), }; Ok(self.users_client.set_first_user(&first_user).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?; } @@ -160,13 +176,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 +236,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 { From 1f3e48ebdff826b04b3efa2f2d28a48651934e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 10 Jun 2025 14:32:13 +0100 Subject: [PATCH 2/4] fix(rust): export user and root only when defined --- rust/agama-lib/src/users/settings.rs | 2 ++ rust/agama-lib/src/users/store.rs | 53 +++++++++++++++++----------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/rust/agama-lib/src/users/settings.rs b/rust/agama-lib/src/users/settings.rs index 4cc3683922..0258af05b1 100644 --- a/rust/agama-lib/src/users/settings.rs +++ b/rust/agama-lib/src/users/settings.rs @@ -27,7 +27,9 @@ use serde::{Deserialize, Serialize}; #[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, } diff --git a/rust/agama-lib/src/users/store.rs b/rust/agama-lib/src/users/store.rs index 0ff5a0681a..42cfee2e4b 100644 --- a/rust/agama-lib/src/users/store.rs +++ b/rust/agama-lib/src/users/store.rs @@ -50,33 +50,44 @@ impl UsersStore { pub async fn load(&self) -> UsersStoreResult { let first_user = self.users_client.first_user().await?; - let user_password = UserPassword { - password: first_user.password, - hashed_password: first_user.hashed_password, - }; - let first_user = FirstUserSettings { - user_name: Some(first_user.user_name), - full_name: Some(first_user.full_name), - password: if user_password.password.is_empty() { + let first_user = if first_user.user_name.is_empty() { + None + } else { + let user_password = if first_user.password.is_empty() { None } else { - Some(user_password) - }, + Some(UserPassword { + password: first_user.password, + hashed_password: first_user.hashed_password, + }) + }; + + Some(FirstUserSettings { + user_name: Some(first_user.user_name), + full_name: Some(first_user.full_name), + password: user_password, + }) }; + let root_user = self.users_client.root_user().await?; - let root_password = root_user.password.map(|password| UserPassword { - password, - hashed_password: root_user.hashed_password.unwrap_or_default(), - }); - let root_user = RootUserSettings { - password: root_password, - ssh_public_key: root_user.ssh_public_key, + let root_password = root_user + .password + .filter(|password| !password.is_empty()) + .map(|password| UserPassword { + password, + hashed_password: root_user.hashed_password.unwrap_or_default(), + }); + let ssh_public_key = root_user.ssh_public_key.filter(|key| !key.is_empty()); + let root = if root_password.is_some() || ssh_public_key.is_some() { + Some(RootUserSettings { + password: root_password, + ssh_public_key, + }) + } else { + None }; - Ok(UserSettings { - first_user: Some(first_user), - root: Some(root_user), - }) + Ok(UserSettings { first_user, root }) } pub async fn store(&self, settings: &UserSettings) -> UsersStoreResult<()> { From 478247ebbbd800980718cc9f2bc2fa4c790f5d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 10 Jun 2025 14:34:53 +0100 Subject: [PATCH 3/4] docs(rust): update changes file --- rust/package/agama.changes | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 5f081e3f36..671aa006e3 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. + ------------------------------------------------------------------- Fri Jun 6 13:22:58 UTC 2025 - Knut Anderssen From 6e662130b7d83669b18041e2d678c0964713c16b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 10 Jun 2025 23:09:55 +0100 Subject: [PATCH 4/4] refactor(rust): use the From trait to convert user types --- rust/agama-lib/src/users/client.rs | 24 ++++- rust/agama-lib/src/users/settings.rs | 127 +++++++++++++++++++++++++++ rust/agama-lib/src/users/store.rs | 61 +++---------- 3 files changed, 161 insertions(+), 51 deletions(-) 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 0258af05b1..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. @@ -49,6 +51,44 @@ pub struct FirstUserSettings { 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. @@ -75,3 +115,90 @@ pub struct RootUserSettings { #[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 42cfee2e4b..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, settings::UserPassword, FirstUser, FirstUserSettings, - RootUserSettings, UserSettings, UsersHTTPClient, + http_client::UsersHTTPClientError, FirstUserSettings, RootUserSettings, UserSettings, + UsersHTTPClient, }; use crate::http::BaseHTTPClient; @@ -50,43 +50,17 @@ impl UsersStore { pub async fn load(&self) -> UsersStoreResult { let first_user = self.users_client.first_user().await?; - let first_user = if first_user.user_name.is_empty() { - None - } else { - let user_password = if first_user.password.is_empty() { - None - } else { - Some(UserPassword { - password: first_user.password, - hashed_password: first_user.hashed_password, - }) - }; - - Some(FirstUserSettings { - user_name: Some(first_user.user_name), - full_name: Some(first_user.full_name), - password: user_password, - }) - }; - - let root_user = self.users_client.root_user().await?; - let root_password = root_user - .password - .filter(|password| !password.is_empty()) - .map(|password| UserPassword { - password, - hashed_password: root_user.hashed_password.unwrap_or_default(), - }); - let ssh_public_key = root_user.ssh_public_key.filter(|key| !key.is_empty()); - let root = if root_password.is_some() || ssh_public_key.is_some() { - Some(RootUserSettings { - password: root_password, - ssh_public_key, - }) + 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 }) } @@ -103,21 +77,7 @@ 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 - .as_ref() - .map(|p| p.password.clone()) - .unwrap_or_default(), - hashed_password: settings - .password - .as_ref() - .map(|p| p.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<()> { @@ -139,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;