# module Puppet::Util::Windows::Security

### Constants

MASK_TO_MODE
MODE_TO_MASK
NO_INHERITANCE
PROTECTED_DACL_SECURITY_INFORMATION

  constants that are missing from Windows::Security

S_IEXTRA
S_IRGRP
S_IROTH
S_IRUSR

  file modes

S_IRWXG
S_IRWXO
S_IRWXU
S_ISVTX
S_IWGRP
S_IWOTH
S_IWUSR
S_IXGRP
S_IXOTH
S_IXUSR
UNPROTECTED_DACL_SECURITY_INFORMATION

### Public Instance Methods

add_access_allowed_ace(acl, mask, sid, inherit = NO_INHERITANCE) click to toggle source

    # File lib/puppet/util/windows/security.rb, line 416
    def add_access_allowed_ace(acl, mask, sid, inherit = NO_INHERITANCE)
      string_to_sid_ptr(sid) do |sid_ptr|
        raise Puppet::Util::Windows::Error.new("Invalid SID") unless IsValidSid(sid_ptr)

        unless AddAccessAllowedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr)
          raise Puppet::Util::Windows::Error.new("Failed to add access control entry")
        end
      end
    end

add_access_denied_ace(acl, mask, sid) click to toggle source

    # File lib/puppet/util/windows/security.rb, line 426
    def add_access_denied_ace(acl, mask, sid)
      string_to_sid_ptr(sid) do |sid_ptr|
        raise Puppet::Util::Windows::Error.new("Invalid SID") unless IsValidSid(sid_ptr)

        unless AddAccessDeniedAce(acl, ACL_REVISION, mask, sid_ptr)
          raise Puppet::Util::Windows::Error.new("Failed to add access control entry")
        end
      end
    end

add_attributes(path, flags) click to toggle source

    # File lib/puppet/util/windows/security.rb, line 198
    def add_attributes(path, flags)
      oldattrs = get_attributes(path)

      if (oldattrs | flags) != oldattrs
        set_attributes(path, oldattrs | flags)
      end
    end

change_sid(old_sid, new_sid, info, path) click to toggle source

    # File lib/puppet/util/windows/security.rb, line 165
    def change_sid(old_sid, new_sid, info, path)
      if old_sid != new_sid
        mode = get_mode(path)

        string_to_sid_ptr(new_sid) do |psid|
          with_privilege(SE_RESTORE_NAME) do
            open_file(path, WRITE_OWNER) do |handle|
              set_security_info(handle, info, psid)
            end
          end
        end

        # rebuild dacl now that sid has changed
        set_mode(mode, path)
      end
    end

get_attributes(path) click to toggle source

    # File lib/puppet/util/windows/security.rb, line 190
    def get_attributes(path)
      attributes = GetFileAttributes(path)

      raise Puppet::Util::Windows::Error.new("Failed to get file attributes") if attributes == INVALID_FILE_ATTRIBUTES

      attributes
    end

get_dacl(handle) click to toggle source

    # File lib/puppet/util/windows/security.rb, line 436
    def get_dacl(handle)
      get_dacl_ptr(handle) do |dacl_ptr|
        # REMIND: need to handle NULL DACL
        raise Puppet::Util::Windows::Error.new("Invalid DACL") unless IsValidAcl(dacl_ptr)

        # ACL structure, size and count are the important parts. The
        # size includes both the ACL structure and all the ACEs.
        #
        # BYTE AclRevision
        # BYTE Padding1
        # WORD AclSize
        # WORD AceCount
        # WORD Padding2
        acl_buf = 0.chr * 8
        memcpy(acl_buf, dacl_ptr, acl_buf.size)
        ace_count = acl_buf.unpack('CCSSS')[3]

        dacl = []

        # deny all
        return dacl if ace_count == 0

        0.upto(ace_count - 1) do |i|
          ace_ptr = [0].pack('L')

          next unless GetAce(dacl_ptr, i, ace_ptr)

          # ACE structures vary depending on the type. All structures
          # begin with an ACE header, which specifies the type, flags
          # and size of what follows. We are only concerned with
          # ACCESS_ALLOWED_ACE and ACCESS_DENIED_ACEs, which have the
          # same structure:
          #
          # BYTE  C AceType
          # BYTE  C AceFlags
          # WORD  S AceSize
          # DWORD L ACCESS_MASK
          # DWORD L Sid
          # ..      ...
          # DWORD L Sid

          ace_buf = 0.chr * 8
          memcpy(ace_buf, ace_ptr.unpack('L')[0], ace_buf.size)

          ace_type, ace_flags, size, mask = ace_buf.unpack('CCSL')

          # skip aces that only serve to propagate inheritance
          next if (ace_flags & INHERIT_ONLY_ACE).nonzero?

          case ace_type
          when ACCESS_ALLOWED_ACE_TYPE
            sid_ptr = ace_ptr.unpack('L')[0] + 8 # address of ace_ptr->SidStart
            raise Puppet::Util::Windows::Error.new("Failed to read DACL, invalid SID") unless IsValidSid(sid_ptr)
            sid = sid_ptr_to_string(sid_ptr)
            dacl << {:sid => sid, :type => ace_type, :mask => mask}
          else
            Puppet.warning "Unsupported access control entry type: 0x#{ace_type.to_s(16)}"
          end
        end

        dacl
      end
    end

get_dacl_ptr(handle) { |unpack('L')[0]| ... } click to toggle source

    # File lib/puppet/util/windows/security.rb, line 500
    def get_dacl_ptr(handle)
      dacl = [0].pack('L')
      sd = [0].pack('L')

      rv = GetSecurityInfo(
           handle,
           SE_FILE_OBJECT,
           DACL_SECURITY_INFORMATION,
           nil,
           nil,
           dacl, #dacl
           nil, #sacl
           sd) #sec desc
      raise Puppet::Util::Windows::Error.new("Failed to get DACL") unless rv == ERROR_SUCCESS
      begin
        yield dacl.unpack('L')[0]
      ensure
        LocalFree(sd.unpack('L')[0])
      end
    end

get_group(path) click to toggle source

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.

    # File lib/puppet/util/windows/security.rb, line 146
    def get_group(path)
      return unless supports_acl?(path)

      get_sid(GROUP_SECURITY_INFORMATION, path)
    end

get_mode(path) click to toggle source

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.

    # File lib/puppet/util/windows/security.rb, line 230
    def get_mode(path)
      return unless supports_acl?(path)

      owner_sid = get_owner(path)
      group_sid = get_group(path)
      well_known_world_sid = Win32::Security::SID::Everyone
      well_known_nobody_sid = Win32::Security::SID::Nobody

      with_privilege(SE_BACKUP_NAME) do
        open_file(path, READ_CONTROL) do |handle|
          mode = 0

          get_dacl(handle).each do |ace|
            case ace[:sid]
            when owner_sid
              MASK_TO_MODE.each_pair do |k,v|
                if (ace[:mask] & k) == k
                  mode |= (v << 6)
                end
              end
            when group_sid
              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) and (ace[:mask] & (FILE_WRITE_DATA | FILE_EXECUTE | FILE_DELETE_CHILD)) == (FILE_WRITE_DATA | FILE_EXECUTE)
                mode |= S_ISVTX;
              end
            when well_known_nobody_sid
              if (ace[:mask] & FILE_APPEND_DATA).nonzero?
                mode |= S_ISVTX
              end
            else
              #puts "Warning, unable to map SID into POSIX mode: #{ace[:sid]}"
              mode |= S_IEXTRA
            end

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

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

get_owner(path) click to toggle source

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.

    # File lib/puppet/util/windows/security.rb, line 124
    def get_owner(path)
      return unless supports_acl?(path)

      get_sid(OWNER_SECURITY_INFORMATION, path)
    end

get_security_info(handle, info) click to toggle source

Get the SID string, e.g. “S-1-5-32-544”, for the specified handle and type of information (owner, group).

    # File lib/puppet/util/windows/security.rb, line 536
    def get_security_info(handle, info)
      sid = [0].pack('L')
      sd = [0].pack('L')

      rv = GetSecurityInfo(
           handle,
           SE_FILE_OBJECT,
           info, # security info
           info == OWNER_SECURITY_INFORMATION ? sid : nil,
           info == GROUP_SECURITY_INFORMATION ? sid : nil,
           nil, #dacl
           nil, #sacl
           sd) #sec desc
      raise Puppet::Util::Windows::Error.new("Failed to get security information") unless rv == ERROR_SUCCESS

      begin
        return sid_ptr_to_string(sid.unpack('L')[0])
      ensure
        LocalFree(sd.unpack('L')[0])
      end
    end

get_sid(info, path) click to toggle source

    # File lib/puppet/util/windows/security.rb, line 182
    def get_sid(info, path)
      with_privilege(SE_BACKUP_NAME) do
        open_file(path, READ_CONTROL) do |handle|
          get_security_info(handle, info)
        end
      end
    end

open_file(path, access) { |handle| ... } click to toggle source

