diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 24f0192949..972575c8c5 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -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"] @@ -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" diff --git a/rust/agama-lib/src/users/client.rs b/rust/agama-lib/src/users/client.rs index ee85124b3c..9747619236 100644 --- a/rust/agama-lib/src/users/client.rs +++ b/rust/agama-lib/src/users/client.rs @@ -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, } @@ -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, }) } } @@ -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(), ) diff --git a/rust/agama-lib/src/users/proxies.rs b/rust/agama-lib/src/users/proxies.rs index 73cba89ce5..b3743701de 100644 --- a/rust/agama-lib/src/users/proxies.rs +++ b/rust/agama-lib/src/users/proxies.rs @@ -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. @@ -55,6 +56,7 @@ pub type FirstUser = ( String, String, bool, + bool, std::collections::HashMap, ); @@ -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)>; diff --git a/rust/agama-lib/src/users/settings.rs b/rust/agama-lib/src/users/settings.rs index 54bdaf282e..bd5eea47af 100644 --- a/rust/agama-lib/src/users/settings.rs +++ b/rust/agama-lib/src/users/settings.rs @@ -43,6 +43,8 @@ pub struct FirstUserSettings { pub user_name: Option, /// First user's password (in clear text) pub password: Option, + /// Whether the password is encrypted or is plain text + pub encrypted_password: Option, /// Whether auto-login should enabled or not pub autologin: Option, } @@ -56,6 +58,9 @@ pub struct RootUserSettings { /// Root's password (in clear text) #[serde(skip_serializing)] pub password: Option, + /// Whether the password is encrypted or is plain text + #[serde(skip_serializing)] + pub encrypted_password: Option, /// Root SSH public key pub ssh_public_key: Option, } diff --git a/rust/agama-lib/src/users/store.rs b/rust/agama-lib/src/users/store.rs index f9ec93cecd..1c0635dca0 100644 --- a/rust/agama-lib/src/users/store.rs +++ b/rust/agama-lib/src/users/store.rs @@ -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?; @@ -77,6 +78,7 @@ 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?; @@ -84,9 +86,11 @@ impl UsersStore { } 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?; } diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index b4300b1920..0b23599ec8 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -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)); } diff --git a/service/.rubocop.yml b/service/.rubocop.yml index ada6a9f15e..f5bea90ce8 100644 --- a/service/.rubocop.yml +++ b/service/.rubocop.yml @@ -28,3 +28,6 @@ Lint/UselessAssignment: # be less strict Metrics/AbcSize: Max: 32 + +Metrics/ParameterLists: + Max: 6 diff --git a/service/lib/agama/autoyast/root_reader.rb b/service/lib/agama/autoyast/root_reader.rb index 1598cc5f92..14df63e3c2 100755 --- a/service/lib/agama/autoyast/root_reader.rb +++ b/service/lib/agama/autoyast/root_reader.rb @@ -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 } diff --git a/service/lib/agama/autoyast/user_reader.rb b/service/lib/agama/autoyast/user_reader.rb index 1df0ec05b3..04c62d8e69 100755 --- a/service/lib/agama/autoyast/user_reader.rb +++ b/service/lib/agama/autoyast/user_reader.rb @@ -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 diff --git a/service/lib/agama/dbus/users.rb b/service/lib/agama/dbus/users.rb index d9632a4e34..e44e2b09a8 100644 --- a/service/lib/agama/dbus/users.rb +++ b/service/lib/agama/dbus/users.rb @@ -58,7 +58,8 @@ 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 @@ -66,7 +67,7 @@ def issues 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| @@ -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 }, []) @@ -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), {} ] diff --git a/service/lib/agama/users.rb b/service/lib/agama/users.rb index c294b3e819..84c17cef65 100644 --- a/service/lib/agama/users.rb +++ b/service/lib/agama/users.rb @@ -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? diff --git a/service/test/agama/dbus/users_test.rb b/service/test/agama/dbus/users_test.rb index 744d6cb7db..7dc02cf856 100644 --- a/service/test/agama/dbus/users_test.rb +++ b/service/test/agama/dbus/users_test.rb @@ -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) } @@ -69,16 +70,18 @@ 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 @@ -86,7 +89,7 @@ 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 diff --git a/service/test/agama/users_test.rb b/service/test/agama/users_test.rb index 28808cefa4..4314ec25eb 100644 --- a/service/test/agama/users_test.rb +++ b/service/test/agama/users_test.rb @@ -81,7 +81,7 @@ 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")) @@ -89,11 +89,11 @@ 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 @@ -104,23 +104,23 @@ 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 @@ -128,7 +128,7 @@ 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 @@ -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) @@ -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 diff --git a/web/src/components/users/FirstUserForm.tsx b/web/src/components/users/FirstUserForm.tsx index 48b945a934..f0acb0adb1 100644 --- a/web/src/components/users/FirstUserForm.tsx +++ b/web/src/components/users/FirstUserForm.tsx @@ -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; diff --git a/web/src/components/users/RootPasswordPopup.jsx b/web/src/components/users/RootPasswordPopup.jsx index 15383a5964..2461fd43fc 100644 --- a/web/src/components/users/RootPasswordPopup.jsx +++ b/web/src/components/users/RootPasswordPopup.jsx @@ -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(); }; diff --git a/web/src/components/users/RootPasswordPopup.test.jsx b/web/src/components/users/RootPasswordPopup.test.jsx index f6ddd724dc..0624a56161 100644 --- a/web/src/components/users/RootPasswordPopup.test.jsx +++ b/web/src/components/users/RootPasswordPopup.test.jsx @@ -75,7 +75,10 @@ describe("when it is open", () => { expect(confirmButton).toBeEnabled(); await user.click(confirmButton); - expect(mockRootUserMutation.mutateAsync).toHaveBeenCalledWith({ password }); + expect(mockRootUserMutation.mutateAsync).toHaveBeenCalledWith({ + password, + passwordEncrypted: false, + }); expect(onCloseCallback).toHaveBeenCalled(); }); diff --git a/web/src/queries/users.ts b/web/src/queries/users.ts index e9df67f065..5f59051bc2 100644 --- a/web/src/queries/users.ts +++ b/web/src/queries/users.ts @@ -83,11 +83,12 @@ const useFirstUserChanges = () => { return client.onEvent((event) => { if (event.type === "FirstUserChanged") { - const { fullName, userName, password, autologin, data } = event; + const { fullName, userName, password, passwordEncrypted, autologin, data } = event; queryClient.setQueryData(["users", "firstUser"], { fullName, userName, password, + passwordEncrypted, autologin, data, }); @@ -122,7 +123,7 @@ const useRootUserMutation = () => { }; /** - * Listens for first user changes. + * Listens for root user changes. */ const useRootUserChanges = () => { const client = useInstallerClient(); @@ -138,6 +139,7 @@ const useRootUserChanges = () => { const newRoot = { ...oldRoot }; if (password !== undefined) { newRoot.password = password; + newRoot.encryptedPassword = false; } if (sshkey) { diff --git a/web/src/types/users.ts b/web/src/types/users.ts index 4714669f06..47e29bdfbe 100644 --- a/web/src/types/users.ts +++ b/web/src/types/users.ts @@ -24,11 +24,13 @@ type FirstUser = { fullName: string; userName: string; password: string; + encryptedPassword: boolean; autologin: boolean; }; type RootUser = { password: boolean; + encryptedPassword: boolean; sshkey: string; };