# frozen_string_literal: true

# This class maps POSIX owner, group, and modes to the Windows
# security model, and back.
#
# The primary goal of this mapping is to ensure that owner, group, and
# modes can be round-tripped in a consistent and deterministic
# way. Otherwise, Puppet might think file resources are out-of-sync
# every time it runs. A secondary goal is to provide equivalent
# permissions for common use-cases. For example, setting the owner to
# "Administrators", group to "Users", and mode to 750 (which also
# denies access to everyone else.
#
# There are some well-known problems mapping windows and POSIX
# permissions due to differences between the two security
# models. Search for "POSIX permission mapping leak". In POSIX, access
# to a file is determined solely based on the most specific class
# (user, group, other). So a mode of 460 would deny write access to
# the owner even if they are a member of the group. But in Windows,
# the entire access control list is walked until the user is
# explicitly denied or allowed (denied take precedence, and if neither
# occurs they are denied). As a result, a user could be allowed access
# based on their group membership. To solve this problem, other people
# have used deny access control entries to more closely model POSIX,
# but this introduces a lot of complexity.
#
# In general, this implementation only supports "typical" permissions,
# where group permissions are a subset of user, and other permissions
# are a subset of group, e.g. 754, but not 467.  However, there are
# some Windows quirks to be aware of.
#
# * The owner can be either a user or group SID, and most system files
#   are owned by the Administrators group.
# * The group can be either a user or group SID.
# * Unexpected results can occur if the owner and group are the
#   same, but the user and group classes are different, e.g. 750. In
#   this case, it is not possible to allow write access to the owner,
#   but not the group. As a result, the actual permissions set on the
#   file would be 770.
# * In general, only privileged users can set the owner, group, or
#   change the mode for files they do not own. In 2003, the user must
#   be a member of the Administrators group. In Vista/2008, the user
#   must be running with elevated privileges.
# * A file/dir can be deleted by anyone with the DELETE access right
#   OR by anyone that has the FILE_DELETE_CHILD access right for the
#   parent. See https://support.microsoft.com/kb/238018. But on Unix,
#   the user must have write access to the file/dir AND execute access
#   to all of the parent path components.
# * Many access control entries are inherited from parent directories,
#   and it is common for file/dirs to have more than 3 entries,
#   e.g. Users, Power Users, Administrators, SYSTEM, etc, which cannot
#   be mapped into the 3 class POSIX model. The get_mode method will
#   set the S_IEXTRA bit flag indicating that an access control entry
#   was found whose SID is neither the owner, group, or other. This
#   enables Puppet to detect when file/dirs are out-of-sync,
#   especially those that Puppet did not create, but is attempting
#   to manage.
# * A special case of this is S_ISYSTEM_MISSING, which is set when the
#   SYSTEM permissions are *not* present on the DACL.
# * On Unix, the owner and group can be modified without changing the
#   mode. But on Windows, an access control entry specifies which SID
#   it applies to. As a result, the set_owner and set_group methods
#   automatically rebuild the access control list based on the new
#   (and different) owner or group.

require_relative '../../../puppet/util/windows'
require 'pathname'
require 'ffi'