Open an existing file with the specified access mode, and execute a block with the opened file HANDLE.

    # File lib/puppet/util/windows/security.rb, line 560
    def open_file(path, access)
      handle = CreateFile(
               path,
               access,
               FILE_SHARE_READ | FILE_SHARE_WRITE,
               0, # security_attributes
               OPEN_EXISTING,
               FILE_FLAG_BACKUP_SEMANTICS,
               0) # template
      raise Puppet::Util::Windows::Error.new("Failed to open '#{path}'") if handle == INVALID_HANDLE_VALUE
      begin
        yield handle
      ensure
        CloseHandle(handle)
      end
    end

remove_attributes(path, flags) click to toggle source

    # File lib/puppet/util/windows/security.rb, line 206
    def remove_attributes(path, flags)
      oldattrs = get_attributes(path)

      if (oldattrs & ~flags) != oldattrs
        set_attributes(path, oldattrs & ~flags)
      end
    end

set_acl(path, protected = true) { |acl| ... } click to toggle source

setting DACL requires both READ_CONTROL and WRITE_DACL access rights, and their respective privileges, SE_BACKUP_NAME and SE_RESTORE_NAME.

    # File lib/puppet/util/windows/security.rb, line 391
    def set_acl(path, protected = true)
      with_privilege(SE_BACKUP_NAME) do
        with_privilege(SE_RESTORE_NAME) do
          open_file(path, READ_CONTROL | WRITE_DAC) do |handle|
            acl = 0.chr * 1024 # This can be increased later as needed

            unless InitializeAcl(acl, acl.size, ACL_REVISION)
              raise Puppet::Util::Windows::Error.new("Failed to initialize ACL")
            end

            raise Puppet::Util::Windows::Error.new("Invalid DACL") unless IsValidAcl(acl)

            yield acl

            # protected means the object does not inherit aces from its parent
            info = DACL_SECURITY_INFORMATION
            info |= protected ? PROTECTED_DACL_SECURITY_INFORMATION : UNPROTECTED_DACL_SECURITY_INFORMATION

            # set the DACL
            set_security_info(handle, info, acl)
          end
        end
      end
    end

set_attributes(path, flags) click to toggle source

    # File lib/puppet/util/windows/security.rb, line 214
    def set_attributes(path, flags)
      raise Puppet::Util::Windows::Error.new("Failed to set file attributes") unless SetFileAttributes(path, flags)
    end

set_group(group_sid, path) click to toggle source

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).

    # File lib/puppet/util/windows/security.rb, line 135
    def set_group(group_sid, path)
      old_sid = get_group(path)

      change_sid(old_sid, group_sid, GROUP_SECURITY_INFORMATION, path)
    end

set_mode(mode, path, protected = true) click to toggle source

Set the mode of the object referenced by `path` to the specified `mode`. The mode should be specified as POSIX-stye 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.

    # File lib/puppet/util/windows/security.rb, line 306
    def set_mode(mode, path, protected = true)
      owner_sid = get_owner(path)
      group_sid = get_group(path)
      well_known_world_sid = Win32::Security::SID::Everyone
      well_known_nobody_sid = Win32::Security::SID::Nobody

      owner_allow = STANDARD_RIGHTS_ALL  | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES
      group_allow = STANDARD_RIGHTS_READ | FILE_READ_ATTRIBUTES | SYNCHRONIZE
      other_allow = STANDARD_RIGHTS_READ | FILE_READ_ATTRIBUTES | SYNCHRONIZE
      nobody_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

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

      isdir = File.directory?(path)

      if isdir
        if (mode & (S_IWUSR | S_IXUSR)) == (S_IWUSR | S_IXUSR)
          owner_allow |= FILE_DELETE_CHILD
        end
        if (mode & (S_IWGRP | S_IXGRP)) == (S_IWGRP | S_IXGRP) and (mode & S_ISVTX) == 0
          group_allow |= FILE_DELETE_CHILD
        end
        if (mode & (S_IWOTH | S_IXOTH)) == (S_IWOTH | S_IXOTH) and (mode & S_ISVTX) == 0
          other_allow |= FILE_DELETE_CHILD
        end
      end

      # if owner and group the same, then map group permissions to the one owner ACE
      isownergroup = owner_sid == group_sid
      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_WRITE_DATA) == FILE_WRITE_DATA
        remove_attributes(path, FILE_ATTRIBUTE_READONLY)
      end

      set_acl(path, protected) do |acl|
        #puts "ace: owner #{owner_sid}, mask 0x#{owner_allow.to_s(16)}"
        add_access_allowed_ace(acl, owner_allow, owner_sid)

        unless isownergroup
          #puts "ace: group #{group_sid}, mask 0x#{group_allow.to_s(16)}"
          add_access_allowed_ace(acl, group_allow, group_sid)
        end

        #puts "ace: other #{well_known_world_sid}, mask 0x#{other_allow.to_s(16)}"
        add_access_allowed_ace(acl, other_allow, well_known_world_sid)

        #puts "ace: nobody #{well_known_nobody_sid}, mask 0x#{nobody_allow.to_s(16)}"
        add_access_allowed_ace(acl, nobody_allow, well_known_nobody_sid)

        # add inherit-only aces for child dirs and files that are created within the dir
        if isdir
          inherit = INHERIT_ONLY_ACE | CONTAINER_INHERIT_ACE
          add_access_allowed_ace(acl, owner_allow, Win32::Security::SID::CreatorOwner, inherit)
          add_access_allowed_ace(acl, group_allow, Win32::Security::SID::CreatorGroup, inherit)

          inherit = INHERIT_ONLY_ACE |  OBJECT_INHERIT_ACE
          add_access_allowed_ace(acl, owner_allow & ~FILE_EXECUTE, Win32::Security::SID::CreatorOwner, inherit)
          add_access_allowed_ace(acl, group_allow & ~FILE_EXECUTE, Win32::Security::SID::CreatorGroup, inherit)
        end
      end

      nil
    end

