Skip to content

Instantly share code, notes, and snippets.

@ancorgs
Last active May 13, 2021 09:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ancorgs/d955ddff5f20ea8af399f519869f38d6 to your computer and use it in GitHub Desktop.
Save ancorgs/d955ddff5f20ea8af399f519869f38d6 to your computer and use it in GitHub Desktop.
Options for last_change

The initial situation

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.

Option 1. Very simplistic class.

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.

Option 2. A class that exposes how each attribute invalidates the others

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

Option 3. Similar to option 2, just a different API

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment