Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion rust/agama-lib/src/users/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")]
Expand Down
155 changes: 146 additions & 9 deletions rust/agama-lib/src/users/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

use serde::{Deserialize, Serialize};

use super::{FirstUser, RootUser};

/// User settings
///
/// Holds the user settings for the installation.
Expand All @@ -41,12 +43,62 @@ pub struct UserSettings {
pub struct FirstUserSettings {
/// First user's full name
pub full_name: Option<String>,
/// First user password
#[serde(flatten)]
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<UserPassword>,
/// First user's username
pub user_name: Option<String>,
/// First user's password (in clear text)
pub password: Option<String>,
}

impl FirstUserSettings {
/// Whether it is a valid user.
pub fn is_valid(&self) -> bool {
self.user_name.is_some()
}
}

impl From<FirstUser> 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<bool>,
pub hashed_password: bool,
}

/// Root user settings
Expand All @@ -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<String>,
/// Whether the password is hashed or is plain text
#[serde(skip_serializing)]
pub hashed_password: Option<bool>,
/// Root user password
#[serde(flatten)]
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<UserPassword>,
/// Root SSH public key
#[serde(skip_serializing_if = "Option::is_none")]
pub ssh_public_key: Option<String>,
}

impl RootUserSettings {
pub fn is_empty(&self) -> bool {
self.password.is_none() && self.ssh_public_key.is_none()
}
}

impl From<RootUser> 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()));
}
}
68 changes: 32 additions & 36 deletions rust/agama-lib/src/users/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -50,23 +50,18 @@ impl UsersStore {

pub async fn load(&self) -> UsersStoreResult<UserSettings> {
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<()> {
Expand All @@ -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?;
}

Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions rust/package/agama.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
-------------------------------------------------------------------
Tue Jun 10 13:33:09 UTC 2025 - Imobach Gonzalez Sosa <[email protected]>

- 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 <[email protected]>

Expand Down