The third field of /etc/shadow
contains the date of last password change. It can contain:
- An empty string: means that aging features are disabled
- A zero: means the user must change the password in next login
- A set of digits: represents the date of the latest password change
We currently have this, which doesn't fly because we cannot distinguish if a value of nil really means that aging is disabled or simply that we do not know the date/state, for example for a new user that is not written to /etc/shadow
yet.
class Password
# 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_accessor :last_change
end
We decided to turn that into a class. But I see several options.
This somehow would hide the fact that the same field is abused for several things in /etc/shadow
.
It moves all the responsibility of knowing the inter-relation of the three concepts (aging enabled/disabled, force change and last change) to the readers and writers (or to whoever is using the Password
class, in general)
For example, the caller should know that if #enabled?
returns false, then the value of #force_changed?
and #last_change
are kind of irrelevant because an emtpy string will be written to /etc/shadow
and then the other two values would be lost.
class Password
# Aging information for the password
#
# [PasswordAging, nil] nil if aging information is unknown yet (eg. a new user created programatically)
attr_accessor :aging
class PasswordAging
attr_writer :enabled
def enabled?; !!@enabled; end
attr_writer :force_change
def force_change?; !!@force_change; end
# Last password change
attr_accessor :last_change
def initialize(enabled, last_change: nil, force_change: false)
@enabled = enabled
@last_change = last_change
@force_change = force_change
end
end
end
Is this worth it? Basically there is no difference between having the class and having all the attributes directly in Password
.
This exposes the fact that the three concepts are mixed at /etc/shadow
, which should maybe be a responsibility of the Linux writer and reader.
class Password
# Aging information for the password
#
# [PasswordAging, nil] nil if aging information is unknown yet (eg. a new user created programatically)
attr_accessor :aging
class PasswordAging
attr_writer :enabled
def enabled?; !!@enabled; end
def force_change?
return false unless enabled?
!!@force_change_flag
end
def last_change
return nil unless enabled?
return nil if force_change?
@last_change_date
end
def initialize(enabled, last_change: nil, force_change: false)
@last_change_date = last_change
@force_change_flag = force_change
self.enabled = enabled
end
attr_accessor :last_change_date
attr_accessor :force_change_flag
end
end
class Password
# Aging information for the password
#
# [PasswordAging, nil] nil if aging information is unknown yet (eg. a new user created programatically)
attr_accessor :aging
class PasswordAging
def enabled?; !!@enabled; end
def force_change?; !!@force_change; end
# Last password change
#
# This is nil if the date of the last change is irrelevant
# The date is irrelevant in this two scenarios:
#
# - password aging features are disabled (see {#enabled?})
# - the user is forced to change the password in the next login (see {#force_change?})
#
# @return [Date, nil]
attr_reader :last_change
def last_change=(value)
raise ArgumentError, "#{value} is not a date" unless value.is_a?(Date)
@force_change = false
@enabled = true
@last_change = value
end
def enabled=(value)
if value
@last_change ||= Date.today unless force_changed?
else
@last_change = nil
@force_changed = false
end
@enabled = value
end
def force_change=(value)
if value
@last_change = nil
@enabled = true
elsif enabled?
@last_change ||= Date.today
else
@last_change = nil
end
@force_change = value
end
end
end