Skip to content

Commit

Permalink
Support for encrypted passwords
Browse files Browse the repository at this point in the history
Allow to use an encrypted (hashed) password in autoinstallation
and in Agama CLI.
  • Loading branch information
lslezak committed Nov 15, 2024
1 parent 07d5dab commit 057ed19
Show file tree
Hide file tree
Showing 18 changed files with 90 additions and 29 deletions.
12 changes: 10 additions & 2 deletions rust/agama-lib/share/profile.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -391,9 +391,13 @@
"examples": ["jane.doe"]
},
"password": {
"title": "User password",
"title": "User password (plain text or encrypted depending on the \"passwordEncrypted\" field)",
"type": "string",
"examples": ["nots3cr3t"]
},
"passwordEncrypted": {
"title": "Flag for encrypted password (true) or plain text password (false or not defined)",
"type": "boolean"
}
},
"required": ["fullName", "userName", "password"]
Expand All @@ -404,9 +408,13 @@
"additionalProperties": false,
"properties": {
"password": {
"title": "Root password",
"title": "Root password (plain text or encrypted depending on the \"passwordEncrypted\" field)",
"type": "string"
},
"passwordEncrypted": {
"title": "Flag for encrypted password (true) or plain text password (false or not defined)",
"type": "boolean"
},
"sshPublicKey": {
"title": "SSH public key",
"type": "string"
Expand Down
6 changes: 5 additions & 1 deletion rust/agama-lib/src/users/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub struct FirstUser {
pub user_name: String,
/// First user's password (in clear text)
pub password: String,
/// Whether the password is encrypted (true) or is plain text (false)
pub encrypted_password: bool,
/// Whether auto-login should enabled or not
pub autologin: bool,
}
Expand All @@ -46,7 +48,8 @@ impl FirstUser {
full_name: data.0,
user_name: data.1,
password: data.2,
autologin: data.3,
encrypted_password: data.3,
autologin: data.4,
})
}
}
Expand Down Expand Up @@ -107,6 +110,7 @@ impl<'a> UsersClient<'a> {
&first_user.full_name,
&first_user.user_name,
&first_user.password,
first_user.encrypted_password,
first_user.autologin,
std::collections::HashMap::new(),
)
Expand Down
3 changes: 3 additions & 0 deletions rust/agama-lib/src/users/proxies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ use zbus::proxy;
/// * full name
/// * user name
/// * password
/// * encrypted_password (true = encrypted, false = plain text)
/// * auto-login (enabled or not)
/// * some optional and additional data
// NOTE: Manually added to this file.
Expand All @@ -55,6 +56,7 @@ pub type FirstUser = (
String,
String,
bool,
bool,
std::collections::HashMap<String, zbus::zvariant::OwnedValue>,
);

Expand All @@ -77,6 +79,7 @@ pub trait Users1 {
full_name: &str,
user_name: &str,
password: &str,
encrypted_password: bool,
auto_login: bool,
data: std::collections::HashMap<&str, &zbus::zvariant::Value<'_>>,
) -> zbus::Result<(bool, Vec<String>)>;
Expand Down
5 changes: 5 additions & 0 deletions rust/agama-lib/src/users/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ pub struct FirstUserSettings {
pub user_name: Option<String>,
/// First user's password (in clear text)
pub password: Option<String>,
/// Whether the password is encrypted or is plain text
pub encrypted_password: Option<bool>,
/// Whether auto-login should enabled or not
pub autologin: Option<bool>,
}
Expand All @@ -56,6 +58,9 @@ pub struct RootUserSettings {
/// Root's password (in clear text)
#[serde(skip_serializing)]
pub password: Option<String>,
/// Whether the password is encrypted or is plain text
#[serde(skip_serializing)]
pub encrypted_password: Option<bool>,
/// Root SSH public key
pub ssh_public_key: Option<String>,
}
8 changes: 7 additions & 1 deletion rust/agama-lib/src/users/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ impl UsersStore {
autologin: Some(first_user.autologin),
full_name: Some(first_user.full_name),
password: Some(first_user.password),
encrypted_password: Some(first_user.encrypted_password),
};
let mut root_user = RootUserSettings::default();
let ssh_public_key = self.users_client.root_ssh_key().await?;
Expand Down Expand Up @@ -77,16 +78,19 @@ impl UsersStore {
full_name: settings.full_name.clone().unwrap_or_default(),
autologin: settings.autologin.unwrap_or_default(),
password: settings.password.clone().unwrap_or_default(),
encrypted_password: settings.encrypted_password.clone().unwrap_or_default(),
..Default::default()
};
self.users_client.set_first_user(&first_user).await?;
Ok(())
}

