diff --git a/lib/puppet/util/windows.rb b/lib/puppet/util/windows.rb new file mode 100644 index 000000000..53b2380aa --- /dev/null +++ b/lib/puppet/util/windows.rb @@ -0,0 +1,4 @@ +module Puppet::Util::Windows + require 'puppet/util/windows/error' + require 'puppet/util/windows/security' +end diff --git a/lib/puppet/util/windows/error.rb b/lib/puppet/util/windows/error.rb new file mode 100644 index 000000000..749a64160 --- /dev/null +++ b/lib/puppet/util/windows/error.rb @@ -0,0 +1,16 @@ +require 'puppet/util/windows' + +# represents an error resulting from a Win32 error code +class Puppet::Util::Windows::Error < Puppet::Error + require 'windows/error' + include Windows::Error + + attr_reader :code + + def initialize(message, code = GetLastError.call) + super(message + ": #{get_last_error(code)}") + + @code = code + end +end + diff --git a/lib/puppet/util/windows/security.rb b/lib/puppet/util/windows/security.rb new file mode 100644 index 000000000..e136f645c --- /dev/null +++ b/lib/puppet/util/windows/security.rb @@ -0,0 +1,587 @@ +# 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 http://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. +# * 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 'puppet/util/windows' + +require 'win32/security' + +require 'windows/file' +require 'windows/handle' +require 'windows/security' +require 'windows/process' +require 'windows/memory' + +module Puppet::Util::Windows::Security + include Windows::File + include Windows::Handle + include Windows::Security + include Windows::Process + include Windows::Memory + include Windows::MSVCRT::Buffer + + # file modes + S_IRUSR = 0000400 + S_IRGRP = 0000040 + S_IROTH = 0000004 + S_IWUSR = 0000200 + S_IWGRP = 0000020 + S_IWOTH = 0000002 + S_IXUSR = 0000100 + S_IXGRP = 0000010 + S_IXOTH = 0000001 + S_IRWXU = 0000700 + S_IRWXG = 0000070 + S_IRWXO = 0000007 + S_IEXTRA = 02000000 # represents an extra ace + + # constants that are missing from Windows::Security + PROTECTED_DACL_SECURITY_INFORMATION = 0x80000000 + UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000 + NO_INHERITANCE = 0x0 + + # 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) + old_sid = get_owner(path) + + change_sid(old_sid, owner_sid, OWNER_SECURITY_INFORMATION, path) + 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) + get_sid(OWNER_SECURITY_INFORMATION, path) + 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) + old_sid = get_group(path) + + change_sid(old_sid, group_sid, GROUP_SECURITY_INFORMATION, path) + 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) + get_sid(GROUP_SECURITY_INFORMATION, path) + end + + 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 + + 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 + + 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 + + def add_attributes(path, flags) + set_attributes(path, get_attributes(path) | flags) + end + + def remove_attributes(path, flags) + set_attributes(path, get_attributes(path) & ~flags) + end + + def set_attributes(path, flags) + raise Puppet::Util::Windows::Error.new("Failed to set file attributes") if SetFileAttributes(path, flags) == 0 + end + + MASK_TO_MODE = { + FILE_GENERIC_READ => S_IROTH, + FILE_GENERIC_WRITE => S_IWOTH, + (FILE_GENERIC_EXECUTE & ~FILE_READ_ATTRIBUTES) => S_IXOTH + } + + # 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. Other + # modes, e.g. S_ISVTX, are not supported. 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) + owner_sid = get_owner(path) + group_sid = get_group(path) + well_known_world_sid = Win32::Security::SID::Everyone + + 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 + 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 + + MODE_TO_MASK = { + S_IROTH => FILE_GENERIC_READ, + S_IWOTH => FILE_GENERIC_WRITE, + S_IXOTH => (FILE_GENERIC_EXECUTE & ~FILE_READ_ATTRIBUTES), + (S_IWOTH | S_IXUSR) => FILE_DELETE_CHILD, + } + + # 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. Other modes, e.g. S_ISVTX, are not supported. 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) + owner_sid = get_owner(path) + group_sid = get_group(path) + well_known_world_sid = Win32::Security::SID::Everyone + + 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 + + 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 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 + + 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) + + # add inheritable aces for child dirs and files that are created within the dir + if File.directory?(path) + inherit = INHERIT_ONLY_ACE | OBJECT_INHERIT_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) + add_access_allowed_ace(acl, other_allow, well_known_world_sid, inherit) + end + end + + # if any ACE allows write, then clear readonly bit + if ((owner_allow | group_allow | other_allow ) & FILE_WRITE_DATA) == FILE_WRITE_DATA + remove_attributes(path, FILE_ATTRIBUTE_READONLY) + end + + nil + 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_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") if IsValidAcl(acl) == 0 + + 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 + + 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") if IsValidSid(sid_ptr) == 0 + + if AddAccessAllowedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr) == 0 + raise Puppet::Util::Windows::Error.new("Failed to add access control entry") + end + end + end + + def add_access_denied_ace(acl, mask, sid) + string_to_sid_ptr(sid) do |sid_ptr| + raise Puppet::Util::Windows::Error.new("Invalid SID") if IsValidSid(sid_ptr) == 0 + + if AddAccessDeniedAce(acl, ACL_REVISION, mask, sid_ptr) == 0 + raise Puppet::Util::Windows::Error.new("Failed to add access control entry") + end + end + end + + 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") if IsValidAcl(dacl_ptr) == 0 + + # 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 if GetAce(dacl_ptr, i, ace_ptr) == 0 + + # 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 + + 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 + + # Set the security info on the specified handle. + 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 + + # Get the SID string, e.g. "S-1-5-32-544", for the specified handle + # and type of information (owner, group). + 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 + + # Convert a SID pointer to a string, e.g. "S-1-5-32-544". + def sid_ptr_to_string(psid) + sid_buf = 0.chr * 256 + str_ptr = 0.chr * 4 + + raise Puppet::Util::Windows::Error.new("Invalid SID") if IsValidSid(psid) == 0 + + raise Puppet::Util::Windows::Error.new("Failed to convert binary SID") if ConvertSidToStringSid(psid, str_ptr) == 0 + + begin + strncpy(sid_buf, str_ptr.unpack('L')[0], sid_buf.size - 1) + sid_buf[sid_buf.size - 1] = 0.chr + return sid_buf.strip + ensure + LocalFree(str_ptr.unpack('L')[0]) + end + end + + # Convert a SID string, e.g. "S-1-5-32-544" to a pointer (containing the + # address of the binary SID structure). The returned value can be used in + # Win32 APIs that expect a PSID, e.g. IsValidSid. + def string_to_sid_ptr(string) + sid_buf = 0.chr * 80 + string_addr = [string].pack('p*').unpack('L')[0] + + raise Puppet::Util::Windows::Error.new("Failed to convert string SID: #{string}") unless ConvertStringSidToSid(string_addr, sid_buf) + + sid_ptr = sid_buf.unpack('L')[0] + begin + yield sid_ptr + ensure + LocalFree(sid_ptr) + end + end + + # Open an existing file with the specified access mode, and execute a + # block with the opened file HANDLE. + 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 + + # Execute a block with the specified privilege enabled + def with_privilege(privilege) + set_privilege(privilege, true) + yield + ensure + set_privilege(privilege, false) + end + + # 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? + + with_process_token(TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY) do |token| + tmpLuid = 0.chr * 8 + + # Get the LUID for specified privilege. + if LookupPrivilegeValue("", privilege, tmpLuid) == 0 + 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') + + if AdjustTokenPrivileges(token, 0, tkp, tkp.length , nil, nil) == 0 + raise Puppet::Util::Windows::Error.new("Failed to adjust process privileges") + end + end + end + + # Execute a block with the current process token + def with_process_token(access) + token = 0.chr * 4 + + if OpenProcessToken(GetCurrentProcess(), access, token) == 0 + 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 +end diff --git a/spec/integration/util/windows/security_spec.rb b/spec/integration/util/windows/security_spec.rb new file mode 100755 index 000000000..27a92a667 --- /dev/null +++ b/spec/integration/util/windows/security_spec.rb @@ -0,0 +1,427 @@ +#!/usr/bin/env ruby +require 'spec_helper' + +require 'puppet/util/adsi' + +if Puppet.features.microsoft_windows? + class WindowsSecurityTester + require 'puppet/util/windows/security' + include Puppet::Util::Windows::Security + end +end + +describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_windows? do + include PuppetSpec::Files + + before :all do + current_user = Sys::Admin.get_user(Sys::Admin.get_login) + + @sids = { + :current_user => current_user.sid, + :admin => Sys::Admin.get_user("Administrator").sid, + :guest => Sys::Admin.get_user("Guest").sid, + + :users => Win32::Security::SID::BuiltinUsers, + :power_users => Win32::Security::SID::PowerUsers, + } + end + + let (:sids) { @sids } + let (:winsec) { WindowsSecurityTester.new } + + shared_examples "a securable object" do + describe "for a normal user" do + before :each do + Puppet.features.stubs(:root?).returns(false) + end + + after :each do + winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) + end + + describe "when setting the owner sid" do + it "should allow setting to the current user" do + winsec.set_owner(sids[:current_user], path) + end + + it "should raise an exception when setting to a different user" do + lambda { winsec.set_owner(sids[:guest], path) }.should raise_error(Puppet::Error, /This security ID may not be assigned as the owner of this object./) + end + end + + describe "when getting the owner sid" do + it "it should not be empty" do + winsec.get_owner(path).should_not be_empty + end + + it "should raise an exception if an invalid path is provided" do + lambda { winsec.get_owner("c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) + end + end + + describe "when setting the group sid" do + it "should allow setting to a group the current owner is a member of" do + winsec.set_group(sids[:users], path) + end + + # Unlike unix, if the user has permission to WRITE_OWNER, which the file owner has by default, + # then they can set the primary group to a group that the user does not belong to. + it "should allow setting to a group the current owner is not a member of" do + winsec.set_group(sids[:power_users], path) + end + end + + describe "when getting the group sid" do + it "should not be empty" do + winsec.get_group(path).should_not be_empty + end + + it "should raise an exception if an invalid path is provided" do + lambda { winsec.get_group("c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) + end + end + + describe "setting the mode" do + [0000, 0100, 0200, 0300, 0400, 0500, 0600, 0700].each do |mode| + it "should enforce mode #{mode.to_s(8)}" do + winsec.set_mode(mode, path) + + check_access(mode, path) + end + end + + it "should round-trip all 64 modes that do not require deny ACEs" do + 0.upto(7).each do |u| + 0.upto(u).each do |g| + 0.upto(g).each do |o| + # if user is superset of group, and group superset of other, then + # no deny ace is required, and mode can be converted to win32 + # access mask, and back to mode without loss of information + # (provided the owner and group are not the same) + next if ((u & g) != g) or ((g & o) != o) + + mode = (u << 6 | g << 3 | o << 0) + winsec.set_mode(mode, path) + winsec.get_mode(path).to_s(8).should == mode.to_s(8) + end + end + end + end + + describe "for modes that require deny aces" do + it "should map everyone to group and owner" do + winsec.set_mode(0426, path) + winsec.get_mode(path).to_s(8).should == "666" + end + + it "should combine user and group modes when owner and group sids are equal" do + winsec.set_group(winsec.get_owner(path), path) + + winsec.set_mode(0410, path) + winsec.get_mode(path).to_s(8).should == "550" + end + end + + describe "for read-only objects" do + before :each do + winsec.add_attributes(path, WindowsSecurityTester::FILE_ATTRIBUTE_READONLY) + (winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should be_nonzero + end + + it "should make them writable if any sid has write permission" do + winsec.set_mode(WindowsSecurityTester::S_IWUSR, path) + (winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should == 0 + end + + it "should leave them read-only if no sid has write permission" do + winsec.set_mode(WindowsSecurityTester::S_IRUSR | WindowsSecurityTester::S_IXGRP, path) + (winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should be_nonzero + end + end + + it "should raise an exception if an invalid path is provided" do + lambda { winsec.set_mode(sids[:guest], "c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) + end + end + + describe "getting the mode" do + it "should report when extra aces are encounted" do + mode = winsec.get_mode(path) + (mode & WindowsSecurityTester::S_IEXTRA).should_not == 0 + end + + it "should warn if a deny ace is encountered" do + winsec.set_acl(path) do |acl| + winsec.add_access_denied_ace(acl, WindowsSecurityTester::FILE_GENERIC_WRITE, sids[:guest]) + winsec.add_access_allowed_ace(acl, WindowsSecurityTester::STANDARD_RIGHTS_ALL | WindowsSecurityTester::SPECIFIC_RIGHTS_ALL, sids[:current_user]) + end + + Puppet.expects(:warning).with("Unsupported access control entry type: 0x1") + + winsec.get_mode(path) + end + + it "should skip inherit-only ace" do + winsec.set_acl(path) do |acl| + winsec.add_access_allowed_ace(acl, WindowsSecurityTester::STANDARD_RIGHTS_ALL | WindowsSecurityTester::SPECIFIC_RIGHTS_ALL, sids[:current_user]) + winsec.add_access_allowed_ace(acl, WindowsSecurityTester::FILE_GENERIC_READ, Win32::Security::SID::Everyone, WindowsSecurityTester::INHERIT_ONLY_ACE | WindowsSecurityTester::OBJECT_INHERIT_ACE) + end + + (winsec.get_mode(path) & WindowsSecurityTester::S_IRWXO).should == 0 + end + + it "should raise an exception if an invalid path is provided" do + lambda { winsec.get_mode("c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) + end + end + + describe "inherited access control entries" do + it "should be absent when the access control list is protected" do + winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) + (winsec.get_mode(path) & WindowsSecurityTester::S_IEXTRA).should == 0 + end + + it "should be present when the access control list is unprotected" do + winsec.set_mode(WindowsSecurityTester::S_IRWXU, path, false) + (winsec.get_mode(path) & WindowsSecurityTester::S_IEXTRA).should == WindowsSecurityTester::S_IEXTRA + end + end + end + + describe "for an administrator", :if => Puppet.features.root? do + before :each do + winsec.set_owner(sids[:guest], path) + winsec.set_group(sids[:guest], path) + winsec.set_mode(WindowsSecurityTester::S_IRWXU | WindowsSecurityTester::S_IRWXG, path) + lambda { File.open(path, 'r') }.should raise_error(Errno::EACCES) + end + + after :each do + winsec.set_owner(sids[:current_user], path) + winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) + end + + describe "when setting the owner sid" do + it "should accept a user sid" do + winsec.set_owner(sids[:admin], path) + winsec.get_owner(path).should == sids[:admin] + end + + it "should accept a group sid" do + winsec.set_owner(sids[:power_users], path) + winsec.get_owner(path).should == sids[:power_users] + end + + it "should raise an exception if an invalid sid is provided" do + lambda { winsec.set_owner("foobar", path) }.should raise_error(Puppet::Error, /Failed to convert string SID/) + end + + it "should raise an exception if an invalid path is provided" do + lambda { winsec.set_owner(sids[:guest], "c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) + end + end + + describe "when setting the group sid" do + it "should accept a group sid" do + winsec.set_group(sids[:power_users], path) + winsec.get_group(path).should == sids[:power_users] + end + + it "should accept a user sid" do + winsec.set_group(sids[:admin], path) + winsec.get_group(path).should == sids[:admin] + end + + it "should allow owner and group to be the same sid" do + winsec.set_owner(sids[:power_users], path) + winsec.set_group(sids[:power_users], path) + winsec.set_mode(0610, path) + + winsec.get_owner(path).should == sids[:power_users] + winsec.get_group(path).should == sids[:power_users] + # note group execute permission added to user ace, and then group rwx value + # reflected to match + winsec.get_mode(path).to_s(8).should == "770" + end + + it "should raise an exception if an invalid sid is provided" do + lambda { winsec.set_group("foobar", path) }.should raise_error(Puppet::Error, /Failed to convert string SID/) + end + + it "should raise an exception if an invalid path is provided" do + lambda { winsec.set_group(sids[:guest], "c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) + end + end + + describe "when the sid is NULL" do + it "should retrieve an empty owner sid" + it "should retrieve an empty group sid" + end + + describe "when the sid refers to a deleted trustee" do + it "should retrieve the user sid" do + sid = nil + user = Puppet::Util::ADSI::User.create("delete_me_user") + user.commit + begin + sid = Sys::Admin::get_user(user.name).sid + winsec.set_owner(sid, path) + winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) + ensure + Puppet::Util::ADSI::User.delete(user.name) + end + + winsec.get_owner(path).should == sid + winsec.get_mode(path).should == WindowsSecurityTester::S_IRWXU + end + + it "should retrieve the group sid" do + sid = nil + group = Puppet::Util::ADSI::Group.create("delete_me_group") + group.commit + begin + sid = Sys::Admin::get_group(group.name).sid + winsec.set_group(sid, path) + winsec.set_mode(WindowsSecurityTester::S_IRWXG, path) + ensure + Puppet::Util::ADSI::Group.delete(group.name) + end + winsec.get_group(path).should == sid + winsec.get_mode(path).should == WindowsSecurityTester::S_IRWXG + end + end + + describe "when getting the dacl" do + it "should deny all access when the DACL is empty" do + winsec.set_acl(path, true) { |acl| } + + winsec.get_mode(path).should == 0 + end + + # REMIND: ruby crashes when trying to set a NULL DACL + # it "should allow all when it is nil" do + # winsec.set_owner(sids[:current_user], path) + # winsec.open_file(path, WindowsSecurityTester::READ_CONTROL | WindowsSecurityTester::WRITE_DAC) do |handle| + # winsec.set_security_info(handle, WindowsSecurityTester::DACL_SECURITY_INFORMATION | WindowsSecurityTester::PROTECTED_DACL_SECURITY_INFORMATION, nil) + # end + # winsec.get_mode(path).to_s(8).should == "777" + # end + end + end + end + + describe "file" do + let :path do + path = tmpfile('win_sec_test_file') + File.new(path, "w").close + path + end + + it_behaves_like "a securable object" do + def check_access(mode, path) + if (mode & WindowsSecurityTester::S_IRUSR).nonzero? + check_read(path) + else + lambda { check_read(path) }.should raise_error(Errno::EACCES) + end + + if (mode & WindowsSecurityTester::S_IWUSR).nonzero? + check_write(path) + else + lambda { check_write(path) }.should raise_error(Errno::EACCES) + end + + if (mode & WindowsSecurityTester::S_IXUSR).nonzero? + lambda { check_execute(path) }.should raise_error(Errno::ENOEXEC) + else + lambda { check_execute(path) }.should raise_error(Errno::EACCES) + end + end + + def check_read(path) + File.open(path, 'r').close + end + + def check_write(path) + File.open(path, 'w').close + end + + def check_execute(path) + Kernel.exec(path) + end + end + + describe "locked files" do + let (:explorer) { File.join(Dir::WINDOWS, "explorer.exe") } + + it "should get the owner" do + winsec.get_owner(explorer).should match /^S-1-5-/ + end + + it "should get the group" do + winsec.get_group(explorer).should match /^S-1-5-/ + end + + it "should get the mode" do + winsec.get_mode(explorer).should == (WindowsSecurityTester::S_IRWXU | WindowsSecurityTester::S_IRWXG | WindowsSecurityTester::S_IEXTRA) + end + end + end + + describe "directory" do + let :path do + tmpdir('win_sec_test_dir') + end + + it_behaves_like "a securable object" do + def check_access(mode, path) + if (mode & WindowsSecurityTester::S_IRUSR).nonzero? + check_read(path) + else + lambda { check_read(path) }.should raise_error(Errno::EACCES) + end + + if (mode & WindowsSecurityTester::S_IWUSR).nonzero? + check_write(path) + else + lambda { check_write(path) }.should raise_error(Errno::EACCES) + end + + if (mode & WindowsSecurityTester::S_IXUSR).nonzero? + check_execute(path) + else + lambda { check_execute(path) }.should raise_error(Errno::EACCES) + end + end + + def check_read(path) + Dir.entries(path) + end + + def check_write(path) + Dir.mkdir(File.join(path, "subdir")) + end + + def check_execute(path) + Dir.chdir(path) {|dir| } + end + end + + describe "inheritable aces" do + it "should be applied to child objects" do + mode640 = WindowsSecurityTester::S_IRUSR | WindowsSecurityTester::S_IWUSR | WindowsSecurityTester::S_IRGRP + winsec.set_mode(mode640, path) + + newfile = File.join(path, "newfile.txt") + File.new(newfile, "w").close + + newdir = File.join(path, "newdir") + Dir.mkdir(newdir) + + [newfile, newdir].each do |p| + winsec.get_mode(p).to_s(8).should == mode640.to_s(8) + end + end + end + end +end