diff --git a/package/yast2-users.changes b/package/yast2-users.changes index b69c4114b..9b9fd424b 100644 --- a/package/yast2-users.changes +++ b/package/yast2-users.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Fri Nov 2 08:46:35 UTC 2018 - igonzalezsosa@suse.com + +- Allow the root user to use a public key for authentication + (fate#324690). +- 4.0.8 + ------------------------------------------------------------------- Fri Oct 19 13:58:31 UTC 2018 - snwint@suse.com diff --git a/package/yast2-users.spec b/package/yast2-users.spec index 4ec1cf42b..8de6f7acb 100644 --- a/package/yast2-users.spec +++ b/package/yast2-users.spec @@ -17,7 +17,7 @@ Name: yast2-users -Version: 4.0.7 +Version: 4.0.8 Release: 0 BuildRoot: %{_tmppath}/%{name}-%{version}-build @@ -38,6 +38,8 @@ BuildRequires: yast2-perl-bindings BuildRequires: yast2-security BuildRequires: yast2-testsuite BuildRequires: rubygem(%rb_default_ruby_abi:rspec) +# ssh-keygen +BuildRequires: openssh Requires: cracklib Requires: perl-Digest-SHA1 @@ -64,6 +66,9 @@ Requires: yast2-core >= 2.21.0 Requires: yast2-ruby-bindings >= 1.0.0 +# ssh-keygen +Requires: openssh + Summary: YaST2 - User and Group Configuration License: GPL-2.0-only Group: System/YaST diff --git a/src/Makefile.am b/src/Makefile.am index 564b6a82f..ebe378cbb 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -58,12 +58,19 @@ ylibdialog_DATA = \ lib/users/dialogs/users_to_import.rb \ lib/users/dialogs/encryption_method.rb +ywidgetdir = @ylibdir@/users/widgets +ywidget_DATA = \ + lib/users/widgets/inst_root_first.rb \ + lib/users/widgets/public_key_selector.rb + ylibdir = @ylibdir@/users ylib_DATA = \ lib/users/ca_password_validator.rb \ lib/users/local_password.rb \ lib/users/encryption_method.rb \ + lib/users/leaf_blk_device.rb \ lib/users/proposal.rb \ + lib/users/ssh_public_key.rb \ lib/users/encryption_proposal.rb \ lib/users/ssh_authorized_keys_file.rb \ lib/users/ssh_authorized_keyring.rb \ diff --git a/src/lib/users/dialogs/inst_root_first.rb b/src/lib/users/dialogs/inst_root_first.rb index 75722a909..9957e825d 100644 --- a/src/lib/users/dialogs/inst_root_first.rb +++ b/src/lib/users/dialogs/inst_root_first.rb @@ -19,69 +19,46 @@ # current contact information at www.novell.com. # ------------------------------------------------------------------------------ -require "users/widgets" -require "users/ca_password_validator" -require "users/local_password" - -require "ui/widgets" +require "cwm/dialog" +require "users/widgets/inst_root_first" +Yast.import "Mode" +Yast.import "UsersSimple" module Yast # This library provides a simple dialog for setting new password for the # system adminitrator (root) including checking quality of the password # itself. The new password is not stored here, just set in UsersSimple module # and stored later during inst_finish. - class InstRootFirstDialog - include Yast::Logger - include Yast::I18n - include Yast::UIShortcuts - - def run - Yast.import "UI" - Yast.import "Mode" - Yast.import "UsersSimple" - Yast.import "CWM" - + class InstRootFirstDialog < ::CWM::Dialog + def initialize textdomain "users" + end - return :auto unless root_password_dialog_needed? - - # We do not need to create a wizard dialog in installation, but it's - # helpful when testing all manually on a running system - Wizard.CreateDialog if separate_wizard_needed? - - Wizard.SetTitleIcon("yast-users") - - ret = CWM.show( - content, - # Title for root-password dialogue - caption: _("Password for the System Administrator \"root\""), - ) - - Wizard.CloseDialog if separate_wizard_needed? - - ret + # @return [String] Dialog's title + # @see CWM::AbstractWidget + def title + _("Authentication for the System Administrator \"root\"") end - private + # @see CWM::Dialog + def run + return :auto unless root_password_dialog_needed? + super + end # Returns a UI widget-set for the dialog - def content - VBox( - VStretch(), - HSquash( - VBox( - ::Users::PasswordWidget.new(focus: true), - VSpacing(2.4), - ::UI::Widgets::KeyboardLayoutTest.new - ) - ), - VStretch() - ) + def contents + VBox(Y2Users::Widgets::InstRootFirst.new) end + private + # Returns whether we need/ed to create new UI Wizard - def separate_wizard_needed? + # + # @note We do not need to create a wizard dialog in installation, but it's helpful when testing + # all manually on a running system + def should_open_dialog? Mode.normal end diff --git a/src/lib/users/leaf_blk_device.rb b/src/lib/users/leaf_blk_device.rb new file mode 100644 index 000000000..fc1ce23af --- /dev/null +++ b/src/lib/users/leaf_blk_device.rb @@ -0,0 +1,122 @@ +# encoding: utf-8 + +# Copyright (c) [2018] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "json" +require "yast2/execute" + +module Y2Users + # This class represents a block device reported by `lsblk` as a leaf node + # + # As we cannot use a devicegraph for the time being, this class extracts the + # block devices information from the lsblk command. Only the leaf devices + # are taken into account and it only includes information which is relevant + # for the {Y2Users::Widgets::DiskSelector} widget. + class LeafBlkDevice + class << self + # Returns all relevant block devices + # + # @note It takes the information from `lsblk` + # + # @return [Array] List of relevant block devices + def all + lsblk["blockdevices"].map { |h| new_from_hash(h) } + end + + # Instantiates a new object + # + # @note It uses a Hash with information from `lsblk`. + # + # @return [LeafBlkDevice] New LeafBlkDevice instance + def new_from_hash(hash) + parent = find_root_device(hash) + new( + name: hash["name"], disk: parent["name"], model: parent["model"], + transport: parent["tran"], fstype: hash["fstype"] + ) + end + + private + + # Gets `lsblk` into a Hash + # + # @return [Hash] Hash containing data from `lsblk` + def lsblk + output = Yast::Execute.locally( + "/usr/bin/lsblk", "--inverse", "--json", "--paths", + "--output", "NAME,TRAN,FSTYPE,MODEL", stdout: :capture + ) + return { "blockdevices" => [] } if output.nil? + JSON.parse(output) + end + + # Finds the root for a given device + # + # @return [Hash] + def find_root_device(hash) + hash.key?("children") ? find_root_device(hash["children"][0]) : hash + end + end + + # @return [String] Kernel name + attr_reader :name + + # @return [String] Hardware model + attr_reader :model + + # @return [String] Disk's kernel name + attr_reader :disk + + # @return [Symbol] Disk's transport (:usb, :ata, etc.) + attr_reader :transport + + # @return [Symbol] Filesystem type + attr_reader :fstype + + # Constructor + # + # @param name [String] Kernel name + # @param disk [String] Disk's kernel name + # @param model [String] Hardware model + # @param transport [symbol] Transport + # @param fstype [Symbol] Filesystem type + def initialize(name:, disk:, model:, transport: nil, fstype: nil) + @name = name + @model = model + @disk = disk + @transport = transport.to_sym if transport + @fstype = fstype.to_sym if fstype + end + + # Determines whether the device has a filesystem + # + # @return [Boolean] + def filesystem? + !!fstype + end + + # Determines whether the device has a transport + # + # @return [Boolean] + def transport? + !!transport + end + end +end diff --git a/src/lib/users/ssh_public_key.rb b/src/lib/users/ssh_public_key.rb new file mode 100644 index 000000000..9930d4a73 --- /dev/null +++ b/src/lib/users/ssh_public_key.rb @@ -0,0 +1,76 @@ +# encoding: utf-8 + +# Copyright (c) [2018] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "yast2/execute" + +module Y2Users + # This class is a simplified representation of a OpenSSH public key. + # + # @example Read a public key + # key = Y2Users::SSHPublicKey.new(File.read("id_rsa.pub")) + # key.fingerprint # => "SHA256:uadPyDQj9VlFZVjK8UNp57jOnWwzGgKQJpeJEhZyV0I" + class SSHPublicKey + # Not a valid SSH public key + class InvalidKey < StandardError; end + + # @return [String] Key fingerprint + attr_reader :fingerprint + + # Constructor + # + # @param raw [String] Public key content + # + # @raise InvalidKey + def initialize(raw) + @fingerprint = fingerprint_from(raw) + @raw = raw.strip + end + + # Returns the key comment + # + # @return [String] Comment field + def comment + @comment ||= @raw.split(" ")[2] + end + + # Returns the string version of the public key + # + # @return [String] + def to_s + @raw + end + + private + + # Gets the fingerprint for the given OpenSSH public key + # + # @return [String] Key fingerprint + # @raise InvalidKey + def fingerprint_from(raw) + output = Yast::Execute.locally!( + ["echo", raw], ["ssh-keygen", "-l", "-f", "/dev/stdin"], stdout: :capture + ) + output.split(" ")[1].to_s + rescue Cheetah::ExecutionFailed + raise InvalidKey + end + end +end diff --git a/src/lib/users/widgets.rb b/src/lib/users/widgets.rb index 8e27418c3..da353a6ec 100644 --- a/src/lib/users/widgets.rb +++ b/src/lib/users/widgets.rb @@ -45,10 +45,11 @@ class << self # If `little_space` is `true`, the helpful label is omitted # and the password fields are laid out horizontally. # @param focus [Boolean] if set, then widget set focus to first password input field - def initialize(little_space: false, focus: false) + def initialize(little_space: false, focus: false, allow_empty: false) textdomain "users" @little_space = little_space @focus = focus + @allow_empty = allow_empty end def contents @@ -97,6 +98,8 @@ def init def validate password1 = Yast::UI.QueryWidget(Id(:pw1), :Value) password2 = Yast::UI.QueryWidget(Id(:pw2), :Value) + return true if allow_empty? && password1.empty? + if password1 != password2 # report misspellings of the password Yast::Popup.Message(_("The passwords do not match.\nTry again.")) @@ -134,6 +137,7 @@ def validate end def store + return if allow_empty? && empty? password1 = Yast::UI.QueryWidget(Id(:pw1), :Value) Yast::UsersSimple.SetRootPassword(password1) end @@ -180,5 +184,23 @@ def help helptext << ::Users::CAPasswordValidator.new.help_text end + + # Determines whether the widget is empty or not + # + # @return [Boolean] + def empty? + pw1 = Yast::UI.QueryWidget(Id(:pw1), :Value) + pw2 = Yast::UI.QueryWidget(Id(:pw2), :Value) + pw1.to_s.empty? && pw2.to_s.empty? + end + + # Determines whether is allowed to do not fill the password + # + # @note In that case, the password will not be validated or stored if it is left empty. + # + # @return [Boolean] + def allow_empty? + @allow_empty + end end end diff --git a/src/lib/users/widgets/inst_root_first.rb b/src/lib/users/widgets/inst_root_first.rb new file mode 100644 index 000000000..1e3df295d --- /dev/null +++ b/src/lib/users/widgets/inst_root_first.rb @@ -0,0 +1,79 @@ +# encoding: utf-8 + +# Copyright (c) [2018] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "cwm" +require "users/widgets" +require "users/widgets/public_key_selector" +require "ui/widgets" +require "yast2/popup" + +module Y2Users + module Widgets + # This class displays the initial configuration settings for the root user. + class InstRootFirst < ::CWM::CustomWidget + # Returns a UI widget-set for the dialog + def contents + @contents ||= + VBox( + VStretch(), + HSquash( + VBox( + password_widget, + ::UI::Widgets::KeyboardLayoutTest.new, + VSpacing(2.4), + public_key_selector + ) + ), + VStretch() + ) + end + + # @see CWM::AbstractWidget + def validate + return true unless password_widget.empty? && public_key_selector.empty? + Yast2::Popup.show( + _("You need to provide at least a password or a public key."), headline: :error + ) + false + end + + private + + # Returns a password widget + # + # @note The widget is memoized + # + # @return [Users::PasswordWidget] Password widget + def password_widget + @password_widget ||= ::Users::PasswordWidget.new(focus: true, allow_empty: true) + end + + # Returns a public key selection widget + # + # @note The widget is memoized + # + # @return [Y2Users::Widgets::PublicKeySelector] Public key selection widget + def public_key_selector + @public_key_selector ||= ::Y2Users::Widgets::PublicKeySelector.new + end + end + end +end diff --git a/src/lib/users/widgets/public_key_selector.rb b/src/lib/users/widgets/public_key_selector.rb new file mode 100644 index 000000000..307452b5a --- /dev/null +++ b/src/lib/users/widgets/public_key_selector.rb @@ -0,0 +1,287 @@ +# encoding: utf-8 + +# Copyright (c) [2018] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "cwm" +require "users/leaf_blk_device" +require "users/ssh_public_key" +require "yast2/popup" +require "tmpdir" + +Yast.import "Label" +Yast.import "UI" +Yast.import "SSHAuthorizedKeys" + +module Y2Users + module Widgets + # This widget allows to select a public key from a removable device + class PublicKeySelector < ::CWM::CustomWidget + + class << self + # We want this information (the selected device name and the SSH key) to be remembered + attr_accessor :selected_blk_device_name, :value + end + + # Constructor + def initialize + textdomain "users" + end + + # @return [String] Widget label + def label + _("Import Public Key") + end + + # @return [Yast::Term] Dialog content + # @see CWM::CustomWidget + def contents + VBox( + Left(Label(label)), + ReplacePoint(Id(:inner_content), inner_content) + ) + end + + # Events handler + # + # @see CWM::AbstractWdiget + def handle(event) + case event["ID"] + when :browse + read_key + when :remove + remove_key + when :refresh + reset_available_blk_devices + end + update + + nil + end + + # @see CWM::AbstractWdiget + def store + Yast::SSHAuthorizedKeys.import_keys("/root", [value.to_s]) if value + nil + end + + # Forces this widget to listen to all events + # + # @return [Boolean] + def handle_all_events + true + end + + # Determines whether a public key is selected or not + # + # @return [Boolean] true if empty; false otherwise + def empty? + value.nil? + end + + # Helper method to get the current value (the selected public key) + # + # @return [SSHPublicKey,nil] Return the selected public key (nor nil if no key is selected) + def value + self.class.value + end + + # Returns the help text regarding the use of a public key for authentication + # + # @return [String] Help text + def help + @help ||= _( + "

\n" \ + "In some situations it is preferred to access to the system remotely via SSH\n" \ + "using a public key instead of a password. This screen allows you to select\n" \ + "one public key from an USB stick, a CD/DVD ROM or even from an existing\n" \ + "partition.\n\n" \ + "Take into account that the root password and the public key are not mutually\n" \ + "exclusive: you can provide both if you want.\n" \ + "

\n" + ) + end + + private + + # Helper method to set the current value (the selected public key) + # + # @param [SSHPublicKey] Return the current public key + def value=(key) + self.class.value = key + end + + def selected_blk_device_name + self.class.selected_blk_device_name + end + + # Widget's inner content + # + # It displays the public key content or a disk selector if no key has been selected. + # + # @return [Yast::Term] + def inner_content + VBox(empty? ? blk_device_selector : public_key_content) + end + + # Disk selector + # + # This widget displays includes a list of selectable disk and a button to browse the + # selected one. + # + # @return [Yast::Term] + def blk_device_selector + VBox( + Left( + HBox( + blk_devices_combo_box, + PushButton(Id(:refresh), Opt(:notify), Yast::Label.RefreshButton), + ) + ), + Left(PushButton(Id(:browse), Opt(:notify), Yast::Label.BrowseButton)), + ) + end + + # UI which shows the public key content + # + # @return [Yast::Term] + def public_key_content + VBox( + Left(Label(value.fingerprint)), + HBox( + Left(Label(value.comment)), + Right(PushButton(Id(:remove), Opt(:notify), Yast::Label.RemoveButton)) + ) + ) + end + + # Key comment to show to the user + # + # @note When no comment is present, the widget shows 'no comment' just as ssh-keygen does. + # + # @return [String] + def comment_value + # TRANSLATORS: the public key does not contain a comment (which is often used as some sort + # of description in order to identify the key) + value.comment || _("no comment") + end + + # Disk combo box + # + # Displays a combo box containing al selectable devices. + # + # @return [Yast::Term] + def blk_devices_combo_box + options = available_blk_devices.map do |dev| + Item(Id(dev.name), "#{dev.model} (#{dev.name})", dev.name == selected_blk_device_name) + end + ComboBox(Id(:blk_device), Opt(:hstretch), "", options) + end + + # Returns a list of devices that can be selected + # + # Only the devices that meets those conditions are considered: + # + # * It has a transport (so loop devices are automatically discarded). + # * It has a filesystem but it is not squashfs, as it is used by the installer. + # + # The first condition should be enough. However, we want to avoid future problems if the lsblk + # authors decide to show some information in the 'TRAN' (transport) property for those devices + # that does not have one (for instance, something like 'none'). + # + # @return [Array] List of devices + def available_blk_devices + @available_blk_devices ||= LeafBlkDevice.all.select do |dev| + dev.filesystem? && dev.fstype != :squash && dev.transport? + end + end + + # Selects the current block device + def select_blk_device + self.class.selected_blk_device_name = Yast::UI.QueryWidget(Id(:blk_device), :Value) + end + + # Refreshes widget content + def update + Yast::UI.ReplaceWidget(Id(:inner_content), inner_content) + end + + # Reads the key selected by the user + # + # @note This method mounts the selected filesystem. + # + # @return [String] Key content + def read_key + select_blk_device + dir = Dir.mktmpdir + begin + mounted = Yast::SCR.Execute( + Yast::Path.new(".target.mount"), [selected_blk_device_name, dir], "-o ro" + ) + if mounted + read_key_from(dir) + else + report_mount_error(selected_blk_device_name) + end + ensure + Yast::SCR.Execute(Yast::Path.new(".target.umount"), dir) if mounted + FileUtils.remove_entry_secure(dir) + end + end + + # Reads a key from the given directory + # + # @note Asks the user to select a file and tries to read it. + def read_key_from(dir) + # TRANSLATORS: title of a dialog which allows to select a file to be used + # as SSH public key + path = Yast::UI.AskForExistingFile(dir, "*.pub", _("Select a public key")) + return unless path && File.exist?(path) + self.value = SSHPublicKey.new(File.read(path)) + rescue SSHPublicKey::InvalidKey + report_invalid_key + end + + # Removes the selected key + def remove_key + self.value = nil + end + + # Resets the devices list + def reset_available_blk_devices + @available_blk_devices = nil + end + + # Displays an error about the device which failed to be mounted + # + # @param device [String] Device's name + def report_mount_error(device) + message = format(_("Could not mount device %s"), device) + Yast2::Popup.show(message, headline: :error) + end + + # Displays an error about an invalid SSH key + def report_invalid_key + Yast2::Popup.show( + _("The selected file does not contain a valid public key"), headline: :error + ) + end + end + end +end diff --git a/src/modules/UsersSimple.pm b/src/modules/UsersSimple.pm index f8443a757..98fc1f0da 100644 --- a/src/modules/UsersSimple.pm +++ b/src/modules/UsersSimple.pm @@ -114,6 +114,7 @@ YaST::YCP::Import ("SCR"); YaST::YCP::Import ("Stage"); YaST::YCP::Import ("SystemFilesCopy"); YaST::YCP::Import ("UsersUI"); +YaST::YCP::Import ("SSHAuthorizedKeys"); # known system users (hard-written here to check user name conflicts) # number may mean the UID (but it don't have to be defined) @@ -364,8 +365,13 @@ sub CryptPassword { BEGIN { $TYPEINFO{WriteRootPassword} = ["function", "boolean"];} sub WriteRootPassword { - my $self = shift; - my $crypted = $self->CryptPassword ($root_password, "system"); + my $self = shift; + my $crypted = "!"; + + if ($root_password ne "") { + $crypted = $self->CryptPassword ($root_password, "system"); + } + return SCR->Write (".target.passwd.root", $crypted); } @@ -773,10 +779,9 @@ BEGIN { $TYPEINFO{Write} = ["function", "boolean"];} sub Write { my $self = shift; - if ($root_password ne "") { - # write root password now - return $self->WriteRootPassword (); - } + # write root password now + $self->WriteRootPassword (); + SSHAuthorizedKeys->write_keys("/root"); return bool (1); } diff --git a/test/Makefile.am b/test/Makefile.am index 5c3e52305..d90462726 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -4,6 +4,11 @@ TESTS = \ lib/users/encryption_method_test.rb \ lib/users/ssh_authorized_keys_file_test.rb \ lib/users/users_database_test.rb \ + lib/users/leaf_blk_device_test.rb \ + lib/users/ssh_public_key_test.rb \ + lib/users/dialogs/inst_root_first_test.rb \ + lib/users/widgets/inst_root_first_test.rb \ + lib/users/widgets/public_key_selector_test.rb \ dialogs_test.rb \ ssh_authorized_keys_test.rb \ users_test.rb \ diff --git a/test/fixtures/id_rsa.pub b/test/fixtures/id_rsa.pub new file mode 100644 index 000000000..f843f00c1 --- /dev/null +++ b/test/fixtures/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCpZC8ctjmn90B/MxLOdSjYM3Yl3qd+BhTWYdBNgO3B1fJ1JSegTgCpDM0krMHqd/OAslW5H3MRED7g7g9WkKZh5xTMGvH56yRitJySfSiK8uSxCu6Jg7NM11kqOs5/RwycHO8955QrEYyiWOx80unD+CBJxGEZCOu/DH3ca4yEigAt2HSuC8NPicmRJWua6IbDa+VSICvdOTdFTM8izScSd5WBFH1ULz0bBfLnyi/pIiMjuHB69AN4gsUGYgKjzUsnufKli+DmzACgVWTdQ3Ukax/4/wgXFMr3KsDNpTbn7ZZOKzPpIXpzlP9AwbHQdym6J2NAPYV+DDY3Kcr/vql9 dummy1@example.net diff --git a/test/fixtures/id_rsa_no_comment.pub b/test/fixtures/id_rsa_no_comment.pub new file mode 100644 index 000000000..ddeaff355 --- /dev/null +++ b/test/fixtures/id_rsa_no_comment.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCpZC8ctjmn90B/MxLOdSjYM3Yl3qd+BhTWYdBNgO3B1fJ1JSegTgCpDM0krMHqd/OAslW5H3MRED7g7g9WkKZh5xTMGvH56yRitJySfSiK8uSxCu6Jg7NM11kqOs5/RwycHO8955QrEYyiWOx80unD+CBJxGEZCOu/DH3ca4yEigAt2HSuC8NPicmRJWua6IbDa+VSICvdOTdFTM8izScSd5WBFH1ULz0bBfLnyi/pIiMjuHB69AN4gsUGYgKjzUsnufKli+DmzACgVWTdQ3Ukax/4/wgXFMr3KsDNpTbn7ZZOKzPpIXpzlP9AwbHQdym6J2NAPYV+DDY3Kcr/vql9 diff --git a/test/fixtures/lsblk.txt b/test/fixtures/lsblk.txt new file mode 100644 index 000000000..51e7dc749 --- /dev/null +++ b/test/fixtures/lsblk.txt @@ -0,0 +1,16 @@ +{ + "blockdevices": [ + {"name": "/dev/sda1", "tran": null, "fstype": "vfat", "rm": "0", "model": null, + "children": [ + {"name": "/dev/sda", "tran": "sata", "fstype": null, "rm": "0", "model": "WDC WD10EZEX-75M"} + ] + }, + {"name": "/dev/sda2", "tran": null, "fstype": "ext4", "rm": "0", "model": null, + "children": [ + {"name": "/dev/sda", "tran": "sata", "fstype": null, "rm": "0", "model": "WDC WD10EZEX-75M"} + ] + }, + {"name": "/dev/sr0", "tran": "sata", "fstype": null, "rm": "1", "model": "DVD-ROM DS-8DBSH"}, + {"name": "/dev/sr1", "tran": "usb", "fstype": "iso9660", "rm": "1", "model": "File-CD Gadget "} + ] +} diff --git a/test/lib/users/dialogs/inst_root_first_test.rb b/test/lib/users/dialogs/inst_root_first_test.rb new file mode 100644 index 000000000..5853b9fa4 --- /dev/null +++ b/test/lib/users/dialogs/inst_root_first_test.rb @@ -0,0 +1,44 @@ +#!/usr/bin/env rspec +# encoding: utf-8 + +# Copyright (c) [2018] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../../test_helper" +require "users/dialogs/inst_root_first" +require "cwm/rspec" + +describe Yast::InstRootFirstDialog do + subject(:dialog) { described_class.new } + + include_examples "CWM::Dialog" + + describe "#run" do + context "when the root user does not need a separate password" do + before do + allow(Yast::UsersSimple).to receive(:RootPasswordDialogSkipped) + .and_return(true) + end + + it "returns :auto" do + expect(dialog.run).to eq(:auto) + end + end + end +end diff --git a/test/lib/users/leaf_blk_device_test.rb b/test/lib/users/leaf_blk_device_test.rb new file mode 100644 index 000000000..a37c5347d --- /dev/null +++ b/test/lib/users/leaf_blk_device_test.rb @@ -0,0 +1,103 @@ +#!/usr/bin/env rspec +# encoding: utf-8 + +# Copyright (c) [2018] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../test_helper" +require "users/leaf_blk_device" + +describe Y2Users::LeafBlkDevice do + describe ".all" do + let(:lsblk_output) { File.read(FIXTURES_PATH.join("lsblk.txt")) } + + before do + allow(Yast::Execute).to receive(:locally).and_return(lsblk_output) + end + + it "returns all leaf block devices" do + expect(described_class.all).to contain_exactly( + an_object_having_attributes(name: "/dev/sda1", fstype: :vfat), + an_object_having_attributes(name: "/dev/sda2", fstype: :ext4), + an_object_having_attributes(name: "/dev/sr0", fstype: nil), + an_object_having_attributes(name: "/dev/sr1", fstype: :iso9660) + ) + end + + context "when lsblk fails" do + before do + allow(Yast::Execute).to receive(:locally).and_return(nil) + end + + it "returns an empty array" do + expect(described_class.all).to eq([]) + end + end + end + + describe "#filesystem?" do + let(:fstype) { "ext4" } + + subject do + Y2Users::LeafBlkDevice.new( + name: "/dev/sdb1", model: "MyBrand 8G", disk: "/dev/sdb", fstype: fstype + ) + end + + context "when the device has a filesystem" do + it "returns true" do + expect(subject.filesystem?).to eq(true) + end + end + + context "when the device does not have a filesystem" do + let(:fstype) { nil } + + it "returns false" do + expect(subject.filesystem?).to eq(false) + end + end + end + + describe "#transport?" do + subject do + Y2Users::LeafBlkDevice.new( + name: "/dev/sdb1", model: "MyBrand 8G", disk: "/dev/sdb", fstype: "ext4", + transport: transport + ) + end + + context "when the device has a transport" do + let(:transport) { "usb" } + + it "returns true" do + expect(subject.transport?).to eq(true) + end + end + + context "when the device does not have a transport" do + let(:transport) { nil } + + it "returns false" do + expect(subject.transport?).to eq(false) + end + end + end +end + diff --git a/test/lib/users/ssh_public_key_test.rb b/test/lib/users/ssh_public_key_test.rb new file mode 100644 index 000000000..5366aa066 --- /dev/null +++ b/test/lib/users/ssh_public_key_test.rb @@ -0,0 +1,59 @@ +#!/usr/bin/env rspec +# encoding: utf-8 + +# Copyright (c) [2018] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../test_helper" +require "users/ssh_public_key" + +describe Y2Users::SSHPublicKey do + subject(:key) { described_class.new(File.read(path)) } + let(:path) { FIXTURES_PATH.join("id_rsa.pub") } + + describe ".new" do + context "when the key is not valid" do + let(:content) { "some-not-valid-key" } + + it "raises an InvalidKey error" do + expect { described_class.new(content) }.to raise_error(Y2Users::SSHPublicKey::InvalidKey) + end + end + end + + describe "#fingerprint" do + it "returns the key fingerprint" do + expect(subject.fingerprint).to eq("SHA256:uadPyDQj9VlFZVjK8UNp57jOnWwzGgKQJpeJEhZyV0I") + end + end + + describe "#comment" do + it "returns the key comment" do + expect(key.comment).to eq("dummy1@example.net") + end + + context "when there is no comment" do + let(:path) { FIXTURES_PATH.join("id_rsa_no_comment.pub") } + + it "it returns nil" do + expect(key.comment).to be_nil + end + end + end +end diff --git a/test/lib/users/widgets/inst_root_first_test.rb b/test/lib/users/widgets/inst_root_first_test.rb new file mode 100644 index 000000000..db7b52847 --- /dev/null +++ b/test/lib/users/widgets/inst_root_first_test.rb @@ -0,0 +1,92 @@ +#!/usr/bin/env rspec +# encoding: utf-8 + +# Copyright (c) [2018] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../../test_helper" +require "users/widgets/inst_root_first" +require "cwm/rspec" + +describe Y2Users::Widgets::InstRootFirst do + subject(:widget) { described_class.new } + + include_examples "CWM::CustomWidget" + + before do + allow(Yast2::Popup).to receive(:show) + end + + describe "#validate" do + let(:password?) { false } + let(:key?) { false } + + let(:password_widget) do + instance_double(Users::PasswordWidget, empty?: !password?) + end + + let(:public_key_selector) do + instance_double(Y2Users::Widgets::PublicKeySelector, empty?: !key?) + end + + before do + allow(Users::PasswordWidget).to receive(:new).and_return(password_widget) + allow(Y2Users::Widgets::PublicKeySelector).to receive(:new).and_return(public_key_selector) + end + + context "when neither a password nor public key was given" do + before do + end + + it "returns false" do + expect(widget.validate).to eq(false) + end + + it "displays an error" do + expect(Yast2::Popup).to receive(:show) + .with(/to provide at least a password/, headline: :error) + widget.validate + end + end + + context "when a password was given" do + let(:password?) { true } + + it "returns true" do + expect(widget.validate).to eq(true) + end + + it "does not display any error" do + expect(Yast2::Popup).to_not receive(:show) + end + end + + context "when a public key was given" do + let(:key?) { true } + + it "returns true" do + expect(widget.validate).to eq(true) + end + + it "does not display any error" do + expect(Yast2::Popup).to_not receive(:show) + end + end + end +end diff --git a/test/lib/users/widgets/public_key_selector_test.rb b/test/lib/users/widgets/public_key_selector_test.rb new file mode 100644 index 000000000..b958fbde2 --- /dev/null +++ b/test/lib/users/widgets/public_key_selector_test.rb @@ -0,0 +1,253 @@ +#!/usr/bin/env rspec +# encoding: utf-8 + +# Copyright (c) [2018] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../../test_helper" +require "users/widgets/public_key_selector" +require "cwm/rspec" + +describe Y2Users::Widgets::PublicKeySelector do + subject(:widget) { described_class.new } + + include_examples "CWM::CustomWidget" + + let(:blk_devices) { [] } + let(:key) { Y2Users::SSHPublicKey.new(File.read(FIXTURES_PATH.join("id_rsa.pub"))) } + + before do + allow(Y2Users::LeafBlkDevice).to receive(:all).and_return(blk_devices) + described_class.selected_blk_device_name = nil + described_class.value = nil + end + + describe "#contents" do + let(:blk_devices) { [usb_with_fs, usb_no_fs, squashfs, no_transport] } + + let(:usb_with_fs) do + Y2Users::LeafBlkDevice.new( + name: "/dev/sdb1", model: "MyBrand 8G", disk: "/dev/sdb", transport: :usb, + fstype: :vfat + ) + end + + let(:usb_no_fs) do + Y2Users::LeafBlkDevice.new( + name: "/dev/sdc1", model: "MyBrand 4G", disk: "/dev/sdc", transport: :usb, + fstype: nil + ) + end + + let(:squashfs) do + Y2Users::LeafBlkDevice.new( + name: "/dev/some", model: "MyBrand 4G", disk: "/dev/sdc", transport: :unknown, + fstype: :squashfs + ) + end + + let(:no_transport) do + Y2Users::LeafBlkDevice.new( + name: "/dev/loop1", model: "MyBrand 8G", disk: "/dev/sdb", transport: nil, + fstype: :unknown + ) + end + + before do + allow(widget).to receive(:value).and_return(value) + end + + context "block device selector" do + let(:value) { nil } + + it "includes devices containing a filesystem" do + expect(widget.contents.to_s).to include("/dev/sdb1") + end + + it "does not include devices which does not have a filesystem" do + expect(widget.contents.to_s).to_not include("/dev/sdc1") + end + + it "does not include devices which has a squashfs filesystem" do + expect(widget.contents.to_s).to_not include("/dev/loop1") + end + + it "does not include devices which does not have a transport" do + expect(widget.contents.to_s).to_not include("/dev/loop1") + end + + context "when a key is selected" do + let(:value) { key } + + it "is not displayed" do + expect(widget.contents.to_s).to_not include("MyBrand") + end + end + end + + context "public key summary" do + let(:value) { key } + + it "includes the key fingerprint and the comment" do + expect(widget.contents.to_s).to include(key.fingerprint) + expect(widget.contents.to_s).to include(key.comment) + end + + context "when no key is selected" do + let(:value) { nil } + + it "is not displayed" do + expect(widget.contents.to_s).to_not include(key.comment) + end + end + end + end + + describe "#handle" do + context "searching for a key" do + let(:tmpdir) { TESTS_PATH.join("tmp") } + let(:event) { { "ID" => :browse } } + let(:disk) { "/dev/sr0" } + let(:mounted?) { true } + + before do + allow(Dir).to receive(:mktmpdir).and_return(tmpdir.to_s) + + allow(subject).to receive(:selected_blk_device_name).and_return(disk) + allow(Yast::UI).to receive(:QueryWidget).with(Id(:blk_device), :Value) + .and_return(disk) + allow(Yast::SCR).to receive(:Execute) + .with(Yast::Path.new(".target.mount"), ["/dev/sr0", tmpdir.to_s], "-o ro") + .and_return(mounted?) + allow(Yast::SCR).to receive(:Execute) + .with(Yast::Path.new(".target.umount"), tmpdir.to_s) + FileUtils.mkdir(tmpdir) + end + + context "when the user selects a key" do + let(:key_path) { FIXTURES_PATH.join("id_rsa.pub") } + let(:key_content) { File.read(key_path).strip } + + before do + allow(Yast::UI).to receive(:AskForExistingFile).with(tmpdir.to_s, "*.pub", anything) + .and_return(key_path) + end + + it "reads the key" do + widget.handle(event) + expect(widget.value.to_s).to eq(key_content) + end + + it "saves the key for later use" do + widget.handle(event) + expect(described_class.value.to_s).to eq(key_content) + end + end + + context "when the user cancels the dialog" do + let(:key_path) { nil } + + it "does not import any value" do + widget.handle(event) + expect(widget.value).to be_nil + end + end + + context "when the selected device cannot be mounted" do + let(:mounted?) { false } + + it "reports the problem" do + expect(Yast2::Popup).to receive(:show) + widget.handle(event) + end + end + + it "saves the selected device for later use" do + expect { widget.handle(event) }.to change { described_class.selected_blk_device_name } + .from(nil).to("/dev/sr0") + end + end + + context "removing the key" do + let(:event) { { "ID" => :remove } } + + before do + described_class.value = key + end + + it "removes the current key" do + expect { widget.handle(event) }.to change { widget.value }.from(key).to(nil) + end + end + + context "refreshing the devices list" do + let(:event) { { "ID" => :refresh } } + + it "refreshes the devices list" do + widget.contents + widget.handle(event) + widget.contents + end + end + end + + describe "#store" do + before do + allow(widget).to receive(:value).and_return(key) + end + + context "when a key was read" do + it "imports the key" do + expect(Yast::SSHAuthorizedKeys).to receive(:import_keys).with("/root", [key.to_s]) + widget.store + end + end + + context "when no key was read" do + let(:key) { nil } + + it "does not try to import any key" do + expect(Yast::SSHAuthorizedKeys).to_not receive(:import_keys) + widget.store + end + end + end + + describe "#empty?" do + before do + allow(subject).to receive(:value).and_return(key) + end + + context "when no key is selected" do + let(:key) { nil } + + it "returns true" do + expect(widget).to be_empty + end + end + + context "when key is selected" do + let(:key) { instance_double(Y2Users::SSHPublicKey) } + + it "returns false" do + expect(widget).to_not be_empty + end + end + end +end diff --git a/test/widgets_test.rb b/test/widgets_test.rb index 317b2329e..ca4a5f013 100755 --- a/test/widgets_test.rb +++ b/test/widgets_test.rb @@ -116,4 +116,63 @@ def stub_widget_value(id, value) subject.store end + + context "when the widget is allowed to be empty" do + subject { described_class.new(allow_empty: true) } + + it "does not validate the password" do + stub_widget_value(:pw1, "") + stub_widget_value(:pw2, "") + + expect(subject.validate).to eq(true) + end + + it "does not store the value if empty" do + stub_widget_value(:pw1, "") + stub_widget_value(:pw2, "") + + expect(Yast::UsersSimple).to_not receive(:SetRootPassword) + subject.store + end + + it "stores the value if not empty" do + stub_widget_value(:pw1, "new cool password") + stub_widget_value(:pw2, "new cool password") + + expect(Yast::UsersSimple).to receive(:SetRootPassword).with("new cool password") + subject.store + end + end + + describe "#empty?" do + let(:pw1) { "" } + let(:pw2) { "" } + + before do + stub_widget_value(:pw1, pw1) + stub_widget_value(:pw2, pw2) + end + + context "when no password has been introduced" do + it "returns true" do + expect(subject.empty?).to eq(true) + end + end + + context "when a password was introduced in the password field" do + let(:pw1) { "secret" } + + it "returns false" do + expect(subject.empty?).to eq(false) + end + end + + context "when a password was introduced in the confirmation field" do + let(:pw2) { "secret" } + + it "returns false" do + expect(subject.empty?).to eq(false) + end + end + end end