Skip to content

Commit 7378f2e

Browse files
authored
Fixes for the users service (#3026)
Fixes for the users service - Improve the detection of missing information for the root and the first user. - Crate the first user and/or set the root password/public key at the end of the installation.
2 parents fc1f6e8 + 5b5805a commit 7378f2e

File tree

4 files changed

+201
-26
lines changed

4 files changed

+201
-26
lines changed

rust/agama-manager/src/service.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@ impl MessageHandler<message::RunAction> for Service {
685685
storage: self.storage.clone(),
686686
files: self.files.clone(),
687687
progress: self.progress.clone(),
688+
users: self.users.clone(),
688689
};
689690
action.run();
690691
}
@@ -755,6 +756,7 @@ struct InstallAction {
755756
storage: Handler<storage::Service>,
756757
files: Handler<files::Service>,
757758
progress: Handler<progress::Service>,
759+
users: Handler<users::Service>,
758760
}
759761

760762
impl InstallAction {
@@ -823,6 +825,7 @@ impl InstallAction {
823825
self.files.call(files::message::WriteFiles).await?;
824826
self.network.install().await?;
825827
self.hostname.call(hostname::message::Install).await?;
828+
self.users.call(users::message::Install).await?;
826829

827830
// call files before storage finish as it unmount /mnt/run which is important for chrooted scripts
828831
self.files

rust/agama-users/src/model.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use std::fs;
2525
use std::fs::{OpenOptions, Permissions};
2626
use std::io::Write;
2727
use std::os::unix::fs::PermissionsExt;
28+
use std::path::{Path, PathBuf};
2829
use std::process::{Command, Stdio};
2930

3031
/// Abstract the users-related configuration from the underlying system.
@@ -61,7 +62,17 @@ pub trait ModelAdapter: Send + 'static {
6162
}
6263

6364
/// [ModelAdapter] implementation for systemd-based systems.
64-
pub struct Model {}
65+
pub struct Model {
66+
install_dir: PathBuf,
67+
}
68+
69+
impl Model {
70+
pub fn new<P: AsRef<Path>>(install_dir: P) -> Self {
71+
Self {
72+
install_dir: PathBuf::from(install_dir.as_ref()),
73+
}
74+
}
75+
}
6576

6677
impl ModelAdapter for Model {
6778
fn install(&self, config: &Config) -> Result<(), service::Error> {
@@ -84,7 +95,10 @@ impl ModelAdapter for Model {
8495
return Err(service::Error::MissingUserData);
8596
};
8697

87-
let useradd = Command::new("/usr/sbin/useradd").arg(user_name).output()?;
98+
let useradd = Command::new("chroot")
99+
.arg(&self.install_dir)
100+
.args(["useradd", &user_name])
101+
.output()?;
88102

89103
if !useradd.status.success() {
90104
tracing::error!("User {} creation failed", user_name);
@@ -126,7 +140,9 @@ impl ModelAdapter for Model {
126140
user_name: &str,
127141
user_password: &UserPassword,
128142
) -> Result<(), service::Error> {
129-
let mut passwd_cmd = Command::new("/usr/sbin/chpasswd");
143+
let mut passwd_cmd = Command::new("chroot");
144+
passwd_cmd.arg(&self.install_dir);
145+
passwd_cmd.arg("chpasswd");
130146

131147
if user_password.hashed_password {
132148
passwd_cmd.arg("-e");
@@ -156,7 +172,7 @@ impl ModelAdapter for Model {
156172

157173
/// Updates root's authorized_keys file with SSH key
158174
fn update_authorized_keys(&self, ssh_key: &str) -> Result<(), service::Error> {
159-
let file_name = String::from("/root/.ssh/authorized_keys");
175+
let file_name = self.install_dir.join("root/.ssh/authorized_keys");
160176
let mut authorized_keys_file = OpenOptions::new()
161177
.create(true)
162178
.append(true)
@@ -177,8 +193,9 @@ impl ModelAdapter for Model {
177193
return Ok(());
178194
};
179195

180-
let chfn = Command::new("/usr/bin/chfn")
181-
.args(["-f", &full_name, &user_name])
196+
let chfn = Command::new("chroot")
197+
.arg(&self.install_dir)
198+
.args(["chfn", "-f", &full_name, &user_name])
182199
.output()?;
183200

184201
if !chfn.status.success() {

rust/agama-users/src/service.rs

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ impl Starter {
8181
pub async fn start(self) -> Result<Handler<Service>, Error> {
8282
let model = match self.model {
8383
Some(model) => model,
84-
None => Box::new(Model {}),
84+
None => Box::new(Model::new("/mnt")),
8585
};
8686
let service = Service {
8787
full_config: Config::new(),
@@ -117,28 +117,21 @@ impl Service {
117117
}
118118

119119
fn get_proposal(&self) -> Option<api::users::Config> {
120-
if self.find_issues().is_empty() {
120+
if !self.full_config.is_empty() {
121121
return self.full_config.to_api();
122122
}
123123

124124
None
125125
}
126126

127+
/// Updates the service issues.
128+
///
129+
/// At least one user is mandatory
130+
/// - typicaly root or
131+
/// - first user which will operate throught sudo
127132
fn update_issues(&self) -> Result<(), Error> {
128-
let issues = self.find_issues();
129-
self.issues
130-
.cast(issue::message::Set::new(Scope::Users, issues))?;
131-
Ok(())
132-
}
133-
134-
fn find_issues(&self) -> Vec<Issue> {
135133
let mut issues = vec![];
136-
137-
tracing::debug!("hello: {:?}", &self.full_config);
138-
// At least one user is mandatory
139-
// - typicaly root or
140-
// - first user which will operate throught sudo
141-
if self.full_config.root.is_none() && self.full_config.first_user.is_none() {
134+
if self.full_config.is_empty() {
142135
issues.push(Issue::new(
143136
"users.no_auth",
144137
&gettext(
@@ -147,7 +140,21 @@ impl Service {
147140
));
148141
}
149142

150-
issues
143+
if self
144+
.full_config
145+
.first_user
146+
.as_ref()
147+
.is_some_and(|u| !u.is_valid())
148+
{
149+
issues.push(Issue::new(
150+
"users.invalid_user",
151+
&gettext("First user information is incomplete"),
152+
));
153+
}
154+
155+
self.issues
156+
.cast(issue::message::Set::new(Scope::Users, issues))?;
157+
Ok(())
151158
}
152159
}
153160

rust/agama-utils/src/api/users/config.rs

Lines changed: 152 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ impl Config {
5151

5252
Some(self.clone())
5353
}
54+
55+
pub fn is_empty(&self) -> bool {
56+
if self.root.as_ref().is_some_and(|r| !r.is_empty()) {
57+
return false;
58+
}
59+
60+
if self.first_user.as_ref().is_some_and(|u| !u.is_empty()) {
61+
return false;
62+
}
63+
64+
true
65+
}
5466
}
5567

5668
/// First user settings
@@ -73,9 +85,15 @@ pub struct FirstUserConfig {
7385
}
7486

7587
impl FirstUserConfig {
76-
/// Whether it is a valid user.
88+
/// Whether it is an empty user.
89+
pub fn is_empty(&self) -> bool {
90+
self.user_name.is_none()
91+
}
92+
7793
pub fn is_valid(&self) -> bool {
78-
self.user_name.is_some()
94+
self.user_name.as_ref().is_some_and(|n| !n.is_empty())
95+
&& self.full_name.as_ref().is_some_and(|n| !n.is_empty())
96+
&& self.password.as_ref().is_some_and(|p| !p.is_empty())
7997
}
8098
}
8199

@@ -94,6 +112,12 @@ pub struct UserPassword {
94112
pub hashed_password: bool,
95113
}
96114

115+
impl UserPassword {
116+
pub fn is_empty(&self) -> bool {
117+
self.password.is_empty()
118+
}
119+
}
120+
97121
fn overwrite_if_not_empty(old: &mut String, new: String) {
98122
if !new.is_empty() {
99123
*old = new;
@@ -119,13 +143,25 @@ pub struct RootUserConfig {
119143

120144
impl RootUserConfig {
121145
pub fn is_empty(&self) -> bool {
122-
self.password.is_none() && self.ssh_public_key.is_none()
146+
if self
147+
.password
148+
.as_ref()
149+
.is_some_and(|p| !p.password.is_empty())
150+
{
151+
return false;
152+
}
153+
154+
if self.ssh_public_key.as_ref().is_some_and(|p| !p.is_empty()) {
155+
return false;
156+
}
157+
158+
return true;
123159
}
124160
}
125161

126162
#[cfg(test)]
127163
mod test {
128-
use super::{FirstUserConfig, RootUserConfig, UserPassword};
164+
use super::{Config, FirstUserConfig, RootUserConfig, UserPassword};
129165

130166
#[test]
131167
fn test_parse_user_password() {
@@ -139,4 +175,116 @@ mod test {
139175
assert_eq!(&password.password, "$a$b123");
140176
assert_eq!(password.hashed_password, false);
141177
}
178+
179+
#[test]
180+
fn test_is_empty() {
181+
assert_eq!(Config::default().is_empty(), true);
182+
183+
let empty_user_config = Config {
184+
first_user: Some(FirstUserConfig::default()),
185+
..Default::default()
186+
};
187+
assert_eq!(empty_user_config.is_empty(), true);
188+
189+
let empty_root_config = Config {
190+
root: Some(RootUserConfig::default()),
191+
..Default::default()
192+
};
193+
assert_eq!(empty_root_config.is_empty(), true);
194+
195+
let password = UserPassword {
196+
password: "secret".to_string(),
197+
hashed_password: false,
198+
};
199+
let empty_password = UserPassword {
200+
password: "".to_string(),
201+
hashed_password: false,
202+
};
203+
204+
let user_with_password = FirstUserConfig {
205+
user_name: Some("jane".to_string()),
206+
password: Some(password.clone()),
207+
..Default::default()
208+
};
209+
let user_with_password_config = Config {
210+
first_user: Some(user_with_password),
211+
..Default::default()
212+
};
213+
assert_eq!(user_with_password_config.is_empty(), false);
214+
215+
let root_with_password = RootUserConfig {
216+
password: Some(password.clone()),
217+
..Default::default()
218+
};
219+
let root_with_password_config = Config {
220+
root: Some(root_with_password),
221+
..Default::default()
222+
};
223+
assert_eq!(root_with_password_config.is_empty(), false);
224+
225+
let root_with_empty_password = RootUserConfig {
226+
password: Some(empty_password.clone()),
227+
..Default::default()
228+
};
229+
let root_with_empty_password_config = Config {
230+
root: Some(root_with_empty_password),
231+
..Default::default()
232+
};
233+
assert_eq!(root_with_empty_password_config.is_empty(), true);
234+
235+
let root_with_ssh_key = RootUserConfig {
236+
ssh_public_key: Some("12345678".to_string()),
237+
..Default::default()
238+
};
239+
let root_with_ssh_key_config = Config {
240+
root: Some(root_with_ssh_key),
241+
..Default::default()
242+
};
243+
assert_eq!(root_with_ssh_key_config.is_empty(), false);
244+
}
245+
246+
#[test]
247+
fn test_user_is_valid() {
248+
assert_eq!(FirstUserConfig::default().is_valid(), false);
249+
250+
let valid_user = FirstUserConfig {
251+
user_name: Some("firstuser".to_string()),
252+
full_name: Some("First User".to_string()),
253+
password: Some(UserPassword {
254+
password: "12345678".to_string(),
255+
hashed_password: false,
256+
}),
257+
};
258+
assert_eq!(valid_user.is_valid(), true);
259+
260+
let empty_user_name = FirstUserConfig {
261+
user_name: Some("".to_string()),
262+
full_name: Some("First User".to_string()),
263+
password: Some(UserPassword {
264+
password: "12345678".to_string(),
265+
hashed_password: false,
266+
}),
267+
};
268+
assert_eq!(empty_user_name.is_valid(), false);
269+
270+
let empty_full_name = FirstUserConfig {
271+
user_name: Some("firstuser".to_string()),
272+
full_name: Some("".to_string()),
273+
password: Some(UserPassword {
274+
password: "12345678".to_string(),
275+
hashed_password: false,
276+
}),
277+
};
278+
assert_eq!(empty_full_name.is_valid(), false);
279+
280+
let empty_password = FirstUserConfig {
281+
user_name: Some("firstuser".to_string()),
282+
full_name: Some("First User".to_string()),
283+
password: Some(UserPassword {
284+
password: "".to_string(),
285+
hashed_password: false,
286+
}),
287+
};
288+
assert_eq!(empty_password.is_valid(), false);
289+
}
142290
}

0 commit comments

Comments
 (0)