set_owner(owner_sid, path) click to toggle source

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.

    # File lib/puppet/util/windows/security.rb, line 113
    def set_owner(owner_sid, path)
      old_sid = get_owner(path)

      change_sid(old_sid, owner_sid, OWNER_SECURITY_INFORMATION, path)
    end

set_privilege(privilege, enable) click to toggle source

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.

    # File lib/puppet/util/windows/security.rb, line 587
    def set_privilege(privilege, enable)
      return unless Puppet.features.root?

      with_process_token(TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY) do |token|
        tmpLuid = 0.chr * 8

        # Get the LUID for specified privilege.
        unless LookupPrivilegeValue("", privilege, tmpLuid)
          raise Puppet::Util::Windows::Error.new("Failed to lookup privilege")
        end

        # DWORD + [LUID + DWORD]
        tkp = [1].pack('L') + tmpLuid + [enable ? SE_PRIVILEGE_ENABLED : 0].pack('L')

        unless AdjustTokenPrivileges(token, 0, tkp, tkp.length , nil, nil)
          raise Puppet::Util::Windows::Error.new("Failed to adjust process privileges")
        end
      end
    end

set_security_info(handle, info, ptr) click to toggle source

Set the security info on the specified handle.

    # File lib/puppet/util/windows/security.rb, line 522
    def set_security_info(handle, info, ptr)
      rv = SetSecurityInfo(
           handle,
           SE_FILE_OBJECT,
           info,
           (info & OWNER_SECURITY_INFORMATION) == OWNER_SECURITY_INFORMATION ? ptr : nil,
           (info & GROUP_SECURITY_INFORMATION) == GROUP_SECURITY_INFORMATION ? ptr : nil,
           (info & DACL_SECURITY_INFORMATION) == DACL_SECURITY_INFORMATION ? ptr : nil,
           nil)
      raise Puppet::Util::Windows::Error.new("Failed to set security information") unless rv == ERROR_SUCCESS
    end

supports_acl?(path) click to toggle source

    # File lib/puppet/util/windows/security.rb, line 152
    def supports_acl?(path)
      flags = 0.chr * 4

      root = Pathname.new(path).enum_for(:ascend).to_a.last.to_s
      # 'A trailing backslash is required'
      root = "#{root}\\" unless root =~ /[\/\]$/
      unless GetVolumeInformation(root, nil, 0, nil, nil, flags, nil, 0)
        raise Puppet::Util::Windows::Error.new("Failed to get volume information")
      end

      (flags.unpack('L')[0] & Windows::File::FILE_PERSISTENT_ACLS) != 0
    end

with_privilege(privilege) { || ... } click to toggle source

Execute a block with the specified privilege enabled

    # File lib/puppet/util/windows/security.rb, line 578
    def with_privilege(privilege)
      set_privilege(privilege, true)
      yield
    ensure
      set_privilege(privilege, false)
    end

with_process_token(access) { |token| ... } click to toggle source

Execute a block with the current process token

    # File lib/puppet/util/windows/security.rb, line 608
    def with_process_token(access)
      token = 0.chr * 4

      unless OpenProcessToken(GetCurrentProcess(), access, token)
        raise Puppet::Util::Windows::Error.new("Failed to open process token")
      end
      begin
        token = token.unpack('L')[0]

        yield token
      ensure
        CloseHandle(token)
      end
    end