# frozen_string_literal: true

require_relative '../../../puppet/util'
require_relative '../../../puppet/util/user_attr'
require 'date'

Puppet::Type.type(:user).provide :user_role_add, :parent => :useradd, :source => :useradd do
  desc "User and role management on Solaris, via `useradd` and `roleadd`."

  defaultfor 'os.family' => :solaris

  commands :add => "useradd", :delete => "userdel", :modify => "usermod", :password => "passwd", :role_add => "roleadd", :role_delete => "roledel", :role_modify => "rolemod"
  options :home, :flag => "-d", :method => :dir
  options :comment, :method => :gecos
  options :groups, :flag => "-G"
  options :shell, :flag => "-s"
  options :roles, :flag => "-R"
  options :auths, :flag => "-A"
  options :profiles, :flag => "-P"
  options :password_min_age, :flag => "-n"
  options :password_max_age, :flag => "-x"
  options :password_warn_days, :flag => "-w"

  verify :gid, "GID must be an integer" do |value|
    value.is_a? Integer
  end

  verify :groups, "Groups must be comma-separated" do |value|
    value !~ /\s/
  end

  def shell=(value)
    check_valid_shell
    set("shell", value)
  end

  has_features :manages_homedir, :allows_duplicates, :manages_solaris_rbac, :manages_roles, :manages_passwords, :manages_password_age, :manages_shell

  def check_valid_shell
    unless File.exist?(@resource.should(:shell))
      raise(Puppet::Error, "Shell #{@resource.should(:shell)} must exist")
    end
    unless File.executable?(@resource.should(:shell).to_s)
      raise(Puppet::Error, "Shell #{@resource.should(:shell)} must be executable")
    end
  end

  # must override this to hand the keyvalue pairs
  def add_properties
    cmd = []
    Puppet::Type.type(:user).validproperties.each do |property|
      # skip the password because we can't create it with the solaris useradd
      next if [:ensure, :password, :password_min_age, :password_max_age, :password_warn_days].include?(property)

      # 1680 Now you can set the hashed passwords on solaris:lib/puppet/provider/user/user_role_add.rb
      # the value needs to be quoted, mostly because -c might
      # have spaces in it
      value = @resource.should(property)
      if value && value != ""
        if property == :keys
          cmd += build_keys_cmd(value)
        else
          cmd << flag(property) << value
        end
      end
    end
    cmd
  end

  def user_attributes
    @user_attributes ||= UserAttr.get_attributes_by_name(@resource[:name])
  end

  def flush
    @user_attributes = nil
  end

  def command(cmd)
    cmd = "role_#{cmd}".intern if is_role? or (!exists? and @resource[:ensure] == :role)
    super(cmd)
  end

  def is_role?
    user_attributes and user_attributes[:type] == "role"
  end

  def run(cmd, msg)
    execute(cmd)
  rescue Puppet::ExecutionFailure => detail
    raise Puppet::Error, "Could not #{msg} #{@resource.class.name} #{@resource.name}: #{detail}", detail.backtrace
  end

  def transition(type)
    cmd = [command(:modify)]
    cmd << "-K" << "type=#{type}"
    cmd += add_properties
    cmd << @resource[:name]
  end

  def create
    if is_role?
      run(transition("normal"), "transition role to")
    else
      run(addcmd, "create")
      cmd = passcmd
      if cmd
        run(cmd, "change password policy for")
      end
    end
    # added to handle case when password is specified
    self.password = @resource[:password] if @resource[:password]
  end

  def destroy
    run(deletecmd, "delete " + (is_role? ? "role" : "user"))
  end

  def create_role
    if exists? and !is_role?
      run(transition("role"), "transition user to")
    else
      run(addcmd, "create role")
    end
  end

  def roles
    user_attributes[:roles] if user_attributes
  end

  def auths
    user_attributes[:auths] if user_attributes
  end

  def profiles
    user_attributes[:profiles] if user_attributes
  end

  def project
    user_attributes[:project] if user_attributes
  end

  def managed_attributes
    [:name, :type, :roles, :auths, :profiles, :project]
  end

  def remove_managed_attributes
    managed = managed_attributes
    user_attributes.select { |k, _v| !managed.include?(k) }.each_with_object({}) { |array, hash| hash[array[0]] = array[1]; }
  end

  def keys
    if user_attributes
      # we have to get rid of all the keys we are managing another way
      remove_managed_attributes
    end
  end

  def build_keys_cmd(keys_hash)
    cmd = []
    keys_hash.each do |k, v|
      cmd << "-K" << "#{k}=#{v}"
    end
    cmd
  end

  def keys=(keys_hash)
    run([command(:modify)] + build_keys_cmd(keys_hash) << @resource[:name], "modify attribute key pairs")
  end

  # This helper makes it possible to test this on stub data without having to
  # do too many crazy things!
  def target_file_path
    "/etc/shadow"
  end
  private :target_file_path

  # Read in /etc/shadow, find the line for this user (skipping comments, because who knows) and return it
  # No abstraction, all esoteric knowledge of file formats, yay
  def shadow_entry
    return @shadow_entry if defined? @shadow_entry

    @shadow_entry =
      File.readlines(target_file_path)
          .reject { |r| r =~ /^[^\w]/ }
          .collect { |l| l.chomp.split(':', -1) } # Don't suppress empty fields (PUP-229)
          .find { |user, _| user == @resource[:name] }
  end

  def password
    return :absent unless shadow_entry

    shadow_entry[1]
  end

  def password_min_age
    return :absent unless shadow_entry

    shadow_entry[3].empty? ? -1 : shadow_entry[3]
  end

  def password_max_age
    return :absent unless shadow_entry

    shadow_entry[4].empty? ? -1 : shadow_entry[4]
  end

  def password_warn_days
    return :absent unless shadow_entry

    shadow_entry[5].empty? ? -1 : shadow_entry[5]
  end

  def has_sensitive_data?(property = nil)
    false
  end

  # Read in /etc/shadow, find the line for our used and rewrite it with the
  # new pw.  Smooth like 80 grit sandpaper.
  #
  # Now uses the `replace_file` mechanism to minimize the chance that we lose
  # data, but it is still terrible.  We still skip platform locking, so a
  # concurrent `vipw -s` session will have no idea we risk data loss.
  def password=(cryptopw)
    shadow = File.read(target_file_path)

    # Go Mifune loves the race here where we can lose data because
    # /etc/shadow changed between reading it and writing it.
    # --daniel 2012-02-05
    Puppet::Util.replace_file(target_file_path, 0o640) do |fh|
      shadow.each_line do |line|
        line_arr = line.split(':')
        if line_arr[0] == @resource[:name]
          line_arr[1] = cryptopw
          line_arr[2] = (Date.today - Date.new(1970, 1, 1)).to_i.to_s
          line = line_arr.join(':')
        end
        fh.print line
      end
    end
  rescue => detail
    self.fail Puppet::Error, "Could not write replace #{target_file_path}: #{detail}", detail
  end
end