Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b4585cf
Cleanup: "unused" warnings
mchf Mar 12, 2026
c9c5a4c
Refactoring: function for storing ssh keys modified to handle vector
mchf Mar 12, 2026
9412e85
Store ssh keys for user if any
mchf Mar 12, 2026
5295715
Handling of multiple ssh keys for root in users service
mchf Mar 12, 2026
1c4851f
Updated schema
mchf Mar 12, 2026
5012534
Modified AutoYast root reader to convert list of ssh keys
mchf Mar 13, 2026
c843ba0
Modified AutoYast user reader to allow conversion of authorized keys
mchf Mar 13, 2026
8213b9a
Happy rubocop
mchf Mar 13, 2026
3b3c78f
Fixed FirsUserConfig test
mchf Mar 13, 2026
33b371e
Fixed ruby tests
mchf Mar 13, 2026
eb4cbac
Fixed FirstUserConfig test
mchf Mar 13, 2026
b14d897
Merge branch 'master' into multiple_ssh_keys
mchf Mar 13, 2026
9012bef
Made sshPublicKeys an alias for sshPublicKey in case of root user
mchf Mar 13, 2026
13d46a7
Fixed tests
mchf Mar 13, 2026
d13f719
Formatting
mchf Mar 13, 2026
4a6f56d
Fixed OpenApi
mchf Mar 13, 2026
da6ab1f
Improvement for OpenAPI fix
mchf Mar 13, 2026
30412a4
Cleanup: removed duplicities using serde alias
mchf Mar 13, 2026
1707679
Merge branch 'master' into multiple_ssh_keys
mchf Mar 13, 2026
c938bb8
Open SSH port and activate SSH service even when for first user
mchf Mar 13, 2026
49b0190
Refactoring: parsing of sshPublicKey for user and root
mchf Mar 13, 2026
2f37766
Refactoring: replaced if ! with unless on request :-/
mchf Mar 13, 2026
0180da4
Refactoring: cleaned according to clippy
mchf Mar 13, 2026
cb20375
Temporal adjustment to the system schema
ancorgs Mar 13, 2026
a3d696d
service: Hotfix for wrong report of TpmFde encryption
ancorgs Mar 13, 2026
ef005fc
web: Adapt system openAPI export
ancorgs Mar 13, 2026
700ad4b
Changelogs
ancorgs Mar 13, 2026
e42a209
Fixed rust tests
mchf Mar 14, 2026
a904e95
Updated changelog
mchf Mar 14, 2026
02d8f36
StringOrList for user as is for root user. Second try.
mchf Mar 15, 2026
c7b2907
Merge branch 'master' into multiple_ssh_keys
mchf Mar 15, 2026
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
14 changes: 14 additions & 0 deletions rust/agama-lib/share/profile.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,13 @@
"hashedPassword": {
"title": "Flag for hashed password (true) or plain text password (false or not defined)",
"type": "boolean"
},
"sshPublicKeys": {
"title": "List of SSH public keys",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["fullName", "userName", "password"]
Expand All @@ -727,6 +734,13 @@
"sshPublicKey": {
"title": "SSH public key",
"type": "string"
},
"sshPublicKeys": {
"title": "List of SSH public keys",
"type": "array",
"items": {
"type": "string"
}
}
}
},
Expand Down
42 changes: 35 additions & 7 deletions rust/agama-users/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,18 @@ impl Model {
)));
}

self.set_user_group(user_name)?;
if let Some(ssh_keys) = &user.ssh_public_keys {
// TODO:
// 1) enable sshd & open port as in case of root or not?
// 2) do some magic about user's home dir path or stay
// with hardcoded default?
self.update_authorized_keys(
&PathBuf::from(format!("/home/{}/.ssh", user_name)),
&ssh_keys.iter().collect::<Vec<&String>>(),
)?;
}

let _ = self.set_user_group(user_name);
self.set_user_password(user_name, user_password)?;
self.update_user_fullname(user)
}
Expand All @@ -145,8 +156,18 @@ impl Model {
}

// store ssh key for root if any
let mut ssh_keys = if let Some(ssh_keys) = &root.ssh_public_keys {
ssh_keys.iter().collect::<Vec<&String>>()
} else {
vec![]
};
// for historical reason and backward compatibility. Originally
// there was at most one public SSH key for the root
if let Some(ref root_ssh_key) = root.ssh_public_key {
self.update_authorized_keys(root_ssh_key)?;
// TODO: deal with possible duplicates?
ssh_keys.push(root_ssh_key);

self.update_authorized_keys(&PathBuf::from("root/.ssh/authorized_keys"), &ssh_keys)?;
self.enable_sshd_service()?;
self.open_ssh_port()?;
}
Expand Down Expand Up @@ -210,9 +231,13 @@ impl Model {
}

