diff --git a/src/Makefile.am b/src/Makefile.am index 2ea07fdcb..f9ce11168 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -109,6 +109,7 @@ ylib_y2users_clients_DATA = \ ylib_y2users_linuxdir = @ylibdir@/y2users/linux ylib_y2users_linux_DATA = \ + lib/y2users/linux/base_reader.rb \ lib/y2users/linux/local_reader.rb \ lib/y2users/linux/reader.rb \ lib/y2users/linux/writer.rb diff --git a/src/lib/y2users/linux/base_reader.rb b/src/lib/y2users/linux/base_reader.rb new file mode 100644 index 000000000..b0c9037d5 --- /dev/null +++ b/src/lib/y2users/linux/base_reader.rb @@ -0,0 +1,111 @@ +# Copyright (c) [2021] 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 "abstract_method" +require "y2users/config" +require "y2users/parsers/group" +require "y2users/parsers/passwd" +require "y2users/parsers/shadow" +require "users/ssh_authorized_keyring" + +module Y2Users + module Linux + # Base class for reading users configuration from the system + class BaseReader + include Yast::Logger + + # Generates a new config with the users and groups from the read content + # + # @return [Config] + def read + elements = read_users + read_groups + + config = Config.new.attach(elements) + + read_passwords(config) + read_authorized_keys(config) + + config + end + + private + + # Parses the content retrieved by {#load_users} and returns a collection of users + # + # @see Parsers::Passwd#parse + # @return [Array] + def read_users + parser = Parsers::Passwd.new + parser.parse(load_users) + end + + # @!method load_users + # @return [String] loaded users from the system + abstract_method :load_users + + # Parses the content retrieved by {#load_groups} and returns a collection of groups + # + # @see Parsers::Group#parse + # @return [Array] + def read_groups + parser = Parsers::Group.new + parser.parse(load_groups) + end + + # @!method load_groups + # @return [String] loaded groups from the system + abstract_method :load_groups + + # Parses the content retrieved by {#load_passwords} and sets user passwords + # + # @see Parsers::Shadow#parse + # @return [Hash] + def read_passwords(config) + parser = Parsers::Shadow.new + + passwords = parser.parse(load_passwords) + passwords.each_pair do |name, password| + user = config.users.by_name(name) + if !user + log.warn "Found password for non existing user #{name}." + next + end + + user.password = password + end + end + + # @!method load_passwords + # @return [String] loaded passwords from the system + abstract_method :load_passwords + + # Reads users authorized keys + # + # @see Yast::Users::SSHAuthorizedKeyring#read_keys + # @return [Array] + def read_authorized_keys(config) + config.users.each do |user| + next unless user.home + + user.authorized_keys = Yast::Users::SSHAuthorizedKeyring.new(user.home).read_keys + end + end + end + end +end diff --git a/src/lib/y2users/linux/local_reader.rb b/src/lib/y2users/linux/local_reader.rb index 0026b03d4..62ed24d5b 100644 --- a/src/lib/y2users/linux/local_reader.rb +++ b/src/lib/y2users/linux/local_reader.rb @@ -17,79 +17,54 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require "y2users/config" -require "y2users/parsers/group" -require "y2users/parsers/passwd" -require "y2users/parsers/shadow" -require "users/ssh_authorized_keyring" +require "y2users/linux/base_reader" module Y2Users module Linux # Reads local users configuration from the system using /etc files. - class LocalReader - include Yast::Logger - + class LocalReader < BaseReader + # Constructor + # + # @param source_dir [String, Pathname] path of source directory for reading files def initialize(source_dir = "/") @source_dir = source_dir end - # Generates a new config with the users and groups from the /etc files - # - # @return [Config] - def read - elements = read_users + read_groups - - config = Config.new.attach(elements) - - # read passwords after user, as user has to exist in advance - read_passwords(config) - - # read authorized keys - read_authorized_keys(config) - - config - end - private + # Source directory for reading files content + # + # @see #load_file + # @return [String, Pathname] attr_reader :source_dir - def read_users - content = File.read(File.join(source_dir, "/etc/passwd")) - parser = Parsers::Passwd.new - - parser.parse(content) + # Loads the content of /etc/passwd file + # + # @return [String] + def load_users + load_file("/etc/passwd") end - def read_groups - content = File.read(File.join(source_dir, "/etc/group")) - parser = Parsers::Group.new - - parser.parse(content) + # Loads the content of /etc/group file + # + # @return [String] + def load_groups + load_file("/etc/group") end - def read_passwords(config) - content = File.read(File.join(source_dir, "/etc/shadow")) - parser = Parsers::Shadow.new - - passwords = parser.parse(content) - passwords.each_pair do |name, password| - user = config.users.by_name(name) - if !user - log.warn "Found password for non existing user #{password.name}." - next - end - - user.password = password - end + # Loads the content of /etc/shadow file + # + # @return [String] + def load_passwords + load_file("/etc/shadow") end - def read_authorized_keys(config) - config.users.each do |user| - next unless user.home - - user.authorized_keys = Yast::Users::SSHAuthorizedKeyring.new(user.home).read_keys - end + # Loads the content of given file path within the {#source_dir} + # + # @param path [String, Pathname] the path to the file to be read + # @return [String] the content of the read file + def load_file(path) + File.read(File.join(source_dir, path)) end end end diff --git a/src/lib/y2users/linux/reader.rb b/src/lib/y2users/linux/reader.rb index f19930d23..5fe0eefe7 100644 --- a/src/lib/y2users/linux/reader.rb +++ b/src/lib/y2users/linux/reader.rb @@ -18,73 +18,46 @@ # find current contact information at www.suse.com. require "yast2/execute" -require "y2users/config" -require "y2users/parsers/group" -require "y2users/parsers/passwd" -require "y2users/parsers/shadow" -require "users/ssh_authorized_keyring" +require "y2users/linux/base_reader" module Y2Users module Linux - # Reads users configuration from the system using getent utility. - class Reader - include Yast::Logger + # Reads users configuration from the system using `getent` command. + class Reader < BaseReader + private # rubocop:disable Layout/IndentationWidth - # Generates a new config with the users and groups from the system + # Loads entries from `passwd` database # - # @return [Config] - def read - elements = read_users + read_groups - - config = Config.new.attach(elements) - - # read passwords after user, as user has to exist in advance - read_passwords(config) - - # read authorized keys - read_authorized_keys(config) - - config - end - - private - - def read_users - getent = Yast::Execute.on_target!("/usr/bin/getent", "passwd", stdout: :capture) - parser = Parsers::Passwd.new - - parser.parse(getent) + # @see #getent + # @return [String] + def load_users + getent("passwd") end - def read_groups - getent = Yast::Execute.on_target!("/usr/bin/getent", "group", stdout: :capture) - parser = Parsers::Group.new - - parser.parse(getent) + # Loads entries from `group` database + # + # @see #getent + # @return [String] + def load_groups + getent("group") end - def read_passwords(config) - getent = Yast::Execute.on_target!("/usr/bin/getent", "shadow", stdout: :capture) - parser = Parsers::Shadow.new - - passwords = parser.parse(getent) - passwords.each_pair do |name, password| - user = config.users.by_name(name) - if !user - log.warn "Found password for non existing user #{password.name}." - next - end - - user.password = password - end + # Loads entries from `shadow` database + # + # @see #getent + # @return [String] + def load_passwords + getent("shadow") end - def read_authorized_keys(config) - config.users.each do |user| - next unless user.home - - user.authorized_keys = Yast::Users::SSHAuthorizedKeyring.new(user.home).read_keys - end + # Executes the `getent` command for getting entries for given Name Service Switch database + # + # @see https://www.man7.org/linux/man-pages/man1/getent.1.html + # + # @param database [String] a database supported by the Name Service Switch libraries + # @return [String] the getent command output + def getent(database) + Yast::Execute.on_target!("/usr/bin/getent", database, stdout: :capture) end end end diff --git a/test/Makefile.am b/test/Makefile.am index 71482512b..7a1a8e80d 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -17,6 +17,7 @@ TESTS = \ lib/y2users/config_element_examples.rb \ lib/y2users/password_helper_test.rb \ lib/y2users/clients/inst_root_first_test.rb \ + lib/y2users/linux/base_reader_test.rb \ lib/y2users/linux/local_reader_test.rb \ lib/y2users/linux/reader_test.rb \ lib/y2users/linux/writer_test.rb \ diff --git a/test/lib/y2users/linux/base_reader_test.rb b/test/lib/y2users/linux/base_reader_test.rb new file mode 100755 index 000000000..ff5e43ca4 --- /dev/null +++ b/test/lib/y2users/linux/base_reader_test.rb @@ -0,0 +1,75 @@ +#!/usr/bin/env rspec + +# Copyright (c) [2021] 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 "y2users/config" +require "y2users/linux/base_reader" + +describe Y2Users::Linux::BaseReader do + around do |example| + # Let's use test/fixtures/home as src root for reading authorized keys from there + change_scr_root(FIXTURES_PATH.join("home")) { example.run } + end + + describe "#read" do + let(:passwd_content) { File.read(File.join(FIXTURES_PATH, "/root/etc/passwd")) } + let(:group_content) { File.read(File.join(FIXTURES_PATH, "/root/etc/group")) } + let(:shadow_content) { File.read(File.join(FIXTURES_PATH, "/root/etc/shadow")) } + let(:root_home) { FIXTURES_PATH.join("home", "root").to_s } + let(:expected_root_auth_keys) { authorized_keys_from(root_home) } + + before do + # mock Yast::Execute calls and provide file content from fixture + allow(subject).to receive(:load_users).and_return(passwd_content) + allow(subject).to receive(:load_groups).and_return(group_content) + allow(subject).to receive(:load_passwords).and_return(shadow_content) + + allow(subject.log).to receive(:warn) + end + + it "generates a config with read data" do + config = subject.read + + expect(config).to be_a(Y2Users::Config) + + expect(config.users.size).to eq 18 + expect(config.groups.size).to eq 37 + + root_user = config.users.root + expect(root_user.uid).to eq "0" + expect(root_user.home).to eq "/root" + expect(root_user.shell).to eq "/bin/bash" + expect(root_user.primary_group.name).to eq "root" + expect(root_user.password.value.encrypted?).to eq true + expect(root_user.password.value.content).to match(/^\$6\$pL/) + expect(root_user.authorized_keys).to eq(expected_root_auth_keys) + end + + it "logs warning if password found for not existing user" do + shadow_content << "fakeuser:$6$fakepassword.:16899::::::\n" + + expect(subject.log).to receive(:warn).with(/Found password for.*fakeuser./) + + subject.read + end + end +end