module Puppet::Util::Windows::Security
  include Puppet::Util::Windows::String

  extend Puppet::Util::Windows::Security
  extend FFI::Library

  # file modes
  S_IRUSR = 0o000400
  S_IRGRP = 0o000040
  S_IROTH = 0o000004
  S_IWUSR = 0o000200
  S_IWGRP = 0o000020
  S_IWOTH = 0o000002
  S_IXUSR = 0o000100
  S_IXGRP = 0o000010
  S_IXOTH = 0o000001
  S_IRWXU = 0o000700
  S_IRWXG = 0o000070
  S_IRWXO = 0o000007
  S_ISVTX = 0o001000
  S_IEXTRA = 0o2000000 # represents an extra ace
  S_ISYSTEM_MISSING = 0o4000000

  # constants that are missing from Windows::Security
  PROTECTED_DACL_SECURITY_INFORMATION   = 0x80000000
  UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000
  NO_INHERITANCE = 0x0
  SE_DACL_PROTECTED = 0x1000

  FILE = Puppet::Util::Windows::File

  SE_BACKUP_NAME              = 'SeBackupPrivilege'
  SE_DEBUG_NAME               = 'SeDebugPrivilege'
  SE_RESTORE_NAME             = 'SeRestorePrivilege'

  DELETE                      = 0x00010000
  READ_CONTROL                = 0x20000
  WRITE_DAC                   = 0x40000
  WRITE_OWNER                 = 0x80000

  OWNER_SECURITY_INFORMATION  = 1
  GROUP_SECURITY_INFORMATION  = 2
  DACL_SECURITY_INFORMATION   = 4

  # Set the owner of the object referenced by +path+ to the specified
  # +owner_sid+.  The owner sid should be of the form "S-1-5-32-544"
  # and can either be a user or group.  Only a user with the
  # SE_RESTORE_NAME privilege in their process token can overwrite the
  # object's owner to something other than the current user.
  def set_owner(owner_sid, path)
    sd = get_security_descriptor(path)

    if owner_sid != sd.owner
      sd.owner = owner_sid
      set_security_descriptor(path, sd)
    end
  end

  # Get the owner of the object referenced by +path+.  The returned
  # value is a SID string, e.g. "S-1-5-32-544".  Any user with read
  # access to an object can get the owner. Only a user with the
  # SE_BACKUP_NAME privilege in their process token can get the owner
  # for objects they do not have read access to.
  def get_owner(path)
    return unless supports_acl?(path)

    get_security_descriptor(path).owner
  end

  # Set the owner of the object referenced by +path+ to the specified
  # +group_sid+.  The group sid should be of the form "S-1-5-32-544"
  # and can either be a user or group.  Any user with WRITE_OWNER
  # access to the object can change the group (regardless of whether
  # the current user belongs to that group or not).
  def set_group(group_sid, path)
    sd = get_security_descriptor(path)

    if group_sid != sd.group
      sd.group = group_sid
      set_security_descriptor(path, sd)
    end
  end

  # Get the group of the object referenced by +path+.  The returned
  # value is a SID string, e.g. "S-1-5-32-544".  Any user with read
  # access to an object can get the group. Only a user with the
  # SE_BACKUP_NAME privilege in their process token can get the group
  # for objects they do not have read access to.
  def get_group(path)
    return unless supports_acl?(path)

    get_security_descriptor(path).group
  end

  FILE_PERSISTENT_ACLS = 0x00000008

  def supports_acl?(path)
    supported = false
    root = Pathname.new(path).enum_for(:ascend).to_a.last.to_s
    # 'A trailing backslash is required'
    root = "#{root}\\" unless root =~ %r{[/\\]$}

    FFI::MemoryPointer.new(:pointer, 1) do |flags_ptr|
      if GetVolumeInformationW(wide_string(root), FFI::Pointer::NULL, 0,
                               FFI::Pointer::NULL, FFI::Pointer::NULL,
                               flags_ptr, FFI::Pointer::NULL, 0) == FFI::WIN32_FALSE
        raise Puppet::Util::Windows::Error, _("Failed to get volume information")
      end

      supported = flags_ptr.read_dword & FILE_PERSISTENT_ACLS == FILE_PERSISTENT_ACLS
    end

    supported
  end

  MASK_TO_MODE = {
    FILE::FILE_GENERIC_READ => S_IROTH,
    FILE::FILE_GENERIC_WRITE => S_IWOTH,
    (FILE::FILE_GENERIC_EXECUTE & ~FILE::FILE_READ_ATTRIBUTES) => S_IXOTH
  }

  def get_aces_for_path_by_sid(path, sid)
    get_security_descriptor(path).dacl.select { |ace| ace.sid == sid }
  end

  # Get the mode of the object referenced by +path+.  The returned
  # integer value represents the POSIX-style read, write, and execute
  # modes for the user, group, and other classes, e.g. 0640.  Any user
  # with read access to an object can get the mode. Only a user with
  # the SE_BACKUP_NAME privilege in their process token can get the
  # mode for objects they do not have read access to.
  def get_mode(path)
    return unless supports_acl?(path)

    well_known_world_sid = Puppet::Util::Windows::SID::Everyone
    well_known_nobody_sid = Puppet::Util::Windows::SID::Nobody
    well_known_system_sid = Puppet::Util::Windows::SID::LocalSystem
    well_known_app_packages_sid = Puppet::Util::Windows::SID::AllAppPackages

    mode = S_ISYSTEM_MISSING

    sd = get_security_descriptor(path)
    sd.dacl.each do |ace|
      next if ace.inherit_only?

      case ace.sid
      when sd.owner
        MASK_TO_MODE.each_pair do |k, v|
          if (ace.mask & k) == k
            mode |= (v << 6)
          end
        end
      when sd.group
        MASK_TO_MODE.each_pair do |k, v|
          if (ace.mask & k) == k
            mode |= (v << 3)
          end
        end
      when well_known_world_sid
        MASK_TO_MODE.each_pair do |k, v|
          if (ace.mask & k) == k
            mode |= (v << 6) | (v << 3) | v
          end
        end
        if File.directory?(path) &&
           (ace.mask & (FILE::FILE_WRITE_DATA | FILE::FILE_EXECUTE | FILE::FILE_DELETE_CHILD)) == (FILE::FILE_WRITE_DATA | FILE::FILE_EXECUTE)
          mode |= S_ISVTX;
        end
      when well_known_nobody_sid
        if (ace.mask & FILE::FILE_APPEND_DATA).nonzero?
          mode |= S_ISVTX
        end
      when well_known_app_packages_sid, well_known_system_sid
        # do nothing
      else
        # puts "Warning, unable to map SID into POSIX mode: #{ace.sid}"
        mode |= S_IEXTRA
      end

      if ace.sid == well_known_system_sid
        mode &= ~S_ISYSTEM_MISSING
      end

      # if owner and group the same, then user and group modes are the OR of both
      if sd.owner == sd.group
        mode |= ((mode & S_IRWXG) << 3) | ((mode & S_IRWXU) >> 3)
        # puts "owner: #{sd.group}, 0x#{ace.mask.to_s(16)}, #{mode.to_s(8)}"
      end
    end

    # puts "get_mode: #{mode.to_s(8)}"
    mode
  end

  MODE_TO_MASK = {
    S_IROTH => FILE::FILE_GENERIC_READ,
    S_IWOTH => FILE::FILE_GENERIC_WRITE,
    S_IXOTH => (FILE::FILE_GENERIC_EXECUTE & ~FILE::FILE_READ_ATTRIBUTES),
  }

  # Set the mode of the object referenced by +path+ to the specified
  # +mode+. The mode should be specified as POSIX-style read, write,
  # and execute modes for the user, group, and other classes,
  # e.g. 0640. The sticky bit, S_ISVTX, is supported, but is only
  # meaningful for directories. If set, group and others are not
  # allowed to delete child objects for which they are not the owner.
  # By default, the DACL is set to protected, meaning it does not
  # inherit access control entries from parent objects. This can be
  # changed by setting +protected+ to false. The owner of the object
  # (with READ_CONTROL and WRITE_DACL access) can always change the
  # mode. Only a user with the SE_BACKUP_NAME and SE_RESTORE_NAME
  # privileges in their process token can change the mode for objects
  # that they do not have read and write access to.
  def set_mode(mode, path, protected = true, managing_owner = false, managing_group = false)
    sd = get_security_descriptor(path)
    well_known_world_sid = Puppet::Util::Windows::SID::Everyone
    well_known_nobody_sid = Puppet::Util::Windows::SID::Nobody
    well_known_system_sid = Puppet::Util::Windows::SID::LocalSystem

    owner_allow = FILE::STANDARD_RIGHTS_ALL  |
                  FILE::FILE_READ_ATTRIBUTES |
                  FILE::FILE_WRITE_ATTRIBUTES
    # this prevents a mode that is not 7 from taking ownership of a file based
    # on group membership and rewriting it / making it executable
    group_allow = FILE::STANDARD_RIGHTS_READ |
                  FILE::FILE_READ_ATTRIBUTES |
                  FILE::SYNCHRONIZE
    other_allow = FILE::STANDARD_RIGHTS_READ |
                  FILE::FILE_READ_ATTRIBUTES |
                  FILE::SYNCHRONIZE
    nobody_allow = 0
    system_allow = 0

    MODE_TO_MASK.each do |k, v|
      if ((mode >> 6) & k) == k
        owner_allow |= v
      end
      if ((mode >> 3) & k) == k
        group_allow |= v
      end
      if (mode & k) == k
        other_allow |= v
      end
    end

    # With a mode value of '7' for group / other, the value must then include
    # additional perms beyond STANDARD_RIGHTS_READ to allow DACL modification
    if (mode & S_IRWXG) == S_IRWXG
      group_allow |= FILE::DELETE | FILE::WRITE_DAC | FILE::WRITE_OWNER
    end
    if (mode & S_IRWXO) == S_IRWXO
      other_allow |= FILE::DELETE | FILE::WRITE_DAC | FILE::WRITE_OWNER
    end

    if (mode & S_ISVTX).nonzero?
      nobody_allow |= FILE::FILE_APPEND_DATA;
    end

    isownergroup = sd.owner == sd.group

    # caller is NOT managing SYSTEM by using group or owner, so set to FULL
    if ![sd.owner, sd.group].include? well_known_system_sid
      # we don't check S_ISYSTEM_MISSING bit, but automatically carry over existing SYSTEM perms
      # by default set SYSTEM perms to full
      system_allow = FILE::FILE_ALL_ACCESS
    else
      # It is possible to set SYSTEM with a mode other than Full Control (7) however this makes no sense and in practical terms
      # should not be done.  We can trap these instances and correct them before being applied.
      if (sd.owner == well_known_system_sid) && (owner_allow != FILE::FILE_ALL_ACCESS)
        # If owner and group are both SYSTEM but group is unmanaged the control rights of system will be set to FullControl by
        # the unmanaged group, so there is no need for the warning
        if managing_owner && (!isownergroup || managing_group)
          # TRANSLATORS 'SYSTEM' is a Windows name and should not be translated
          Puppet.warning _("Setting control rights for %{path} owner SYSTEM to less than Full Control rights. Setting SYSTEM rights to less than Full Control may have unintented consequences for operations on this file") % { path: path }
        elsif managing_owner && isownergroup
          # TRANSLATORS 'SYSTEM' is a Windows name and should not be translated
          Puppet.debug { _("%{path} owner and group both set to user SYSTEM, but group is not managed directly: SYSTEM user rights will be set to FullControl by group") % { path: path } }
        else
          # TRANSLATORS 'SYSTEM' is a Windows name and should not be translated
          Puppet.debug { _("An attempt to set mode %{mode} on item %{path} would result in the owner, SYSTEM, to have less than Full Control rights. This attempt has been corrected to Full Control") % { mode: mode.to_s(8), path: path } }
          owner_allow = FILE::FILE_ALL_ACCESS
        end
      end

      if (sd.group == well_known_system_sid) && (group_allow != FILE::FILE_ALL_ACCESS)
        # If owner and group are both SYSTEM but owner is unmanaged the control rights of system will be set to FullControl by
        # the unmanaged owner, so there is no need for the warning.
        if managing_group && (!isownergroup || managing_owner)
          # TRANSLATORS 'SYSTEM' is a Windows name and should not be translated
          Puppet.warning _("Setting control rights for %{path} group SYSTEM to less than Full Control rights. Setting SYSTEM rights to less than Full Control may have unintented consequences for operations on this file") % { path: path }
        elsif managing_group && isownergroup
          # TRANSLATORS 'SYSTEM' is a Windows name and should not be translated
          Puppet.debug { _("%{path} owner and group both set to user SYSTEM, but owner is not managed directly: SYSTEM user rights will be set to FullControl by owner") % { path: path } }
        else
          # TRANSLATORS 'SYSTEM' is a Windows name and should not be translated
          Puppet.debug { _("An attempt to set mode %{mode} on item %{path} would result in the group, SYSTEM, to have less than Full Control rights. This attempt has been corrected to Full Control") % { mode: mode.to_s(8), path: path } }
          group_allow = FILE::FILE_ALL_ACCESS
        end
      end
    end

    # even though FILE_DELETE_CHILD only applies to directories, it can be set on files
    # this is necessary to do to ensure a file ends up with (F) FullControl
    if (mode & (S_IWUSR | S_IXUSR)) == (S_IWUSR | S_IXUSR)
      owner_allow |= FILE::FILE_DELETE_CHILD
    end
    if (mode & (S_IWGRP | S_IXGRP)) == (S_IWGRP | S_IXGRP) && (mode & S_ISVTX) == 0
      group_allow |= FILE::FILE_DELETE_CHILD
    end
    if (mode & (S_IWOTH | S_IXOTH)) == (S_IWOTH | S_IXOTH) && (mode & S_ISVTX) == 0
      other_allow |= FILE::FILE_DELETE_CHILD
    end

    # if owner and group the same, then map group permissions to the one owner ACE
    if isownergroup
      owner_allow |= group_allow
    end

    # if any ACE allows write, then clear readonly bit, but do this before we overwrite
    # the DACl and lose our ability to set the attribute
    if ((owner_allow | group_allow | other_allow) & FILE::FILE_WRITE_DATA) == FILE::FILE_WRITE_DATA
      FILE.remove_attributes(path, FILE::FILE_ATTRIBUTE_READONLY)
    end

    isdir = File.directory?(path)
    dacl = Puppet::Util::Windows::AccessControlList.new
    dacl.allow(sd.owner, owner_allow)
    unless isownergroup
      dacl.allow(sd.group, group_allow)
    end
    dacl.allow(well_known_world_sid, other_allow)
    dacl.allow(well_known_nobody_sid, nobody_allow)

    # TODO: system should be first?
    flags = !isdir ? 0 :
      Puppet::Util::Windows::AccessControlEntry::CONTAINER_INHERIT_ACE |
        Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE
    dacl.allow(well_known_system_sid, system_allow, flags)

    # add inherit-only aces for child dirs and files that are created within the dir
    inherit_only = Puppet::Util::Windows::AccessControlEntry::INHERIT_ONLY_ACE
    if isdir
      inherit = inherit_only | Puppet::Util::Windows::AccessControlEntry::CONTAINER_INHERIT_ACE
      dacl.allow(Puppet::Util::Windows::SID::CreatorOwner, owner_allow, inherit)
      dacl.allow(Puppet::Util::Windows::SID::CreatorGroup, group_allow, inherit)

      inherit = inherit_only | Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE
      # allow any previously set bits *except* for these
      perms_to_strip = ~(FILE::FILE_EXECUTE + FILE::WRITE_OWNER + FILE::WRITE_DAC)
      dacl.allow(Puppet::Util::Windows::SID::CreatorOwner, owner_allow & perms_to_strip, inherit)
      dacl.allow(Puppet::Util::Windows::SID::CreatorGroup, group_allow & perms_to_strip, inherit)
    end

    new_sd = Puppet::Util::Windows::SecurityDescriptor.new(sd.owner, sd.group, dacl, protected)
    set_security_descriptor(path, new_sd)

    nil
  end

  ACL_REVISION = 2

  def add_access_allowed_ace(acl, mask, sid, inherit = nil)
    inherit ||= NO_INHERITANCE

    Puppet::Util::Windows::SID.string_to_sid_ptr(sid) do |sid_ptr|
      if Puppet::Util::Windows::SID.IsValidSid(sid_ptr) == FFI::WIN32_FALSE
        raise Puppet::Util::Windows::Error, _("Invalid SID")
      end

      if AddAccessAllowedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr) == FFI::WIN32_FALSE
        raise Puppet::Util::Windows::Error, _("Failed to add access control entry")
      end
    end

    # ensure this method is void if it doesn't raise
    nil
  end

  def add_access_denied_ace(acl, mask, sid, inherit = nil)
    inherit ||= NO_INHERITANCE

    Puppet::Util::Windows::SID.string_to_sid_ptr(sid) do |sid_ptr|
      if Puppet::Util::Windows::SID.IsValidSid(sid_ptr) == FFI::WIN32_FALSE
        raise Puppet::Util::Windows::Error, _("Invalid SID")
      end

      if AddAccessDeniedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr) == FFI::WIN32_FALSE
        raise Puppet::Util::Windows::Error, _("Failed to add access control entry")
      end
    end

    # ensure this method is void if it doesn't raise
    nil
  end

  def parse_dacl(dacl_ptr)
    # REMIND: need to handle NULL DACL
    if IsValidAcl(dacl_ptr) == FFI::WIN32_FALSE
      raise Puppet::Util::Windows::Error, _("Invalid DACL")
    end

    dacl_struct = ACL.new(dacl_ptr)
    ace_count = dacl_struct[:AceCount]

    dacl = Puppet::Util::Windows::AccessControlList.new

    # deny all
    return dacl if ace_count == 0

    0.upto(ace_count - 1) do |i|
      FFI::MemoryPointer.new(:pointer, 1) do |ace_ptr|
        next if GetAce(dacl_ptr, i, ace_ptr) == FFI::WIN32_FALSE

        # ACE structures vary depending on the type. We are only concerned with
        # ACCESS_ALLOWED_ACE and ACCESS_DENIED_ACEs, which have the same layout
        ace = GENERIC_ACCESS_ACE.new(ace_ptr.get_pointer(0)) # deref LPVOID *

        ace_type = ace[:Header][:AceType]
        if ace_type != Puppet::Util::Windows::AccessControlEntry::ACCESS_ALLOWED_ACE_TYPE &&
           ace_type != Puppet::Util::Windows::AccessControlEntry::ACCESS_DENIED_ACE_TYPE
          Puppet.warning _("Unsupported access control entry type: 0x%{type}") % { type: ace_type.to_s(16) }
          next
        end

        # using pointer addition gives the FFI::Pointer a size, but that's OK here
        sid = Puppet::Util::Windows::SID.sid_ptr_to_string(ace.pointer + GENERIC_ACCESS_ACE.offset_of(:SidStart))
        mask = ace[:Mask]
        ace_flags = ace[:Header][:AceFlags]

        case ace_type
        when Puppet::Util::Windows::AccessControlEntry::ACCESS_ALLOWED_ACE_TYPE
          dacl.allow(sid, mask, ace_flags)
        when Puppet::Util::Windows::AccessControlEntry::ACCESS_DENIED_ACE_TYPE
          dacl.deny(sid, mask, ace_flags)
        end
      end
    end

    dacl
  end

  # Open an existing file with the specified access mode, and execute a
  # block with the opened file HANDLE.
  def open_file(path, access, &block)
    handle = CreateFileW(
      wide_string(path),
      access,
      FILE::FILE_SHARE_READ | FILE::FILE_SHARE_WRITE,
      FFI::Pointer::NULL, # security_attributes
      FILE::OPEN_EXISTING,
      FILE::FILE_FLAG_OPEN_REPARSE_POINT | FILE::FILE_FLAG_BACKUP_SEMANTICS,
      FFI::Pointer::NULL_HANDLE
    ) # template

    if handle == Puppet::Util::Windows::File::INVALID_HANDLE_VALUE
      raise Puppet::Util::Windows::Error, _("Failed to open '%{path}'") % { path: path }
    end

    begin
      yield handle
    ensure
      FFI::WIN32.CloseHandle(handle) if handle
    end

    # handle has already had CloseHandle called against it, nothing to return
    nil
  end

  # Execute a block with the specified privilege enabled
  def with_privilege(privilege, &block)
    set_privilege(privilege, true)
    yield
  ensure
    set_privilege(privilege, false)
  end

  SE_PRIVILEGE_ENABLED    = 0x00000002
  TOKEN_ADJUST_PRIVILEGES = 0x0020

  # Enable or disable a privilege. Note this doesn't add any privileges the
  # user doesn't already has, it just enables privileges that are disabled.
  def set_privilege(privilege, enable)
    return unless Puppet.features.root?

    Puppet::Util::Windows::Process.with_process_token(TOKEN_ADJUST_PRIVILEGES) do |token|
      Puppet::Util::Windows::Process.lookup_privilege_value(privilege) do |luid|
        FFI::MemoryPointer.new(Puppet::Util::Windows::Process::LUID_AND_ATTRIBUTES.size) do |luid_and_attributes_ptr|
          # allocate unmanaged memory for structs that we clean up afterwards
          luid_and_attributes = Puppet::Util::Windows::Process::LUID_AND_ATTRIBUTES.new(luid_and_attributes_ptr)
          luid_and_attributes[:Luid] = luid
          luid_and_attributes[:Attributes] = enable ? SE_PRIVILEGE_ENABLED : 0

          FFI::MemoryPointer.new(Puppet::Util::Windows::Process::TOKEN_PRIVILEGES.size) do |token_privileges_ptr|
            token_privileges = Puppet::Util::Windows::Process::TOKEN_PRIVILEGES.new(token_privileges_ptr)
            token_privileges[:PrivilegeCount] = 1
            token_privileges[:Privileges][0] = luid_and_attributes

            # size is correct given we only have 1 LUID, otherwise would be:
            # [:PrivilegeCount].size + [:PrivilegeCount] * LUID_AND_ATTRIBUTES.size
            if AdjustTokenPrivileges(token, FFI::WIN32_FALSE,
                                     token_privileges, token_privileges.size,
                                     FFI::MemoryPointer::NULL, FFI::MemoryPointer::NULL) == FFI::WIN32_FALSE
              raise Puppet::Util::Windows::Error, _("Failed to adjust process privileges")
            end
          end
        end
      end
    end

    # token / luid structs freed by this point, so return true as nothing raised
    true
  end

  def get_security_descriptor(path)
    sd = nil

    with_privilege(SE_BACKUP_NAME) do
      open_file(path, READ_CONTROL) do |handle|
        FFI::MemoryPointer.new(:pointer, 1) do |owner_sid_ptr_ptr|
          FFI::MemoryPointer.new(:pointer, 1) do |group_sid_ptr_ptr|
            FFI::MemoryPointer.new(:pointer, 1) do |dacl_ptr_ptr|
              FFI::MemoryPointer.new(:pointer, 1) do |sd_ptr_ptr|
                rv = GetSecurityInfo(
                  handle,
                  :SE_FILE_OBJECT,
                  OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
                  owner_sid_ptr_ptr,
                  group_sid_ptr_ptr,
                  dacl_ptr_ptr,
                  FFI::Pointer::NULL, # sacl
                  sd_ptr_ptr
                ) # sec desc
                raise Puppet::Util::Windows::Error, _("Failed to get security information") if rv != FFI::ERROR_SUCCESS

                # these 2 convenience params are not freed since they point inside sd_ptr
                owner = Puppet::Util::Windows::SID.sid_ptr_to_string(owner_sid_ptr_ptr.get_pointer(0))
                group = Puppet::Util::Windows::SID.sid_ptr_to_string(group_sid_ptr_ptr.get_pointer(0))

                FFI::MemoryPointer.new(:word, 1) do |control|
                  FFI::MemoryPointer.new(:dword, 1) do |revision|
                    sd_ptr_ptr.read_win32_local_pointer do |sd_ptr|
                      if GetSecurityDescriptorControl(sd_ptr, control, revision) == FFI::WIN32_FALSE
                        raise Puppet::Util::Windows::Error, _("Failed to get security descriptor control")
                      end

                      protect = (control.read_word & SE_DACL_PROTECTED) == SE_DACL_PROTECTED
                      dacl = parse_dacl(dacl_ptr_ptr.get_pointer(0))
                      sd = Puppet::Util::Windows::SecurityDescriptor.new(owner, group, dacl, protect)
                    end
                  end
                end
              end
            end
          end
        end
      end
    end

    sd
  end

  def get_max_generic_acl_size(ace_count)
    # https://msdn.microsoft.com/en-us/library/windows/desktop/aa378853(v=vs.85).aspx
    # To calculate the initial size of an ACL, add the following together, and then align the result to the nearest DWORD:
    # * Size of the ACL structure.
    # * Size of each ACE structure that the ACL is to contain minus the SidStart member (DWORD) of the ACE.
    # * Length of the SID that each ACE is to contain.
    ACL.size + ace_count * MAXIMUM_GENERIC_ACE_SIZE
  end

  # setting DACL requires both READ_CONTROL and WRITE_DACL access rights,
  # and their respective privileges, SE_BACKUP_NAME and SE_RESTORE_NAME.
  def set_security_descriptor(path, sd)
    FFI::MemoryPointer.new(:byte, get_max_generic_acl_size(sd.dacl.count)) do |acl_ptr|
      if InitializeAcl(acl_ptr, acl_ptr.size, ACL_REVISION) == FFI::WIN32_FALSE
        raise Puppet::Util::Windows::Error, _("Failed to initialize ACL")
      end

      if IsValidAcl(acl_ptr) == FFI::WIN32_FALSE
        raise Puppet::Util::Windows::Error, _("Invalid DACL")
      end

      with_privilege(SE_BACKUP_NAME) do
        with_privilege(SE_RESTORE_NAME) do
          open_file(path, READ_CONTROL | WRITE_DAC | WRITE_OWNER) do |handle|
            Puppet::Util::Windows::SID.string_to_sid_ptr(sd.owner) do |owner_sid_ptr|
              Puppet::Util::Windows::SID.string_to_sid_ptr(sd.group) do |group_sid_ptr|
                sd.dacl.each do |ace|
                  case ace.type
                  when Puppet::Util::Windows::AccessControlEntry::ACCESS_ALLOWED_ACE_TYPE
                    # puts "ace: allow, sid #{Puppet::Util::Windows::SID.sid_to_name(ace.sid)}, mask 0x#{ace.mask.to_s(16)}"
                    add_access_allowed_ace(acl_ptr, ace.mask, ace.sid, ace.flags)
                  when Puppet::Util::Windows::AccessControlEntry::ACCESS_DENIED_ACE_TYPE
                    # puts "ace: deny, sid #{Puppet::Util::Windows::SID.sid_to_name(ace.sid)}, mask 0x#{ace.mask.to_s(16)}"
                    add_access_denied_ace(acl_ptr, ace.mask, ace.sid, ace.flags)
                  else
                    raise "We should never get here"
                    # TODO: this should have been a warning in an earlier commit
                  end
                end

                # protected means the object does not inherit aces from its parent
                flags = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION
                flags |= sd.protect ? PROTECTED_DACL_SECURITY_INFORMATION : UNPROTECTED_DACL_SECURITY_INFORMATION

                rv = SetSecurityInfo(handle,
                                     :SE_FILE_OBJECT,
                                     flags,
                                     owner_sid_ptr,
                                     group_sid_ptr,
                                     acl_ptr,
                                     FFI::MemoryPointer::NULL)

                if rv != FFI::ERROR_SUCCESS
                  raise Puppet::Util::Windows::Error, _("Failed to set security information")
                end
              end
            end
          end
        end
      end
    end
  end

  ffi_convention :stdcall

  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx
  # HANDLE WINAPI CreateFile(
  #   _In_      LPCTSTR lpFileName,
  #   _In_      DWORD dwDesiredAccess,
  #   _In_      DWORD dwShareMode,
  #   _In_opt_  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  #   _In_      DWORD dwCreationDisposition,
  #   _In_      DWORD dwFlagsAndAttributes,
  #   _In_opt_  HANDLE hTemplateFile
  # );
  ffi_lib :kernel32
  attach_function_private :CreateFileW,
                          [:lpcwstr, :dword, :dword, :pointer, :dword, :dword, :handle], :handle

  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa364993(v=vs.85).aspx
  # BOOL WINAPI GetVolumeInformation(
  #   _In_opt_   LPCTSTR lpRootPathName,
  #   _Out_opt_  LPTSTR lpVolumeNameBuffer,
  #   _In_       DWORD nVolumeNameSize,
  #   _Out_opt_  LPDWORD lpVolumeSerialNumber,
  #   _Out_opt_  LPDWORD lpMaximumComponentLength,
  #   _Out_opt_  LPDWORD lpFileSystemFlags,
  #   _Out_opt_  LPTSTR lpFileSystemNameBuffer,
  #   _In_       DWORD nFileSystemNameSize
  # );
  ffi_lib :kernel32
  attach_function_private :GetVolumeInformationW,
                          [:lpcwstr, :lpwstr, :dword, :lpdword, :lpdword, :lpdword, :lpwstr, :dword], :win32_bool

  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa374951(v=vs.85).aspx
  # BOOL WINAPI AddAccessAllowedAceEx(
  #   _Inout_  PACL pAcl,
  #   _In_     DWORD dwAceRevision,
  #   _In_     DWORD AceFlags,
  #   _In_     DWORD AccessMask,
  #   _In_     PSID pSid
  # );
  ffi_lib :advapi32
  attach_function_private :AddAccessAllowedAceEx,
                          [:pointer, :dword, :dword, :dword, :pointer], :win32_bool

  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa374964(v=vs.85).aspx
  # BOOL WINAPI AddAccessDeniedAceEx(
  #   _Inout_  PACL pAcl,
  #   _In_     DWORD dwAceRevision,
  #   _In_     DWORD AceFlags,
  #   _In_     DWORD AccessMask,
  #   _In_     PSID pSid
  # );
  ffi_lib :advapi32
  attach_function_private :AddAccessDeniedAceEx,
                          [:pointer, :dword, :dword, :dword, :pointer], :win32_bool

  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa374931(v=vs.85).aspx
  # typedef struct _ACL {
  #   BYTE AclRevision;
  #   BYTE Sbz1;
  #   WORD AclSize;
  #   WORD AceCount;
  #   WORD Sbz2;
  # } ACL, *PACL;
  class ACL < FFI::Struct
    layout :AclRevision, :byte,
           :Sbz1, :byte,
           :AclSize, :word,
           :AceCount, :word,
           :Sbz2, :word
  end

  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa374912(v=vs.85).aspx
  # ACE types
  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa374919(v=vs.85).aspx
  # typedef struct _ACE_HEADER {
  #   BYTE AceType;
  #   BYTE AceFlags;
  #   WORD AceSize;
  # } ACE_HEADER, *PACE_HEADER;
  class ACE_HEADER < FFI::Struct
    layout :AceType, :byte,
           :AceFlags, :byte,
           :AceSize,  :word
  end

  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa374892(v=vs.85).aspx
  # ACCESS_MASK

  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa374847(v=vs.85).aspx
  # typedef struct _ACCESS_ALLOWED_ACE {
  #   ACE_HEADER  Header;
  #   ACCESS_MASK Mask;
  #   DWORD       SidStart;
  # } ACCESS_ALLOWED_ACE, *PACCESS_ALLOWED_ACE;
  #
  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa374879(v=vs.85).aspx
  # typedef struct _ACCESS_DENIED_ACE {
  #   ACE_HEADER  Header;
  #   ACCESS_MASK Mask;
  #   DWORD       SidStart;
  # } ACCESS_DENIED_ACE, *PACCESS_DENIED_ACE;
  class GENERIC_ACCESS_ACE < FFI::Struct
    # ACE structures must be aligned on DWORD boundaries. All Windows
    # memory-management functions return DWORD-aligned handles to memory
    pack 4
    layout :Header, ACE_HEADER,
           :Mask, :dword,
           :SidStart, :dword
  end

  # https://stackoverflow.com/a/1792930
  MAXIMUM_SID_BYTES_LENGTH = 68
  MAXIMUM_GENERIC_ACE_SIZE = GENERIC_ACCESS_ACE.offset_of(:SidStart) +
                             MAXIMUM_SID_BYTES_LENGTH

  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa446634(v=vs.85).aspx
  # BOOL WINAPI GetAce(
  #   _In_   PACL pAcl,
  #   _In_   DWORD dwAceIndex,
  #   _Out_  LPVOID *pAce
  # );
  ffi_lib :advapi32
  attach_function_private :GetAce,
                          [:pointer, :dword, :pointer], :win32_bool

  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa375202(v=vs.85).aspx
  # BOOL WINAPI AdjustTokenPrivileges(
  #   _In_       HANDLE TokenHandle,
  #   _In_       BOOL DisableAllPrivileges,
  #   _In_opt_   PTOKEN_PRIVILEGES NewState,
  #   _In_       DWORD BufferLength,
  #   _Out_opt_  PTOKEN_PRIVILEGES PreviousState,
  #   _Out_opt_  PDWORD ReturnLength
  # );
  ffi_lib :advapi32
  attach_function_private :AdjustTokenPrivileges,
                          [:handle, :win32_bool, :pointer, :dword, :pointer, :pdword], :win32_bool

  # https://msdn.microsoft.com/en-us/library/windows/hardware/ff556610(v=vs.85).aspx
  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa379561(v=vs.85).aspx
  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa446647(v=vs.85).aspx
  # typedef WORD SECURITY_DESCRIPTOR_CONTROL, *PSECURITY_DESCRIPTOR_CONTROL;
  # BOOL WINAPI GetSecurityDescriptorControl(
  #   _In_   PSECURITY_DESCRIPTOR pSecurityDescriptor,
  #   _Out_  PSECURITY_DESCRIPTOR_CONTROL pControl,
  #   _Out_  LPDWORD lpdwRevision
  # );
  ffi_lib :advapi32
  attach_function_private :GetSecurityDescriptorControl,
                          [:pointer, :lpword, :lpdword], :win32_bool

  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa378853(v=vs.85).aspx
  # BOOL WINAPI InitializeAcl(
  #   _Out_  PACL pAcl,
  #   _In_   DWORD nAclLength,
  #   _In_   DWORD dwAclRevision
  # );
  ffi_lib :advapi32
  attach_function_private :InitializeAcl,
                          [:pointer, :dword, :dword], :win32_bool

  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa379142(v=vs.85).aspx
  # BOOL WINAPI IsValidAcl(
  #   _In_  PACL pAcl
  # );
  ffi_lib :advapi32
  attach_function_private :IsValidAcl,
                          [:pointer], :win32_bool

  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa379593(v=vs.85).aspx
  SE_OBJECT_TYPE = enum(
    :SE_UNKNOWN_OBJECT_TYPE, 0,
    :SE_FILE_OBJECT,
    :SE_SERVICE,
    :SE_PRINTER,
    :SE_REGISTRY_KEY,
    :SE_LMSHARE,
    :SE_KERNEL_OBJECT,
    :SE_WINDOW_OBJECT,
    :SE_DS_OBJECT,
    :SE_DS_OBJECT_ALL,
    :SE_PROVIDER_DEFINED_OBJECT,
    :SE_WMIGUID_OBJECT,
    :SE_REGISTRY_WOW64_32KEY
  )

  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa446654(v=vs.85).aspx
  # DWORD WINAPI GetSecurityInfo(
  #   _In_       HANDLE handle,
  #   _In_       SE_OBJECT_TYPE ObjectType,
  #   _In_       SECURITY_INFORMATION SecurityInfo,
  #   _Out_opt_  PSID *ppsidOwner,
  #   _Out_opt_  PSID *ppsidGroup,
  #   _Out_opt_  PACL *ppDacl,
  #   _Out_opt_  PACL *ppSacl,
  #   _Out_opt_  PSECURITY_DESCRIPTOR *ppSecurityDescriptor
  # );
  ffi_lib :advapi32
  attach_function_private :GetSecurityInfo,
                          [:handle, SE_OBJECT_TYPE, :dword, :pointer, :pointer, :pointer, :pointer, :pointer], :dword

  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa379588(v=vs.85).aspx
  # DWORD WINAPI SetSecurityInfo(
  #   _In_      HANDLE handle,
  #   _In_      SE_OBJECT_TYPE ObjectType,
  #   _In_      SECURITY_INFORMATION SecurityInfo,
  #   _In_opt_  PSID psidOwner,
  #   _In_opt_  PSID psidGroup,
  #   _In_opt_  PACL pDacl,
  #   _In_opt_  PACL pSacl
  # );
  ffi_lib :advapi32
  # TODO: SECURITY_INFORMATION is actually a bitmask the size of a DWORD
  attach_function_private :SetSecurityInfo,
                          [:handle, SE_OBJECT_TYPE, :dword, :pointer, :pointer, :pointer, :pointer], :dword
end