/// Updates root's authorized_keys file with SSH key
fn update_authorized_keys(&self, ssh_key: &str) -> Result<(), service::Error> {
fn update_authorized_keys(
&self,
keys_path: &PathBuf,
ssh_keys: &[&String],
) -> Result<(), service::Error> {
let mode = 0o644;
let file_name = self.install_dir.join("root/.ssh/authorized_keys");
let file_name = self.install_dir.join(keys_path);
let mut authorized_keys_file = OpenOptions::new()
.create(true)
.append(true)
Expand All @@ -223,9 +248,12 @@ impl Model {
// sets mode also for an existing file
fs::set_permissions(&file_name, Permissions::from_mode(mode))?;

writeln!(authorized_keys_file, "{}", ssh_key.trim())?;

Ok(())
ssh_keys
.iter()
.try_for_each(|ssh_key| -> Result<(), service::Error> {
writeln!(authorized_keys_file, "{}", ssh_key.trim())?;
Ok(())
})
}

/// Enables sshd service in the target system
Expand Down
35 changes: 35 additions & 0 deletions rust/agama-utils/src/api/users/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ pub struct FirstUserConfig {
/// First user's username
#[merge(strategy = merge::option::overwrite_none)]
pub user_name: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[serde(skip_serializing_if = "Option::is_none")]
pub ssh_public_keys: Option<Vec<String>>,
}

impl FirstUserConfig {
Expand Down Expand Up @@ -140,6 +143,9 @@ pub struct RootUserConfig {
#[merge(strategy = merge::option::overwrite_none)]
#[serde(skip_serializing_if = "Option::is_none")]
pub ssh_public_key: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[serde(skip_serializing_if = "Option::is_none")]
pub ssh_public_keys: Option<Vec<String>>,
}

impl RootUserConfig {
Expand All @@ -156,6 +162,10 @@ impl RootUserConfig {
return false;
}

if self.ssh_public_keys.as_ref().is_some_and(|k| !k.is_empty()) {
return false;
}

true
}
}
Expand Down Expand Up @@ -242,6 +252,16 @@ mod test {
..Default::default()
};
assert!(!root_with_ssh_key_config.is_empty());

let root_with_ssh_keys = RootUserConfig {
ssh_public_keys: Some(vec!["12345678".to_string()]),
..Default::default()
};
let root_with_ssh_keys_config = Config {
root: Some(root_with_ssh_keys),
..Default::default()
};
assert!(!root_with_ssh_keys_config.is_empty());
}

#[test]
Expand All @@ -255,6 +275,7 @@ mod test {
password: "12345678".to_string(),
hashed_password: false,
}),
ssh_public_keys: None,
};
assert!(valid_user.is_valid());

Expand All @@ -265,6 +286,7 @@ mod test {
password: "12345678".to_string(),
hashed_password: false,
}),
ssh_public_keys: None,
};
assert!(!empty_user_name.is_valid());

Expand All @@ -275,6 +297,7 @@ mod test {
password: "12345678".to_string(),
hashed_password: false,
}),
ssh_public_keys: None,
};
assert!(!empty_full_name.is_valid());

Expand All @@ -285,7 +308,19 @@ mod test {
password: "".to_string(),
hashed_password: false,
}),
ssh_public_keys: None,
};
assert!(!empty_password.is_valid());

let with_ssh_keys = FirstUserConfig {
user_name: Some("firstuser".to_string()),
ssh_public_keys: Some(vec!["12345678".to_string()]),
..Default::default()
};
let with_ssh_keys_config = Config {
first_user: Some(with_ssh_keys),
..Default::default()
};
assert!(!with_ssh_keys_config.is_empty());
}
}
14 changes: 12 additions & 2 deletions service/lib/agama/autoyast/root_reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ def read
hsh["hashedPassword"] = true if password.value.encrypted?
end

public_key = root_user.authorized_keys.first
hsh["sshPublicKey"] = public_key if public_key
hsh = hsh.merge(setup_ssh(root_user))

return {} if hsh.empty?

Expand All @@ -66,6 +65,17 @@ def config
result = reader.read
@config = result.config
end

def setup_ssh(root_user)
hsh = {}

public_key = root_user.authorized_keys.first

hsh["sshPublicKey"] = public_key if public_key
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you exporting the sshPublicKey? I would expect to only use sshPublicKeys.

hsh["sshPublicKeys"] = root_user.authorized_keys if !root_user.authorized_keys.empty?

hsh
end
end
end
end
14 changes: 10 additions & 4 deletions service/lib/agama/autoyast/user_reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,16 @@ def read
user = config.users.find { |u| !u.system? && !u.root? }
return {} unless user

hsh = {
"userName" => user.name,
"fullName" => user.gecos.first.to_s
}
hsh = basic_user_info(user)

password = user.password
if password
hsh["password"] = password.value.to_s
hsh["hashedPassword"] = true if password.value.encrypted?
end

hsh["sshPublicKeys"] = user.authorized_keys if !user.authorized_keys.empty?

{ "user" => hsh }
end

Expand All @@ -63,6 +62,13 @@ def config
result = reader.read
@config = result.config
end

def basic_user_info(user)
{
"userName" => user.name,
"fullName" => user.gecos.first.to_s
}
end
end
end
end
4 changes: 3 additions & 1 deletion service/test/agama/autoyast/root_reader_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@
it "includes a 'root' key with the root user data" do
root = subject.read["root"]
expect(root).to eq(
"password" => "123456", "sshPublicKey" => "ssh-key 1"
"password" => "123456",
"sshPublicKey" => "ssh-key 1",
"sshPublicKeys" => ["ssh-key 1", "ssh-key 2"]
)
end

Expand Down
Loading