async fn store_root_user(&self, settings: &RootUserSettings) -> Result<(), ServiceError> {
let encrypted_password = settings.encrypted_password.clone().unwrap_or_default();

if let Some(root_password) = &settings.password {
self.users_client
.set_root_password(root_password, false)
.set_root_password(root_password, encrypted_password)
.await?;
}

Expand Down Expand Up @@ -150,11 +154,13 @@ mod test {
full_name: Some("Tux".to_owned()),
user_name: Some("tux".to_owned()),
password: Some("fish".to_owned()),
encrypted_password: Some(false),
autologin: Some(true),
};
let root_user = RootUserSettings {
// FIXME this is weird: no matter what HTTP reports, we end up with None
password: None,
encrypted_password: None,
ssh_public_key: Some("keykeykey".to_owned()),
};
let expected = UserSettings {
Expand Down
3 changes: 2 additions & 1 deletion rust/agama-server/src/users/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ async fn first_user_changed_stream(
full_name: user.0,
user_name: user.1,
password: user.2,
autologin: user.3,
encrypted_password: user.3,
autologin: user.4,
};
return Some(Event::FirstUserChanged(user_struct));
}
Expand Down
3 changes: 3 additions & 0 deletions service/.rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ Lint/UselessAssignment:
# be less strict
Metrics/AbcSize:
Max: 32

Metrics/ParameterLists:
Max: 6
2 changes: 2 additions & 0 deletions service/lib/agama/autoyast/root_reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def read
return {} unless root_user

hsh = { "password" => root_user.password.value.to_s }
hsh["passwordEncrypted"] = true if root_user.password.value.encrypted?

public_key = root_user.authorized_keys.first
hsh["sshPublicKey"] = public_key if public_key
{ "root" => hsh }
Expand Down
3 changes: 3 additions & 0 deletions service/lib/agama/autoyast/user_reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ def read
"fullName" => user.gecos.first.to_s,
"password" => user.password.value.to_s
}

hsh["passwordEncrypted"] = true if user.password.value.encrypted?

{ "user" => hsh }
end

Expand Down
14 changes: 9 additions & 5 deletions service/lib/agama/dbus/users.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,16 @@ def issues
USERS_INTERFACE = "org.opensuse.Agama.Users1"
private_constant :USERS_INTERFACE

FUSER_SIG = "in FullName:s, in UserName:s, in Password:s, in AutoLogin:b, in data:a{sv}"
FUSER_SIG = "in FullName:s, in UserName:s, in Password:s, in EncryptedPassword:b, " \
"in AutoLogin:b, in data:a{sv}"
private_constant :FUSER_SIG

dbus_interface USERS_INTERFACE do
dbus_reader :root_password_set, "b"

dbus_reader :root_ssh_key, "s", dbus_name: "RootSSHKey"

dbus_reader :first_user, "(sssba{sv})"
dbus_reader :first_user, "(sssbba{sv})"

dbus_method :SetRootPassword,
"in Value:s, in Encrypted:b, out result:u" do |value, encrypted|
Expand Down Expand Up @@ -97,9 +98,11 @@ def issues
dbus_method :SetFirstUser,
# It returns an Struct with the first field with the result of the operation as a boolean
# and the second parameter as an array of issues found in case of failure
FUSER_SIG + ", out result:(bas)" do |full_name, user_name, password, auto_login, data|
FUSER_SIG + ", out result:(bas)" do
|full_name, user_name, password, encrypted_password, auto_login, data|
logger.info "Setting first user #{full_name}"
user_issues = backend.assign_first_user(full_name, user_name, password, auto_login, data)
user_issues = backend.assign_first_user(full_name, user_name, password,
encrypted_password, auto_login, data)

if user_issues.empty?
dbus_properties_changed(USERS_INTERFACE, { "FirstUser" => first_user }, [])
Expand Down Expand Up @@ -133,12 +136,13 @@ def root_ssh_key
def first_user
user = backend.first_user

return ["", "", "", false, {}] unless user
return ["", "", "", false, false, {}] unless user

[
user.full_name,
user.name,
user.password_content || "",
user.password&.value&.encrypted? || false,
backend.autologin?(user),
{}
]
Expand Down
10 changes: 8 additions & 2 deletions service/lib/agama/users.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,21 @@ def remove_root_password
# @param full_name [String]
# @param user_name [String]
# @param password [String]
# @param encrypted_password [Boolean] true = encrypted password, false = plain text password
# @param auto_login [Boolean]
# @param _data [Hash]
# @return [Array] the list of fatal issues found
def assign_first_user(full_name, user_name, password, auto_login, _data)
def assign_first_user(full_name, user_name, password, encrypted_password, auto_login, _data)
remove_first_user

user = Y2Users::User.new(user_name)
user.gecos = [full_name]
user.password = Y2Users::Password.create_plain(password)
user.password = if encrypted_password
Y2Users::Password.create_encrypted(password)
else
Y2Users::Password.create_plain(password)
end

fatal_issues = user.issues.map.select(&:error?)
return fatal_issues.map(&:message) unless fatal_issues.empty?

Expand Down
9 changes: 6 additions & 3 deletions service/test/agama/dbus/users_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
require "agama/dbus/interfaces/service_status"
require "agama/dbus/users"
require "agama/users"
require "y2users"

