From ed7bd7353037347d38fbb87ddb5b95db95a6a1e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 30 Apr 2021 09:16:38 +0100 Subject: [PATCH 1/2] Adapt to new API --- src/lib/y2users/config.rb | 175 ++++++++++++++++++++++++++++-- src/lib/y2users/config_element.rb | 92 ++++++++++++++++ src/lib/y2users/group.rb | 93 ++++++++++------ src/lib/y2users/password.rb | 132 ++++++++++++---------- src/lib/y2users/user.rb | 170 +++++++++++++++++------------ 5 files changed, 490 insertions(+), 172 deletions(-) create mode 100644 src/lib/y2users/config_element.rb diff --git a/src/lib/y2users/config.rb b/src/lib/y2users/config.rb index b6378f17b..b5e2f3231 100644 --- a/src/lib/y2users/config.rb +++ b/src/lib/y2users/config.rb @@ -18,9 +18,22 @@ # find current contact information at www.suse.com. module Y2Users - # Holds references to elements of user configuration like users or groups. - # Class itself holds references to different configuration instances. - # TODO: write example + # Class to represent a configuration of users and groups + # + # @example + # user = User.new("john") + # group = Group.new("users") + # + # config1 = Config.new("config1") + # config1.users #=> [] + # config1.attach(user, group) + # config1.users #=> [user] + # config1.groups #=> [group] + # + # config2 = config1.clone_as("config2") + # user2 = config2.users.first + # config2.detach(user2) + # config2.users #=> [] class Config class << self def get(name) @@ -48,7 +61,6 @@ def system(reader: nil, force_read: false) reader = Linux::Reader.new end - # TODO: make system config immutable, so it cannot be modified directly res = new(:system) reader.read_to(res) @@ -56,23 +68,164 @@ def system(reader: nil, force_read: false) end end + # Config name + # + # @return [String] attr_reader :name - attr_accessor :users - attr_accessor :groups - def initialize(name, users: [], groups: []) + # Constructor + # + # param name [String] + def initialize(name) @name = name - @users = users - @groups = groups + + @users_manager = ElementManager.new(config) + @groups_manager = ElementManager.new(config) + self.class.register(self) end + # Users that belong to this config + # + # @note The list of users cannot be modified directly. Use {#attach} and {#detach} instead. + # + # @return [Array] + def users + users_manager.elements.dup.freeze + end + + # Groups that belong to this config + # + # @note The list of groups cannot be modified directly. Use {#attach} and {#detach} instead. + # + # @return [Array] + def groups + groups_manager.elements.dup.freeze + end + + # Attaches users and groups to this config + # + # The given users and groups cannot be already attached to a config. + # + # @param elements [Array] + def attach(*elements) + elements.each { |e| attach_element(e) } + end + + # Detaches users and groups from this config + # + # @param elements [Array] + def detach(*elements) + elements.each { |e| detach_element(e) } + end + + # Generates a new config with the very same list of users and groups + # + # Note that the cloned users and groups keep the same id as the original users and groups. + # + # @param name [String] name for the new cloned config + # @return [Config] def clone_as(name) config = self.class.new(name) - config.users = users.map { |u| u.clone_to(config) } - config.groups = groups.map { |g| g.clone_to(config) } + + elements = users + groups + elements.each { |e| config.clone_element(e) } config end + + protected + + # Clones a given user or group and attaches it into this config + # + # Note that the cloned element keep the same id as the source element. + # + # @param element [User, Group] + def clone_element(element) + cloned = element.clone + cloned.assign_internal_id(element.id) + + attach(cloned) + end + + private + + # Manager for users + # + # @return [ElementManager] + attr_reader :users_manager + + # Manager for groups + # + # @return [ElementManager] + attr_reader :groups_manager + + # Generates the id for the next attached user or group + # + # @return [Integer] + def self.next_element_id + @last_element_id ||= 0 + @last_element_id += 1 + end + + private_class_method(:next_element_id) + + # Attaches an user or group + # + # An id is assigned to the given user/group, if needed. + # + # @param element [User, Group] + def attach_element(element) + element.assign_internal_id(self.class.next_element_id) if element.id.nil? + + element.is_a?(User) ? users_manager.attach(element) : groups_manager.attach(element) + end + + # Detaches an user or group + # + # @param element [User, Group] + def detach_element(element) + element.is_a?(User) ? users_manager.detach(element) : groups_manager.detach(element) + end + + # Helper class to manage a list of users or groups + class ElementManager + # @return [Array] + attr_reader :elements + + # Constructor + # + # @param config [Config] + def initialize(config) + @config = config + @elements = [] + end + + # Attaches the element to the config + # + # @raise [RuntimeError] if the element is already attached + # + # @param element [User, Group] + def attach(element) + raise "Element already attached: #{element}" if element.attached? + + @elements << element + + element.assign_config(config) + end + + # Detaches the element from the config + # + # @param element [User, Group] + def detach(element) + return if element.config != config + + index = @elements.find_index { |e| e.is?(element) } + @elements.delete_at(index) if index + + element.assing_config(nil) + element.assign_internal_id(nil) + end + end end end diff --git a/src/lib/y2users/config_element.rb b/src/lib/y2users/config_element.rb new file mode 100644 index 000000000..01af79d71 --- /dev/null +++ b/src/lib/y2users/config_element.rb @@ -0,0 +1,92 @@ +# 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. + +module Y2Users + # Methods for an element that can be attached to a {Config} (e.g., {User}, {Group}). + module ConfigElement + # {Config} in which the element is attached to + # + # @return [Config, nil] nil if the element is not attached yet + attr_reader :config + + # Internal identifier to distinguish elements + # + # Two elements are considered to be the same if they have the same id, even when they live in + # different configs. + # + # @return [Integer, nil] the id is assigned by the config when attaching the element + attr_reader :id + + # Assigns the internal id for this element + # + # @note The id of an element should not be modified. This method is exposed in the public API + # only to make possible to set the id when attaching/detaching an element to/from a config, + # see {Config#attach} and {Config#detach}. + # + # @param id [Integer] + def assign_internal_id(id) + @id = id + end + + # Assigns the config which the element belongs to + # + # @note The config of an element should not be modified. This method is exposed in the public + # API only to make possible to set the config reference when attaching/detaching an element + # to/from a config, see {Config#attach} and {Config#detach}. + # + # @param config [Config] + def assign_config(config) + @config = config + end + + # Whether the element is currently attached to a {Config} + # + # @return [Boolean] + def attached? + !config.nil? + end + + # Whether this element is considered the same as other + # + # Two elements are considered the same when they have the same id, independently on the rest of + # attributes. + # + # @param other [User, Group] + # @return [Boolean] + def is?(other) + return false unless self.class == other.class + return false if id.nil? || other.id.nil? + + id == other.id + end + + # Generates a new cloned element without an specific config or id. + # + # Note that the new cloned element is not attached to any config. + # + # @return [User, Group] + def clone + cloned = super + cloned.config = nil + cloned.id = nil + + cloned + end + end +end diff --git a/src/lib/y2users/group.rb b/src/lib/y2users/group.rb index 07b3245b5..e6f3624a0 100644 --- a/src/lib/y2users/group.rb +++ b/src/lib/y2users/group.rb @@ -17,57 +17,80 @@ # 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" +require "y2users/config_element" module Y2Users - # Represents user groups on system. + # Class to represent user groups + # + # @example + # group = Group.new("admins") + # group.gid = 110 + # group.attached? #=> false + # group.id #=> nil + # + # config = Config.new("my_config") + # config.attach(group) + # + # group.config #=> config + # group.id #=> 56 + # group.attached? #=> true class Group - # @return [Y2Users::Config] reference to configuration in which it lives - attr_reader :config + include ConfigElement - # @return [String] group name - attr_reader :name + # Group name + # + # @return [String] + attr_accessor :name - # @return [String, nil] group id or nil if it is not yet assigned. - attr_reader :gid + # Group id + # + # @return [String, nil] nil if it is not assigned yet + attr_accessor :gid - # @return [Array] list of user names - # @note to get list of users in given group use method #users - attr_reader :users_name + # Names of users that become to this group + # + # To get the list of users (and not only their names), see {#users}. + # + # @return [Array] + attr_accessor :users_name - # @return[:local, :ldap, :unknown] where is user defined - attr_reader :source + # Where the group is defined + # + # @return[:local, :ldap, :unknown] + attr_accessor :source - # @see respective attributes for possible values - def initialize(config, name, gid: nil, users_name: [], source: :unknown) - @config = config + # Constructor + # + # @param name [String] + def initialize(name) @name = name - @gid = gid - @users_name = users_name - @source = source + @users_name = [] + @source = :unknown end - # @return [Array] all users in this group, including ones that - # has it as primary group + # Users that become to this group, including users which have this group as primary group + # + # The group must to be attached to a config in order to find its users. + # + # @return [Array] def users - config.users.select { |u| u.gid == gid || users_name.include?(u.name) } - end - - ATTRS = [:name, :gid, :users_name].freeze + return [] unless attached? - # Clones group to different configuration object. - # @return [Y2Users::Group] newly cloned group object - def clone_to(config) - attrs = ATTRS.each_with_object({}) { |a, r| r[a] = public_send(a) } - attrs.delete(:name) # name is separate argument - self.class.new(config, name, attrs) + config.users.select { |u| u.gid == gid || users_name.include?(u.name) } end - # Compares group object if all attributes are same excluding configuration reference. - # @return [Boolean] true if it is equal + # Whether two groups are equal + # + # Only relevant attributes are compared. For example, the config in which the group is attached + # and the internal group id are not considered. + # + # @return [Boolean] def ==(other) - # do not compare configuration to allow comparison between different configs - ATTRS.all? { |a| public_send(a) == other.public_send(a) } + [:name, :gid, :users_name, :source].all? do |a| + public_send(a) == other.public_send(a) + end end + + alias_method :eql?, :== end end diff --git a/src/lib/y2users/password.rb b/src/lib/y2users/password.rb index 73aa69d49..f66a9262c 100644 --- a/src/lib/y2users/password.rb +++ b/src/lib/y2users/password.rb @@ -17,128 +17,142 @@ # 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" require "yast2/secret_attributes" module Y2Users - # Password configuration for user including its hashed value. + # Password configuration for an user class Password - # @return [String] login name for given password - attr_reader :name - - # @return [Value, nil] password value. Can be any subclass of Value. nil when password is - # not set at all. + # Password value. Can be any subclass of {PasswordValue}. + # + # @return [PasswordValue, nil] nil if password is not set attr_accessor :value - # @return [Date, :force_change, nil] Possible value are date of the last change, :force_change - # when next login force user to change it and nil for disabled aging feature + # Last password change + # + # @return [Date, :force_change, nil] date of the last change or :force_change when the next + # login forces the user to change the password or nil for disabled aging feature. attr_reader :last_change - # @return [Integer] Minimum number of days before next password change. 0 means no restriction. + # The minimum number of days required between password changes + # + # @return [Integer] 0 means no restriction attr_reader :minimum_age - # @return [Integer, nil] Maximum number of days after which user is forced to change password. - # nil means no restriction. + # The maximum number of days the password is valid. After that, the password is forced to be + # changed. + # + # @return [Integer, nil] nil means no restriction attr_reader :maximum_age - # @return [Integer] Number of days before expire date happen. 0 means no warning. + # The number of days before the password is to expire that the user is warned for changing the + # password. + # + # @return [Integer] 0 means no warning attr_reader :warning_period - # @return [Integer, nil] Number of days after expire date when old password can be still used. - # nil means no limit + # The number of days after the password expires that account is disabled + # + # @return [Integer, nil] nil means no limit attr_reader :inactivity_period - # @return [Date, nil] Date when whole account expire or nil if there are no account expiration. + # Date when whole account expires + # + # @return [Date, nil] nil if there is no account expiration attr_reader :account_expiration - # @return [:local, :ldap, :unknown] where is user defined - attr_reader :source - + # Creates a new password with a plain value + # + # @param value [String] plain password + # @return [Password] def self.create_plain(value) - # TODO: remove nils when adapting all constructors - new(nil, nil, value: PasswordPlainValue.new(value)) + new(PasswordPlainValue.new(value)) end + # Creates a new password with an encrypted value + # + # @param value [String] encrypted password + # @return [Password] def self.create_encrypted(value) - # TODO: remove nils when adapting all constructors - new(nil, nil, value: PasswordEncryptedValue.new(value)) + new(PasswordEncryptedValue.new(value)) end - # @see respective attributes for possible values - # @todo: avoid long list of parameters - # rubocop: disable Metrics/ParameterLists - def initialize(config, name, value: nil, last_change: nil, minimum_age: nil, - maximum_age: nil, warning_period: nil, inactivity_period: nil, - account_expiration: nil, source: :unknown) - @config = config - @name = name - self.value = value - @last_change = last_change - @minimum_age = minimum_age - @maximum_age = maximum_age - @warning_period = warning_period - @inactivity_period = inactivity_period - @account_expiration = account_expiration - @source = source + # Constructor + # + # @param value [PasswordValue] + def initialize(value) + @value = value end - # rubocop: enable Metrics/ParameterLists - ATTRS = [:name, :value, :last_change, :minimum_age, :maximum_age, :warning_period, - :inactivity_period, :account_expiration].freeze - - # Clones password to different configuration object. - # @return [Y2Users::Password] newly cloned password object - def clone_to(config) - attrs = ATTRS.each_with_object({}) { |a, r| r[a] = public_send(a) } - attrs.delete(:name) # name is separate argument - self.class.new(config, name, attrs) - end + def clone + cloned = super + cloned.value = value.clone - # Compares password object if all attributes are same excluding configuration reference. - # @return [Boolean] true if it is equal - def ==(other) - # do not compare configuration to allow comparison between different configs - ATTRS.all? { |a| public_send(a) == other.public_send(a) } + cloned end end - # Represents password value. Its specific type is defined as subclass and can be queried + # Represents a password value. Its specific type is defined as subclass and can be queried. class PasswordValue include Yast2::SecretAttributes secret_attr :content + # Constructor + # + # @param content [String] password value def initialize(content) self.content = content end + # Whether it is a plain password + # + # @return [Boolean] def plain? false end + # Whether it is an encrypted password + # + # @return [Boolean] def encrypted? false end + + def clone + cloned = super + + secret = instance_variable_get(:@content).dup + cloned.instance_variable_set(:@content, secret) + + cloned + end end - # Represents encrypted password value or special values like disabled or locked password that - # is specified in encrypted password field. + # Represents an encrypted password value class PasswordEncryptedValue < PasswordValue + # @see PasswordValue#encrypted? def encrypted? true end + # Whether the encrypted password is locked + # + # @return [Boolean] def locked? content.start_with?("!$") end + # Whether the encrypted password is disabled + # + # @return [Boolean] def disabled? ["*", "!"].include?(content) end end - # Represents plain password value + # Represents a plain password value class PasswordPlainValue < PasswordValue + # @see PasswordValue#plain? def plain? true end diff --git a/src/lib/y2users/user.rb b/src/lib/y2users/user.rb index ce574a2ad..aefee3b2c 100644 --- a/src/lib/y2users/user.rb +++ b/src/lib/y2users/user.rb @@ -17,70 +17,103 @@ # 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" +require "y2users/config_element" module Y2Users - # Representing user configuration on system in contenxt of given User Configuration - # @note Immutable class. + # Class to represent an user + # + # @example + # user = User.new("john") + # user.uid = 1001 + # user.system? #=> false + # user.attached? #=> false + # user.id #=> nil + # + # config = Config.new("my_config") + # config.attach(user) + # + # user.config #=> config + # user.id #=> 23 + # user.attached? #=> true class User - Yast.import "ShadowConfig" + include ConfigElement - # @return [Y2Users::Config] reference to configuration in which it lives - attr_reader :config + Yast.import "ShadowConfig" - # @return [String] user name - attr_reader :name + # User name + # + # @return [String] + attr_accessor :name - # @return [String, nil] user ID or nil if it is not yet assigned - attr_reader :uid + # User ID + # + # @return [String, nil] nil if it is not assigned yet + attr_accessor :uid - # @return [String, nil] primary group ID or nil if it is not yet assigned - # @note to get primary group use method #primary_group - attr_reader :gid + # Primary group ID + # + # @note To get the primary group (and not only its ID), see {#primary_group} + # + # @return [String, nil] nil if it is not assigned yet + attr_accessor :gid - # @return [String, nil] default shell or nil if it is not yet assigned - attr_reader :shell + # Default user shell + # + # @return [String, nil] nil if it is not assigned yet + attr_accessor :shell - # @return [String, nil] home directory or nil if it is not yet assigned - attr_reader :home + # Path to the home directory + # + # @return [String, nil] nil if it is not assigned yet + attr_accessor :home - # @return [Array] Fields in GECOS entry - attr_reader :gecos + # Fields for the GECOS entry + # + # @return [Array] + attr_accessor :gecos - # @return [:local, :ldap, :unknown] where is user defined - attr_reader :source + # Where the user is defined + # + # @return [:local, :ldap, :unknown] + attr_accessor :source - # @return [Password] password configuration related to given user + # Password for the user + # + # @return [Password] attr_accessor :password - # @see respective attributes for possible values - # @todo: avoid long list of parameters - # rubocop: disable Metrics/ParameterLists - def initialize(config, name, - uid: nil, gid: nil, shell: nil, home: nil, gecos: [], source: :unknown) - # TODO: GECOS - @config = config + # Constructor + # + # @param name [String] + def initialize(name) @name = name - @uid = uid - @gid = gid - @shell = shell - @home = home - @source = source - @gecos = gecos + # TODO: GECOS + @gecos = [] + @source = :unknown # See #system? @system = false end - # rubocop: enable Metrics/ParameterLists - # @return [Y2Users::Group, nil] primary group set to given user or - # nil if group is not set yet + # Primary group for the user + # + # The user must to be attached to a config in order to find its primary group. + # + # @return [Group, nil] nil if the group is not set yet def primary_group + return nil unless attached? + config.groups.find { |g| g.gid == gid } end - # @return [Array] list of groups where is user included including primary group + # Groups where the user is included. It also contains the primary group. + # + # The user must to be attached to a config in order to find its groups. + # + # @return [Array] def groups + return [] unless attached? + config.groups.select { |g| g.users.include?(self) } end @@ -89,32 +122,29 @@ def expire_date password&.account_expiration end - # @return [String] Returns full name from gecos entry or username if not specified in gecos. + # User full name + # + # It is extracted from GECOS if possible. Otherwise, the user name is considered as the full + # name. + # + # @return [String] def full_name gecos.first || name end - ATTRS = [:name, :uid, :gid, :shell, :home].freeze - - # Clones user to different configuration object. - # @return [Y2Users::User] newly cloned user object - def clone_to(config) - attrs = ATTRS.each_with_object({}) { |a, r| r[a] = public_send(a) } - attrs.delete(:name) # name is separate argument - cloned = self.class.new(config, name, attrs) - cloned.password = password.clone_to(config) if password - - cloned - end - - # Compares user object if all attributes are same excluding configuration reference. - # @return [Boolean] true if it is equal + # Whether two users are equal + # + # Only relevant attributes are compared. For example, the config in which the user is attached + # and the internal user id are not considered. + # + # @return [Boolean] def ==(other) - # do not compare configuration to allow comparison between different configs - ATTRS.all? { |a| public_send(a) == other.public_send(a) } + [:name, :uid, :gid, :shell, :home, :gecos, :source, :password].all? do |a| + public_send(a) == other.public_send(a) + end end - # Whether it is the root user + # Whether the user is root # # @return [Boolean] def root? @@ -123,12 +153,10 @@ def root? # Whether this is a system user # - # This is important for several reasons: - # - System users are represented as its own category in the YaST Users UI - # - During creation: - # - the uid is chosen in the SYS_UID_MIN-SYS_UID_MAX range (defined in /etc/login.defs) - # - no aging information is added to /etc/shadow - # - by default, the home directory is not created + # This is important when creating an user because several reasons: + # * The uid is chosen in the SYS_UID_MIN-SYS_UID_MAX range (defined in /etc/login.defs). + # * No aging information is added to /etc/shadow. + # * By default, the home directory is not created. # # For users that still don't have an uid, it is possible to enforce whether they should be # considered as a system user (and created as such in the system) via {#system=}. @@ -138,10 +166,10 @@ def system? uid ? system_uid? : @system end - # Sets whether the current user should be considered as a system one + # Sets whether the user should be considered as a system one # # @raise [RuntimeError] if the user already has an uid, because forcing the value only makes - # sense for users for which the uid is still not known + # sense for a user which uid is still not known. # # @see #system? # @@ -152,9 +180,17 @@ def system=(value) @system = value end + # @see ConfigElement#clone + def clone + cloned = super + cloned.password = password.clone + + cloned + end + private - # Whether the user is a system user according to its uid + # Whether the user uid corresponds to a system user uid # # @return [Boolean] def system_uid? From 614d9667454c9da8811e13443a961203e210a5c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 30 Apr 2021 11:35:14 +0100 Subject: [PATCH 2/2] Improve doc --- src/lib/y2users/config.rb | 13 +++++++------ src/lib/y2users/group.rb | 2 +- src/lib/y2users/user.rb | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/lib/y2users/config.rb b/src/lib/y2users/config.rb index b5e2f3231..2d7e7ee19 100644 --- a/src/lib/y2users/config.rb +++ b/src/lib/y2users/config.rb @@ -21,19 +21,20 @@ module Y2Users # Class to represent a configuration of users and groups # # @example - # user = User.new("john") + # user1 = User.new("john") + # user2 = User.new("peter") # group = Group.new("users") # # config1 = Config.new("config1") # config1.users #=> [] - # config1.attach(user, group) - # config1.users #=> [user] + # config1.attach(user1, user2, group) + # config1.users #=> [user1, user2] # config1.groups #=> [group] # # config2 = config1.clone_as("config2") - # user2 = config2.users.first - # config2.detach(user2) - # config2.users #=> [] + # user = config2.users.first + # config2.detach(user) + # config2.users #=> [user2] class Config class << self def get(name) diff --git a/src/lib/y2users/group.rb b/src/lib/y2users/group.rb index e6f3624a0..4aca57503 100644 --- a/src/lib/y2users/group.rb +++ b/src/lib/y2users/group.rb @@ -70,7 +70,7 @@ def initialize(name) # Users that become to this group, including users which have this group as primary group # - # The group must to be attached to a config in order to find its users. + # The group must be attached to a config in order to find its users. # # @return [Array] def users diff --git a/src/lib/y2users/user.rb b/src/lib/y2users/user.rb index aefee3b2c..f15846172 100644 --- a/src/lib/y2users/user.rb +++ b/src/lib/y2users/user.rb @@ -97,7 +97,7 @@ def initialize(name) # Primary group for the user # - # The user must to be attached to a config in order to find its primary group. + # The user must be attached to a config in order to find its primary group. # # @return [Group, nil] nil if the group is not set yet def primary_group @@ -108,7 +108,7 @@ def primary_group # Groups where the user is included. It also contains the primary group. # - # The user must to be attached to a config in order to find its groups. + # The user must be attached to a config in order to find its groups. # # @return [Array] def groups