diff --git a/lib/puppet/util/windows/security.rb b/lib/puppet/util/windows/security.rb index cedb26a28..cc4fe74bd 100644 --- a/lib/puppet/util/windows/security.rb +++ b/lib/puppet/util/windows/security.rb @@ -1,626 +1,635 @@ # 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 extend Puppet::Util::Windows::Security # 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_ISVTX = 0001000 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) + oldattrs = get_attributes(path) + + if (oldattrs | flags) != oldattrs + set_attributes(path, oldattrs | flags) + end end def remove_attributes(path, flags) - set_attributes(path, get_attributes(path) & ~flags) + oldattrs = get_attributes(path) + + if (oldattrs & ~flags) != oldattrs + set_attributes(path, oldattrs & ~flags) + end end def set_attributes(path, flags) - raise Puppet::Util::Windows::Error.new("Failed to set file attributes") if SetFileAttributes(path, flags) == 0 + raise Puppet::Util::Windows::Error.new("Failed to set file attributes") unless SetFileAttributes(path, flags) 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. 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 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 MODE_TO_MASK = { S_IROTH => FILE_GENERIC_READ, S_IWOTH => FILE_GENERIC_WRITE, S_IXOTH => (FILE_GENERIC_EXECUTE & ~FILE_READ_ATTRIBUTES), } # 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. 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 inheritable aces for child dirs and files that are created within the dir if isdir 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") 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 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 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 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 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") unless IsValidSid(psid) raise Puppet::Util::Windows::Error.new("Failed to convert binary SID") unless ConvertSidToStringSid(psid, str_ptr) 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 if block_given? yield sid_ptr else true end 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. 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 # Execute a block with the current process token 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 end diff --git a/spec/integration/util/windows/security_spec.rb b/spec/integration/util/windows/security_spec.rb index 0eb35fad5..3ce58586d 100755 --- a/spec/integration/util/windows/security_spec.rb +++ b/spec/integration/util/windows/security_spec.rb @@ -1,605 +1,605 @@ #!/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 @sids = { :current_user => Puppet::Util::ADSI.sid_for_account(Sys::Admin.get_login), :admin => Puppet::Util::ADSI.sid_for_account("Administrator"), :guest => Puppet::Util::ADSI.sid_for_account("Guest"), :users => Win32::Security::SID::BuiltinUsers, :power_users => Win32::Security::SID::PowerUsers, } end let (:sids) { @sids } let (:winsec) { WindowsSecurityTester.new } shared_examples_for "only child owner" do it "should allow child owner" do check_child_owner end it "should deny parent owner" do lambda { check_parent_owner }.should raise_error(Errno::EACCES) end it "should deny group" do lambda { check_group }.should raise_error(Errno::EACCES) end it "should deny other" do lambda { check_other }.should raise_error(Errno::EACCES) end end shared_examples_for "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, parent) winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) if File.exists?(path) end describe "#owner=" 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 "#owner" 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 "#group=" 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 "#group" 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 "#mode=" do (0000..0700).step(0100).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 128 modes that do not require deny ACEs" do 0.upto(1).each do |s| 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 = (s << 9 | 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 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 "#mode" do it "should report when extra aces are encounted" do winsec.set_acl(path, true) do |acl| (544..547).each do |rid| winsec.add_access_allowed_ace(acl, WindowsSecurityTester::STANDARD_RIGHTS_ALL, "S-1-5-32-#{rid}") end end 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 # add a bunch of aces to the parent with permission to add children allow = WindowsSecurityTester::STANDARD_RIGHTS_ALL | WindowsSecurityTester::SPECIFIC_RIGHTS_ALL inherit = WindowsSecurityTester::OBJECT_INHERIT_ACE | WindowsSecurityTester::CONTAINER_INHERIT_ACE winsec.set_acl(parent, true) do |acl| winsec.add_access_allowed_ace(acl, allow, "S-1-1-0", inherit) # everyone (544..547).each do |rid| winsec.add_access_allowed_ace(acl, WindowsSecurityTester::STANDARD_RIGHTS_ALL, "S-1-5-32-#{rid}", inherit) end end # unprotect child, it should inherit from parent 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) + winsec.set_group(sids[:guest], path) + winsec.set_owner(sids[:guest], path) lambda { File.open(path, 'r') }.should raise_error(Errno::EACCES) end after :each do if File.exists?(path) winsec.set_owner(sids[:current_user], path) winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) end end describe "#owner=" 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 "#group=" 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_mode(0610, path) 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 "#mode" 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 describe "#string_to_sid_ptr" do it "should raise an error if an invalid SID is specified" do expect do winsec.string_to_sid_ptr('foobar') end.to raise_error(Puppet::Util::Windows::Error) { |error| error.code.should == 1337 } end it "should yield if a block is given" do yielded = nil winsec.string_to_sid_ptr('S-1-1-0') do |sid| yielded = sid end yielded.should_not be_nil end it "should allow no block to be specified" do winsec.string_to_sid_ptr('S-1-1-0').should be_true end end describe "when the parent directory" do before :each do winsec.set_owner(sids[:current_user], parent) winsec.set_owner(sids[:current_user], path) winsec.set_mode(0777, path, false) end def check_child_owner winsec.set_group(sids[:guest], parent) winsec.set_owner(sids[:guest], parent) check_delete(path) end def check_parent_owner winsec.set_group(sids[:guest], path) winsec.set_owner(sids[:guest], path) check_delete(path) end def check_group winsec.set_group(sids[:current_user], path) winsec.set_owner(sids[:guest], path) winsec.set_owner(sids[:guest], parent) check_delete(path) end def check_other winsec.set_group(sids[:guest], path) winsec.set_owner(sids[:guest], path) winsec.set_owner(sids[:guest], parent) check_delete(path) end describe "is writable and executable" do describe "and sticky bit is set" do before :each do winsec.set_mode(01777, parent) end it "should allow child owner" do check_child_owner end it "should allow parent owner" do check_parent_owner end it "should deny group" do lambda { check_group }.should raise_error(Errno::EACCES) end it "should deny other" do lambda { check_other }.should raise_error(Errno::EACCES) end end describe "and sticky bit is not set" do before :each do winsec.set_mode(0777, parent) end it "should allow child owner" do check_child_owner end it "should allow parent owner" do check_parent_owner end it "should allow group" do check_group end it "should allow other" do check_other end end end describe "is not writable" do before :each do winsec.set_mode(0555, parent) end it_behaves_like "only child owner" end describe "is not executable" do before :each do winsec.set_mode(0666, parent) end it_behaves_like "only child owner" end end end end describe "file" do let (:parent) do tmpdir('win_sec_test_file') end let (:path) do path = File.join(parent, 'childfile') 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 def check_delete(path) File.delete(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 (:parent) do tmpdir('win_sec_test_dir') end let (:path) do path = File.join(parent, 'childdir') Dir.mkdir(path) 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? 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) {} end def check_delete(path) Dir.rmdir(path) 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