describe Agama::DBus::Users do
subject { described_class.new(backend, logger) }
Expand Down Expand Up @@ -69,24 +70,26 @@
let(:user) { nil }

it "returns default data" do
expect(subject.first_user).to eq(["", "", "", false, {}])
expect(subject.first_user).to eq(["", "", "", false, false, {}])
end
end

context "if there is an user" do
let(:password) { Y2Users::Password.create_encrypted("12345") }
let(:user) do
instance_double(Y2Users::User,
full_name: "Test user",
name: "test",
password_content: "12345")
password: password,
password_content: password.value.to_s)
end

before do
allow(backend).to receive(:autologin?).with(user).and_return(true)
end

it "returns the first user data" do
expect(subject.first_user).to eq(["Test user", "test", "12345", true, {}])
expect(subject.first_user).to eq(["Test user", "test", password.value.to_s, true, true, {}])
end
end
end
Expand Down
20 changes: 10 additions & 10 deletions service/test/agama/users_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,19 @@
describe "#assign_first_user" do
context "when the options given do not present any issue" do
it "adds the user to the user's configuration" do
subject.assign_first_user("Jane Doe", "jane", "12345", false, {})
subject.assign_first_user("Jane Doe", "jane", "12345", false, false, {})
user = users_config.users.by_name("jane")
expect(user.full_name).to eq("Jane Doe")
expect(user.password).to eq(Y2Users::Password.create_plain("12345"))
end

context "when a first user exists" do
before do
subject.assign_first_user("Jane Doe", "jane", "12345", false, {})
subject.assign_first_user("Jane Doe", "jane", "12345", false, false, {})
end

it "replaces the user with the new one" do
subject.assign_first_user("John Doe", "john", "12345", false, {})
subject.assign_first_user("John Doe", "john", "12345", false, false, {})

user = users_config.users.by_name("jane")
expect(user).to be_nil
Expand All @@ -104,31 +104,31 @@
end

it "returns an empty array of issues" do
issues = subject.assign_first_user("Jane Doe", "jane", "12345", false, {})
issues = subject.assign_first_user("Jane Doe", "jane", "12345", false, false, {})
expect(issues).to be_empty
end
end

context "when the given arguments presents some critical error" do
it "does not add the user to the config" do
subject.assign_first_user("Jonh Doe", "john", "", false, {})
subject.assign_first_user("Jonh Doe", "john", "", false, false, {})
user = users_config.users.by_name("john")
expect(user).to be_nil
subject.assign_first_user("Ldap user", "ldap", "12345", false, {})
subject.assign_first_user("Ldap user", "ldap", "12345", false, false, {})
user = users_config.users.by_name("ldap")
expect(user).to be_nil
end

it "returns an array with all the issues" do
issues = subject.assign_first_user("Root user", "root", "12345", false, {})
issues = subject.assign_first_user("Root user", "root", "12345", false, false, {})
expect(issues.size).to eql(1)
end
end
end

describe "#remove_first_user" do
before do
subject.assign_first_user("Jane Doe", "jane", "12345", false, {})
subject.assign_first_user("Jane Doe", "jane", "12345", false, false, {})
end

it "removes the already defined first user" do
Expand Down Expand Up @@ -156,7 +156,7 @@
end

it "writes system and installer defined users" do
subject.assign_first_user("Jane Doe", "jane", "12345", false, {})
subject.assign_first_user("Jane Doe", "jane", "12345", false, false, {})

expect(Y2Users::Linux::Writer).to receive(:new) do |target_config, _old_config|
user_names = target_config.users.map(&:name)
Expand Down Expand Up @@ -196,7 +196,7 @@

context "when a first user is defined" do
before do
subject.assign_first_user("Jane Doe", "jdoe", "123456", false, {})
subject.assign_first_user("Jane Doe", "jdoe", "123456", false, false, {})
end

it "returns an empty list" do
Expand Down
4 changes: 4 additions & 0 deletions web/src/components/users/FirstUserForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ export default function FirstUserForm() {

if (!changePassword) {
delete user.password;
} else {
// the web UI only supports plain text passwords, this resets the flag if an encrypted
// password was previously set from CLI
user.encryptedPassword = false;
}
delete user.passwordConfirmation;
user.autologin = !!user.autologin;
Expand Down
4 changes: 3 additions & 1 deletion web/src/components/users/RootPasswordPopup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ export default function RootPasswordPopup({ title = _("Root password"), isOpen,
const accept = async (e) => {
e.preventDefault();
// TODO: handle errors
if (password !== "") await setRootUser.mutateAsync({ password });
// the web UI only supports plain text passwords, this resets the flag if an encrypted password
// was previously set from CLI
if (password !== "") await setRootUser.mutateAsync({ password, passwordEncrypted: false });
close();
};

Expand Down
Loading

0 comments on commit 057ed19

Please sign in to comment.