diff --git a/lib/puppet/provider/file/windows.rb b/lib/puppet/provider/file/windows.rb index 833409f7d..c0ef68cd9 100644 --- a/lib/puppet/provider/file/windows.rb +++ b/lib/puppet/provider/file/windows.rb @@ -1,104 +1,104 @@ Puppet::Type.type(:file).provide :windows do desc "Uses Microsoft Windows functionality to manage file ownership and permissions." confine :operatingsystem => :windows has_feature :manages_symlinks if Puppet.features.manages_symlinks? include Puppet::Util::Warnings if Puppet.features.microsoft_windows? require 'puppet/util/windows' include Puppet::Util::Windows::Security end # Determine if the account is valid, and if so, return the UID def name2id(value) - Puppet::Util::Windows::Security.name_to_sid(value) + Puppet::Util::Windows::SID.name_to_sid(value) end # If it's a valid SID, get the name. Otherwise, it's already a name, # so just return it. def id2name(id) - if Puppet::Util::Windows::Security.valid_sid?(id) - Puppet::Util::Windows::Security.sid_to_name(id) + if Puppet::Util::Windows::SID.valid_sid?(id) + Puppet::Util::Windows::SID.sid_to_name(id) else id end end # We use users and groups interchangeably, so use the same methods for both # (the type expects different methods, so we have to oblige). alias :uid2name :id2name alias :gid2name :id2name alias :name2gid :name2id alias :name2uid :name2id def owner return :absent unless resource.stat get_owner(resource[:path]) end def owner=(should) begin set_owner(should, resolved_path) rescue => detail raise Puppet::Error, "Failed to set owner to '#{should}': #{detail}", detail.backtrace end end def group return :absent unless resource.stat get_group(resource[:path]) end def group=(should) begin set_group(should, resolved_path) rescue => detail raise Puppet::Error, "Failed to set group to '#{should}': #{detail}", detail.backtrace end end def mode if resource.stat mode = get_mode(resource[:path]) mode ? mode.to_s(8) : :absent else :absent end end def mode=(value) begin set_mode(value.to_i(8), resource[:path]) rescue => detail error = Puppet::Error.new("failed to set mode #{mode} on #{resource[:path]}: #{detail.message}") error.set_backtrace detail.backtrace raise error end :file_changed end def validate if [:owner, :group, :mode].any?{|p| resource[p]} and !supports_acl?(resource[:path]) resource.fail("Can only manage owner, group, and mode on filesystems that support Windows ACLs, such as NTFS") end end attr_reader :file private def file @file ||= Puppet::FileSystem.pathname(resource[:path]) end def resolved_path path = file() # under POSIX, :manage means use lchown - i.e. operate on the link return path.to_s if resource[:links] == :manage # otherwise, use chown -- that will resolve the link IFF it is a link # otherwise it will operate on the path Puppet::FileSystem.symlink?(path) ? Puppet::FileSystem.readlink(path) : path.to_s end end diff --git a/lib/puppet/provider/group/windows_adsi.rb b/lib/puppet/provider/group/windows_adsi.rb index 8ed2443f0..c6db2af92 100644 --- a/lib/puppet/provider/group/windows_adsi.rb +++ b/lib/puppet/provider/group/windows_adsi.rb @@ -1,86 +1,86 @@ require 'puppet/util/windows' Puppet::Type.type(:group).provide :windows_adsi do desc "Local group management for Windows. Group members can be both users and groups. Additionally, local groups can contain domain users." defaultfor :operatingsystem => :windows confine :operatingsystem => :windows has_features :manages_members def members_insync?(current, should) return false unless current # By comparing account SIDs we don't have to worry about case # sensitivity, or canonicalization of account names. # Cannot use munge of the group property to canonicalize @should # since the default array_matching comparison is not commutative should_empty = should.nil? or should.empty? return false if current.empty? != should_empty # dupes automatically weeded out when hashes built Puppet::Util::Windows::ADSI::Group.name_sid_hash(current) == Puppet::Util::Windows::ADSI::Group.name_sid_hash(should) end def members_to_s(users) return '' if users.nil? or !users.kind_of?(Array) users = users.map do |user_name| - sid = Puppet::Util::Windows::Security.name_to_sid_object(user_name) + sid = Puppet::Util::Windows::SID.name_to_sid_object(user_name) if sid.account =~ /\\/ account, _ = Puppet::Util::Windows::ADSI::User.parse_name(sid.account) else account = sid.account end resource.debug("#{sid.domain}\\#{account} (#{sid.to_s})") "#{sid.domain}\\#{account}" end return users.join(',') end def group @group ||= Puppet::Util::Windows::ADSI::Group.new(@resource[:name]) end def members group.members end def members=(members) group.set_members(members) end def create @group = Puppet::Util::Windows::ADSI::Group.create(@resource[:name]) @group.commit self.members = @resource[:members] end def exists? Puppet::Util::Windows::ADSI::Group.exists?(@resource[:name]) end def delete Puppet::Util::Windows::ADSI::Group.delete(@resource[:name]) end # Only flush if we created or modified a group, not deleted def flush @group.commit if @group end def gid - Puppet::Util::Windows::Security.name_to_sid(@resource[:name]) + Puppet::Util::Windows::SID.name_to_sid(@resource[:name]) end def gid=(value) fail "gid is read-only" end def self.instances Puppet::Util::Windows::ADSI::Group.map { |g| new(:ensure => :present, :name => g.name) } end end diff --git a/lib/puppet/provider/scheduled_task/win32_taskscheduler.rb b/lib/puppet/provider/scheduled_task/win32_taskscheduler.rb index 45b073da0..52ac26edf 100644 --- a/lib/puppet/provider/scheduled_task/win32_taskscheduler.rb +++ b/lib/puppet/provider/scheduled_task/win32_taskscheduler.rb @@ -1,564 +1,564 @@ require 'puppet/parameter' if Puppet.features.microsoft_windows? require 'win32/taskscheduler' end Puppet::Type.type(:scheduled_task).provide(:win32_taskscheduler) do desc %q{This provider uses the win32-taskscheduler gem to manage scheduled tasks on Windows. Puppet requires version 0.2.1 or later of the win32-taskscheduler gem; previous versions can cause "Could not evaluate: The operation completed successfully" errors.} defaultfor :operatingsystem => :windows confine :operatingsystem => :windows def self.instances Win32::TaskScheduler.new.tasks.collect do |job_file| job_title = File.basename(job_file, '.job') new( :provider => :win32_taskscheduler, :name => job_title ) end end def exists? Win32::TaskScheduler.new.exists? resource[:name] end def task return @task if @task @task ||= Win32::TaskScheduler.new @task.activate(resource[:name] + '.job') if exists? @task end def clear_task @task = nil @triggers = nil end def enabled task.flags & Win32::TaskScheduler::DISABLED == 0 ? :true : :false end def command task.application_name end def arguments task.parameters end def working_dir task.working_directory end def user account = task.account_information return 'system' if account == '' account end def trigger return @triggers if @triggers @triggers = [] task.trigger_count.times do |i| trigger = begin task.trigger(i) rescue Win32::TaskScheduler::Error # Win32::TaskScheduler can't handle all of the # trigger types Windows uses, so we need to skip the # unhandled types to prevent "puppet resource" from # blowing up. nil end next unless trigger and scheduler_trigger_types.include?(trigger['trigger_type']) puppet_trigger = {} case trigger['trigger_type'] when Win32::TaskScheduler::TASK_TIME_TRIGGER_DAILY puppet_trigger['schedule'] = 'daily' puppet_trigger['every'] = trigger['type']['days_interval'].to_s when Win32::TaskScheduler::TASK_TIME_TRIGGER_WEEKLY puppet_trigger['schedule'] = 'weekly' puppet_trigger['every'] = trigger['type']['weeks_interval'].to_s puppet_trigger['on'] = days_of_week_from_bitfield(trigger['type']['days_of_week']) when Win32::TaskScheduler::TASK_TIME_TRIGGER_MONTHLYDATE puppet_trigger['schedule'] = 'monthly' puppet_trigger['months'] = months_from_bitfield(trigger['type']['months']) puppet_trigger['on'] = days_from_bitfield(trigger['type']['days']) when Win32::TaskScheduler::TASK_TIME_TRIGGER_MONTHLYDOW puppet_trigger['schedule'] = 'monthly' puppet_trigger['months'] = months_from_bitfield(trigger['type']['months']) puppet_trigger['which_occurrence'] = occurrence_constant_to_name(trigger['type']['weeks']) puppet_trigger['day_of_week'] = days_of_week_from_bitfield(trigger['type']['days_of_week']) when Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE puppet_trigger['schedule'] = 'once' end puppet_trigger['start_date'] = self.class.normalized_date("#{trigger['start_year']}-#{trigger['start_month']}-#{trigger['start_day']}") puppet_trigger['start_time'] = self.class.normalized_time("#{trigger['start_hour']}:#{trigger['start_minute']}") puppet_trigger['enabled'] = trigger['flags'] & Win32::TaskScheduler::TASK_TRIGGER_FLAG_DISABLED == 0 puppet_trigger['index'] = i @triggers << puppet_trigger end @triggers = @triggers[0] if @triggers.length == 1 @triggers end def user_insync?(current, should) return false unless current # Win32::TaskScheduler can return the 'SYSTEM' account as the # empty string. current = 'system' if current == '' # By comparing account SIDs we don't have to worry about case # sensitivity, or canonicalization of the account name. - Puppet::Util::Windows::Security.name_to_sid(current) == Puppet::Util::Windows::Security.name_to_sid(should[0]) + Puppet::Util::Windows::SID.name_to_sid(current) == Puppet::Util::Windows::SID.name_to_sid(should[0]) end def trigger_insync?(current, should) should = [should] unless should.is_a?(Array) current = [current] unless current.is_a?(Array) return false unless current.length == should.length current_in_sync = current.all? do |c| should.any? {|s| triggers_same?(c, s)} end should_in_sync = should.all? do |s| current.any? {|c| triggers_same?(c,s)} end current_in_sync && should_in_sync end def command=(value) task.application_name = value end def arguments=(value) task.parameters = value end def working_dir=(value) task.working_directory = value end def enabled=(value) if value == :true task.flags = task.flags & ~Win32::TaskScheduler::DISABLED else task.flags = task.flags | Win32::TaskScheduler::DISABLED end end def trigger=(value) desired_triggers = value.is_a?(Array) ? value : [value] current_triggers = trigger.is_a?(Array) ? trigger : [trigger] extra_triggers = [] desired_to_search = desired_triggers.dup current_triggers.each do |current| if found = desired_to_search.find {|desired| triggers_same?(current, desired)} desired_to_search.delete(found) else extra_triggers << current['index'] end end needed_triggers = [] current_to_search = current_triggers.dup desired_triggers.each do |desired| if found = current_to_search.find {|current| triggers_same?(current, desired)} current_to_search.delete(found) else needed_triggers << desired end end extra_triggers.reverse_each do |index| task.delete_trigger(index) end needed_triggers.each do |trigger_hash| # Even though this is an assignment, the API for # Win32::TaskScheduler ends up appending this trigger to the # list of triggers for the task, while #add_trigger is only able # to replace existing triggers. *shrug* task.trigger = translate_hash_to_trigger(trigger_hash) end end def user=(value) - self.fail("Invalid user: #{value}") unless Puppet::Util::Windows::Security.name_to_sid(value) + self.fail("Invalid user: #{value}") unless Puppet::Util::Windows::SID.name_to_sid(value) if value.to_s.downcase != 'system' task.set_account_information(value, resource[:password]) else # Win32::TaskScheduler treats a nil/empty username & password as # requesting the SYSTEM account. task.set_account_information(nil, nil) end end def create clear_task @task = Win32::TaskScheduler.new(resource[:name], dummy_time_trigger) self.command = resource[:command] [:arguments, :working_dir, :enabled, :trigger, :user].each do |prop| send("#{prop}=", resource[prop]) if resource[prop] end end def destroy Win32::TaskScheduler.new.delete(resource[:name] + '.job') end def flush unless resource[:ensure] == :absent self.fail('Parameter command is required.') unless resource[:command] task.save @task = nil end end def triggers_same?(current_trigger, desired_trigger) return false unless current_trigger['schedule'] == desired_trigger['schedule'] return false if current_trigger.has_key?('enabled') && !current_trigger['enabled'] desired = desired_trigger.dup desired['every'] ||= current_trigger['every'] if current_trigger.has_key?('every') desired['months'] ||= current_trigger['months'] if current_trigger.has_key?('months') desired['on'] ||= current_trigger['on'] if current_trigger.has_key?('on') desired['day_of_week'] ||= current_trigger['day_of_week'] if current_trigger.has_key?('day_of_week') translate_hash_to_trigger(current_trigger) == translate_hash_to_trigger(desired) end def self.normalized_date(date_string) date = Date.parse("#{date_string}") "#{date.year}-#{date.month}-#{date.day}" end def self.normalized_time(time_string) Time.parse("#{time_string}").strftime('%H:%M') end def dummy_time_trigger now = Time.now { 'flags' => 0, 'random_minutes_interval' => 0, 'end_day' => 0, "end_year" => 0, "trigger_type" => 0, "minutes_interval" => 0, "end_month" => 0, "minutes_duration" => 0, 'start_year' => now.year, 'start_month' => now.month, 'start_day' => now.day, 'start_hour' => now.hour, 'start_minute' => now.min, 'trigger_type' => Win32::TaskScheduler::ONCE, } end def translate_hash_to_trigger(puppet_trigger, user_provided_input=false) trigger = dummy_time_trigger if user_provided_input self.fail "'enabled' is read-only on triggers" if puppet_trigger.has_key?('enabled') self.fail "'index' is read-only on triggers" if puppet_trigger.has_key?('index') end puppet_trigger.delete('index') if puppet_trigger.delete('enabled') == false trigger['flags'] |= Win32::TaskScheduler::TASK_TRIGGER_FLAG_DISABLED else trigger['flags'] &= ~Win32::TaskScheduler::TASK_TRIGGER_FLAG_DISABLED end extra_keys = puppet_trigger.keys.sort - ['schedule', 'start_date', 'start_time', 'every', 'months', 'on', 'which_occurrence', 'day_of_week'] self.fail "Unknown trigger option(s): #{Puppet::Parameter.format_value_for_display(extra_keys)}" unless extra_keys.empty? self.fail "Must specify 'start_time' when defining a trigger" unless puppet_trigger['start_time'] case puppet_trigger['schedule'] when 'daily' trigger['trigger_type'] = Win32::TaskScheduler::DAILY trigger['type'] = { 'days_interval' => Integer(puppet_trigger['every'] || 1) } when 'weekly' trigger['trigger_type'] = Win32::TaskScheduler::WEEKLY trigger['type'] = { 'weeks_interval' => Integer(puppet_trigger['every'] || 1) } trigger['type']['days_of_week'] = if puppet_trigger['day_of_week'] bitfield_from_days_of_week(puppet_trigger['day_of_week']) else scheduler_days_of_week.inject(0) {|day_flags,day| day_flags |= day} end when 'monthly' trigger['type'] = { 'months' => bitfield_from_months(puppet_trigger['months'] || (1..12).to_a), } if puppet_trigger.keys.include?('on') if puppet_trigger.has_key?('day_of_week') or puppet_trigger.has_key?('which_occurrence') self.fail "Neither 'day_of_week' nor 'which_occurrence' can be specified when creating a monthly date-based trigger" end trigger['trigger_type'] = Win32::TaskScheduler::MONTHLYDATE trigger['type']['days'] = bitfield_from_days(puppet_trigger['on']) elsif puppet_trigger.keys.include?('which_occurrence') or puppet_trigger.keys.include?('day_of_week') self.fail 'which_occurrence cannot be specified as an array' if puppet_trigger['which_occurrence'].is_a?(Array) %w{day_of_week which_occurrence}.each do |field| self.fail "#{field} must be specified when creating a monthly day-of-week based trigger" unless puppet_trigger.has_key?(field) end trigger['trigger_type'] = Win32::TaskScheduler::MONTHLYDOW trigger['type']['weeks'] = occurrence_name_to_constant(puppet_trigger['which_occurrence']) trigger['type']['days_of_week'] = bitfield_from_days_of_week(puppet_trigger['day_of_week']) else self.fail "Don't know how to create a 'monthly' schedule with the options: #{puppet_trigger.keys.sort.join(', ')}" end when 'once' self.fail "Must specify 'start_date' when defining a one-time trigger" unless puppet_trigger['start_date'] trigger['trigger_type'] = Win32::TaskScheduler::ONCE else self.fail "Unknown schedule type: #{puppet_trigger["schedule"].inspect}" end if start_date = puppet_trigger['start_date'] start_date = Date.parse(start_date) self.fail "start_date must be on or after 1753-01-01" unless start_date >= Date.new(1753, 1, 1) trigger['start_year'] = start_date.year trigger['start_month'] = start_date.month trigger['start_day'] = start_date.day end start_time = Time.parse(puppet_trigger['start_time']) trigger['start_hour'] = start_time.hour trigger['start_minute'] = start_time.min trigger end def validate_trigger(value) value = [value] unless value.is_a?(Array) # translate_hash_to_trigger handles the same validation that we # would be doing here at the individual trigger level. value.each {|t| translate_hash_to_trigger(t, true)} true end private def bitfield_from_months(months) bitfield = 0 months = [months] unless months.is_a?(Array) months.each do |month| integer_month = Integer(month) rescue nil self.fail 'Month must be specified as an integer in the range 1-12' unless integer_month == month.to_f and integer_month.between?(1,12) bitfield |= scheduler_months[integer_month - 1] end bitfield end def bitfield_from_days(days) bitfield = 0 days = [days] unless days.is_a?(Array) days.each do |day| # The special "day" of 'last' is represented by day "number" # 32. 'last' has the special meaning of "the last day of the # month", no matter how many days there are in the month. day = 32 if day == 'last' integer_day = Integer(day) self.fail "Day must be specified as an integer in the range 1-31, or as 'last'" unless integer_day = day.to_f and integer_day.between?(1,32) bitfield |= 1 << integer_day - 1 end bitfield end def bitfield_from_days_of_week(days_of_week) bitfield = 0 days_of_week = [days_of_week] unless days_of_week.is_a?(Array) days_of_week.each do |day_of_week| bitfield |= day_of_week_name_to_constant(day_of_week) end bitfield end def months_from_bitfield(bitfield) months = [] scheduler_months.each do |month| if bitfield & month != 0 months << month_constant_to_number(month) end end months end def days_from_bitfield(bitfield) days = [] i = 0 while bitfield > 0 if bitfield & 1 > 0 # Day 32 has the special meaning of "the last day of the # month", no matter how many days there are in the month. days << (i == 31 ? 'last' : i + 1) end bitfield = bitfield >> 1 i += 1 end days end def days_of_week_from_bitfield(bitfield) days_of_week = [] scheduler_days_of_week.each do |day_of_week| if bitfield & day_of_week != 0 days_of_week << day_of_week_constant_to_name(day_of_week) end end days_of_week end def scheduler_trigger_types [ Win32::TaskScheduler::TASK_TIME_TRIGGER_DAILY, Win32::TaskScheduler::TASK_TIME_TRIGGER_WEEKLY, Win32::TaskScheduler::TASK_TIME_TRIGGER_MONTHLYDATE, Win32::TaskScheduler::TASK_TIME_TRIGGER_MONTHLYDOW, Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE ] end def scheduler_days_of_week [ Win32::TaskScheduler::SUNDAY, Win32::TaskScheduler::MONDAY, Win32::TaskScheduler::TUESDAY, Win32::TaskScheduler::WEDNESDAY, Win32::TaskScheduler::THURSDAY, Win32::TaskScheduler::FRIDAY, Win32::TaskScheduler::SATURDAY ] end def scheduler_months [ Win32::TaskScheduler::JANUARY, Win32::TaskScheduler::FEBRUARY, Win32::TaskScheduler::MARCH, Win32::TaskScheduler::APRIL, Win32::TaskScheduler::MAY, Win32::TaskScheduler::JUNE, Win32::TaskScheduler::JULY, Win32::TaskScheduler::AUGUST, Win32::TaskScheduler::SEPTEMBER, Win32::TaskScheduler::OCTOBER, Win32::TaskScheduler::NOVEMBER, Win32::TaskScheduler::DECEMBER ] end def scheduler_occurrences [ Win32::TaskScheduler::FIRST_WEEK, Win32::TaskScheduler::SECOND_WEEK, Win32::TaskScheduler::THIRD_WEEK, Win32::TaskScheduler::FOURTH_WEEK, Win32::TaskScheduler::LAST_WEEK ] end def day_of_week_constant_to_name(constant) case constant when Win32::TaskScheduler::SUNDAY; 'sun' when Win32::TaskScheduler::MONDAY; 'mon' when Win32::TaskScheduler::TUESDAY; 'tues' when Win32::TaskScheduler::WEDNESDAY; 'wed' when Win32::TaskScheduler::THURSDAY; 'thurs' when Win32::TaskScheduler::FRIDAY; 'fri' when Win32::TaskScheduler::SATURDAY; 'sat' end end def day_of_week_name_to_constant(name) case name when 'sun'; Win32::TaskScheduler::SUNDAY when 'mon'; Win32::TaskScheduler::MONDAY when 'tues'; Win32::TaskScheduler::TUESDAY when 'wed'; Win32::TaskScheduler::WEDNESDAY when 'thurs'; Win32::TaskScheduler::THURSDAY when 'fri'; Win32::TaskScheduler::FRIDAY when 'sat'; Win32::TaskScheduler::SATURDAY end end def month_constant_to_number(constant) month_num = 1 while constant >> month_num - 1 > 1 month_num += 1 end month_num end def occurrence_constant_to_name(constant) case constant when Win32::TaskScheduler::FIRST_WEEK; 'first' when Win32::TaskScheduler::SECOND_WEEK; 'second' when Win32::TaskScheduler::THIRD_WEEK; 'third' when Win32::TaskScheduler::FOURTH_WEEK; 'fourth' when Win32::TaskScheduler::LAST_WEEK; 'last' end end def occurrence_name_to_constant(name) case name when 'first'; Win32::TaskScheduler::FIRST_WEEK when 'second'; Win32::TaskScheduler::SECOND_WEEK when 'third'; Win32::TaskScheduler::THIRD_WEEK when 'fourth'; Win32::TaskScheduler::FOURTH_WEEK when 'last'; Win32::TaskScheduler::LAST_WEEK end end end diff --git a/lib/puppet/provider/user/windows_adsi.rb b/lib/puppet/provider/user/windows_adsi.rb index 00b6b432a..fbca8744b 100644 --- a/lib/puppet/provider/user/windows_adsi.rb +++ b/lib/puppet/provider/user/windows_adsi.rb @@ -1,99 +1,99 @@ require 'puppet/util/windows' Puppet::Type.type(:user).provide :windows_adsi do desc "Local user management for Windows." defaultfor :operatingsystem => :windows confine :operatingsystem => :windows has_features :manages_homedir, :manages_passwords def user @user ||= Puppet::Util::Windows::ADSI::User.new(@resource[:name]) end def groups user.groups.join(',') end def groups=(groups) user.set_groups(groups, @resource[:membership] == :minimum) end def create @user = Puppet::Util::Windows::ADSI::User.create(@resource[:name]) @user.password = @resource[:password] @user.commit [:comment, :home, :groups].each do |prop| send("#{prop}=", @resource[prop]) if @resource[prop] end if @resource.managehome? Puppet::Util::Windows::User.load_profile(@resource[:name], @resource[:password]) end end def exists? Puppet::Util::Windows::ADSI::User.exists?(@resource[:name]) end def delete # lookup sid before we delete account sid = uid if @resource.managehome? Puppet::Util::Windows::ADSI::User.delete(@resource[:name]) if sid Puppet::Util::Windows::ADSI::UserProfile.delete(sid) end end # Only flush if we created or modified a user, not deleted def flush @user.commit if @user end def comment user['Description'] end def comment=(value) user['Description'] = value end def home user['HomeDirectory'] end def home=(value) user['HomeDirectory'] = value end def password user.password_is?( @resource[:password] ) ? @resource[:password] : :absent end def password=(value) user.password = value end def uid - Puppet::Util::Windows::Security.name_to_sid(@resource[:name]) + Puppet::Util::Windows::SID.name_to_sid(@resource[:name]) end def uid=(value) fail "uid is read-only" end [:gid, :shell].each do |prop| define_method(prop) { nil } define_method("#{prop}=") do |v| fail "No support for managing property #{prop} of user #{@resource[:name]} on Windows" end end def self.instances Puppet::Util::Windows::ADSI::User.map { |u| new(:ensure => :present, :name => u.name) } end end diff --git a/lib/puppet/util/windows.rb b/lib/puppet/util/windows.rb index 4302ef23a..c4e1196eb 100644 --- a/lib/puppet/util/windows.rb +++ b/lib/puppet/util/windows.rb @@ -1,27 +1,27 @@ module Puppet::Util::Windows module ADSI class User; end class UserProfile; end class Group; end end module Registry end if Puppet::Util::Platform.windows? # these reference platform specific gems require 'puppet/util/windows/api_types' require 'puppet/util/windows/string' require 'puppet/util/windows/error' require 'puppet/util/windows/sid' + require 'puppet/util/windows/file' require 'puppet/util/windows/security' require 'puppet/util/windows/user' require 'puppet/util/windows/process' - require 'puppet/util/windows/file' require 'puppet/util/windows/root_certs' require 'puppet/util/windows/access_control_entry' require 'puppet/util/windows/access_control_list' require 'puppet/util/windows/security_descriptor' require 'puppet/util/windows/adsi' require 'puppet/util/windows/registry' end end diff --git a/lib/puppet/util/windows/adsi.rb b/lib/puppet/util/windows/adsi.rb index 85172c83f..f621c798f 100644 --- a/lib/puppet/util/windows/adsi.rb +++ b/lib/puppet/util/windows/adsi.rb @@ -1,394 +1,394 @@ module Puppet::Util::Windows::ADSI require 'ffi' class << self extend FFI::Library def connectable?(uri) begin !! connect(uri) rescue false end end def connect(uri) begin WIN32OLE.connect(uri) rescue Exception => e raise Puppet::Error.new( "ADSI connection error: #{e}", e ) end end def create(name, resource_type) Puppet::Util::Windows::ADSI.connect(computer_uri).Create(resource_type, name) end def delete(name, resource_type) Puppet::Util::Windows::ADSI.connect(computer_uri).Delete(resource_type, name) end # taken from winbase.h MAX_COMPUTERNAME_LENGTH = 31 def computer_name unless @computer_name max_length = MAX_COMPUTERNAME_LENGTH + 1 # NULL terminated FFI::MemoryPointer.new(max_length * 2) do |buffer| # wide string FFI::MemoryPointer.new(:dword, 1) do |buffer_size| buffer_size.write_dword(max_length) # length in TCHARs if GetComputerNameW(buffer, buffer_size) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Failed to get computer name") end @computer_name = buffer.read_wide_string(buffer_size.read_dword) end end end @computer_name end def computer_uri(host = '.') "WinNT://#{host}" end def wmi_resource_uri( host = '.' ) "winmgmts:{impersonationLevel=impersonate}!//#{host}/root/cimv2" end # @api private def sid_uri_safe(sid) return sid_uri(sid) if sid.kind_of?(Win32::Security::SID) begin sid = Win32::Security::SID.new(Win32::Security::SID.string_to_sid(sid)) sid_uri(sid) rescue Win32::Security::SID::Error return nil end end def sid_uri(sid) raise Puppet::Error.new( "Must use a valid SID object" ) if !sid.kind_of?(Win32::Security::SID) "WinNT://#{sid.to_s}" end def uri(resource_name, resource_type, host = '.') "#{computer_uri(host)}/#{resource_name},#{resource_type}" end def wmi_connection connect(wmi_resource_uri) end def execquery(query) wmi_connection.execquery(query) end def sid_for_account(name) Puppet.deprecation_warning "Puppet::Util::Windows::ADSI.sid_for_account is deprecated and will be removed in 3.0, use Puppet::Util::Windows::SID.name_to_sid instead." - Puppet::Util::Windows::Security.name_to_sid(name) + Puppet::Util::Windows::SID.name_to_sid(name) end ffi_convention :stdcall # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724295(v=vs.85).aspx # BOOL WINAPI GetComputerName( # _Out_ LPTSTR lpBuffer, # _Inout_ LPDWORD lpnSize # ); ffi_lib :kernel32 attach_function_private :GetComputerNameW, [:lpwstr, :lpdword], :win32_bool end class User extend Enumerable attr_accessor :native_user attr_reader :name, :sid def initialize(name, native_user = nil) @name = name @native_user = native_user end def self.parse_name(name) if name =~ /\// raise Puppet::Error.new( "Value must be in DOMAIN\\user style syntax" ) end matches = name.scan(/((.*)\\)?(.*)/) domain = matches[0][1] || '.' account = matches[0][2] return account, domain end def native_user @native_user ||= Puppet::Util::Windows::ADSI.connect(self.class.uri(*self.class.parse_name(@name))) end def sid - @sid ||= Puppet::Util::Windows::Security.octet_string_to_sid_object(native_user.objectSID) + @sid ||= Puppet::Util::Windows::SID.octet_string_to_sid_object(native_user.objectSID) end def self.uri(name, host = '.') if sid_uri = Puppet::Util::Windows::ADSI.sid_uri_safe(name) then return sid_uri end host = '.' if ['NT AUTHORITY', 'BUILTIN', Socket.gethostname].include?(host) Puppet::Util::Windows::ADSI.uri(name, 'user', host) end def uri self.class.uri(sid.account, sid.domain) end def self.logon(name, password) Puppet::Util::Windows::User.password_is?(name, password) end def [](attribute) native_user.Get(attribute) end def []=(attribute, value) native_user.Put(attribute, value) end def commit begin native_user.SetInfo unless native_user.nil? rescue Exception => e raise Puppet::Error.new( "User update failed: #{e}", e ) end self end def password_is?(password) self.class.logon(name, password) end def add_flag(flag_name, value) flag = native_user.Get(flag_name) rescue 0 native_user.Put(flag_name, flag | value) commit end def password=(password) native_user.SetPassword(password) commit fADS_UF_DONT_EXPIRE_PASSWD = 0x10000 add_flag("UserFlags", fADS_UF_DONT_EXPIRE_PASSWD) end def groups # WIN32OLE objects aren't enumerable, so no map groups = [] native_user.Groups.each {|g| groups << g.Name} rescue nil groups end def add_to_groups(*group_names) group_names.each do |group_name| Puppet::Util::Windows::ADSI::Group.new(group_name).add_member_sids(sid) end end alias add_to_group add_to_groups def remove_from_groups(*group_names) group_names.each do |group_name| Puppet::Util::Windows::ADSI::Group.new(group_name).remove_member_sids(sid) end end alias remove_from_group remove_from_groups def set_groups(desired_groups, minimum = true) return if desired_groups.nil? or desired_groups.empty? desired_groups = desired_groups.split(',').map(&:strip) current_groups = self.groups # First we add the user to all the groups it should be in but isn't groups_to_add = desired_groups - current_groups add_to_groups(*groups_to_add) # Then we remove the user from all groups it is in but shouldn't be, if # that's been requested groups_to_remove = current_groups - desired_groups remove_from_groups(*groups_to_remove) unless minimum end def self.create(name) # Windows error 1379: The specified local group already exists. raise Puppet::Error.new( "Cannot create user if group '#{name}' exists." ) if Puppet::Util::Windows::ADSI::Group.exists? name new(name, Puppet::Util::Windows::ADSI.create(name, 'user')) end def self.exists?(name) Puppet::Util::Windows::ADSI::connectable?(User.uri(*User.parse_name(name))) end def self.delete(name) Puppet::Util::Windows::ADSI.delete(name, 'user') end def self.each(&block) wql = Puppet::Util::Windows::ADSI.execquery('select name from win32_useraccount where localaccount = "TRUE"') users = [] wql.each do |u| users << new(u.name) end users.each(&block) end end class UserProfile def self.delete(sid) begin Puppet::Util::Windows::ADSI.wmi_connection.Delete("Win32_UserProfile.SID='#{sid}'") rescue => e # http://social.technet.microsoft.com/Forums/en/ITCG/thread/0f190051-ac96-4bf1-a47f-6b864bfacee5 # Prior to Vista SP1, there's no builtin way to programmatically # delete user profiles (except for delprof.exe). So try to delete # but warn if we fail raise e unless e.message.include?('80041010') Puppet.warning "Cannot delete user profile for '#{sid}' prior to Vista SP1" end end end class Group extend Enumerable attr_accessor :native_group attr_reader :name def initialize(name, native_group = nil) @name = name @native_group = native_group end def uri self.class.uri(name) end def self.uri(name, host = '.') if sid_uri = Puppet::Util::Windows::ADSI.sid_uri_safe(name) then return sid_uri end Puppet::Util::Windows::ADSI.uri(name, 'group', host) end def native_group @native_group ||= Puppet::Util::Windows::ADSI.connect(uri) end def commit begin native_group.SetInfo unless native_group.nil? rescue Exception => e raise Puppet::Error.new( "Group update failed: #{e}", e ) end self end def self.name_sid_hash(names) return [] if names.nil? or names.empty? sids = names.map do |name| - sid = Puppet::Util::Windows::Security.name_to_sid_object(name) + sid = Puppet::Util::Windows::SID.name_to_sid_object(name) raise Puppet::Error.new( "Could not resolve username: #{name}" ) if !sid [sid.to_s, sid] end Hash[ sids ] end def add_members(*names) Puppet.deprecation_warning('Puppet::Util::Windows::ADSI::Group#add_members is deprecated; please use Puppet::Util::Windows::ADSI::Group#add_member_sids') sids = self.class.name_sid_hash(names) add_member_sids(*sids.values) end alias add_member add_members def remove_members(*names) Puppet.deprecation_warning('Puppet::Util::Windows::ADSI::Group#remove_members is deprecated; please use Puppet::Util::Windows::ADSI::Group#remove_member_sids') sids = self.class.name_sid_hash(names) remove_member_sids(*sids.values) end alias remove_member remove_members def add_member_sids(*sids) sids.each do |sid| native_group.Add(Puppet::Util::Windows::ADSI.sid_uri(sid)) end end def remove_member_sids(*sids) sids.each do |sid| native_group.Remove(Puppet::Util::Windows::ADSI.sid_uri(sid)) end end def members # WIN32OLE objects aren't enumerable, so no map members = [] native_group.Members.each {|m| members << m.Name} members end def member_sids sids = [] native_group.Members.each do |m| - sids << Puppet::Util::Windows::Security.octet_string_to_sid_object(m.objectSID) + sids << Puppet::Util::Windows::SID.octet_string_to_sid_object(m.objectSID) end sids end def set_members(desired_members) return if desired_members.nil? or desired_members.empty? current_hash = Hash[ self.member_sids.map { |sid| [sid.to_s, sid] } ] desired_hash = self.class.name_sid_hash(desired_members) # First we add all missing members members_to_add = (desired_hash.keys - current_hash.keys).map { |sid| desired_hash[sid] } add_member_sids(*members_to_add) # Then we remove all extra members members_to_remove = (current_hash.keys - desired_hash.keys).map { |sid| current_hash[sid] } remove_member_sids(*members_to_remove) end def self.create(name) # Windows error 2224: The account already exists. raise Puppet::Error.new( "Cannot create group if user '#{name}' exists." ) if Puppet::Util::Windows::ADSI::User.exists? name new(name, Puppet::Util::Windows::ADSI.create(name, 'group')) end def self.exists?(name) Puppet::Util::Windows::ADSI.connectable?(Group.uri(name)) end def self.delete(name) Puppet::Util::Windows::ADSI.delete(name, 'group') end def self.each(&block) wql = Puppet::Util::Windows::ADSI.execquery( 'select name from win32_group where localaccount = "TRUE"' ) groups = [] wql.each do |g| groups << new(g.name) end groups.each(&block) end end end diff --git a/lib/puppet/util/windows/api_types.rb b/lib/puppet/util/windows/api_types.rb index c109a34f1..a1cc82de2 100644 --- a/lib/puppet/util/windows/api_types.rb +++ b/lib/puppet/util/windows/api_types.rb @@ -1,162 +1,166 @@ require 'ffi' require 'puppet/util/windows/string' module Puppet::Util::Windows::APITypes module ::FFI WIN32_FALSE = 0 + + # standard Win32 error codes + ERROR_SUCCESS = 0 end module ::FFI::Library # Wrapper method for attach_function + private def attach_function_private(*args) attach_function(*args) private args[0] end end class ::FFI::Pointer NULL_HANDLE = 0 NULL_TERMINATOR_WCHAR = 0 def self.from_string_to_wide_string(str, &block) str = Puppet::Util::Windows::String.wide_string(str) FFI::MemoryPointer.new(:byte, str.bytesize) do |ptr| # uchar here is synonymous with byte ptr.put_array_of_uchar(0, str.bytes.to_a) yield ptr end # ptr has already had free called, so nothing to return nil end def read_win32_bool # BOOL is always a 32-bit integer in Win32 # some Win32 APIs return 1 for true, while others are non-0 read_int32 != FFI::WIN32_FALSE end alias_method :read_dword, :read_uint32 def read_handle type_size == 4 ? read_uint32 : read_uint64 end alias_method :read_wchar, :read_uint16 + alias_method :read_word, :read_uint16 def read_wide_string(char_length) # char_length is number of wide chars (typically excluding NULLs), *not* bytes str = get_bytes(0, char_length * 2).force_encoding('UTF-16LE') str.encode(Encoding.default_external) end def read_arbitrary_wide_string_up_to(max_char_length = 512) # max_char_length is number of wide chars (typically excluding NULLs), *not* bytes # use a pointer to read one UTF-16LE char (2 bytes) at a time wchar_ptr = FFI::Pointer.new(:wchar, address) # now iterate 2 bytes at a time until an offset lower than max_char_length is found 0.upto(max_char_length - 1) do |i| if wchar_ptr[i].read_wchar == NULL_TERMINATOR_WCHAR return read_wide_string(i) end end read_wide_string(max_char_length) end def read_win32_local_pointer(&block) ptr = nil begin ptr = read_pointer yield ptr ensure if ptr && ! ptr.null? if FFI::WIN32::LocalFree(ptr.address) != FFI::Pointer::NULL_HANDLE Puppet.debug "LocalFree memory leak" end end end # ptr has already had LocalFree called, so nothing to return nil end alias_method :write_dword, :write_uint32 end # FFI Types # https://github.com/ffi/ffi/wiki/Types # Windows - Common Data Types # http://msdn.microsoft.com/en-us/library/cc230309.aspx # Windows Data Types # http://msdn.microsoft.com/en-us/library/windows/desktop/aa383751(v=vs.85).aspx FFI.typedef :uint16, :word FFI.typedef :uint32, :dword # uintptr_t is defined in an FFI conf as platform specific, either # ulong_long on x64 or just ulong on x86 FFI.typedef :uintptr_t, :handle # buffer_inout is similar to pointer (platform specific), but optimized for buffers FFI.typedef :buffer_inout, :lpwstr # buffer_in is similar to pointer (platform specific), but optimized for CONST read only buffers FFI.typedef :buffer_in, :lpcwstr # string is also similar to pointer, but should be used for const char * # NOTE that this is not wide, useful only for A suffixed functions FFI.typedef :string, :lpcstr # pointer in FFI is platform specific # NOTE: for API calls with reserved lpvoid parameters, pass a FFI::Pointer::NULL FFI.typedef :pointer, :lpcvoid FFI.typedef :pointer, :lpvoid FFI.typedef :pointer, :lpword FFI.typedef :pointer, :lpdword FFI.typedef :pointer, :pdword FFI.typedef :pointer, :phandle FFI.typedef :pointer, :ulong_ptr FFI.typedef :pointer, :pbool # any time LONG / ULONG is in a win32 API definition DO NOT USE platform specific width # which is what FFI uses by default # instead create new aliases for these very special cases # NOTE: not a good idea to redefine FFI :ulong since other typedefs may rely on it FFI.typedef :uint32, :win32_ulong FFI.typedef :int32, :win32_long # FFI bool can be only 1 byte at times, # Win32 BOOL is a signed int, and is always 4 bytes, even on x64 # http://blogs.msdn.com/b/oldnewthing/archive/2011/03/28/10146459.aspx FFI.typedef :int32, :win32_bool # NOTE: FFI already defines (u)short as a 16-bit (un)signed like this: # FFI.typedef :uint16, :ushort # FFI.typedef :int16, :short # 8 bits per byte FFI.typedef :uchar, :byte FFI.typedef :uint16, :wchar module ::FFI::WIN32 extend ::FFI::Library ffi_convention :stdcall # http://msdn.microsoft.com/en-us/library/windows/desktop/aa366730(v=vs.85).aspx # HLOCAL WINAPI LocalFree( # _In_ HLOCAL hMem # ); ffi_lib :kernel32 attach_function :LocalFree, [:handle], :handle # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724211(v=vs.85).aspx # BOOL WINAPI CloseHandle( # _In_ HANDLE hObject # ); ffi_lib :kernel32 attach_function_private :CloseHandle, [:handle], :win32_bool end end diff --git a/lib/puppet/util/windows/file.rb b/lib/puppet/util/windows/file.rb index 75c87550f..24d8cfed2 100644 --- a/lib/puppet/util/windows/file.rb +++ b/lib/puppet/util/windows/file.rb @@ -1,309 +1,397 @@ require 'puppet/util/windows' module Puppet::Util::Windows::File require 'ffi' extend FFI::Library extend Puppet::Util::Windows::String + FILE_ATTRIBUTE_READONLY = 0x00000001 + + SYNCHRONIZE = 0x100000 + STANDARD_RIGHTS_REQUIRED = 0xf0000 + STANDARD_RIGHTS_READ = 0x20000 + STANDARD_RIGHTS_WRITE = 0x20000 + STANDARD_RIGHTS_EXECUTE = 0x20000 + STANDARD_RIGHTS_ALL = 0x1F0000 + SPECIFIC_RIGHTS_ALL = 0xFFFF + + FILE_READ_DATA = 1 + FILE_WRITE_DATA = 2 + FILE_APPEND_DATA = 4 + FILE_READ_EA = 8 + FILE_WRITE_EA = 16 + FILE_EXECUTE = 32 + FILE_DELETE_CHILD = 64 + FILE_READ_ATTRIBUTES = 128 + FILE_WRITE_ATTRIBUTES = 256 + + FILE_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x1FF + + FILE_GENERIC_READ = + STANDARD_RIGHTS_READ | + FILE_READ_DATA | + FILE_READ_ATTRIBUTES | + FILE_READ_EA | + SYNCHRONIZE + + FILE_GENERIC_WRITE = + STANDARD_RIGHTS_WRITE | + FILE_WRITE_DATA | + FILE_WRITE_ATTRIBUTES | + FILE_WRITE_EA | + FILE_APPEND_DATA | + SYNCHRONIZE + + FILE_GENERIC_EXECUTE = + STANDARD_RIGHTS_EXECUTE | + FILE_READ_ATTRIBUTES | + FILE_EXECUTE | + SYNCHRONIZE + def replace_file(target, source) target_encoded = wide_string(target.to_s) source_encoded = wide_string(source.to_s) flags = 0x1 backup_file = nil result = ReplaceFileW( target_encoded, source_encoded, backup_file, flags, FFI::Pointer::NULL, FFI::Pointer::NULL ) return true if result != FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("ReplaceFile(#{target}, #{source})") end module_function :replace_file def move_file_ex(source, target, flags = 0) result = MoveFileExW(wide_string(source.to_s), wide_string(target.to_s), flags) return true if result != FFI::WIN32_FALSE raise Puppet::Util::Windows::Error. new("MoveFileEx(#{source}, #{target}, #{flags.to_s(8)})") end module_function :move_file_ex def symlink(target, symlink) flags = File.directory?(target) ? 0x1 : 0x0 result = CreateSymbolicLinkW(wide_string(symlink.to_s), wide_string(target.to_s), flags) return true if result != FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new( "CreateSymbolicLink(#{symlink}, #{target}, #{flags.to_s(8)})") end module_function :symlink - INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF #define INVALID_FILE_ATTRIBUTES (DWORD (-1)) - def self.get_file_attributes(file_name) + INVALID_FILE_ATTRIBUTES = FFI::Pointer.new(-1).address #define INVALID_FILE_ATTRIBUTES (DWORD (-1)) + + def get_file_attributes(file_name) + Puppet.deprecation_warning('Puppet::Util::Windows::File.get_file_attributes is deprecated; please use Puppet::Util::Windows::File.get_attributes') + get_attributes(file_name) + end + module_function :get_file_attributes + + def get_attributes(file_name) result = GetFileAttributesW(wide_string(file_name.to_s)) return result unless result == INVALID_FILE_ATTRIBUTES raise Puppet::Util::Windows::Error.new("GetFileAttributes(#{file_name})") end + module_function :get_attributes + + def add_attributes(path, flags) + oldattrs = get_attributes(path) + + if (oldattrs | flags) != oldattrs + set_attributes(path, oldattrs | flags) + end + end + module_function :add_attributes + + def remove_attributes(path, flags) + oldattrs = get_attributes(path) + + if (oldattrs & ~flags) != oldattrs + set_attributes(path, oldattrs & ~flags) + end + end + module_function :remove_attributes + + def set_attributes(path, flags) + success = SetFileAttributesW(wide_string(path), flags) != FFI::WIN32_FALSE + raise Puppet::Util::Windows::Error.new("Failed to set file attributes") if !success + + success + end + module_function :set_attributes INVALID_HANDLE_VALUE = -1 #define INVALID_HANDLE_VALUE ((HANDLE)(LONG_PTR)-1) def self.create_file(file_name, desired_access, share_mode, security_attributes, creation_disposition, flags_and_attributes, template_file_handle) result = CreateFileW(wide_string(file_name.to_s), desired_access, share_mode, security_attributes, creation_disposition, flags_and_attributes, template_file_handle) return result unless result == INVALID_HANDLE_VALUE raise Puppet::Util::Windows::Error.new( "CreateFile(#{file_name}, #{desired_access.to_s(8)}, #{share_mode.to_s(8)}, " + "#{security_attributes}, #{creation_disposition.to_s(8)}, " + "#{flags_and_attributes.to_s(8)}, #{template_file_handle})") end def self.get_reparse_point_data(handle, &block) # must be multiple of 1024, min 10240 FFI::MemoryPointer.new(REPARSE_DATA_BUFFER.size) do |reparse_data_buffer_ptr| device_io_control(handle, FSCTL_GET_REPARSE_POINT, nil, reparse_data_buffer_ptr) yield REPARSE_DATA_BUFFER.new(reparse_data_buffer_ptr) end # underlying struct MemoryPointer has been cleaned up by this point, nothing to return nil end def self.device_io_control(handle, io_control_code, in_buffer = nil, out_buffer = nil) if out_buffer.nil? raise Puppet::Util::Windows::Error.new("out_buffer is required") end FFI::MemoryPointer.new(:dword, 1) do |bytes_returned_ptr| result = DeviceIoControl( handle, io_control_code, in_buffer, in_buffer.nil? ? 0 : in_buffer.size, out_buffer, out_buffer.size, bytes_returned_ptr, nil ) if result == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new( "DeviceIoControl(#{handle}, #{io_control_code}, " + "#{in_buffer}, #{in_buffer ? in_buffer.size : ''}, " + "#{out_buffer}, #{out_buffer ? out_buffer.size : ''}") end end out_buffer end FILE_ATTRIBUTE_REPARSE_POINT = 0x400 def symlink?(file_name) begin - attributes = get_file_attributes(file_name) + attributes = get_attributes(file_name) (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT rescue # raised INVALID_FILE_ATTRIBUTES is equivalent to file not found false end end module_function :symlink? GENERIC_READ = 0x80000000 FILE_SHARE_READ = 1 + FILE_SHARE_WRITE = 2 OPEN_EXISTING = 3 FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 def self.open_symlink(link_name) begin yield handle = create_file( link_name, GENERIC_READ, FILE_SHARE_READ, nil, # security_attributes OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, 0) # template_file ensure FFI::WIN32.CloseHandle(handle) if handle end # handle has had CloseHandle called against it, so nothing to return nil end def readlink(link_name) link = nil open_symlink(link_name) do |handle| link = resolve_symlink(handle) end link end module_function :readlink def stat(file_name) file_name = file_name.to_s # accomodate PathName or String stat = File.stat(file_name) singleton_class = class << stat; self; end target_path = file_name if symlink?(file_name) target_path = readlink(file_name) link_ftype = File.stat(target_path).ftype # sigh, monkey patch instance method for instance, and close over link_ftype singleton_class.send(:define_method, :ftype) do link_ftype end end singleton_class.send(:define_method, :mode) do Puppet::Util::Windows::Security.get_mode(target_path) end stat end module_function :stat def lstat(file_name) file_name = file_name.to_s # accomodate PathName or String # monkey'ing around! stat = File.lstat(file_name) singleton_class = class << stat; self; end singleton_class.send(:define_method, :mode) do Puppet::Util::Windows::Security.get_mode(file_name) end if symlink?(file_name) def stat.ftype "link" end end stat end module_function :lstat private # http://msdn.microsoft.com/en-us/library/windows/desktop/aa364571(v=vs.85).aspx FSCTL_GET_REPARSE_POINT = 0x900a8 def self.resolve_symlink(handle) path = nil get_reparse_point_data(handle) do |reparse_data| offset = reparse_data[:PrintNameOffset] length = reparse_data[:PrintNameLength] ptr = reparse_data.pointer + reparse_data.offset_of(:PathBuffer) + offset path = ptr.read_wide_string(length / 2) # length is bytes, need UTF-16 wchars end path end ffi_convention :stdcall # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365512(v=vs.85).aspx # BOOL WINAPI ReplaceFile( # _In_ LPCTSTR lpReplacedFileName, # _In_ LPCTSTR lpReplacementFileName, # _In_opt_ LPCTSTR lpBackupFileName, # _In_ DWORD dwReplaceFlags - 0x1 REPLACEFILE_WRITE_THROUGH, # 0x2 REPLACEFILE_IGNORE_MERGE_ERRORS, # 0x4 REPLACEFILE_IGNORE_ACL_ERRORS # _Reserved_ LPVOID lpExclude, # _Reserved_ LPVOID lpReserved # ); ffi_lib :kernel32 attach_function_private :ReplaceFileW, [:lpcwstr, :lpcwstr, :lpcwstr, :dword, :lpvoid, :lpvoid], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365240(v=vs.85).aspx # BOOL WINAPI MoveFileEx( # _In_ LPCTSTR lpExistingFileName, # _In_opt_ LPCTSTR lpNewFileName, # _In_ DWORD dwFlags # ); ffi_lib :kernel32 attach_function_private :MoveFileExW, [:lpcwstr, :lpcwstr, :dword], :win32_bool # BOOLEAN WINAPI CreateSymbolicLink( # _In_ LPTSTR lpSymlinkFileName, - symbolic link to be created # _In_ LPTSTR lpTargetFileName, - name of target for symbolic link # _In_ DWORD dwFlags - 0x0 target is a file, 0x1 target is a directory # ); # rescue on Windows < 6.0 so that code doesn't explode begin ffi_lib :kernel32 attach_function_private :CreateSymbolicLinkW, [:lpwstr, :lpwstr, :dword], :win32_bool rescue LoadError end + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa364944(v=vs.85).aspx # DWORD WINAPI GetFileAttributes( # _In_ LPCTSTR lpFileName # ); ffi_lib :kernel32 attach_function_private :GetFileAttributesW, [:lpcwstr], :dword + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365535(v=vs.85).aspx + # BOOL WINAPI SetFileAttributes( + # _In_ LPCTSTR lpFileName, + # _In_ DWORD dwFileAttributes + # ); + ffi_lib :kernel32 + attach_function_private :SetFileAttributesW, + [:lpcwstr, :dword], :win32_bool + # 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 # http://msdn.microsoft.com/en-us/library/windows/desktop/aa363216(v=vs.85).aspx # BOOL WINAPI DeviceIoControl( # _In_ HANDLE hDevice, # _In_ DWORD dwIoControlCode, # _In_opt_ LPVOID lpInBuffer, # _In_ DWORD nInBufferSize, # _Out_opt_ LPVOID lpOutBuffer, # _In_ DWORD nOutBufferSize, # _Out_opt_ LPDWORD lpBytesReturned, # _Inout_opt_ LPOVERLAPPED lpOverlapped # ); ffi_lib :kernel32 attach_function_private :DeviceIoControl, [:handle, :dword, :lpvoid, :dword, :lpvoid, :dword, :lpdword, :pointer], :win32_bool MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16384 # REPARSE_DATA_BUFFER # http://msdn.microsoft.com/en-us/library/cc232006.aspx # http://msdn.microsoft.com/en-us/library/windows/hardware/ff552012(v=vs.85).aspx # struct is always MAXIMUM_REPARSE_DATA_BUFFER_SIZE bytes class REPARSE_DATA_BUFFER < FFI::Struct layout :ReparseTag, :win32_ulong, :ReparseDataLength, :ushort, :Reserved, :ushort, :SubstituteNameOffset, :ushort, :SubstituteNameLength, :ushort, :PrintNameOffset, :ushort, :PrintNameLength, :ushort, :Flags, :win32_ulong, # max less above fields dword / uint 4 bytes, ushort 2 bytes # technically a WCHAR buffer, but we care about size in bytes here :PathBuffer, [:byte, MAXIMUM_REPARSE_DATA_BUFFER_SIZE - 20] end end diff --git a/lib/puppet/util/windows/process.rb b/lib/puppet/util/windows/process.rb index 29d8ef165..c92c75a5b 100644 --- a/lib/puppet/util/windows/process.rb +++ b/lib/puppet/util/windows/process.rb @@ -1,336 +1,348 @@ require 'puppet/util/windows' require 'win32/process' require 'ffi' module Puppet::Util::Windows::Process extend Puppet::Util::Windows::String extend FFI::Library WAIT_TIMEOUT = 0x102 def execute(command, arguments, stdin, stdout, stderr) Process.create( :command_line => command, :startup_info => {:stdin => stdin, :stdout => stdout, :stderr => stderr}, :close_handles => false ) end module_function :execute def wait_process(handle) while WaitForSingleObject(handle, 0) == WAIT_TIMEOUT sleep(1) end exit_status = -1 FFI::MemoryPointer.new(:dword, 1) do |exit_status_ptr| if GetExitCodeProcess(handle, exit_status_ptr) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Failed to get child process exit code") end exit_status = exit_status_ptr.read_dword # $CHILD_STATUS is not set when calling win32/process Process.create # and since it's read-only, we can't set it. But we can execute a # a shell that simply returns the desired exit status, which has the # desired effect. %x{#{ENV['COMSPEC']} /c exit #{exit_status}} end exit_status end module_function :wait_process def get_current_process # this pseudo-handle does not require closing per MSDN docs GetCurrentProcess() end module_function :get_current_process def open_process_token(handle, desired_access, &block) token_handle = nil begin FFI::MemoryPointer.new(:handle, 1) do |token_handle_ptr| result = OpenProcessToken(handle, desired_access, token_handle_ptr) if result == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new( "OpenProcessToken(#{handle}, #{desired_access.to_s(8)}, #{token_handle_ptr})") end yield token_handle = token_handle_ptr.read_handle end token_handle ensure FFI::WIN32.CloseHandle(token_handle) if token_handle end # token_handle has had CloseHandle called against it, so nothing to return nil end module_function :open_process_token + # Execute a block with the current process token + def with_process_token(access, &block) + handle = get_current_process + open_process_token(handle, access) do |token_handle| + yield token_handle + end + + # all handles have been closed, so nothing to safely return + nil + end + module_function :with_process_token + def lookup_privilege_value(name, system_name = '', &block) FFI::MemoryPointer.new(LUID.size) do |luid_ptr| result = LookupPrivilegeValueW( wide_string(system_name), wide_string(name.to_s), luid_ptr ) if result == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new( "LookupPrivilegeValue(#{system_name}, #{name}, #{luid_ptr})") end yield LUID.new(luid_ptr) end # the underlying MemoryPointer for LUID is cleaned up by this point nil end module_function :lookup_privilege_value def get_token_information(token_handle, token_information, &block) # to determine buffer size FFI::MemoryPointer.new(:dword, 1) do |return_length_ptr| result = GetTokenInformation(token_handle, token_information, nil, 0, return_length_ptr) return_length = return_length_ptr.read_dword if return_length <= 0 raise Puppet::Util::Windows::Error.new( "GetTokenInformation(#{token_handle}, #{token_information}, nil, 0, #{return_length_ptr})") end # re-call API with properly sized buffer for all results FFI::MemoryPointer.new(return_length) do |token_information_buf| result = GetTokenInformation(token_handle, token_information, token_information_buf, return_length, return_length_ptr) if result == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new( "GetTokenInformation(#{token_handle}, #{token_information}, #{token_information_buf}, " + "#{return_length}, #{return_length_ptr})") end yield token_information_buf end end # GetTokenInformation buffer has been cleaned up by this point, nothing to return nil end module_function :get_token_information def parse_token_information_as_token_privileges(token_information_buf) raw_privileges = TOKEN_PRIVILEGES.new(token_information_buf) privileges = { :count => raw_privileges[:PrivilegeCount], :privileges => [] } offset = token_information_buf + TOKEN_PRIVILEGES.offset_of(:Privileges) privilege_ptr = FFI::Pointer.new(LUID_AND_ATTRIBUTES, offset) # extract each instance of LUID_AND_ATTRIBUTES 0.upto(privileges[:count] - 1) do |i| privileges[:privileges] << LUID_AND_ATTRIBUTES.new(privilege_ptr[i]) end privileges end module_function :parse_token_information_as_token_privileges def parse_token_information_as_token_elevation(token_information_buf) TOKEN_ELEVATION.new(token_information_buf) end module_function :parse_token_information_as_token_elevation TOKEN_ALL_ACCESS = 0xF01FF ERROR_NO_SUCH_PRIVILEGE = 1313 def process_privilege_symlink? privilege_symlink = false handle = get_current_process open_process_token(handle, TOKEN_ALL_ACCESS) do |token_handle| lookup_privilege_value('SeCreateSymbolicLinkPrivilege') do |luid| get_token_information(token_handle, :TokenPrivileges) do |token_info| token_privileges = parse_token_information_as_token_privileges(token_info) privilege_symlink = token_privileges[:privileges].any? { |p| p[:Luid].values == luid.values } end end end privilege_symlink rescue Puppet::Util::Windows::Error => e if e.code == ERROR_NO_SUCH_PRIVILEGE false # pre-Vista else raise e end end module_function :process_privilege_symlink? TOKEN_QUERY = 0x0008 # Returns whether or not the owner of the current process is running # with elevated security privileges. # # Only supported on Windows Vista or later. # def elevated_security? # default / pre-Vista elevated = false handle = nil begin handle = get_current_process open_process_token(handle, TOKEN_QUERY) do |token_handle| get_token_information(token_handle, :TokenElevation) do |token_info| token_elevation = parse_token_information_as_token_elevation(token_info) # TokenIsElevated member of the TOKEN_ELEVATION struct elevated = token_elevation[:TokenIsElevated] != 0 end end elevated rescue Puppet::Util::Windows::Error => e raise e if e.code != ERROR_NO_SUCH_PRIVILEGE ensure FFI::WIN32.CloseHandle(handle) if handle end end module_function :elevated_security? ffi_convention :stdcall # http://msdn.microsoft.com/en-us/library/windows/desktop/ms687032(v=vs.85).aspx # DWORD WINAPI WaitForSingleObject( # _In_ HANDLE hHandle, # _In_ DWORD dwMilliseconds # ); ffi_lib :kernel32 attach_function_private :WaitForSingleObject, [:handle, :dword], :dword # http://msdn.microsoft.com/en-us/library/windows/desktop/ms683189(v=vs.85).aspx # BOOL WINAPI GetExitCodeProcess( # _In_ HANDLE hProcess, # _Out_ LPDWORD lpExitCode # ); ffi_lib :kernel32 attach_function_private :GetExitCodeProcess, [:handle, :lpdword], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/ms683179(v=vs.85).aspx # HANDLE WINAPI GetCurrentProcess(void); ffi_lib :kernel32 attach_function_private :GetCurrentProcess, [], :handle # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379295(v=vs.85).aspx # BOOL WINAPI OpenProcessToken( # _In_ HANDLE ProcessHandle, # _In_ DWORD DesiredAccess, # _Out_ PHANDLE TokenHandle # ); ffi_lib :advapi32 attach_function_private :OpenProcessToken, [:handle, :dword, :phandle], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379261(v=vs.85).aspx # typedef struct _LUID { # DWORD LowPart; # LONG HighPart; # } LUID, *PLUID; class LUID < FFI::Struct layout :LowPart, :dword, :HighPart, :win32_long end # http://msdn.microsoft.com/en-us/library/Windows/desktop/aa379180(v=vs.85).aspx # BOOL WINAPI LookupPrivilegeValue( # _In_opt_ LPCTSTR lpSystemName, # _In_ LPCTSTR lpName, # _Out_ PLUID lpLuid # ); ffi_lib :advapi32 attach_function_private :LookupPrivilegeValueW, [:lpcwstr, :lpcwstr, :pointer], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379626(v=vs.85).aspx TOKEN_INFORMATION_CLASS = enum( :TokenUser, 1, :TokenGroups, :TokenPrivileges, :TokenOwner, :TokenPrimaryGroup, :TokenDefaultDacl, :TokenSource, :TokenType, :TokenImpersonationLevel, :TokenStatistics, :TokenRestrictedSids, :TokenSessionId, :TokenGroupsAndPrivileges, :TokenSessionReference, :TokenSandBoxInert, :TokenAuditPolicy, :TokenOrigin, :TokenElevationType, :TokenLinkedToken, :TokenElevation, :TokenHasRestrictions, :TokenAccessInformation, :TokenVirtualizationAllowed, :TokenVirtualizationEnabled, :TokenIntegrityLevel, :TokenUIAccess, :TokenMandatoryPolicy, :TokenLogonSid, :TokenIsAppContainer, :TokenCapabilities, :TokenAppContainerSid, :TokenAppContainerNumber, :TokenUserClaimAttributes, :TokenDeviceClaimAttributes, :TokenRestrictedUserClaimAttributes, :TokenRestrictedDeviceClaimAttributes, :TokenDeviceGroups, :TokenRestrictedDeviceGroups, :TokenSecurityAttributes, :TokenIsRestricted, :MaxTokenInfoClass ) # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379263(v=vs.85).aspx # typedef struct _LUID_AND_ATTRIBUTES { # LUID Luid; # DWORD Attributes; # } LUID_AND_ATTRIBUTES, *PLUID_AND_ATTRIBUTES; class LUID_AND_ATTRIBUTES < FFI::Struct layout :Luid, LUID, :Attributes, :dword end # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379630(v=vs.85).aspx # typedef struct _TOKEN_PRIVILEGES { # DWORD PrivilegeCount; # LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY]; # } TOKEN_PRIVILEGES, *PTOKEN_PRIVILEGES; class TOKEN_PRIVILEGES < FFI::Struct layout :PrivilegeCount, :dword, :Privileges, [LUID_AND_ATTRIBUTES, 1] # placeholder for offset end # http://msdn.microsoft.com/en-us/library/windows/desktop/bb530717(v=vs.85).aspx # typedef struct _TOKEN_ELEVATION { # DWORD TokenIsElevated; # } TOKEN_ELEVATION, *PTOKEN_ELEVATION; class TOKEN_ELEVATION < FFI::Struct layout :TokenIsElevated, :dword end # http://msdn.microsoft.com/en-us/library/windows/desktop/aa446671(v=vs.85).aspx # BOOL WINAPI GetTokenInformation( # _In_ HANDLE TokenHandle, # _In_ TOKEN_INFORMATION_CLASS TokenInformationClass, # _Out_opt_ LPVOID TokenInformation, # _In_ DWORD TokenInformationLength, # _Out_ PDWORD ReturnLength # ); ffi_lib :advapi32 attach_function_private :GetTokenInformation, [:handle, TOKEN_INFORMATION_CLASS, :lpvoid, :dword, :pdword ], :win32_bool end diff --git a/lib/puppet/util/windows/security.rb b/lib/puppet/util/windows/security.rb index 6d07bbb1d..0a9cfebc2 100644 --- a/lib/puppet/util/windows/security.rb +++ b/lib/puppet/util/windows/security.rb @@ -1,662 +1,918 @@ # 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. # * 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 'puppet/util/windows' require 'pathname' require 'ffi' require 'win32/security' -require 'windows/file' -require 'windows/handle' -require 'windows/security' -require 'windows/process' -require 'windows/memory' -require 'windows/msvcrt/buffer' -require 'windows/volume' - module Puppet::Util::Windows::Security - include ::Windows::File - include ::Windows::Handle - include ::Windows::Security - include ::Windows::Process - include ::Windows::Memory - include ::Windows::MSVCRT::Buffer - include ::Windows::Volume - - include Puppet::Util::Windows::SID + include Puppet::Util::Windows::String extend Puppet::Util::Windows::Security extend FFI::Library # 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 S_ISYSTEM_MISSING = 04000000 # 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_RESTORE_NAME = 'SeRestorePrivilege' + + 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 - def supports_acl?(path) - flags = 0.chr * 4 + 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 =~ /[\/\\]$/ - unless GetVolumeInformation(root, nil, 0, nil, nil, flags, nil, 0) - raise Puppet::Util::Windows::Error.new("Failed to get volume information") + + 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.new("Failed to get volume information") + end + supported = flags_ptr.read_dword & FILE_PERSISTENT_ACLS == FILE_PERSISTENT_ACLS end - (flags.unpack('L')[0] & Windows::File::FILE_PERSISTENT_ACLS) != 0 + supported 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 + Puppet.deprecation_warning('Puppet::Util::Windows::Security.get_attributes is deprecated; please use Puppet::Util::Windows::File.get_attributes') + FILE.get_attributes(file_name) end def add_attributes(path, flags) - oldattrs = get_attributes(path) - - if (oldattrs | flags) != oldattrs - set_attributes(path, oldattrs | flags) - end + Puppet.deprecation_warning('Puppet::Util::Windows::Security.add_attributes is deprecated; please use Puppet::Util::Windows::File.add_attributes') + FILE.add_attributes(path, flags) end def remove_attributes(path, flags) - oldattrs = get_attributes(path) - - if (oldattrs & ~flags) != oldattrs - set_attributes(path, oldattrs & ~flags) - end + Puppet.deprecation_warning('Puppet::Util::Windows::Security.remove_attributes is deprecated; please use Puppet::Util::Windows::File.remove_attributes') + FILE.remove_attributes(path, flags) end def set_attributes(path, flags) - raise Puppet::Util::Windows::Error.new("Failed to set file attributes") unless SetFileAttributes(path, flags) + Puppet.deprecation_warning('Puppet::Util::Windows::Security.set_attributes is deprecated; please use Puppet::Util::Windows::File.set_attributes') + FILE.set_attributes(path, flags) end MASK_TO_MODE = { - FILE_GENERIC_READ => S_IROTH, - FILE_GENERIC_WRITE => S_IWOTH, - (FILE_GENERIC_EXECUTE & ~FILE_READ_ATTRIBUTES) => S_IXOTH + 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 = Win32::Security::SID::Everyone well_known_nobody_sid = Win32::Security::SID::Nobody well_known_system_sid = Win32::Security::SID::LocalSystem 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_WRITE_DATA | FILE_EXECUTE | FILE_DELETE_CHILD)) == (FILE_WRITE_DATA | FILE_EXECUTE) + 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_APPEND_DATA).nonzero? + if (ace.mask & FILE::FILE_APPEND_DATA).nonzero? mode |= S_ISVTX end when well_known_system_sid 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_GENERIC_READ, - S_IWOTH => FILE_GENERIC_WRITE, - S_IXOTH => (FILE_GENERIC_EXECUTE & ~FILE_READ_ATTRIBUTES), + 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-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) sd = get_security_descriptor(path) well_known_world_sid = Win32::Security::SID::Everyone well_known_nobody_sid = Win32::Security::SID::Nobody well_known_system_sid = Win32::Security::SID::LocalSystem - 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 + owner_allow = FILE::STANDARD_RIGHTS_ALL | + FILE::FILE_READ_ATTRIBUTES | + FILE::FILE_WRITE_ATTRIBUTES + 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 if (mode & S_ISVTX).nonzero? - nobody_allow |= FILE_APPEND_DATA; + nobody_allow |= FILE::FILE_APPEND_DATA; end # 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_ALL_ACCESS + system_allow = FILE::FILE_ALL_ACCESS end isdir = File.directory?(path) if isdir if (mode & (S_IWUSR | S_IXUSR)) == (S_IWUSR | S_IXUSR) - owner_allow |= FILE_DELETE_CHILD + owner_allow |= FILE::FILE_DELETE_CHILD end if (mode & (S_IWGRP | S_IXGRP)) == (S_IWGRP | S_IXGRP) && (mode & S_ISVTX) == 0 - group_allow |= FILE_DELETE_CHILD + group_allow |= FILE::FILE_DELETE_CHILD end if (mode & (S_IWOTH | S_IXOTH)) == (S_IWOTH | S_IXOTH) && (mode & S_ISVTX) == 0 - other_allow |= FILE_DELETE_CHILD + other_allow |= FILE::FILE_DELETE_CHILD end end # if owner and group the same, then map group permissions to the one owner ACE isownergroup = sd.owner == sd.group 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) + if ((owner_allow | group_allow | other_allow ) & FILE::FILE_WRITE_DATA) == FILE::FILE_WRITE_DATA + FILE.remove_attributes(path, FILE::FILE_ATTRIBUTE_READONLY) end 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? dacl.allow(well_known_system_sid, system_allow) # 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_ACE | CONTAINER_INHERIT_ACE + inherit = inherit_only | Puppet::Util::Windows::AccessControlEntry::CONTAINER_INHERIT_ACE dacl.allow(Win32::Security::SID::CreatorOwner, owner_allow, inherit) dacl.allow(Win32::Security::SID::CreatorGroup, group_allow, inherit) - inherit = INHERIT_ONLY_ACE | OBJECT_INHERIT_ACE - dacl.allow(Win32::Security::SID::CreatorOwner, owner_allow & ~FILE_EXECUTE, inherit) - dacl.allow(Win32::Security::SID::CreatorGroup, group_allow & ~FILE_EXECUTE, inherit) + inherit = inherit_only | Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE + dacl.allow(Win32::Security::SID::CreatorOwner, owner_allow & ~FILE::FILE_EXECUTE, inherit) + dacl.allow(Win32::Security::SID::CreatorGroup, group_allow & ~FILE::FILE_EXECUTE, 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 - string_to_sid_ptr(sid) do |sid_ptr| + 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.new("Invalid SID") end - unless AddAccessAllowedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr.address) + if AddAccessAllowedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("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 - string_to_sid_ptr(sid) do |sid_ptr| + 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.new("Invalid SID") end - unless AddAccessDeniedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr.address) + if AddAccessDeniedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("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 - 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] + if IsValidAcl(dacl_ptr) == FFI::WIN32_FALSE + raise Puppet::Util::Windows::Error.new("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| - 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') - - case ace_type - when ACCESS_ALLOWED_ACE_TYPE - - sid_ptr = FFI::Pointer.new(:pointer, ace_ptr.unpack('L')[0] + 8) # address of ace_ptr->SidStart - if Puppet::Util::Windows::SID.IsValidSid(sid_ptr) == FFI::WIN32_FALSE - raise Puppet::Util::Windows::Error.new("Failed to read DACL, invalid SID") + 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#{ace_type.to_s(16)}" + next end - sid = sid_ptr_to_string(sid_ptr) - dacl.allow(sid, mask, ace_flags) - when ACCESS_DENIED_ACE_TYPE - sid_ptr = FFI::Pointer.new(:pointer, ace_ptr.unpack('L')[0] + 8) # address of ace_ptr->SidStart - if Puppet::Util::Windows::SID.IsValidSid(sid_ptr) == FFI::WIN32_FALSE - raise Puppet::Util::Windows::Error.new("Failed to read DACL, invalid SID") + + # 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 - sid = sid_ptr_to_string(sid_ptr) - dacl.deny(sid, mask, ace_flags) - else - Puppet.warning "Unsupported access control entry type: 0x#{ace_type.to_s(16)}" end end dacl end + INVALID_HANDLE_VALUE = FFI::Pointer.new(-1).address + # 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, + def open_file(path, access, &block) + handle = CreateFileW( + wide_string(path), access, - FILE_SHARE_READ | FILE_SHARE_WRITE, - 0, # security_attributes - OPEN_EXISTING, - FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, - 0) # template + 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 + raise Puppet::Util::Windows::Error.new("Failed to open '#{path}'") if handle == INVALID_HANDLE_VALUE begin yield handle ensure - CloseHandle(handle) + 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) + 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? - 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") + 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.new("Failed to adjust process privileges") + end + end + end 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] + # token / luid structs freed by this point, so return true as nothing raised + true + end + def with_process_token(access, &block) + Puppet.deprecation_warning('Puppet::Util::Windows::Security.with_process_token is deprecated; please use Puppet::Util::Windows::Process.get_process_token') + Puppet::Util::Windows::Process.with_process_token(access) do |token| yield token - ensure - CloseHandle(token) end + + nil end def get_security_descriptor(path) sd = nil with_privilege(SE_BACKUP_NAME) do open_file(path, READ_CONTROL) do |handle| - owner_sid = [0].pack('L') - group_sid = [0].pack('L') - dacl = [0].pack('L') - ppsd = [0].pack('L') - - rv = GetSecurityInfo( - handle, - SE_FILE_OBJECT, - OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, - owner_sid, - group_sid, - dacl, - nil, #sacl - ppsd) #sec desc - raise Puppet::Util::Windows::Error.new("Failed to get security information") unless rv == ERROR_SUCCESS - - begin - owner = sid_ptr_to_string(FFI::Pointer.new(:pointer, owner_sid.unpack('L')[0])) - group = sid_ptr_to_string(FFI::Pointer.new(:pointer, group_sid.unpack('L')[0])) - - FFI::MemoryPointer.new(:word, 1) do |control| - FFI::MemoryPointer.new(:dword, 1) do |revision| - ffsd = FFI::Pointer.new(:pointer, ppsd.unpack('L')[0]) - - if GetSecurityDescriptorControl(ffsd, control, revision) == FFI::WIN32_FALSE - raise Puppet::Util::Windows::Error.new("Failed to get security descriptor control") + 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.new("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.new("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 - - protect = (control.read_uint16 & SE_DACL_PROTECTED) == SE_DACL_PROTECTED - dacl = parse_dacl(dacl.unpack('L')[0]) - sd = Puppet::Util::Windows::SecurityDescriptor.new(owner, group, dacl, protect) end - end - ensure - LocalFree(ppsd.unpack('L')[0]) + end end end end sd end + def get_max_generic_acl_size(ace_count) + # http://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) - # REMIND: FFI - acl = 0.chr * 1024 # This can be increased later as neede - unless InitializeAcl(acl, acl.size, ACL_REVISION) - raise Puppet::Util::Windows::Error.new("Failed to initialize ACL") - end + 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.new("Failed to initialize ACL") + end - raise Puppet::Util::Windows::Error.new("Invalid DACL") unless IsValidAcl(acl) + if IsValidAcl(acl_ptr) == FFI::WIN32_FALSE + raise Puppet::Util::Windows::Error.new("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| - string_to_sid_ptr(sd.owner) do |ownersid| - string_to_sid_ptr(sd.group) do |groupsid| - sd.dacl.each do |ace| - case ace.type - when ACCESS_ALLOWED_ACE_TYPE - #puts "ace: allow, sid #{sid_to_name(ace.sid)}, mask 0x#{ace.mask.to_s(16)}" - add_access_allowed_ace(acl, ace.mask, ace.sid, ace.flags) - when ACCESS_DENIED_ACE_TYPE - #puts "ace: deny, sid #{sid_to_name(ace.sid)}, mask 0x#{ace.mask.to_s(16)}" - add_access_denied_ace(acl, ace.mask, ace.sid, ace.flags) - else - raise "We should never get here" - # TODO: this should have been a warning in an earlier commit + 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 - 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, - ownersid.address, - groupsid.address, - acl, - nil) - raise Puppet::Util::Windows::Error.new("Failed to set security information") unless rv == ERROR_SUCCESS + # 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.new("Failed to set security information") + end + end end end end end end + + def name_to_sid(name) + Puppet.deprecation_warning('Puppet::Util::Windows::Security.name_to_sid is deprecated; please use Puppet::Util::Windows::SID.name_to_sid') + Puppet::Util::Windows::SID.name_to_sid(name) + end + + def name_to_sid_object(name) + Puppet.deprecation_warning('Puppet::Util::Windows::Security.name_to_sid_object is deprecated; please use Puppet::Util::Windows::SID.name_to_sid_object') + Puppet::Util::Windows::SID.name_to_sid_object(name) + end + + def octet_string_to_sid_object(bytes) + Puppet.deprecation_warning('Puppet::Util::Windows::Security.octet_string_to_sid_object is deprecated; please use Puppet::Util::Windows::SID.octet_string_to_sid_object') + Puppet::Util::Windows::SID.octet_string_to_sid_object(bytes) + end + + def sid_to_name(value) + Puppet.deprecation_warning('Puppet::Util::Windows::Security.sid_to_name is deprecated; please use Puppet::Util::Windows::SID.sid_to_name') + Puppet::Util::Windows::SID.sid_to_name(value) + end + + def sid_ptr_to_string(psid) + Puppet.deprecation_warning('Puppet::Util::Windows::Security.sid_ptr_to_string is deprecated; please use Puppet::Util::Windows::SID.sid_ptr_to_string') + Puppet::Util::Windows::SID.sid_ptr_to_string(psid) + end + + def string_to_sid_ptr(string_sid, &block) + Puppet.deprecation_warning('Puppet::Util::Windows::Security.string_to_sid_ptr is deprecated; please use Puppet::Util::Windows::SID.string_to_sid_ptr') + Puppet::Util::Windows::SID.string_to_sid_ptr(string_sid, &block) + end + + def valid_sid?(string_sid) + Puppet.deprecation_warning('Puppet::Util::Windows::Security.valid_sid? is deprecated; please use Puppet::Util::Windows::SID.valid_sid?') + Puppet::Util::Windows::SID.valid_sid?(string_sid) + end end ffi_convention :stdcall + # http://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 + + # http://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 + + # http://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 + + # http://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 + + # http://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 + + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa374912(v=vs.85).aspx + # ACE types + # http://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 + + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa374892(v=vs.85).aspx + # ACCESS_MASK + + # http://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; + # + # http://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 + + # http://stackoverflow.com/a/1792930 + MAXIMUM_SID_BYTES_LENGTH = 68 + MAXIMUM_GENERIC_ACE_SIZE = GENERIC_ACCESS_ACE.offset_of(:SidStart) + + MAXIMUM_SID_BYTES_LENGTH + + # http://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 + + # http://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 + # http://msdn.microsoft.com/en-us/library/windows/hardware/ff556610(v=vs.85).aspx # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379561(v=vs.85).aspx # http://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 + + # http://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 + + # http://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 + + # http://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 + ) + + # http://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 + + # http://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 diff --git a/lib/puppet/util/windows/sid.rb b/lib/puppet/util/windows/sid.rb index c582f78ed..68a780f8a 100644 --- a/lib/puppet/util/windows/sid.rb +++ b/lib/puppet/util/windows/sid.rb @@ -1,155 +1,162 @@ require 'puppet/util/windows' module Puppet::Util::Windows module SID require 'ffi' extend FFI::Library # missing from Windows::Error ERROR_NONE_MAPPED = 1332 ERROR_INVALID_SID_STRUCTURE = 1337 # Convert an account name, e.g. 'Administrators' into a SID string, # e.g. 'S-1-5-32-544'. The name can be specified as 'Administrators', # 'BUILTIN\Administrators', or 'S-1-5-32-544', and will return the # SID. Returns nil if the account doesn't exist. def name_to_sid(name) sid = name_to_sid_object(name) sid ? sid.to_s : nil end + module_function :name_to_sid # Convert an account name, e.g. 'Administrators' into a SID object, # e.g. 'S-1-5-32-544'. The name can be specified as 'Administrators', # 'BUILTIN\Administrators', or 'S-1-5-32-544', and will return the # SID object. Returns nil if the account doesn't exist. def name_to_sid_object(name) # Apparently, we accept a symbol.. name = name.to_s.strip if name # if it's in SID string form, convert to user parsed_sid = Win32::Security::SID.string_to_sid(name) rescue nil parsed_sid ? Win32::Security::SID.new(parsed_sid) : Win32::Security::SID.new(name) rescue nil end + module_function :name_to_sid_object # Converts an octet string array of bytes to a SID object, # e.g. [1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0] is the representation for # S-1-5-18, the local 'SYSTEM' account. # Raises an Error for nil or non-array input. def octet_string_to_sid_object(bytes) if !bytes || !bytes.respond_to?('pack') || bytes.empty? raise Puppet::Util::Windows::Error.new("Octet string must be an array of bytes") end Win32::Security::SID.new(bytes.pack('C*')) end + module_function :octet_string_to_sid_object # Convert a SID string, e.g. "S-1-5-32-544" to a name, # e.g. 'BUILTIN\Administrators'. Returns nil if an account # for that SID does not exist. def sid_to_name(value) sid = Win32::Security::SID.new(Win32::Security::SID.string_to_sid(value)) if sid.domain and sid.domain.length > 0 "#{sid.domain}\\#{sid.account}" else sid.account end rescue nil end + module_function :sid_to_name # http://stackoverflow.com/a/1792930 - 68 bytes, 184 characters in a string MAXIMUM_SID_STRING_LENGTH = 184 # Convert a SID pointer to a SID string, e.g. "S-1-5-32-544". def sid_ptr_to_string(psid) if ! psid.instance_of?(FFI::Pointer) || IsValidSid(psid) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Invalid SID") end sid_string = nil FFI::MemoryPointer.new(:pointer, 1) do |buffer_ptr| if ConvertSidToStringSidW(psid, buffer_ptr) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Failed to convert binary SID") end buffer_ptr.read_win32_local_pointer do |wide_string_ptr| if wide_string_ptr.null? raise Puppet::Error.new("ConvertSidToStringSidW failed to allocate buffer for sid") end sid_string = wide_string_ptr.read_arbitrary_wide_string_up_to(MAXIMUM_SID_STRING_LENGTH) end end sid_string end + module_function :sid_ptr_to_string # 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. The account for this # SID may or may not exist. def string_to_sid_ptr(string_sid, &block) FFI::MemoryPointer.from_string_to_wide_string(string_sid) do |lpcwstr| FFI::MemoryPointer.new(:pointer, 1) do |sid_ptr_ptr| if ConvertStringSidToSidW(lpcwstr, sid_ptr_ptr) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Failed to convert string SID: #{string_sid}") end sid_ptr_ptr.read_win32_local_pointer do |sid_ptr| yield sid_ptr end end end # yielded sid_ptr has already had LocalFree called, nothing to return nil end + module_function :string_to_sid_ptr # Return true if the string is a valid SID, e.g. "S-1-5-32-544", false otherwise. def valid_sid?(string_sid) valid = false begin string_to_sid_ptr(string_sid) { |ptr| valid = ! ptr.nil? && ! ptr.null? } rescue Puppet::Util::Windows::Error => e raise if e.code != ERROR_INVALID_SID_STRUCTURE end valid end + module_function :valid_sid? ffi_convention :stdcall # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379151(v=vs.85).aspx # BOOL WINAPI IsValidSid( # _In_ PSID pSid # ); ffi_lib :advapi32 attach_function_private :IsValidSid, [:pointer], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/aa376399(v=vs.85).aspx # BOOL ConvertSidToStringSid( # _In_ PSID Sid, # _Out_ LPTSTR *StringSid # ); ffi_lib :advapi32 attach_function_private :ConvertSidToStringSidW, [:pointer, :pointer], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/aa376402(v=vs.85).aspx # BOOL WINAPI ConvertStringSidToSid( # _In_ LPCTSTR StringSid, # _Out_ PSID *Sid # ); ffi_lib :advapi32 attach_function_private :ConvertStringSidToSidW, [:lpcwstr, :pointer], :win32_bool end end diff --git a/spec/integration/type/file_spec.rb b/spec/integration/type/file_spec.rb index ed8a9768f..16f44a4b0 100755 --- a/spec/integration/type/file_spec.rb +++ b/spec/integration/type/file_spec.rb @@ -1,1377 +1,1377 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet_spec/files' if Puppet.features.microsoft_windows? require 'puppet/util/windows' class WindowsSecurity extend Puppet::Util::Windows::Security end end describe Puppet::Type.type(:file), :uses_checksums => true do include PuppetSpec::Files let(:catalog) { Puppet::Resource::Catalog.new } let(:path) do # we create a directory first so backups of :path that are stored in # the same directory will also be removed after the tests parent = tmpdir('file_spec') File.join(parent, 'file_testing') end let(:dir) do # we create a directory first so backups of :path that are stored in # the same directory will also be removed after the tests parent = tmpdir('file_spec') File.join(parent, 'dir_testing') end if Puppet.features.posix? def set_mode(mode, file) File.chmod(mode, file) end def get_mode(file) Puppet::FileSystem.lstat(file).mode end def get_owner(file) Puppet::FileSystem.lstat(file).uid end def get_group(file) Puppet::FileSystem.lstat(file).gid end else class SecurityHelper extend Puppet::Util::Windows::Security end def set_mode(mode, file) SecurityHelper.set_mode(mode, file) end def get_mode(file) SecurityHelper.get_mode(file) end def get_owner(file) SecurityHelper.get_owner(file) end def get_group(file) SecurityHelper.get_group(file) end def get_aces_for_path_by_sid(path, sid) SecurityHelper.get_aces_for_path_by_sid(path, sid) end end before do # stub this to not try to create state.yaml Puppet::Util::Storage.stubs(:store) end it "should not attempt to manage files that do not exist if no means of creating the file is specified" do source = tmpfile('source') catalog.add_resource described_class.new :path => source, :mode => 0755 status = catalog.apply.report.resource_statuses["File[#{source}]"] status.should_not be_failed status.should_not be_changed Puppet::FileSystem.exist?(source).should be_false end describe "when ensure is absent" do it "should remove the file if present" do FileUtils.touch(path) catalog.add_resource(described_class.new(:path => path, :ensure => :absent, :backup => :false)) report = catalog.apply.report report.resource_statuses["File[#{path}]"].should_not be_failed Puppet::FileSystem.exist?(path).should be_false end it "should do nothing if file is not present" do catalog.add_resource(described_class.new(:path => path, :ensure => :absent, :backup => :false)) report = catalog.apply.report report.resource_statuses["File[#{path}]"].should_not be_failed Puppet::FileSystem.exist?(path).should be_false end # issue #14599 it "should not fail if parts of path aren't directories" do FileUtils.touch(path) catalog.add_resource(described_class.new(:path => File.join(path,'no_such_file'), :ensure => :absent, :backup => :false)) report = catalog.apply.report report.resource_statuses["File[#{File.join(path,'no_such_file')}]"].should_not be_failed end end describe "when setting permissions" do it "should set the owner" do target = tmpfile_with_contents('target', '') owner = get_owner(target) catalog.add_resource described_class.new( :name => target, :owner => owner ) catalog.apply get_owner(target).should == owner end it "should set the group" do target = tmpfile_with_contents('target', '') group = get_group(target) catalog.add_resource described_class.new( :name => target, :group => group ) catalog.apply get_group(target).should == group end describe "when setting mode" do describe "for directories" do let(:target) { tmpdir('dir_mode') } it "should set executable bits for newly created directories" do catalog.add_resource described_class.new(:path => target, :ensure => :directory, :mode => 0600) catalog.apply (get_mode(target) & 07777).should == 0700 end it "should set executable bits for existing readable directories" do set_mode(0600, target) catalog.add_resource described_class.new(:path => target, :ensure => :directory, :mode => 0644) catalog.apply (get_mode(target) & 07777).should == 0755 end it "should not set executable bits for unreadable directories" do begin catalog.add_resource described_class.new(:path => target, :ensure => :directory, :mode => 0300) catalog.apply (get_mode(target) & 07777).should == 0300 ensure # so we can cleanup set_mode(0700, target) end end it "should set user, group, and other executable bits" do catalog.add_resource described_class.new(:path => target, :ensure => :directory, :mode => 0664) catalog.apply (get_mode(target) & 07777).should == 0775 end it "should set executable bits when overwriting a non-executable file" do target_path = tmpfile_with_contents('executable', '') set_mode(0444, target_path) catalog.add_resource described_class.new(:path => target_path, :ensure => :directory, :mode => 0666, :backup => false) catalog.apply (get_mode(target_path) & 07777).should == 0777 File.should be_directory(target_path) end end describe "for files" do it "should not set executable bits" do catalog.add_resource described_class.new(:path => path, :ensure => :file, :mode => 0666) catalog.apply (get_mode(path) & 07777).should == 0666 end it "should not set executable bits when replacing an executable directory (#10365)" do pending("bug #10365") FileUtils.mkdir(path) set_mode(0777, path) catalog.add_resource described_class.new(:path => path, :ensure => :file, :mode => 0666, :backup => false, :force => true) catalog.apply (get_mode(path) & 07777).should == 0666 end end describe "for links", :if => described_class.defaultprovider.feature?(:manages_symlinks) do let(:link) { tmpfile('link_mode') } describe "when managing links" do let(:link_target) { tmpfile('target') } before :each do FileUtils.touch(link_target) File.chmod(0444, link_target) Puppet::FileSystem.symlink(link_target, link) end it "should not set the executable bit on the link nor the target" do catalog.add_resource described_class.new(:path => link, :ensure => :link, :mode => 0666, :target => link_target, :links => :manage) catalog.apply (Puppet::FileSystem.stat(link).mode & 07777) == 0666 (Puppet::FileSystem.lstat(link_target).mode & 07777) == 0444 end it "should ignore dangling symlinks (#6856)" do File.delete(link_target) catalog.add_resource described_class.new(:path => link, :ensure => :link, :mode => 0666, :target => link_target, :links => :manage) catalog.apply Puppet::FileSystem.exist?(link).should be_false end it "should create a link to the target if ensure is omitted" do FileUtils.touch(link_target) catalog.add_resource described_class.new(:path => link, :target => link_target) catalog.apply Puppet::FileSystem.exist?(link).should be_true Puppet::FileSystem.lstat(link).ftype.should == 'link' Puppet::FileSystem.readlink(link).should == link_target end end describe "when following links" do it "should ignore dangling symlinks (#6856)" do target = tmpfile('dangling') FileUtils.touch(target) Puppet::FileSystem.symlink(target, link) File.delete(target) catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow) catalog.apply end describe "to a directory" do let(:link_target) { tmpdir('dir_target') } before :each do File.chmod(0600, link_target) Puppet::FileSystem.symlink(link_target, link) end after :each do File.chmod(0750, link_target) end describe "that is readable" do it "should set the executable bits when creating the destination (#10315)" do catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow) catalog.apply File.should be_directory(path) (get_mode(path) & 07777).should == 0777 end it "should set the executable bits when overwriting the destination (#10315)" do FileUtils.touch(path) catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow, :backup => false) catalog.apply File.should be_directory(path) (get_mode(path) & 07777).should == 0777 end end describe "that is not readable" do before :each do set_mode(0300, link_target) end # so we can cleanup after :each do set_mode(0700, link_target) end it "should set executable bits when creating the destination (#10315)" do catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow) catalog.apply File.should be_directory(path) (get_mode(path) & 07777).should == 0777 end it "should set executable bits when overwriting the destination" do FileUtils.touch(path) catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow, :backup => false) catalog.apply File.should be_directory(path) (get_mode(path) & 07777).should == 0777 end end end describe "to a file" do let(:link_target) { tmpfile('file_target') } before :each do FileUtils.touch(link_target) Puppet::FileSystem.symlink(link_target, link) end it "should create the file, not a symlink (#2817, #10315)" do catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow) catalog.apply File.should be_file(path) (get_mode(path) & 07777).should == 0600 end it "should overwrite the file" do FileUtils.touch(path) catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow) catalog.apply File.should be_file(path) (get_mode(path) & 07777).should == 0600 end end describe "to a link to a directory" do let(:real_target) { tmpdir('real_target') } let(:target) { tmpfile('target') } before :each do File.chmod(0666, real_target) # link -> target -> real_target Puppet::FileSystem.symlink(real_target, target) Puppet::FileSystem.symlink(target, link) end after :each do File.chmod(0750, real_target) end describe "when following all links" do it "should create the destination and apply executable bits (#10315)" do catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow) catalog.apply File.should be_directory(path) (get_mode(path) & 07777).should == 0700 end it "should overwrite the destination and apply executable bits" do FileUtils.mkdir(path) catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow) catalog.apply File.should be_directory(path) (get_mode(path) & 0111).should == 0100 end end end end end end end describe "when writing files" do with_digest_algorithms do it "should backup files to a filebucket when one is configured" do filebucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" file = described_class.new :path => path, :backup => "mybucket", :content => "foo" catalog.add_resource file catalog.add_resource filebucket File.open(file[:path], "w") { |f| f.write("bar") } d = digest(IO.binread(file[:path])) catalog.apply filebucket.bucket.getfile(d).should == "bar" end it "should backup files in the local directory when a backup string is provided" do file = described_class.new :path => path, :backup => ".bak", :content => "foo" catalog.add_resource file File.open(file[:path], "w") { |f| f.puts "bar" } catalog.apply backup = file[:path] + ".bak" Puppet::FileSystem.exist?(backup).should be_true File.read(backup).should == "bar\n" end it "should fail if no backup can be performed" do dir = tmpdir("backups") file = described_class.new :path => File.join(dir, "testfile"), :backup => ".bak", :content => "foo" catalog.add_resource file File.open(file[:path], 'w') { |f| f.puts "bar" } # Create a directory where the backup should be so that writing to it fails Dir.mkdir(File.join(dir, "testfile.bak")) Puppet::Util::Log.stubs(:newmessage) catalog.apply File.read(file[:path]).should == "bar\n" end it "should not backup symlinks", :if => described_class.defaultprovider.feature?(:manages_symlinks) do link = tmpfile("link") dest1 = tmpfile("dest1") dest2 = tmpfile("dest2") bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" file = described_class.new :path => link, :target => dest2, :ensure => :link, :backup => "mybucket" catalog.add_resource file catalog.add_resource bucket File.open(dest1, "w") { |f| f.puts "whatever" } Puppet::FileSystem.symlink(dest1, link) d = digest(File.read(file[:path])) catalog.apply Puppet::FileSystem.readlink(link).should == dest2 Puppet::FileSystem.exist?(bucket[:path]).should be_false end it "should backup directories to the local filesystem by copying the whole directory" do file = described_class.new :path => path, :backup => ".bak", :content => "foo", :force => true catalog.add_resource file Dir.mkdir(path) otherfile = File.join(path, "foo") File.open(otherfile, "w") { |f| f.print "yay" } catalog.apply backup = "#{path}.bak" FileTest.should be_directory(backup) File.read(File.join(backup, "foo")).should == "yay" end it "should backup directories to filebuckets by backing up each file separately" do bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" file = described_class.new :path => tmpfile("bucket_backs"), :backup => "mybucket", :content => "foo", :force => true catalog.add_resource file catalog.add_resource bucket Dir.mkdir(file[:path]) foofile = File.join(file[:path], "foo") barfile = File.join(file[:path], "bar") File.open(foofile, "w") { |f| f.print "fooyay" } File.open(barfile, "w") { |f| f.print "baryay" } food = digest(File.read(foofile)) bard = digest(File.read(barfile)) catalog.apply bucket.bucket.getfile(food).should == "fooyay" bucket.bucket.getfile(bard).should == "baryay" end end end describe "when recursing" do def build_path(dir) Dir.mkdir(dir) File.chmod(0750, dir) @dirs = [dir] @files = [] %w{one two}.each do |subdir| fdir = File.join(dir, subdir) Dir.mkdir(fdir) File.chmod(0750, fdir) @dirs << fdir %w{three}.each do |file| ffile = File.join(fdir, file) @files << ffile File.open(ffile, "w") { |f| f.puts "test #{file}" } File.chmod(0640, ffile) end end end it "should be able to recurse over a nonexistent file" do @file = described_class.new( :name => path, :mode => 0644, :recurse => true, :backup => false ) catalog.add_resource @file lambda { @file.eval_generate }.should_not raise_error end it "should be able to recursively set properties on existing files" do path = tmpfile("file_integration_tests") build_path(path) file = described_class.new( :name => path, :mode => 0644, :recurse => true, :backup => false ) catalog.add_resource file catalog.apply @dirs.should_not be_empty @dirs.each do |path| (get_mode(path) & 007777).should == 0755 end @files.should_not be_empty @files.each do |path| (get_mode(path) & 007777).should == 0644 end end it "should be able to recursively make links to other files", :if => described_class.defaultprovider.feature?(:manages_symlinks) do source = tmpfile("file_link_integration_source") build_path(source) dest = tmpfile("file_link_integration_dest") @file = described_class.new(:name => dest, :target => source, :recurse => true, :ensure => :link, :backup => false) catalog.add_resource @file catalog.apply @dirs.each do |path| link_path = path.sub(source, dest) Puppet::FileSystem.lstat(link_path).should be_directory end @files.each do |path| link_path = path.sub(source, dest) Puppet::FileSystem.lstat(link_path).ftype.should == "link" end end it "should be able to recursively copy files" do source = tmpfile("file_source_integration_source") build_path(source) dest = tmpfile("file_source_integration_dest") @file = described_class.new(:name => dest, :source => source, :recurse => true, :backup => false) catalog.add_resource @file catalog.apply @dirs.each do |path| newpath = path.sub(source, dest) Puppet::FileSystem.lstat(newpath).should be_directory end @files.each do |path| newpath = path.sub(source, dest) Puppet::FileSystem.lstat(newpath).ftype.should == "file" end end it "should not recursively manage files managed by a more specific explicit file" do dir = tmpfile("recursion_vs_explicit_1") subdir = File.join(dir, "subdir") file = File.join(subdir, "file") FileUtils.mkdir_p(subdir) File.open(file, "w") { |f| f.puts "" } base = described_class.new(:name => dir, :recurse => true, :backup => false, :mode => "755") sub = described_class.new(:name => subdir, :recurse => true, :backup => false, :mode => "644") catalog.add_resource base catalog.add_resource sub catalog.apply (get_mode(file) & 007777).should == 0644 end it "should recursively manage files even if there is an explicit file whose name is a prefix of the managed file" do managed = File.join(path, "file") generated = File.join(path, "file_with_a_name_starting_with_the_word_file") managed_mode = 0700 FileUtils.mkdir_p(path) FileUtils.touch(managed) FileUtils.touch(generated) catalog.add_resource described_class.new(:name => path, :recurse => true, :backup => false, :mode => managed_mode) catalog.add_resource described_class.new(:name => managed, :recurse => true, :backup => false, :mode => "644") catalog.apply (get_mode(generated) & 007777).should == managed_mode end describe "when recursing remote directories" do describe "when sourceselect first" do describe "for a directory" do it "should recursively copy the first directory that exists" do one = File.expand_path('thisdoesnotexist') two = tmpdir('two') FileUtils.mkdir_p(File.join(two, 'three')) FileUtils.touch(File.join(two, 'three', 'four')) catalog.add_resource Puppet::Type.newfile( :path => path, :ensure => :directory, :backup => false, :recurse => true, :sourceselect => :first, :source => [one, two] ) catalog.apply File.should be_directory(path) Puppet::FileSystem.exist?(File.join(path, 'one')).should be_false Puppet::FileSystem.exist?(File.join(path, 'three', 'four')).should be_true end it "should recursively copy an empty directory" do one = File.expand_path('thisdoesnotexist') two = tmpdir('two') three = tmpdir('three') file_in_dir_with_contents(three, 'a', '') catalog.add_resource Puppet::Type.newfile( :path => path, :ensure => :directory, :backup => false, :recurse => true, :sourceselect => :first, :source => [one, two, three] ) catalog.apply File.should be_directory(path) Puppet::FileSystem.exist?(File.join(path, 'a')).should be_false end it "should only recurse one level" do one = tmpdir('one') FileUtils.mkdir_p(File.join(one, 'a', 'b')) FileUtils.touch(File.join(one, 'a', 'b', 'c')) two = tmpdir('two') FileUtils.mkdir_p(File.join(two, 'z')) FileUtils.touch(File.join(two, 'z', 'y')) catalog.add_resource Puppet::Type.newfile( :path => path, :ensure => :directory, :backup => false, :recurse => true, :recurselimit => 1, :sourceselect => :first, :source => [one, two] ) catalog.apply Puppet::FileSystem.exist?(File.join(path, 'a')).should be_true Puppet::FileSystem.exist?(File.join(path, 'a', 'b')).should be_false Puppet::FileSystem.exist?(File.join(path, 'z')).should be_false end end describe "for a file" do it "should copy the first file that exists" do one = File.expand_path('thisdoesnotexist') two = tmpfile_with_contents('two', 'yay') three = tmpfile_with_contents('three', 'no') catalog.add_resource Puppet::Type.newfile( :path => path, :ensure => :file, :backup => false, :sourceselect => :first, :source => [one, two, three] ) catalog.apply File.read(path).should == 'yay' end it "should copy an empty file" do one = File.expand_path('thisdoesnotexist') two = tmpfile_with_contents('two', '') three = tmpfile_with_contents('three', 'no') catalog.add_resource Puppet::Type.newfile( :path => path, :ensure => :file, :backup => false, :sourceselect => :first, :source => [one, two, three] ) catalog.apply File.read(path).should == '' end end end describe "when sourceselect all" do describe "for a directory" do it "should recursively copy all sources from the first valid source" do dest = tmpdir('dest') one = tmpdir('one') two = tmpdir('two') three = tmpdir('three') four = tmpdir('four') file_in_dir_with_contents(one, 'a', one) file_in_dir_with_contents(two, 'a', two) file_in_dir_with_contents(two, 'b', two) file_in_dir_with_contents(three, 'a', three) file_in_dir_with_contents(three, 'c', three) obj = Puppet::Type.newfile( :path => dest, :ensure => :directory, :backup => false, :recurse => true, :sourceselect => :all, :source => [one, two, three, four] ) catalog.add_resource obj catalog.apply File.read(File.join(dest, 'a')).should == one File.read(File.join(dest, 'b')).should == two File.read(File.join(dest, 'c')).should == three end it "should only recurse one level from each valid source" do one = tmpdir('one') FileUtils.mkdir_p(File.join(one, 'a', 'b')) FileUtils.touch(File.join(one, 'a', 'b', 'c')) two = tmpdir('two') FileUtils.mkdir_p(File.join(two, 'z')) FileUtils.touch(File.join(two, 'z', 'y')) obj = Puppet::Type.newfile( :path => path, :ensure => :directory, :backup => false, :recurse => true, :recurselimit => 1, :sourceselect => :all, :source => [one, two] ) catalog.add_resource obj catalog.apply Puppet::FileSystem.exist?(File.join(path, 'a')).should be_true Puppet::FileSystem.exist?(File.join(path, 'a', 'b')).should be_false Puppet::FileSystem.exist?(File.join(path, 'z')).should be_true Puppet::FileSystem.exist?(File.join(path, 'z', 'y')).should be_false end end end end end describe "when generating resources" do before do source = tmpdir("generating_in_catalog_source") s1 = file_in_dir_with_contents(source, "one", "uno") s2 = file_in_dir_with_contents(source, "two", "dos") @file = described_class.new( :name => path, :source => source, :recurse => true, :backup => false ) catalog.add_resource @file end it "should add each generated resource to the catalog" do catalog.apply do |trans| catalog.resource(:file, File.join(path, "one")).must be_a(described_class) catalog.resource(:file, File.join(path, "two")).must be_a(described_class) end end it "should have an edge to each resource in the relationship graph" do catalog.apply do |trans| one = catalog.resource(:file, File.join(path, "one")) catalog.relationship_graph.should be_edge(@file, one) two = catalog.resource(:file, File.join(path, "two")) catalog.relationship_graph.should be_edge(@file, two) end end end describe "when copying files" do it "should be able to copy files with pound signs in their names (#285)" do source = tmpfile_with_contents("filewith#signs", "foo") dest = tmpfile("destwith#signs") catalog.add_resource described_class.new(:name => dest, :source => source) catalog.apply File.read(dest).should == "foo" end it "should be able to copy files with spaces in their names" do dest = tmpfile("destwith spaces") source = tmpfile_with_contents("filewith spaces", "foo") expected_mode = 0755 Puppet::FileSystem.chmod(expected_mode, source) catalog.add_resource described_class.new(:path => dest, :source => source) catalog.apply File.read(dest).should == "foo" (Puppet::FileSystem.stat(dest).mode & 007777).should == expected_mode end it "should be able to copy individual files even if recurse has been specified" do source = tmpfile_with_contents("source", "foo") dest = tmpfile("dest") catalog.add_resource described_class.new(:name => dest, :source => source, :recurse => true) catalog.apply File.read(dest).should == "foo" end end it "should create a file with content if ensure is omitted" do catalog.add_resource described_class.new( :path => path, :content => "this is some content, yo" ) catalog.apply File.read(path).should == "this is some content, yo" end it "should create files with content if both content and ensure are set" do file = described_class.new( :path => path, :ensure => "file", :content => "this is some content, yo" ) catalog.add_resource file catalog.apply File.read(path).should == "this is some content, yo" end it "should delete files with sources but that are set for deletion" do source = tmpfile_with_contents("source_source_with_ensure", "yay") dest = tmpfile_with_contents("source_source_with_ensure", "boo") file = described_class.new( :path => dest, :ensure => :absent, :source => source, :backup => false ) catalog.add_resource file catalog.apply Puppet::FileSystem.exist?(dest).should be_false end describe "when sourcing" do let(:source) { tmpfile_with_contents("source_default_values", "yay") } it "should apply the source metadata values" do set_mode(0770, source) file = described_class.new( :path => path, :ensure => :file, :source => source, :backup => false ) catalog.add_resource file catalog.apply get_owner(path).should == get_owner(source) get_group(path).should == get_group(source) (get_mode(path) & 07777).should == 0770 end it "should override the default metadata values" do set_mode(0770, source) file = described_class.new( :path => path, :ensure => :file, :source => source, :backup => false, :mode => 0440 ) catalog.add_resource file catalog.apply (get_mode(path) & 07777).should == 0440 end describe "on Windows systems", :if => Puppet.features.microsoft_windows? do def expects_sid_granted_full_access_explicitly(path, sid) inherited_ace = Windows::Security::INHERITED_ACE aces = get_aces_for_path_by_sid(path, sid) aces.should_not be_empty aces.each do |ace| ace.mask.should == Windows::File::FILE_ALL_ACCESS (ace.flags & inherited_ace).should_not == inherited_ace end end def expects_system_granted_full_access_explicitly(path) expects_sid_granted_full_access_explicitly(path, @sids[:system]) end def expects_at_least_one_inherited_ace_grants_full_access(path, sid) inherited_ace = Windows::Security::INHERITED_ACE aces = get_aces_for_path_by_sid(path, sid) aces.should_not be_empty aces.any? do |ace| ace.mask == Windows::File::FILE_ALL_ACCESS && (ace.flags & inherited_ace) == inherited_ace end.should be_true end def expects_at_least_one_inherited_system_ace_grants_full_access(path) expects_at_least_one_inherited_ace_grants_full_access(path, @sids[:system]) end it "should provide valid default values when ACLs are not supported" do Puppet::Util::Windows::Security.stubs(:supports_acl?).returns(false) Puppet::Util::Windows::Security.stubs(:supports_acl?).with(source).returns false file = described_class.new( :path => path, :ensure => :file, :source => source, :backup => false ) catalog.add_resource file catalog.apply get_owner(path).should =~ /^S\-1\-5\-.*$/ get_group(path).should =~ /^S\-1\-0\-0.*$/ get_mode(path).should == 0644 end describe "when processing SYSTEM ACEs" do before do @sids = { - :current_user => Puppet::Util::Windows::Security.name_to_sid(Sys::Admin.get_login), + :current_user => Puppet::Util::Windows::SID.name_to_sid(Sys::Admin.get_login), :system => Win32::Security::SID::LocalSystem, - :admin => Puppet::Util::Windows::Security.name_to_sid("Administrator"), - :guest => Puppet::Util::Windows::Security.name_to_sid("Guest"), + :admin => Puppet::Util::Windows::SID.name_to_sid("Administrator"), + :guest => Puppet::Util::Windows::SID.name_to_sid("Guest"), :users => Win32::Security::SID::BuiltinUsers, :power_users => Win32::Security::SID::PowerUsers, :none => Win32::Security::SID::Nobody } end describe "on files" do before :each do @file = described_class.new( :path => path, :ensure => :file, :source => source, :backup => false ) catalog.add_resource @file end describe "when source permissions are ignored" do before :each do @file[:source_permissions] = :ignore end it "preserves the inherited SYSTEM ACE" do catalog.apply expects_at_least_one_inherited_system_ace_grants_full_access(path) end end describe "when permissions are insync?" do it "preserves the explicit SYSTEM ACE" do FileUtils.touch(path) sd = Puppet::Util::Windows::Security.get_security_descriptor(path) sd.protect = true sd.owner = @sids[:none] sd.group = @sids[:none] Puppet::Util::Windows::Security.set_security_descriptor(source, sd) Puppet::Util::Windows::Security.set_security_descriptor(path, sd) catalog.apply expects_system_granted_full_access_explicitly(path) end end describe "when permissions are not insync?" do before :each do @file[:owner] = 'None' @file[:group] = 'None' end it "replaces inherited SYSTEM ACEs with an uninherited one for an existing file" do FileUtils.touch(path) expects_at_least_one_inherited_system_ace_grants_full_access(path) catalog.apply expects_system_granted_full_access_explicitly(path) end it "replaces inherited SYSTEM ACEs for a new file with an uninherited one" do catalog.apply expects_system_granted_full_access_explicitly(path) end end describe "created with SYSTEM as the group" do before :each do @file[:owner] = @sids[:users] @file[:group] = @sids[:system] @file[:mode] = 0644 catalog.apply end it "should allow the user to explicitly set the mode to 4" do system_aces = get_aces_for_path_by_sid(path, @sids[:system]) system_aces.should_not be_empty system_aces.each do |ace| ace.mask.should == Windows::File::FILE_GENERIC_READ end end it "prepends SYSTEM ace when changing group from system to power users" do @file[:group] = @sids[:power_users] catalog.apply system_aces = get_aces_for_path_by_sid(path, @sids[:system]) system_aces.size.should == 1 end end describe "with :links set to :follow" do it "should not fail to apply" do # at minimal, we need an owner and/or group @file[:owner] = @sids[:users] @file[:links] = :follow catalog.apply do |transaction| if transaction.any_failed? pretty_transaction_error(transaction) end end end end end describe "on directories" do before :each do @directory = described_class.new( :path => dir, :ensure => :directory ) catalog.add_resource @directory end def grant_everyone_full_access(path) sd = Puppet::Util::Windows::Security.get_security_descriptor(path) sd.dacl.allow( 'S-1-1-0', #everyone Windows::File::FILE_ALL_ACCESS, Windows::File::OBJECT_INHERIT_ACE | Windows::File::CONTAINER_INHERIT_ACE) Puppet::Util::Windows::Security.set_security_descriptor(path, sd) end after :each do grant_everyone_full_access(dir) end describe "when source permissions are ignored" do before :each do @directory[:source_permissions] = :ignore end it "preserves the inherited SYSTEM ACE" do catalog.apply expects_at_least_one_inherited_system_ace_grants_full_access(dir) end end describe "when permissions are insync?" do it "preserves the explicit SYSTEM ACE" do Dir.mkdir(dir) source_dir = tmpdir('source_dir') @directory[:source] = source_dir sd = Puppet::Util::Windows::Security.get_security_descriptor(source_dir) sd.protect = true sd.owner = @sids[:none] sd.group = @sids[:none] Puppet::Util::Windows::Security.set_security_descriptor(source_dir, sd) Puppet::Util::Windows::Security.set_security_descriptor(dir, sd) catalog.apply expects_system_granted_full_access_explicitly(dir) end end describe "when permissions are not insync?" do before :each do @directory[:owner] = 'None' @directory[:group] = 'None' @directory[:mode] = 0444 end it "replaces inherited SYSTEM ACEs with an uninherited one for an existing directory" do FileUtils.mkdir(dir) expects_at_least_one_inherited_system_ace_grants_full_access(dir) catalog.apply expects_system_granted_full_access_explicitly(dir) end it "replaces inherited SYSTEM ACEs with an uninherited one for an existing directory" do catalog.apply expects_system_granted_full_access_explicitly(dir) end describe "created with SYSTEM as the group" do before :each do @directory[:owner] = @sids[:users] @directory[:group] = @sids[:system] @directory[:mode] = 0644 catalog.apply end it "should allow the user to explicitly set the mode to 4" do system_aces = get_aces_for_path_by_sid(dir, @sids[:system]) system_aces.should_not be_empty system_aces.each do |ace| # unlike files, Puppet sets execute bit on directories that are readable ace.mask.should == Windows::File::FILE_GENERIC_READ | Windows::File::FILE_GENERIC_EXECUTE end end it "prepends SYSTEM ace when changing group from system to power users" do @directory[:group] = @sids[:power_users] catalog.apply system_aces = get_aces_for_path_by_sid(dir, @sids[:system]) system_aces.size.should == 1 end end describe "with :links set to :follow" do it "should not fail to apply" do # at minimal, we need an owner and/or group @directory[:owner] = @sids[:users] @directory[:links] = :follow catalog.apply do |transaction| if transaction.any_failed? pretty_transaction_error(transaction) end end end end end end end end end describe "when purging files" do before do sourcedir = tmpdir("purge_source") destdir = tmpdir("purge_dest") sourcefile = File.join(sourcedir, "sourcefile") @copiedfile = File.join(destdir, "sourcefile") @localfile = File.join(destdir, "localfile") @purgee = File.join(destdir, "to_be_purged") File.open(@localfile, "w") { |f| f.print "oldtest" } File.open(sourcefile, "w") { |f| f.print "funtest" } # this file should get removed File.open(@purgee, "w") { |f| f.print "footest" } lfobj = Puppet::Type.newfile( :title => "localfile", :path => @localfile, :content => "rahtest", :ensure => :file, :backup => false ) destobj = Puppet::Type.newfile( :title => "destdir", :path => destdir, :source => sourcedir, :backup => false, :purge => true, :recurse => true ) catalog.add_resource lfobj, destobj catalog.apply end it "should still copy remote files" do File.read(@copiedfile).should == 'funtest' end it "should not purge managed, local files" do File.read(@localfile).should == 'rahtest' end it "should purge files that are neither remote nor otherwise managed" do Puppet::FileSystem.exist?(@purgee).should be_false end end describe "when using validate_cmd" do it "should fail the file resource if command fails" do catalog.add_resource(described_class.new(:path => path, :content => "foo", :validate_cmd => "/usr/bin/env false")) Puppet::Util::Execution.expects(:execute).with("/usr/bin/env false", {:combine => true, :failonfail => true}).raises(Puppet::ExecutionFailure, "Failed") report = catalog.apply.report report.resource_statuses["File[#{path}]"].should be_failed Puppet::FileSystem.exist?(path).should be_false end it "should succeed the file resource if command succeeds" do catalog.add_resource(described_class.new(:path => path, :content => "foo", :validate_cmd => "/usr/bin/env true")) Puppet::Util::Execution.expects(:execute).with("/usr/bin/env true", {:combine => true, :failonfail => true}).returns '' report = catalog.apply.report report.resource_statuses["File[#{path}]"].should_not be_failed Puppet::FileSystem.exist?(path).should be_true end end def tmpfile_with_contents(name, contents) file = tmpfile(name) File.open(file, "w") { |f| f.write contents } file end def file_in_dir_with_contents(dir, name, contents) full_name = File.join(dir, name) File.open(full_name, "w") { |f| f.write contents } full_name end def pretty_transaction_error(transaction) report = transaction.report status_failures = report.resource_statuses.values.select { |r| r.failed? } status_fail_msg = status_failures. collect(&:events). flatten. select { |event| event.status == 'failure' }. collect { |event| "#{event.resource}: #{event.message}" }.join("; ") raise "Got #{status_failures.length} failure(s) while applying: #{status_fail_msg}" end end diff --git a/spec/integration/util/windows/security_spec.rb b/spec/integration/util/windows/security_spec.rb index 46edd96b8..5aa41f0aa 100755 --- a/spec/integration/util/windows/security_spec.rb +++ b/spec/integration/util/windows/security_spec.rb @@ -1,861 +1,863 @@ #!/usr/bin/env ruby require 'spec_helper' if Puppet.features.microsoft_windows? class WindowsSecurityTester require 'puppet/util/windows/security' include Puppet::Util::Windows::Security end + + FILE = Puppet::Util::Windows::File end describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_windows? do include PuppetSpec::Files before :all do @sids = { - :current_user => Puppet::Util::Windows::Security.name_to_sid(Sys::Admin.get_login), + :current_user => Puppet::Util::Windows::SID.name_to_sid(Sys::Admin.get_login), :system => Win32::Security::SID::LocalSystem, - :admin => Puppet::Util::Windows::Security.name_to_sid("Administrator"), + :admin => Puppet::Util::Windows::SID.name_to_sid("Administrator"), :administrators => Win32::Security::SID::BuiltinAdministrators, - :guest => Puppet::Util::Windows::Security.name_to_sid("Guest"), + :guest => Puppet::Util::Windows::SID.name_to_sid("Guest"), :users => Win32::Security::SID::BuiltinUsers, :power_users => Win32::Security::SID::PowerUsers, :none => Win32::Security::SID::Nobody, :everyone => Win32::Security::SID::Everyone } # The TCP/IP NetBIOS Helper service (aka 'lmhosts') has ended up # disabled on some VMs for reasons we couldn't track down. This # condition causes tests which rely on resolving UNC style paths # (like \\localhost) to fail with unhelpful error messages. # Put a check for this upfront to aid debug should this strike again. service = Puppet::Type.type(:service).new(:name => 'lmhosts') service.provider.status.should == :running end let (:sids) { @sids } let (:winsec) { WindowsSecurityTester.new } def set_group_depending_on_current_user(path) if sids[:current_user] == sids[:system] # if the current user is SYSTEM, by setting the group to # guest, SYSTEM is automagically given full control, so instead # override that behavior with SYSTEM as group and a specific mode winsec.set_group(sids[:system], path) mode = winsec.get_mode(path) winsec.set_mode(mode & ~WindowsSecurityTester::S_IRWXG, path) else winsec.set_group(sids[:guest], path) end end def grant_everyone_full_access(path) sd = winsec.get_security_descriptor(path) everyone = 'S-1-1-0' - inherit = WindowsSecurityTester::OBJECT_INHERIT_ACE | WindowsSecurityTester::CONTAINER_INHERIT_ACE - sd.dacl.allow(everyone, Windows::File::FILE_ALL_ACCESS, inherit) + inherit = Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE | Puppet::Util::Windows::AccessControlEntry::CONTAINER_INHERIT_ACE + sd.dacl.allow(everyone, FILE::FILE_ALL_ACCESS, inherit) winsec.set_security_descriptor(path, sd) end shared_examples_for "only child owner" do it "should allow child owner" do winsec.set_owner(sids[:guest], parent) winsec.set_group(sids[:current_user], parent) winsec.set_mode(0700, parent) check_delete(path) end it "should deny parent owner" do winsec.set_owner(sids[:guest], path) winsec.set_group(sids[:current_user], path) winsec.set_mode(0700, path) lambda { check_delete(path) }.should raise_error(Errno::EACCES) end it "should deny group" do winsec.set_owner(sids[:guest], path) winsec.set_group(sids[:current_user], path) winsec.set_mode(0700, path) lambda { check_delete(path) }.should raise_error(Errno::EACCES) end it "should deny other" do winsec.set_owner(sids[:guest], path) winsec.set_group(sids[:current_user], path) winsec.set_mode(0700, path) lambda { check_delete(path) }.should raise_error(Errno::EACCES) end end shared_examples_for "a securable object" do describe "on a volume that doesn't support ACLs" do [:owner, :group, :mode].each do |p| it "should return nil #{p}" do winsec.stubs(:supports_acl?).returns false winsec.send("get_#{p}", path).should be_nil end end end describe "on a volume that supports ACLs" 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 Puppet::FileSystem.exist?(path) end describe "#supports_acl?" do %w[c:/ c:\\ c:/windows/system32 \\\\localhost\\C$ \\\\127.0.0.1\\C$\\foo].each do |path| it "should accept #{path}" do winsec.should be_supports_acl(path) end end it "should raise an exception if it cannot get volume information" do expect { winsec.supports_acl?('foobar') }.to raise_error(Puppet::Error, /Failed to get volume information/) end 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 it "should preserve inherited full control for SYSTEM when setting owner and group" do # new file has SYSTEM system_aces = winsec.get_aces_for_path_by_sid(path, sids[:system]) system_aces.should_not be_empty # when running under SYSTEM account, multiple ACEs come back # so we only care that we have at least one of these system_aces.any? do |ace| - ace.mask == Windows::File::FILE_ALL_ACCESS + ace.mask == FILE::FILE_ALL_ACCESS end.should be_true # changing the owner/group will no longer make the SD protected winsec.set_group(sids[:power_users], path) winsec.set_owner(sids[:administrators], path) system_aces.find do |ace| - ace.mask == Windows::File::FILE_ALL_ACCESS && ace.inherited? + ace.mask == FILE::FILE_ALL_ACCESS && ace.inherited? end.should_not be_nil end describe "#mode=" do (0000..0700).step(0100) 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 it "should preserve full control for SYSTEM when setting mode" do # new file has SYSTEM system_aces = winsec.get_aces_for_path_by_sid(path, sids[:system]) system_aces.should_not be_empty # when running under SYSTEM account, multiple ACEs come back # so we only care that we have at least one of these system_aces.any? do |ace| - ace.mask == WindowsSecurityTester::FILE_ALL_ACCESS + ace.mask == FILE::FILE_ALL_ACCESS end.should be_true # changing the mode will make the SD protected winsec.set_group(sids[:none], path) winsec.set_mode(0600, path) # and should have a non-inherited SYSTEM ACE(s) system_aces = winsec.get_aces_for_path_by_sid(path, sids[:system]) system_aces.each do |ace| - ace.mask.should == Windows::File::FILE_ALL_ACCESS && ! ace.inherited? + ace.mask.should == FILE::FILE_ALL_ACCESS && ! ace.inherited? 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.set_group(sids[:none], path) winsec.set_mode(0600, path) - winsec.add_attributes(path, WindowsSecurityTester::FILE_ATTRIBUTE_READONLY) - (winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should be_nonzero + FILE.add_attributes(path, FILE::FILE_ATTRIBUTE_READONLY) + (FILE.get_attributes(path) & FILE::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 + (FILE.get_attributes(path) & FILE::FILE_ATTRIBUTE_READONLY).should == 0 end it "should leave them read-only if no sid has write permission and should allow full access for SYSTEM" do winsec.set_mode(WindowsSecurityTester::S_IRUSR | WindowsSecurityTester::S_IXGRP, path) - (winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should be_nonzero + (FILE.get_attributes(path) & FILE::FILE_ATTRIBUTE_READONLY).should be_nonzero system_aces = winsec.get_aces_for_path_by_sid(path, sids[:system]) # when running under SYSTEM account, and set_group / set_owner hasn't been called # SYSTEM full access will be restored system_aces.any? do |ace| - ace.mask == Windows::File::FILE_ALL_ACCESS + ace.mask == FILE::FILE_ALL_ACCESS end.should be_true 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 sd = winsec.get_security_descriptor(path) (544..547).each do |rid| - sd.dacl.allow("S-1-5-32-#{rid}", WindowsSecurityTester::STANDARD_RIGHTS_ALL) + sd.dacl.allow("S-1-5-32-#{rid}", FILE::STANDARD_RIGHTS_ALL) end winsec.set_security_descriptor(path, sd) mode = winsec.get_mode(path) (mode & WindowsSecurityTester::S_IEXTRA).should == WindowsSecurityTester::S_IEXTRA end it "should return deny aces" do sd = winsec.get_security_descriptor(path) - sd.dacl.deny(sids[:guest], WindowsSecurityTester::FILE_GENERIC_WRITE) + sd.dacl.deny(sids[:guest], FILE::FILE_GENERIC_WRITE) winsec.set_security_descriptor(path, sd) guest_aces = winsec.get_aces_for_path_by_sid(path, sids[:guest]) guest_aces.find do |ace| - ace.type == WindowsSecurityTester::ACCESS_DENIED_ACE_TYPE + ace.type == Puppet::Util::Windows::AccessControlEntry::ACCESS_DENIED_ACE_TYPE end.should_not be_nil end it "should skip inherit-only ace" do sd = winsec.get_security_descriptor(path) dacl = Puppet::Util::Windows::AccessControlList.new dacl.allow( - sids[:current_user], WindowsSecurityTester::STANDARD_RIGHTS_ALL | WindowsSecurityTester::SPECIFIC_RIGHTS_ALL + sids[:current_user], FILE::STANDARD_RIGHTS_ALL | FILE::SPECIFIC_RIGHTS_ALL ) dacl.allow( sids[:everyone], - WindowsSecurityTester::FILE_GENERIC_READ, - WindowsSecurityTester::INHERIT_ONLY_ACE | WindowsSecurityTester::OBJECT_INHERIT_ACE + FILE::FILE_GENERIC_READ, + Puppet::Util::Windows::AccessControlEntry::INHERIT_ONLY_ACE | Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE ) winsec.set_security_descriptor(path, sd) (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, and should not remove SYSTEM" do winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) mode = winsec.get_mode(path) [ WindowsSecurityTester::S_IEXTRA, WindowsSecurityTester::S_ISYSTEM_MISSING ].each do |flag| (mode & flag).should_not == flag end 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 + allow = FILE::STANDARD_RIGHTS_ALL | FILE::SPECIFIC_RIGHTS_ALL + inherit = Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE | Puppet::Util::Windows::AccessControlEntry::CONTAINER_INHERIT_ACE sd = winsec.get_security_descriptor(parent) sd.dacl.allow( "S-1-1-0", #everyone allow, inherit ) (544..547).each do |rid| sd.dacl.allow( "S-1-5-32-#{rid}", - WindowsSecurityTester::STANDARD_RIGHTS_ALL, + FILE::STANDARD_RIGHTS_ALL, inherit ) end winsec.set_security_descriptor(parent, sd) # 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_mode(WindowsSecurityTester::S_IRWXU | WindowsSecurityTester::S_IRWXG, path) set_group_depending_on_current_user(path) winsec.set_owner(sids[:guest], path) lambda { File.open(path, 'r') }.should raise_error(Errno::EACCES) end after :each do if Puppet::FileSystem.exist?(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 combine owner and group rights when they are 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 # Exclude missing system ace, since that's not relevant (winsec.get_mode(path) & 0777).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::Windows::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::Windows::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::Windows::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::Windows::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, including SYSTEM" do sd = winsec.get_security_descriptor(path) # don't allow inherited aces to affect the test protect = true new_sd = Puppet::Util::Windows::SecurityDescriptor.new(sd.owner, sd.group, [], protect) winsec.set_security_descriptor(path, new_sd) winsec.get_mode(path).should == WindowsSecurityTester::S_ISYSTEM_MISSING 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 "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 describe "is writable and executable" do describe "and sticky bit is set" do it "should allow child owner" do winsec.set_owner(sids[:guest], parent) winsec.set_group(sids[:current_user], parent) winsec.set_mode(01700, parent) check_delete(path) end it "should allow parent owner" do winsec.set_owner(sids[:current_user], parent) winsec.set_group(sids[:guest], parent) winsec.set_mode(01700, parent) winsec.set_owner(sids[:current_user], path) winsec.set_group(sids[:guest], path) winsec.set_mode(0700, path) check_delete(path) end it "should deny group" do winsec.set_owner(sids[:guest], parent) winsec.set_group(sids[:current_user], parent) winsec.set_mode(01770, parent) winsec.set_owner(sids[:guest], path) winsec.set_group(sids[:current_user], path) winsec.set_mode(0700, path) lambda { check_delete(path) }.should raise_error(Errno::EACCES) end it "should deny other" do winsec.set_owner(sids[:guest], parent) winsec.set_group(sids[:current_user], parent) winsec.set_mode(01777, parent) winsec.set_owner(sids[:guest], path) winsec.set_group(sids[:current_user], path) winsec.set_mode(0700, path) lambda { check_delete(path) }.should raise_error(Errno::EACCES) end end describe "and sticky bit is not set" do it "should allow child owner" do winsec.set_owner(sids[:guest], parent) winsec.set_group(sids[:current_user], parent) winsec.set_mode(0700, parent) check_delete(path) end it "should allow parent owner" do winsec.set_owner(sids[:current_user], parent) winsec.set_group(sids[:guest], parent) winsec.set_mode(0700, parent) winsec.set_owner(sids[:current_user], path) winsec.set_group(sids[:guest], path) winsec.set_mode(0700, path) check_delete(path) end it "should allow group" do winsec.set_owner(sids[:guest], parent) winsec.set_group(sids[:current_user], parent) winsec.set_mode(0770, parent) winsec.set_owner(sids[:guest], path) winsec.set_group(sids[:current_user], path) winsec.set_mode(0700, path) check_delete(path) end it "should allow other" do winsec.set_owner(sids[:guest], parent) winsec.set_group(sids[:current_user], parent) winsec.set_mode(0777, parent) winsec.set_owner(sids[:guest], path) winsec.set_group(sids[:current_user], path) winsec.set_mode(0700, path) check_delete(path) end end end describe "is not writable" do before :each do winsec.set_group(sids[:current_user], parent) winsec.set_mode(0555, parent) end it_behaves_like "only child owner" end describe "is not executable" do before :each do winsec.set_group(sids[:current_user], parent) winsec.set_mode(0666, parent) end it_behaves_like "only child owner" end 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 after :each do # allow temp files to be cleaned up grant_everyone_full_access(parent) 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 after :each do # allow temp files to be cleaned up grant_everyone_full_access(parent) 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| mode = winsec.get_mode(p) (mode & 07777).to_s(8).should == mode640.to_s(8) end end end end context "security descriptor" do let(:path) { tmpfile('sec_descriptor') } let(:read_execute) { 0x201FF } let(:synchronize) { 0x100000 } before :each do FileUtils.touch(path) end it "preserves aces for other users" do dacl = Puppet::Util::Windows::AccessControlList.new sids_in_dacl = [sids[:current_user], sids[:users]] sids_in_dacl.each do |sid| dacl.allow(sid, read_execute) end sd = Puppet::Util::Windows::SecurityDescriptor.new(sids[:guest], sids[:guest], dacl, true) winsec.set_security_descriptor(path, sd) aces = winsec.get_security_descriptor(path).dacl.to_a aces.map(&:sid).should == sids_in_dacl aces.map(&:mask).all? { |mask| mask == read_execute }.should be_true end it "changes the sid for all aces that were assigned to the old owner" do sd = winsec.get_security_descriptor(path) sd.owner.should_not == sids[:guest] sd.dacl.allow(sd.owner, read_execute) sd.dacl.allow(sd.owner, synchronize) sd.owner = sids[:guest] winsec.set_security_descriptor(path, sd) dacl = winsec.get_security_descriptor(path).dacl aces = dacl.find_all { |ace| ace.sid == sids[:guest] } # only non-inherited aces will be reassigned to guest, so # make sure we find at least the two we added aces.size.should >= 2 end it "preserves INHERIT_ONLY_ACEs" do # inherit only aces can only be set on directories dir = tmpdir('inheritonlyace') inherit_flags = Puppet::Util::Windows::AccessControlEntry::INHERIT_ONLY_ACE | Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE | Puppet::Util::Windows::AccessControlEntry::CONTAINER_INHERIT_ACE sd = winsec.get_security_descriptor(dir) - sd.dacl.allow(sd.owner, Windows::File::FILE_ALL_ACCESS, inherit_flags) + sd.dacl.allow(sd.owner, FILE::FILE_ALL_ACCESS, inherit_flags) winsec.set_security_descriptor(dir, sd) sd = winsec.get_security_descriptor(dir) winsec.set_owner(sids[:guest], dir) sd = winsec.get_security_descriptor(dir) sd.dacl.find do |ace| ace.sid == sids[:guest] && ace.inherit_only? end.should_not be_nil end it "allows deny ACEs with inheritance" do # inheritance can only be set on directories dir = tmpdir('denyaces') inherit_flags = Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE | Puppet::Util::Windows::AccessControlEntry::CONTAINER_INHERIT_ACE sd = winsec.get_security_descriptor(dir) - sd.dacl.deny(sids[:guest], Windows::File::FILE_ALL_ACCESS, inherit_flags) + sd.dacl.deny(sids[:guest], FILE::FILE_ALL_ACCESS, inherit_flags) winsec.set_security_descriptor(dir, sd) sd = winsec.get_security_descriptor(dir) sd.dacl.find do |ace| ace.sid == sids[:guest] && ace.flags != 0 end.should_not be_nil end context "when managing mode" do it "removes aces for sids that are neither the owner nor group" do # add a guest ace, it's never owner or group sd = winsec.get_security_descriptor(path) sd.dacl.allow(sids[:guest], read_execute) winsec.set_security_descriptor(path, sd) # setting the mode, it should remove extra aces winsec.set_mode(0770, path) # make sure it's gone dacl = winsec.get_security_descriptor(path).dacl aces = dacl.find_all { |ace| ace.sid == sids[:guest] } aces.should be_empty end end end end diff --git a/spec/unit/provider/file/windows_spec.rb b/spec/unit/provider/file/windows_spec.rb index a0e9d3a4e..f6d7ef0e7 100755 --- a/spec/unit/provider/file/windows_spec.rb +++ b/spec/unit/provider/file/windows_spec.rb @@ -1,154 +1,154 @@ #! /usr/bin/env ruby require 'spec_helper' if Puppet.features.microsoft_windows? require 'puppet/util/windows' class WindowsSecurity extend Puppet::Util::Windows::Security end end describe Puppet::Type.type(:file).provider(:windows), :if => Puppet.features.microsoft_windows? do include PuppetSpec::Files let(:path) { tmpfile('windows_file_spec') } let(:resource) { Puppet::Type.type(:file).new :path => path, :mode => 0777, :provider => described_class.name } let(:provider) { resource.provider } let(:sid) { 'S-1-1-50' } let(:account) { 'quinn' } describe "#mode" do it "should return a string with the higher-order bits stripped away" do FileUtils.touch(path) WindowsSecurity.set_mode(0644, path) provider.mode.should == '644' end it "should return absent if the file doesn't exist" do provider.mode.should == :absent end end describe "#mode=" do it "should chmod the file to the specified value" do FileUtils.touch(path) WindowsSecurity.set_mode(0644, path) provider.mode = '0755' provider.mode.should == '755' end it "should pass along any errors encountered" do expect do provider.mode = '644' end.to raise_error(Puppet::Error, /failed to set mode/) end end describe "#id2name" do it "should return the name of the user identified by the sid" do - Puppet::Util::Windows::Security.expects(:valid_sid?).with(sid).returns(true) - Puppet::Util::Windows::Security.expects(:sid_to_name).with(sid).returns(account) + Puppet::Util::Windows::SID.expects(:valid_sid?).with(sid).returns(true) + Puppet::Util::Windows::SID.expects(:sid_to_name).with(sid).returns(account) provider.id2name(sid).should == account end it "should return the argument if it's already a name" do - Puppet::Util::Windows::Security.expects(:valid_sid?).with(account).returns(false) - Puppet::Util::Windows::Security.expects(:sid_to_name).never + Puppet::Util::Windows::SID.expects(:valid_sid?).with(account).returns(false) + Puppet::Util::Windows::SID.expects(:sid_to_name).never provider.id2name(account).should == account end it "should return nil if the user doesn't exist" do - Puppet::Util::Windows::Security.expects(:valid_sid?).with(sid).returns(true) - Puppet::Util::Windows::Security.expects(:sid_to_name).with(sid).returns(nil) + Puppet::Util::Windows::SID.expects(:valid_sid?).with(sid).returns(true) + Puppet::Util::Windows::SID.expects(:sid_to_name).with(sid).returns(nil) provider.id2name(sid).should == nil end end describe "#name2id" do it "should delegate to name_to_sid" do - Puppet::Util::Windows::Security.expects(:name_to_sid).with(account).returns(sid) + Puppet::Util::Windows::SID.expects(:name_to_sid).with(account).returns(sid) provider.name2id(account).should == sid end end describe "#owner" do it "should return the sid of the owner if the file does exist" do FileUtils.touch(resource[:path]) provider.stubs(:get_owner).with(resource[:path]).returns(sid) provider.owner.should == sid end it "should return absent if the file doesn't exist" do provider.owner.should == :absent end end describe "#owner=" do it "should set the owner to the specified value" do provider.expects(:set_owner).with(sid, resource[:path]) provider.owner = sid end it "should propagate any errors encountered when setting the owner" do provider.stubs(:set_owner).raises(ArgumentError) expect { provider.owner = sid }.to raise_error(Puppet::Error, /Failed to set owner/) end end describe "#group" do it "should return the sid of the group if the file does exist" do FileUtils.touch(resource[:path]) provider.stubs(:get_group).with(resource[:path]).returns(sid) provider.group.should == sid end it "should return absent if the file doesn't exist" do provider.group.should == :absent end end describe "#group=" do it "should set the group to the specified value" do provider.expects(:set_group).with(sid, resource[:path]) provider.group = sid end it "should propagate any errors encountered when setting the group" do provider.stubs(:set_group).raises(ArgumentError) expect { provider.group = sid }.to raise_error(Puppet::Error, /Failed to set group/) end end describe "when validating" do {:owner => 'foo', :group => 'foo', :mode => 0777}.each do |k,v| it "should fail if the filesystem doesn't support ACLs and we're managing #{k}" do described_class.any_instance.stubs(:supports_acl?).returns false expect { Puppet::Type.type(:file).new :path => path, k => v }.to raise_error(Puppet::Error, /Can only manage owner, group, and mode on filesystems that support Windows ACLs, such as NTFS/) end end it "should not fail if the filesystem doesn't support ACLs and we're not managing permissions" do described_class.any_instance.stubs(:supports_acl?).returns false Puppet::Type.type(:file).new :path => path end end end diff --git a/spec/unit/provider/group/windows_adsi_spec.rb b/spec/unit/provider/group/windows_adsi_spec.rb index 2b505b60a..7c9366f72 100644 --- a/spec/unit/provider/group/windows_adsi_spec.rb +++ b/spec/unit/provider/group/windows_adsi_spec.rb @@ -1,168 +1,168 @@ #!/usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:group).provider(:windows_adsi), :if => Puppet.features.microsoft_windows? do let(:resource) do Puppet::Type.type(:group).new( :title => 'testers', :provider => :windows_adsi ) end let(:provider) { resource.provider } let(:connection) { stub 'connection' } before :each do Puppet::Util::Windows::ADSI.stubs(:computer_name).returns('testcomputername') Puppet::Util::Windows::ADSI.stubs(:connect).returns connection end describe ".instances" do it "should enumerate all groups" do names = ['group1', 'group2', 'group3'] stub_groups = names.map{|n| stub(:name => n)} connection.stubs(:execquery).with('select name from win32_group where localaccount = "TRUE"').returns stub_groups described_class.instances.map(&:name).should =~ names end end describe "group type :members property helpers" do let(:user1) { stub(:account => 'user1', :domain => '.', :to_s => 'user1sid') } let(:user2) { stub(:account => 'user2', :domain => '.', :to_s => 'user2sid') } before :each do - Puppet::Util::Windows::Security.stubs(:name_to_sid_object).with('user1').returns(user1) - Puppet::Util::Windows::Security.stubs(:name_to_sid_object).with('user2').returns(user2) + Puppet::Util::Windows::SID.stubs(:name_to_sid_object).with('user1').returns(user1) + Puppet::Util::Windows::SID.stubs(:name_to_sid_object).with('user2').returns(user2) end describe "#members_insync?" do it "should return false when current is nil" do provider.members_insync?(nil, ['user2']).should be_false end it "should return false when should is nil" do provider.members_insync?(['user1'], nil).should be_false end it "should return false for differing lists of members" do provider.members_insync?(['user1'], ['user2']).should be_false provider.members_insync?(['user1'], []).should be_false provider.members_insync?([], ['user2']).should be_false end it "should return true for same lists of members" do provider.members_insync?(['user1', 'user2'], ['user1', 'user2']).should be_true end it "should return true for same lists of unordered members" do provider.members_insync?(['user1', 'user2'], ['user2', 'user1']).should be_true end it "should return true for same lists of members irrespective of duplicates" do provider.members_insync?(['user1', 'user2', 'user2'], ['user2', 'user1', 'user1']).should be_true end end describe "#members_to_s" do it "should return an empty string on non-array input" do [Object.new, {}, 1, :symbol, ''].each do |input| provider.members_to_s(input).should be_empty end end it "should return an empty string on empty or nil users" do provider.members_to_s([]).should be_empty provider.members_to_s(nil).should be_empty end it "should return a user string like DOMAIN\\USER" do provider.members_to_s(['user1']).should == '.\user1' end it "should return a user string like DOMAIN\\USER,DOMAIN2\\USER2" do provider.members_to_s(['user1', 'user2']).should == '.\user1,.\user2' end end end describe "when managing members" do it "should be able to provide a list of members" do provider.group.stubs(:members).returns ['user1', 'user2', 'user3'] provider.members.should =~ ['user1', 'user2', 'user3'] end it "should be able to set group members" do provider.group.stubs(:members).returns ['user1', 'user2'] member_sids = [ stub(:account => 'user1', :domain => 'testcomputername'), stub(:account => 'user2', :domain => 'testcomputername'), stub(:account => 'user3', :domain => 'testcomputername'), ] provider.group.stubs(:member_sids).returns(member_sids[0..1]) - Puppet::Util::Windows::Security.expects(:name_to_sid_object).with('user2').returns(member_sids[1]) - Puppet::Util::Windows::Security.expects(:name_to_sid_object).with('user3').returns(member_sids[2]) + Puppet::Util::Windows::SID.expects(:name_to_sid_object).with('user2').returns(member_sids[1]) + Puppet::Util::Windows::SID.expects(:name_to_sid_object).with('user3').returns(member_sids[2]) provider.group.expects(:remove_member_sids).with(member_sids[0]) provider.group.expects(:add_member_sids).with(member_sids[2]) provider.members = ['user2', 'user3'] end end describe 'when creating groups' do it "should be able to create a group" do resource[:members] = ['user1', 'user2'] group = stub 'group' Puppet::Util::Windows::ADSI::Group.expects(:create).with('testers').returns group create = sequence('create') group.expects(:commit).in_sequence(create) group.expects(:set_members).with(['user1', 'user2']).in_sequence(create) provider.create end it 'should not create a group if a user by the same name exists' do Puppet::Util::Windows::ADSI::Group.expects(:create).with('testers').raises( Puppet::Error.new("Cannot create group if user 'testers' exists.") ) expect{ provider.create }.to raise_error( Puppet::Error, /Cannot create group if user 'testers' exists./ ) end it 'should commit a newly created group' do provider.group.expects( :commit ) provider.flush end end it "should be able to test whether a group exists" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) Puppet::Util::Windows::ADSI.stubs(:connect).returns stub('connection') provider.should be_exists Puppet::Util::Windows::ADSI.stubs(:connect).returns nil provider.should_not be_exists end it "should be able to delete a group" do connection.expects(:Delete).with('group', 'testers') provider.delete end it "should report the group's SID as gid" do - Puppet::Util::Windows::Security.expects(:name_to_sid).with('testers').returns('S-1-5-32-547') + Puppet::Util::Windows::SID.expects(:name_to_sid).with('testers').returns('S-1-5-32-547') provider.gid.should == 'S-1-5-32-547' end it "should fail when trying to manage the gid property" do provider.expects(:fail).with { |msg| msg =~ /gid is read-only/ } provider.send(:gid=, 500) end it "should prefer the domain component from the resolved SID" do provider.members_to_s(['.\Administrators']).should == 'BUILTIN\Administrators' end end diff --git a/spec/unit/provider/scheduled_task/win32_taskscheduler_spec.rb b/spec/unit/provider/scheduled_task/win32_taskscheduler_spec.rb index 4a950dad3..7a6215603 100644 --- a/spec/unit/provider/scheduled_task/win32_taskscheduler_spec.rb +++ b/spec/unit/provider/scheduled_task/win32_taskscheduler_spec.rb @@ -1,1574 +1,1574 @@ #! /usr/bin/env ruby require 'spec_helper' require 'win32/taskscheduler' if Puppet.features.microsoft_windows? shared_examples_for "a trigger that handles start_date and start_time" do let(:trigger) do described_class.new( :name => 'Shared Test Task', :command => 'C:\Windows\System32\notepad.exe' ).translate_hash_to_trigger(trigger_hash) end before :each do Win32::TaskScheduler.any_instance.stubs(:save) end describe 'the given start_date' do before :each do trigger_hash['start_time'] = '00:00' end def date_component { 'start_year' => trigger['start_year'], 'start_month' => trigger['start_month'], 'start_day' => trigger['start_day'] } end it 'should be able to be specified in ISO 8601 calendar date format' do trigger_hash['start_date'] = '2011-12-31' date_component.should == { 'start_year' => 2011, 'start_month' => 12, 'start_day' => 31 } end it 'should fail if before 1753-01-01' do trigger_hash['start_date'] = '1752-12-31' expect { date_component }.to raise_error( Puppet::Error, 'start_date must be on or after 1753-01-01' ) end it 'should succeed if on 1753-01-01' do trigger_hash['start_date'] = '1753-01-01' date_component.should == { 'start_year' => 1753, 'start_month' => 1, 'start_day' => 1 } end it 'should succeed if after 1753-01-01' do trigger_hash['start_date'] = '1753-01-02' date_component.should == { 'start_year' => 1753, 'start_month' => 1, 'start_day' => 2 } end end describe 'the given start_time' do before :each do trigger_hash['start_date'] = '2011-12-31' end def time_component { 'start_hour' => trigger['start_hour'], 'start_minute' => trigger['start_minute'] } end it 'should be able to be specified as a 24-hour "hh:mm"' do trigger_hash['start_time'] = '17:13' time_component.should == { 'start_hour' => 17, 'start_minute' => 13 } end it 'should be able to be specified as a 12-hour "hh:mm am"' do trigger_hash['start_time'] = '3:13 am' time_component.should == { 'start_hour' => 3, 'start_minute' => 13 } end it 'should be able to be specified as a 12-hour "hh:mm pm"' do trigger_hash['start_time'] = '3:13 pm' time_component.should == { 'start_hour' => 15, 'start_minute' => 13 } end end end describe Puppet::Type.type(:scheduled_task).provider(:win32_taskscheduler), :if => Puppet.features.microsoft_windows? do before :each do Puppet::Type.type(:scheduled_task).stubs(:defaultprovider).returns(described_class) end describe 'when retrieving' do before :each do @mock_task = mock @mock_task.responds_like(Win32::TaskScheduler.new) described_class.any_instance.stubs(:task).returns(@mock_task) Win32::TaskScheduler.stubs(:new).returns(@mock_task) end let(:resource) { Puppet::Type.type(:scheduled_task).new(:name => 'Test Task', :command => 'C:\Windows\System32\notepad.exe') } describe 'the triggers for a task' do describe 'with only one trigger' do before :each do @mock_task.expects(:trigger_count).returns(1) end it 'should handle a single daily trigger' do @mock_task.expects(:trigger).with(0).returns({ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_DAILY, 'start_year' => 2011, 'start_month' => 9, 'start_day' => 12, 'start_hour' => 13, 'start_minute' => 20, 'flags' => 0, 'type' => { 'days_interval' => 2 }, }) resource.provider.trigger.should == { 'start_date' => '2011-9-12', 'start_time' => '13:20', 'schedule' => 'daily', 'every' => '2', 'enabled' => true, 'index' => 0, } end it 'should handle a single weekly trigger' do scheduled_days_of_week = Win32::TaskScheduler::MONDAY | Win32::TaskScheduler::WEDNESDAY | Win32::TaskScheduler::FRIDAY | Win32::TaskScheduler::SUNDAY @mock_task.expects(:trigger).with(0).returns({ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_WEEKLY, 'start_year' => 2011, 'start_month' => 9, 'start_day' => 12, 'start_hour' => 13, 'start_minute' => 20, 'flags' => 0, 'type' => { 'weeks_interval' => 2, 'days_of_week' => scheduled_days_of_week } }) resource.provider.trigger.should == { 'start_date' => '2011-9-12', 'start_time' => '13:20', 'schedule' => 'weekly', 'every' => '2', 'on' => ['sun', 'mon', 'wed', 'fri'], 'enabled' => true, 'index' => 0, } end it 'should handle a single monthly date-based trigger' do scheduled_months = Win32::TaskScheduler::JANUARY | Win32::TaskScheduler::FEBRUARY | Win32::TaskScheduler::AUGUST | Win32::TaskScheduler::SEPTEMBER | Win32::TaskScheduler::DECEMBER # 1 3 5 15 'last' scheduled_days = 1 | 1 << 2 | 1 << 4 | 1 << 14 | 1 << 31 @mock_task.expects(:trigger).with(0).returns({ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_MONTHLYDATE, 'start_year' => 2011, 'start_month' => 9, 'start_day' => 12, 'start_hour' => 13, 'start_minute' => 20, 'flags' => 0, 'type' => { 'months' => scheduled_months, 'days' => scheduled_days } }) resource.provider.trigger.should == { 'start_date' => '2011-9-12', 'start_time' => '13:20', 'schedule' => 'monthly', 'months' => [1, 2, 8, 9, 12], 'on' => [1, 3, 5, 15, 'last'], 'enabled' => true, 'index' => 0, } end it 'should handle a single monthly day-of-week-based trigger' do scheduled_months = Win32::TaskScheduler::JANUARY | Win32::TaskScheduler::FEBRUARY | Win32::TaskScheduler::AUGUST | Win32::TaskScheduler::SEPTEMBER | Win32::TaskScheduler::DECEMBER scheduled_days_of_week = Win32::TaskScheduler::MONDAY | Win32::TaskScheduler::WEDNESDAY | Win32::TaskScheduler::FRIDAY | Win32::TaskScheduler::SUNDAY @mock_task.expects(:trigger).with(0).returns({ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_MONTHLYDOW, 'start_year' => 2011, 'start_month' => 9, 'start_day' => 12, 'start_hour' => 13, 'start_minute' => 20, 'flags' => 0, 'type' => { 'months' => scheduled_months, 'weeks' => Win32::TaskScheduler::FIRST_WEEK, 'days_of_week' => scheduled_days_of_week } }) resource.provider.trigger.should == { 'start_date' => '2011-9-12', 'start_time' => '13:20', 'schedule' => 'monthly', 'months' => [1, 2, 8, 9, 12], 'which_occurrence' => 'first', 'day_of_week' => ['sun', 'mon', 'wed', 'fri'], 'enabled' => true, 'index' => 0, } end it 'should handle a single one-time trigger' do @mock_task.expects(:trigger).with(0).returns({ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE, 'start_year' => 2011, 'start_month' => 9, 'start_day' => 12, 'start_hour' => 13, 'start_minute' => 20, 'flags' => 0, }) resource.provider.trigger.should == { 'start_date' => '2011-9-12', 'start_time' => '13:20', 'schedule' => 'once', 'enabled' => true, 'index' => 0, } end end it 'should handle multiple triggers' do @mock_task.expects(:trigger_count).returns(3) @mock_task.expects(:trigger).with(0).returns({ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE, 'start_year' => 2011, 'start_month' => 10, 'start_day' => 13, 'start_hour' => 14, 'start_minute' => 21, 'flags' => 0, }) @mock_task.expects(:trigger).with(1).returns({ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE, 'start_year' => 2012, 'start_month' => 11, 'start_day' => 14, 'start_hour' => 15, 'start_minute' => 22, 'flags' => 0, }) @mock_task.expects(:trigger).with(2).returns({ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE, 'start_year' => 2013, 'start_month' => 12, 'start_day' => 15, 'start_hour' => 16, 'start_minute' => 23, 'flags' => 0, }) resource.provider.trigger.should =~ [ { 'start_date' => '2011-10-13', 'start_time' => '14:21', 'schedule' => 'once', 'enabled' => true, 'index' => 0, }, { 'start_date' => '2012-11-14', 'start_time' => '15:22', 'schedule' => 'once', 'enabled' => true, 'index' => 1, }, { 'start_date' => '2013-12-15', 'start_time' => '16:23', 'schedule' => 'once', 'enabled' => true, 'index' => 2, } ] end it 'should skip triggers Win32::TaskScheduler cannot handle' do @mock_task.expects(:trigger_count).returns(3) @mock_task.expects(:trigger).with(0).returns({ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE, 'start_year' => 2011, 'start_month' => 10, 'start_day' => 13, 'start_hour' => 14, 'start_minute' => 21, 'flags' => 0, }) @mock_task.expects(:trigger).with(1).raises( Win32::TaskScheduler::Error.new('Unhandled trigger type!') ) @mock_task.expects(:trigger).with(2).returns({ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE, 'start_year' => 2013, 'start_month' => 12, 'start_day' => 15, 'start_hour' => 16, 'start_minute' => 23, 'flags' => 0, }) resource.provider.trigger.should =~ [ { 'start_date' => '2011-10-13', 'start_time' => '14:21', 'schedule' => 'once', 'enabled' => true, 'index' => 0, }, { 'start_date' => '2013-12-15', 'start_time' => '16:23', 'schedule' => 'once', 'enabled' => true, 'index' => 2, } ] end it 'should skip trigger types Puppet does not handle' do @mock_task.expects(:trigger_count).returns(3) @mock_task.expects(:trigger).with(0).returns({ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE, 'start_year' => 2011, 'start_month' => 10, 'start_day' => 13, 'start_hour' => 14, 'start_minute' => 21, 'flags' => 0, }) @mock_task.expects(:trigger).with(1).returns({ 'trigger_type' => Win32::TaskScheduler::TASK_EVENT_TRIGGER_AT_LOGON, }) @mock_task.expects(:trigger).with(2).returns({ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE, 'start_year' => 2013, 'start_month' => 12, 'start_day' => 15, 'start_hour' => 16, 'start_minute' => 23, 'flags' => 0, }) resource.provider.trigger.should =~ [ { 'start_date' => '2011-10-13', 'start_time' => '14:21', 'schedule' => 'once', 'enabled' => true, 'index' => 0, }, { 'start_date' => '2013-12-15', 'start_time' => '16:23', 'schedule' => 'once', 'enabled' => true, 'index' => 2, } ] end end it 'should get the working directory from the working_directory on the task' do @mock_task.expects(:working_directory).returns('C:\Windows\System32') resource.provider.working_dir.should == 'C:\Windows\System32' end it 'should get the command from the application_name on the task' do @mock_task.expects(:application_name).returns('C:\Windows\System32\notepad.exe') resource.provider.command.should == 'C:\Windows\System32\notepad.exe' end it 'should get the command arguments from the parameters on the task' do @mock_task.expects(:parameters).returns('these are my arguments') resource.provider.arguments.should == 'these are my arguments' end it 'should get the user from the account_information on the task' do @mock_task.expects(:account_information).returns('this is my user') resource.provider.user.should == 'this is my user' end describe 'whether the task is enabled' do it 'should report tasks with the disabled bit set as disabled' do @mock_task.stubs(:flags).returns(Win32::TaskScheduler::DISABLED) resource.provider.enabled.should == :false end it 'should report tasks without the disabled bit set as enabled' do @mock_task.stubs(:flags).returns(~Win32::TaskScheduler::DISABLED) resource.provider.enabled.should == :true end it 'should not consider triggers for determining if the task is enabled' do @mock_task.stubs(:flags).returns(~Win32::TaskScheduler::DISABLED) @mock_task.stubs(:trigger_count).returns(1) @mock_task.stubs(:trigger).with(0).returns({ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE, 'start_year' => 2011, 'start_month' => 10, 'start_day' => 13, 'start_hour' => 14, 'start_minute' => 21, 'flags' => Win32::TaskScheduler::TASK_TRIGGER_FLAG_DISABLED, }) resource.provider.enabled.should == :true end end end describe '#exists?' do before :each do @mock_task = mock @mock_task.responds_like(Win32::TaskScheduler.new) described_class.any_instance.stubs(:task).returns(@mock_task) Win32::TaskScheduler.stubs(:new).returns(@mock_task) end let(:resource) { Puppet::Type.type(:scheduled_task).new(:name => 'Test Task', :command => 'C:\Windows\System32\notepad.exe') } it "should delegate to Win32::TaskScheduler using the resource's name" do @mock_task.expects(:exists?).with('Test Task').returns(true) resource.provider.exists?.should == true end end describe '#clear_task' do before :each do @mock_task = mock @new_mock_task = mock @mock_task.responds_like(Win32::TaskScheduler.new) @new_mock_task.responds_like(Win32::TaskScheduler.new) Win32::TaskScheduler.stubs(:new).returns(@mock_task, @new_mock_task) described_class.any_instance.stubs(:exists?).returns(false) end let(:resource) { Puppet::Type.type(:scheduled_task).new(:name => 'Test Task', :command => 'C:\Windows\System32\notepad.exe') } it 'should clear the cached task object' do resource.provider.task.should == @mock_task resource.provider.task.should == @mock_task resource.provider.clear_task resource.provider.task.should == @new_mock_task end it 'should clear the cached list of triggers for the task' do @mock_task.stubs(:trigger_count).returns(1) @mock_task.stubs(:trigger).with(0).returns({ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE, 'start_year' => 2011, 'start_month' => 10, 'start_day' => 13, 'start_hour' => 14, 'start_minute' => 21, 'flags' => 0, }) @new_mock_task.stubs(:trigger_count).returns(1) @new_mock_task.stubs(:trigger).with(0).returns({ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE, 'start_year' => 2012, 'start_month' => 11, 'start_day' => 14, 'start_hour' => 15, 'start_minute' => 22, 'flags' => 0, }) mock_task_trigger = { 'start_date' => '2011-10-13', 'start_time' => '14:21', 'schedule' => 'once', 'enabled' => true, 'index' => 0, } resource.provider.trigger.should == mock_task_trigger resource.provider.trigger.should == mock_task_trigger resource.provider.clear_task resource.provider.trigger.should == { 'start_date' => '2012-11-14', 'start_time' => '15:22', 'schedule' => 'once', 'enabled' => true, 'index' => 0, } end end describe '.instances' do it 'should use the list of .job files to construct the list of scheduled_tasks' do job_files = ['foo.job', 'bar.job', 'baz.job'] Win32::TaskScheduler.any_instance.stubs(:tasks).returns(job_files) job_files.each do |job| job = File.basename(job, '.job') described_class.expects(:new).with(:provider => :win32_taskscheduler, :name => job) end described_class.instances end end describe '#user_insync?', :if => Puppet.features.microsoft_windows? do let(:resource) { described_class.new(:name => 'foobar', :command => 'C:\Windows\System32\notepad.exe') } it 'should consider the user as in sync if the name matches' do - Puppet::Util::Windows::Security.expects(:name_to_sid).with('joe').twice.returns('SID A') + Puppet::Util::Windows::SID.expects(:name_to_sid).with('joe').twice.returns('SID A') resource.should be_user_insync('joe', ['joe']) end it 'should consider the user as in sync if the current user is fully qualified' do - Puppet::Util::Windows::Security.expects(:name_to_sid).with('joe').returns('SID A') - Puppet::Util::Windows::Security.expects(:name_to_sid).with('MACHINE\joe').returns('SID A') + Puppet::Util::Windows::SID.expects(:name_to_sid).with('joe').returns('SID A') + Puppet::Util::Windows::SID.expects(:name_to_sid).with('MACHINE\joe').returns('SID A') resource.should be_user_insync('MACHINE\joe', ['joe']) end it 'should consider a current user of the empty string to be the same as the system user' do - Puppet::Util::Windows::Security.expects(:name_to_sid).with('system').twice.returns('SYSTEM SID') + Puppet::Util::Windows::SID.expects(:name_to_sid).with('system').twice.returns('SYSTEM SID') resource.should be_user_insync('', ['system']) end it 'should consider different users as being different' do - Puppet::Util::Windows::Security.expects(:name_to_sid).with('joe').returns('SID A') - Puppet::Util::Windows::Security.expects(:name_to_sid).with('bob').returns('SID B') + Puppet::Util::Windows::SID.expects(:name_to_sid).with('joe').returns('SID A') + Puppet::Util::Windows::SID.expects(:name_to_sid).with('bob').returns('SID B') resource.should_not be_user_insync('joe', ['bob']) end end describe '#trigger_insync?' do let(:resource) { described_class.new(:name => 'foobar', :command => 'C:\Windows\System32\notepad.exe') } it 'should not consider any extra current triggers as in sync' do current = [ {'start_date' => '2011-09-12', 'start_time' => '15:15', 'schedule' => 'once'}, {'start_date' => '2012-10-13', 'start_time' => '16:16', 'schedule' => 'once'} ] desired = {'start_date' => '2011-09-12', 'start_time' => '15:15', 'schedule' => 'once'} resource.should_not be_trigger_insync(current, desired) end it 'should not consider any extra desired triggers as in sync' do current = {'start_date' => '2011-09-12', 'start_time' => '15:15', 'schedule' => 'once'} desired = [ {'start_date' => '2011-09-12', 'start_time' => '15:15', 'schedule' => 'once'}, {'start_date' => '2012-10-13', 'start_time' => '16:16', 'schedule' => 'once'} ] resource.should_not be_trigger_insync(current, desired) end it 'should consider triggers to be in sync if the sets of current and desired triggers are equal' do current = [ {'start_date' => '2011-09-12', 'start_time' => '15:15', 'schedule' => 'once'}, {'start_date' => '2012-10-13', 'start_time' => '16:16', 'schedule' => 'once'} ] desired = [ {'start_date' => '2011-09-12', 'start_time' => '15:15', 'schedule' => 'once'}, {'start_date' => '2012-10-13', 'start_time' => '16:16', 'schedule' => 'once'} ] resource.should be_trigger_insync(current, desired) end end describe '#triggers_same?' do let(:provider) { described_class.new(:name => 'foobar', :command => 'C:\Windows\System32\notepad.exe') } it "should not consider a disabled 'current' trigger to be the same" do current = {'schedule' => 'once', 'enabled' => false} desired = {'schedule' => 'once'} provider.should_not be_triggers_same(current, desired) end it 'should not consider triggers with different schedules to be the same' do current = {'schedule' => 'once'} desired = {'schedule' => 'weekly'} provider.should_not be_triggers_same(current, desired) end describe 'comparing daily triggers' do it "should consider 'desired' triggers not specifying 'every' to have the same value as the 'current' trigger" do current = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3} desired = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:30'} provider.should be_triggers_same(current, desired) end it "should consider different 'start_dates' as different triggers" do current = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3} desired = {'schedule' => 'daily', 'start_date' => '2012-09-12', 'start_time' => '15:30', 'every' => 3} provider.should_not be_triggers_same(current, desired) end it "should consider different 'start_times' as different triggers" do current = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3} desired = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:31', 'every' => 3} provider.should_not be_triggers_same(current, desired) end it 'should not consider differences in date formatting to be different triggers' do current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3} desired = {'schedule' => 'weekly', 'start_date' => '2011-9-12', 'start_time' => '15:30', 'every' => 3} provider.should be_triggers_same(current, desired) end it 'should not consider differences in time formatting to be different triggers' do current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '5:30', 'every' => 3} desired = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '05:30', 'every' => 3} provider.should be_triggers_same(current, desired) end it "should consider different 'every' as different triggers" do current = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3} desired = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 1} provider.should_not be_triggers_same(current, desired) end it 'should consider triggers that are the same as being the same' do trigger = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '01:30', 'every' => 1} provider.should be_triggers_same(trigger, trigger) end end describe 'comparing one-time triggers' do it "should consider different 'start_dates' as different triggers" do current = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:30'} desired = {'schedule' => 'daily', 'start_date' => '2012-09-12', 'start_time' => '15:30'} provider.should_not be_triggers_same(current, desired) end it "should consider different 'start_times' as different triggers" do current = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:30'} desired = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:31'} provider.should_not be_triggers_same(current, desired) end it 'should not consider differences in date formatting to be different triggers' do current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30'} desired = {'schedule' => 'weekly', 'start_date' => '2011-9-12', 'start_time' => '15:30'} provider.should be_triggers_same(current, desired) end it 'should not consider differences in time formatting to be different triggers' do current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '1:30'} desired = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '01:30'} provider.should be_triggers_same(current, desired) end it 'should consider triggers that are the same as being the same' do trigger = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '01:30'} provider.should be_triggers_same(trigger, trigger) end end describe 'comparing monthly date-based triggers' do it "should consider 'desired' triggers not specifying 'months' to have the same value as the 'current' trigger" do current = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [3], 'on' => [1,'last']} desired = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'on' => [1, 'last']} provider.should be_triggers_same(current, desired) end it "should consider different 'start_dates' as different triggers" do current = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]} desired = {'schedule' => 'monthly', 'start_date' => '2011-10-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]} provider.should_not be_triggers_same(current, desired) end it "should consider different 'start_times' as different triggers" do current = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]} desired = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '22:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]} provider.should_not be_triggers_same(current, desired) end it 'should not consider differences in date formatting to be different triggers' do current = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]} desired = {'schedule' => 'monthly', 'start_date' => '2011-9-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]} provider.should be_triggers_same(current, desired) end it 'should not consider differences in time formatting to be different triggers' do current = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '5:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]} desired = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '05:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]} provider.should be_triggers_same(current, desired) end it "should consider different 'months' as different triggers" do current = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]} desired = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [1], 'on' => [1, 3, 5, 7]} provider.should_not be_triggers_same(current, desired) end it "should consider different 'on' as different triggers" do current = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]} desired = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 5, 7]} provider.should_not be_triggers_same(current, desired) end it 'should consider triggers that are the same as being the same' do trigger = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]} provider.should be_triggers_same(trigger, trigger) end end describe 'comparing monthly day-of-week-based triggers' do it "should consider 'desired' triggers not specifying 'months' to have the same value as the 'current' trigger" do current = { 'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [3], 'which_occurrence' => 'first', 'day_of_week' => ['mon', 'tues', 'sat'] } desired = { 'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'which_occurrence' => 'first', 'day_of_week' => ['mon', 'tues', 'sat'] } provider.should be_triggers_same(current, desired) end it "should consider different 'start_dates' as different triggers" do current = { 'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [3], 'which_occurrence' => 'first', 'day_of_week' => ['mon', 'tues', 'sat'] } desired = { 'schedule' => 'monthly', 'start_date' => '2011-10-12', 'start_time' => '15:30', 'months' => [3], 'which_occurrence' => 'first', 'day_of_week' => ['mon', 'tues', 'sat'] } provider.should_not be_triggers_same(current, desired) end it "should consider different 'start_times' as different triggers" do current = { 'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [3], 'which_occurrence' => 'first', 'day_of_week' => ['mon', 'tues', 'sat'] } desired = { 'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '22:30', 'months' => [3], 'which_occurrence' => 'first', 'day_of_week' => ['mon', 'tues', 'sat'] } provider.should_not be_triggers_same(current, desired) end it "should consider different 'months' as different triggers" do current = { 'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [3], 'which_occurrence' => 'first', 'day_of_week' => ['mon', 'tues', 'sat'] } desired = { 'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [3, 5, 7, 9], 'which_occurrence' => 'first', 'day_of_week' => ['mon', 'tues', 'sat'] } provider.should_not be_triggers_same(current, desired) end it "should consider different 'which_occurrence' as different triggers" do current = { 'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [3], 'which_occurrence' => 'first', 'day_of_week' => ['mon', 'tues', 'sat'] } desired = { 'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [3], 'which_occurrence' => 'last', 'day_of_week' => ['mon', 'tues', 'sat'] } provider.should_not be_triggers_same(current, desired) end it "should consider different 'day_of_week' as different triggers" do current = { 'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [3], 'which_occurrence' => 'first', 'day_of_week' => ['mon', 'tues', 'sat'] } desired = { 'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [3], 'which_occurrence' => 'first', 'day_of_week' => ['fri'] } provider.should_not be_triggers_same(current, desired) end it 'should consider triggers that are the same as being the same' do trigger = { 'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [3], 'which_occurrence' => 'first', 'day_of_week' => ['mon', 'tues', 'sat'] } provider.should be_triggers_same(trigger, trigger) end end describe 'comparing weekly triggers' do it "should consider 'desired' triggers not specifying 'day_of_week' to have the same value as the 'current' trigger" do current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']} desired = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3} provider.should be_triggers_same(current, desired) end it "should consider different 'start_dates' as different triggers" do current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']} desired = {'schedule' => 'weekly', 'start_date' => '2011-10-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']} provider.should_not be_triggers_same(current, desired) end it "should consider different 'start_times' as different triggers" do current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']} desired = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '22:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']} provider.should_not be_triggers_same(current, desired) end it 'should not consider differences in date formatting to be different triggers' do current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']} desired = {'schedule' => 'weekly', 'start_date' => '2011-9-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']} provider.should be_triggers_same(current, desired) end it 'should not consider differences in time formatting to be different triggers' do current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '1:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']} desired = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '01:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']} provider.should be_triggers_same(current, desired) end it "should consider different 'every' as different triggers" do current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 1, 'day_of_week' => ['mon', 'wed', 'fri']} desired = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']} provider.should_not be_triggers_same(current, desired) end it "should consider different 'day_of_week' as different triggers" do current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']} desired = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['fri']} provider.should_not be_triggers_same(current, desired) end it 'should consider triggers that are the same as being the same' do trigger = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']} provider.should be_triggers_same(trigger, trigger) end end end describe '#normalized_date' do it 'should format the date without leading zeros' do described_class.normalized_date('2011-01-01').should == '2011-1-1' end end describe '#normalized_time' do it 'should format the time as {24h}:{minutes}' do described_class.normalized_time('8:37 PM').should == '20:37' end end describe '#translate_hash_to_trigger' do before :each do @puppet_trigger = { 'start_date' => '2011-1-1', 'start_time' => '01:10' } end let(:provider) { described_class.new(:name => 'Test Task', :command => 'C:\Windows\System32\notepad.exe') } let(:trigger) { provider.translate_hash_to_trigger(@puppet_trigger) } describe 'when given a one-time trigger' do before :each do @puppet_trigger['schedule'] = 'once' end it 'should set the trigger_type to Win32::TaskScheduler::ONCE' do trigger['trigger_type'].should == Win32::TaskScheduler::ONCE end it 'should not set a type' do trigger.should_not be_has_key('type') end it "should require 'start_date'" do @puppet_trigger.delete('start_date') expect { trigger }.to raise_error( Puppet::Error, /Must specify 'start_date' when defining a one-time trigger/ ) end it "should require 'start_time'" do @puppet_trigger.delete('start_time') expect { trigger }.to raise_error( Puppet::Error, /Must specify 'start_time' when defining a trigger/ ) end it_behaves_like "a trigger that handles start_date and start_time" do let(:trigger_hash) {{'schedule' => 'once' }} end end describe 'when given a daily trigger' do before :each do @puppet_trigger['schedule'] = 'daily' end it "should default 'every' to 1" do trigger['type']['days_interval'].should == 1 end it "should use the specified value for 'every'" do @puppet_trigger['every'] = 5 trigger['type']['days_interval'].should == 5 end it "should default 'start_date' to 'today'" do @puppet_trigger.delete('start_date') today = Time.now trigger['start_year'].should == today.year trigger['start_month'].should == today.month trigger['start_day'].should == today.day end it_behaves_like "a trigger that handles start_date and start_time" do let(:trigger_hash) {{'schedule' => 'daily', 'every' => 1}} end end describe 'when given a weekly trigger' do before :each do @puppet_trigger['schedule'] = 'weekly' end it "should default 'every' to 1" do trigger['type']['weeks_interval'].should == 1 end it "should use the specified value for 'every'" do @puppet_trigger['every'] = 4 trigger['type']['weeks_interval'].should == 4 end it "should default 'day_of_week' to be every day of the week" do trigger['type']['days_of_week'].should == Win32::TaskScheduler::MONDAY | Win32::TaskScheduler::TUESDAY | Win32::TaskScheduler::WEDNESDAY | Win32::TaskScheduler::THURSDAY | Win32::TaskScheduler::FRIDAY | Win32::TaskScheduler::SATURDAY | Win32::TaskScheduler::SUNDAY end it "should use the specified value for 'day_of_week'" do @puppet_trigger['day_of_week'] = ['mon', 'wed', 'fri'] trigger['type']['days_of_week'].should == Win32::TaskScheduler::MONDAY | Win32::TaskScheduler::WEDNESDAY | Win32::TaskScheduler::FRIDAY end it "should default 'start_date' to 'today'" do @puppet_trigger.delete('start_date') today = Time.now trigger['start_year'].should == today.year trigger['start_month'].should == today.month trigger['start_day'].should == today.day end it_behaves_like "a trigger that handles start_date and start_time" do let(:trigger_hash) {{'schedule' => 'weekly', 'every' => 1, 'day_of_week' => 'mon'}} end end shared_examples_for 'a monthly schedule' do it "should default 'months' to be every month" do trigger['type']['months'].should == Win32::TaskScheduler::JANUARY | Win32::TaskScheduler::FEBRUARY | Win32::TaskScheduler::MARCH | Win32::TaskScheduler::APRIL | Win32::TaskScheduler::MAY | Win32::TaskScheduler::JUNE | Win32::TaskScheduler::JULY | Win32::TaskScheduler::AUGUST | Win32::TaskScheduler::SEPTEMBER | Win32::TaskScheduler::OCTOBER | Win32::TaskScheduler::NOVEMBER | Win32::TaskScheduler::DECEMBER end it "should use the specified value for 'months'" do @puppet_trigger['months'] = [2, 8] trigger['type']['months'].should == Win32::TaskScheduler::FEBRUARY | Win32::TaskScheduler::AUGUST end end describe 'when given a monthly date-based trigger' do before :each do @puppet_trigger['schedule'] = 'monthly' @puppet_trigger['on'] = [7, 14] end it_behaves_like 'a monthly schedule' it "should not allow 'which_occurrence' to be specified" do @puppet_trigger['which_occurrence'] = 'first' expect {trigger}.to raise_error( Puppet::Error, /Neither 'day_of_week' nor 'which_occurrence' can be specified when creating a monthly date-based trigger/ ) end it "should not allow 'day_of_week' to be specified" do @puppet_trigger['day_of_week'] = 'mon' expect {trigger}.to raise_error( Puppet::Error, /Neither 'day_of_week' nor 'which_occurrence' can be specified when creating a monthly date-based trigger/ ) end it "should require 'on'" do @puppet_trigger.delete('on') expect {trigger}.to raise_error( Puppet::Error, /Don't know how to create a 'monthly' schedule with the options: schedule, start_date, start_time/ ) end it "should default 'start_date' to 'today'" do @puppet_trigger.delete('start_date') today = Time.now trigger['start_year'].should == today.year trigger['start_month'].should == today.month trigger['start_day'].should == today.day end it_behaves_like "a trigger that handles start_date and start_time" do let(:trigger_hash) {{'schedule' => 'monthly', 'months' => 1, 'on' => 1}} end end describe 'when given a monthly day-of-week-based trigger' do before :each do @puppet_trigger['schedule'] = 'monthly' @puppet_trigger['which_occurrence'] = 'first' @puppet_trigger['day_of_week'] = 'mon' end it_behaves_like 'a monthly schedule' it "should not allow 'on' to be specified" do @puppet_trigger['on'] = 15 expect {trigger}.to raise_error( Puppet::Error, /Neither 'day_of_week' nor 'which_occurrence' can be specified when creating a monthly date-based trigger/ ) end it "should require 'which_occurrence'" do @puppet_trigger.delete('which_occurrence') expect {trigger}.to raise_error( Puppet::Error, /which_occurrence must be specified when creating a monthly day-of-week based trigger/ ) end it "should require 'day_of_week'" do @puppet_trigger.delete('day_of_week') expect {trigger}.to raise_error( Puppet::Error, /day_of_week must be specified when creating a monthly day-of-week based trigger/ ) end it "should default 'start_date' to 'today'" do @puppet_trigger.delete('start_date') today = Time.now trigger['start_year'].should == today.year trigger['start_month'].should == today.month trigger['start_day'].should == today.day end it_behaves_like "a trigger that handles start_date and start_time" do let(:trigger_hash) {{'schedule' => 'monthly', 'months' => 1, 'which_occurrence' => 'first', 'day_of_week' => 'mon'}} end end end describe '#validate_trigger' do let(:provider) { described_class.new(:name => 'Test Task', :command => 'C:\Windows\System32\notepad.exe') } it 'should succeed if all passed triggers translate from hashes to triggers' do triggers_to_validate = [ {'schedule' => 'once', 'start_date' => '2011-09-13', 'start_time' => '13:50'}, {'schedule' => 'weekly', 'start_date' => '2011-09-13', 'start_time' => '13:50', 'day_of_week' => 'mon'} ] provider.validate_trigger(triggers_to_validate).should == true end it 'should use the exception from translate_hash_to_trigger when it fails' do triggers_to_validate = [ {'schedule' => 'once', 'start_date' => '2011-09-13', 'start_time' => '13:50'}, {'schedule' => 'monthly', 'this is invalid' => true} ] expect {provider.validate_trigger(triggers_to_validate)}.to raise_error( Puppet::Error, /#{Regexp.escape("Unknown trigger option(s): ['this is invalid']")}/ ) end end describe '#flush' do let(:resource) do Puppet::Type.type(:scheduled_task).new( :name => 'Test Task', :command => 'C:\Windows\System32\notepad.exe', :ensure => @ensure ) end before :each do @mock_task = mock @mock_task.responds_like(Win32::TaskScheduler.new) @mock_task.stubs(:exists?).returns(true) @mock_task.stubs(:activate) Win32::TaskScheduler.stubs(:new).returns(@mock_task) @command = 'C:\Windows\System32\notepad.exe' end describe 'when :ensure is :present' do before :each do @ensure = :present end it 'should save the task' do @mock_task.expects(:save) resource.provider.flush end it 'should fail if the command is not specified' do resource = Puppet::Type.type(:scheduled_task).new( :name => 'Test Task', :ensure => @ensure ) expect { resource.provider.flush }.to raise_error( Puppet::Error, 'Parameter command is required.' ) end end describe 'when :ensure is :absent' do before :each do @ensure = :absent @mock_task.stubs(:activate) end it 'should not save the task if :ensure is :absent' do @mock_task.expects(:save).never resource.provider.flush end it 'should not fail if the command is not specified' do @mock_task.stubs(:save) resource = Puppet::Type.type(:scheduled_task).new( :name => 'Test Task', :ensure => @ensure ) resource.provider.flush end end end describe 'property setter methods' do let(:resource) do Puppet::Type.type(:scheduled_task).new( :name => 'Test Task', :command => 'C:\dummy_task.exe' ) end before :each do @mock_task = mock @mock_task.responds_like(Win32::TaskScheduler.new) @mock_task.stubs(:exists?).returns(true) @mock_task.stubs(:activate) Win32::TaskScheduler.stubs(:new).returns(@mock_task) end describe '#command=' do it 'should set the application_name on the task' do @mock_task.expects(:application_name=).with('C:\Windows\System32\notepad.exe') resource.provider.command = 'C:\Windows\System32\notepad.exe' end end describe '#arguments=' do it 'should set the parameters on the task' do @mock_task.expects(:parameters=).with(['/some /arguments /here']) resource.provider.arguments = ['/some /arguments /here'] end end describe '#working_dir=' do it 'should set the working_directory on the task' do @mock_task.expects(:working_directory=).with('C:\Windows\System32') resource.provider.working_dir = 'C:\Windows\System32' end end describe '#enabled=' do it 'should set the disabled flag if the task should be disabled' do @mock_task.stubs(:flags).returns(0) @mock_task.expects(:flags=).with(Win32::TaskScheduler::DISABLED) resource.provider.enabled = :false end it 'should clear the disabled flag if the task should be enabled' do @mock_task.stubs(:flags).returns(Win32::TaskScheduler::DISABLED) @mock_task.expects(:flags=).with(0) resource.provider.enabled = :true end end describe '#trigger=' do let(:resource) do Puppet::Type.type(:scheduled_task).new( :name => 'Test Task', :command => 'C:\Windows\System32\notepad.exe', :trigger => @trigger ) end before :each do @mock_task = mock @mock_task.responds_like(Win32::TaskScheduler.new) @mock_task.stubs(:exists?).returns(true) @mock_task.stubs(:activate) Win32::TaskScheduler.stubs(:new).returns(@mock_task) end it 'should not consider all duplicate current triggers in sync with a single desired trigger' do @trigger = {'schedule' => 'once', 'start_date' => '2011-09-15', 'start_time' => '15:10'} current_triggers = [ {'schedule' => 'once', 'start_date' => '2011-09-15', 'start_time' => '15:10', 'index' => 0}, {'schedule' => 'once', 'start_date' => '2011-09-15', 'start_time' => '15:10', 'index' => 1}, {'schedule' => 'once', 'start_date' => '2011-09-15', 'start_time' => '15:10', 'index' => 2}, ] resource.provider.stubs(:trigger).returns(current_triggers) @mock_task.expects(:delete_trigger).with(1) @mock_task.expects(:delete_trigger).with(2) resource.provider.trigger = @trigger end it 'should remove triggers not defined in the resource' do @trigger = {'schedule' => 'once', 'start_date' => '2011-09-15', 'start_time' => '15:10'} current_triggers = [ {'schedule' => 'once', 'start_date' => '2011-09-15', 'start_time' => '15:10', 'index' => 0}, {'schedule' => 'once', 'start_date' => '2012-09-15', 'start_time' => '15:10', 'index' => 1}, {'schedule' => 'once', 'start_date' => '2013-09-15', 'start_time' => '15:10', 'index' => 2}, ] resource.provider.stubs(:trigger).returns(current_triggers) @mock_task.expects(:delete_trigger).with(1) @mock_task.expects(:delete_trigger).with(2) resource.provider.trigger = @trigger end it 'should add triggers defined in the resource, but not found on the system' do @trigger = [ {'schedule' => 'once', 'start_date' => '2011-09-15', 'start_time' => '15:10'}, {'schedule' => 'once', 'start_date' => '2012-09-15', 'start_time' => '15:10'}, {'schedule' => 'once', 'start_date' => '2013-09-15', 'start_time' => '15:10'}, ] current_triggers = [ {'schedule' => 'once', 'start_date' => '2011-09-15', 'start_time' => '15:10', 'index' => 0}, ] resource.provider.stubs(:trigger).returns(current_triggers) @mock_task.expects(:trigger=).with(resource.provider.translate_hash_to_trigger(@trigger[1])) @mock_task.expects(:trigger=).with(resource.provider.translate_hash_to_trigger(@trigger[2])) resource.provider.trigger = @trigger end end describe '#user=', :if => Puppet.features.microsoft_windows? do before :each do @mock_task = mock @mock_task.responds_like(Win32::TaskScheduler.new) @mock_task.stubs(:exists?).returns(true) @mock_task.stubs(:activate) Win32::TaskScheduler.stubs(:new).returns(@mock_task) end it 'should use nil for user and password when setting the user to the SYSTEM account' do - Puppet::Util::Windows::Security.stubs(:name_to_sid).with('system').returns('SYSTEM SID') + Puppet::Util::Windows::SID.stubs(:name_to_sid).with('system').returns('SYSTEM SID') resource = Puppet::Type.type(:scheduled_task).new( :name => 'Test Task', :command => 'C:\dummy_task.exe', :user => 'system' ) @mock_task.expects(:set_account_information).with(nil, nil) resource.provider.user = 'system' end it 'should use the specified user and password when setting the user to anything other than SYSTEM' do - Puppet::Util::Windows::Security.stubs(:name_to_sid).with('my_user_name').returns('SID A') + Puppet::Util::Windows::SID.stubs(:name_to_sid).with('my_user_name').returns('SID A') resource = Puppet::Type.type(:scheduled_task).new( :name => 'Test Task', :command => 'C:\dummy_task.exe', :user => 'my_user_name', :password => 'my password' ) @mock_task.expects(:set_account_information).with('my_user_name', 'my password') resource.provider.user = 'my_user_name' end end end describe '#create' do let(:resource) do Puppet::Type.type(:scheduled_task).new( :name => 'Test Task', :enabled => @enabled, :command => @command, :arguments => @arguments, :working_dir => @working_dir, :trigger => { 'schedule' => 'once', 'start_date' => '2011-09-27', 'start_time' => '17:00' } ) end before :each do @enabled = :true @command = 'C:\Windows\System32\notepad.exe' @arguments = '/a /list /of /arguments' @working_dir = 'C:\Windows\Some\Directory' @mock_task = mock @mock_task.responds_like(Win32::TaskScheduler.new) @mock_task.stubs(:exists?).returns(true) @mock_task.stubs(:activate) @mock_task.stubs(:application_name=) @mock_task.stubs(:parameters=) @mock_task.stubs(:working_directory=) @mock_task.stubs(:set_account_information) @mock_task.stubs(:flags) @mock_task.stubs(:flags=) @mock_task.stubs(:trigger_count).returns(0) @mock_task.stubs(:trigger=) @mock_task.stubs(:save) Win32::TaskScheduler.stubs(:new).returns(@mock_task) described_class.any_instance.stubs(:sync_triggers) end it 'should set the command' do resource.provider.expects(:command=).with(@command) resource.provider.create end it 'should set the arguments' do resource.provider.expects(:arguments=).with(@arguments) resource.provider.create end it 'should set the working_dir' do resource.provider.expects(:working_dir=).with(@working_dir) resource.provider.create end it "should set the user" do resource.provider.expects(:user=).with(:system) resource.provider.create end it 'should set the enabled property' do resource.provider.expects(:enabled=) resource.provider.create end it 'should sync triggers' do resource.provider.expects(:trigger=) resource.provider.create end end end diff --git a/spec/unit/provider/user/windows_adsi_spec.rb b/spec/unit/provider/user/windows_adsi_spec.rb index 6dbd695e0..84aa8a74c 100755 --- a/spec/unit/provider/user/windows_adsi_spec.rb +++ b/spec/unit/provider/user/windows_adsi_spec.rb @@ -1,173 +1,173 @@ #!/usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:user).provider(:windows_adsi), :if => Puppet.features.microsoft_windows? do let(:resource) do Puppet::Type.type(:user).new( :title => 'testuser', :comment => 'Test J. User', :provider => :windows_adsi ) end let(:provider) { resource.provider } let(:connection) { stub 'connection' } before :each do Puppet::Util::Windows::ADSI.stubs(:computer_name).returns('testcomputername') Puppet::Util::Windows::ADSI.stubs(:connect).returns connection end describe ".instances" do it "should enumerate all users" do names = ['user1', 'user2', 'user3'] stub_users = names.map{|n| stub(:name => n)} connection.stubs(:execquery).with('select name from win32_useraccount where localaccount = "TRUE"').returns(stub_users) described_class.instances.map(&:name).should =~ names end end it "should provide access to a Puppet::Util::Windows::ADSI::User object" do provider.user.should be_a(Puppet::Util::Windows::ADSI::User) end describe "when managing groups" do it 'should return the list of groups as a comma-separated list' do provider.user.stubs(:groups).returns ['group1', 'group2', 'group3'] provider.groups.should == 'group1,group2,group3' end it "should return absent if there are no groups" do provider.user.stubs(:groups).returns [] provider.groups.should == '' end it 'should be able to add a user to a set of groups' do resource[:membership] = :minimum provider.user.expects(:set_groups).with('group1,group2', true) provider.groups = 'group1,group2' resource[:membership] = :inclusive provider.user.expects(:set_groups).with('group1,group2', false) provider.groups = 'group1,group2' end end describe "when creating a user" do it "should create the user on the system and set its other properties" do resource[:groups] = ['group1', 'group2'] resource[:membership] = :inclusive resource[:comment] = 'a test user' resource[:home] = 'C:\Users\testuser' user = stub 'user' Puppet::Util::Windows::ADSI::User.expects(:create).with('testuser').returns user user.stubs(:groups).returns(['group2', 'group3']) create = sequence('create') user.expects(:password=).in_sequence(create) user.expects(:commit).in_sequence(create) user.expects(:set_groups).with('group1,group2', false).in_sequence(create) user.expects(:[]=).with('Description', 'a test user') user.expects(:[]=).with('HomeDirectory', 'C:\Users\testuser') provider.create end it "should load the profile if managehome is set" do resource[:password] = '0xDeadBeef' resource[:managehome] = true user = stub_everything 'user' Puppet::Util::Windows::ADSI::User.expects(:create).with('testuser').returns user Puppet::Util::Windows::User.expects(:load_profile).with('testuser', '0xDeadBeef') provider.create end it "should set a user's password" do provider.user.expects(:password=).with('plaintextbad') provider.password = "plaintextbad" end it "should test a valid user password" do resource[:password] = 'plaintext' provider.user.expects(:password_is?).with('plaintext').returns true provider.password.should == 'plaintext' end it "should test a bad user password" do resource[:password] = 'plaintext' provider.user.expects(:password_is?).with('plaintext').returns false provider.password.should == :absent end it 'should not create a user if a group by the same name exists' do Puppet::Util::Windows::ADSI::User.expects(:create).with('testuser').raises( Puppet::Error.new("Cannot create user if group 'testuser' exists.") ) expect{ provider.create }.to raise_error( Puppet::Error, /Cannot create user if group 'testuser' exists./ ) end end it 'should be able to test whether a user exists' do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) Puppet::Util::Windows::ADSI.stubs(:connect).returns stub('connection') provider.should be_exists Puppet::Util::Windows::ADSI.stubs(:connect).returns nil provider.should_not be_exists end it 'should be able to delete a user' do connection.expects(:Delete).with('user', 'testuser') provider.delete end it 'should delete the profile if managehome is set' do resource[:managehome] = true sid = 'S-A-B-C' - Puppet::Util::Windows::Security.expects(:name_to_sid).with('testuser').returns(sid) + Puppet::Util::Windows::SID.expects(:name_to_sid).with('testuser').returns(sid) Puppet::Util::Windows::ADSI::UserProfile.expects(:delete).with(sid) connection.expects(:Delete).with('user', 'testuser') provider.delete end it "should commit the user when flushed" do provider.user.expects(:commit) provider.flush end it "should return the user's SID as uid" do - Puppet::Util::Windows::Security.expects(:name_to_sid).with('testuser').returns('S-1-5-21-1362942247-2130103807-3279964888-1111') + Puppet::Util::Windows::SID.expects(:name_to_sid).with('testuser').returns('S-1-5-21-1362942247-2130103807-3279964888-1111') provider.uid.should == 'S-1-5-21-1362942247-2130103807-3279964888-1111' end it "should fail when trying to manage the uid property" do provider.expects(:fail).with { |msg| msg =~ /uid is read-only/ } provider.send(:uid=, 500) end [:gid, :shell].each do |prop| it "should fail when trying to manage the #{prop} property" do provider.expects(:fail).with { |msg| msg =~ /No support for managing property #{prop}/ } provider.send("#{prop}=", 'foo') end end end diff --git a/spec/unit/util/windows/adsi_spec.rb b/spec/unit/util/windows/adsi_spec.rb index 60ad503f4..f569d91e9 100755 --- a/spec/unit/util/windows/adsi_spec.rb +++ b/spec/unit/util/windows/adsi_spec.rb @@ -1,440 +1,440 @@ #!/usr/bin/env ruby require 'spec_helper' require 'puppet/util/windows' describe Puppet::Util::Windows::ADSI, :if => Puppet.features.microsoft_windows? do let(:connection) { stub 'connection' } before(:each) do Puppet::Util::Windows::ADSI.instance_variable_set(:@computer_name, 'testcomputername') Puppet::Util::Windows::ADSI.stubs(:connect).returns connection end after(:each) do Puppet::Util::Windows::ADSI.instance_variable_set(:@computer_name, nil) end it "should generate the correct URI for a resource" do Puppet::Util::Windows::ADSI.uri('test', 'user').should == "WinNT://./test,user" end it "should be able to get the name of the computer" do Puppet::Util::Windows::ADSI.computer_name.should == 'testcomputername' end it "should be able to provide the correct WinNT base URI for the computer" do Puppet::Util::Windows::ADSI.computer_uri.should == "WinNT://." end it "should generate a fully qualified WinNT URI" do Puppet::Util::Windows::ADSI.computer_uri('testcomputername').should == "WinNT://testcomputername" end describe ".sid_for_account" do it "should return nil if the account does not exist" do - Puppet::Util::Windows::Security.expects(:name_to_sid).with('foobar').returns nil + Puppet::Util::Windows::SID.expects(:name_to_sid).with('foobar').returns nil Puppet::Util::Windows::ADSI.sid_for_account('foobar').should be_nil end it "should return a SID for a passed user or group name" do - Puppet::Util::Windows::Security.expects(:name_to_sid).with('testers').returns 'S-1-5-32-547' + Puppet::Util::Windows::SID.expects(:name_to_sid).with('testers').returns 'S-1-5-32-547' Puppet::Util::Windows::ADSI.sid_for_account('testers').should == 'S-1-5-32-547' end it "should return a SID for a passed fully-qualified user or group name" do - Puppet::Util::Windows::Security.expects(:name_to_sid).with('MACHINE\testers').returns 'S-1-5-32-547' + Puppet::Util::Windows::SID.expects(:name_to_sid).with('MACHINE\testers').returns 'S-1-5-32-547' Puppet::Util::Windows::ADSI.sid_for_account('MACHINE\testers').should == 'S-1-5-32-547' end end describe ".computer_name" do it "should return a non-empty ComputerName string" do Puppet::Util::Windows::ADSI.instance_variable_set(:@computer_name, nil) Puppet::Util::Windows::ADSI.computer_name.should_not be_empty end end describe ".sid_uri" do it "should raise an error when the input is not a SID object" do [Object.new, {}, 1, :symbol, '', nil].each do |input| expect { Puppet::Util::Windows::ADSI.sid_uri(input) }.to raise_error(Puppet::Error, /Must use a valid SID object/) end end it "should return a SID uri for a well-known SID (SYSTEM)" do sid = Win32::Security::SID.new('SYSTEM') Puppet::Util::Windows::ADSI.sid_uri(sid).should == 'WinNT://S-1-5-18' end end describe Puppet::Util::Windows::ADSI::User do let(:username) { 'testuser' } let(:domain) { 'DOMAIN' } let(:domain_username) { "#{domain}\\#{username}"} it "should generate the correct URI" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) Puppet::Util::Windows::ADSI::User.uri(username).should == "WinNT://./#{username},user" end it "should generate the correct URI for a user with a domain" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) Puppet::Util::Windows::ADSI::User.uri(username, domain).should == "WinNT://#{domain}/#{username},user" end it "should be able to parse a username without a domain" do Puppet::Util::Windows::ADSI::User.parse_name(username).should == [username, '.'] end it "should be able to parse a username with a domain" do Puppet::Util::Windows::ADSI::User.parse_name(domain_username).should == [username, domain] end it "should raise an error with a username that contains a /" do expect { Puppet::Util::Windows::ADSI::User.parse_name("#{domain}/#{username}") }.to raise_error(Puppet::Error, /Value must be in DOMAIN\\user style syntax/) end it "should be able to create a user" do adsi_user = stub('adsi') connection.expects(:Create).with('user', username).returns(adsi_user) Puppet::Util::Windows::ADSI::Group.expects(:exists?).with(username).returns(false) user = Puppet::Util::Windows::ADSI::User.create(username) user.should be_a(Puppet::Util::Windows::ADSI::User) user.native_user.should == adsi_user end it "should be able to check the existence of a user" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) Puppet::Util::Windows::ADSI.expects(:connect).with("WinNT://./#{username},user").returns connection Puppet::Util::Windows::ADSI::User.exists?(username).should be_true end it "should be able to check the existence of a domain user" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) Puppet::Util::Windows::ADSI.expects(:connect).with("WinNT://#{domain}/#{username},user").returns connection Puppet::Util::Windows::ADSI::User.exists?(domain_username).should be_true end it "should be able to confirm the existence of a user with a well-known SID" do system_user = Win32::Security::SID::LocalSystem # ensure that the underlying OS is queried here Puppet::Util::Windows::ADSI.unstub(:connect) Puppet::Util::Windows::ADSI::User.exists?(system_user).should be_true end it "should return nil with an unknown SID" do bogus_sid = 'S-1-2-3-4' # ensure that the underlying OS is queried here Puppet::Util::Windows::ADSI.unstub(:connect) Puppet::Util::Windows::ADSI::User.exists?(bogus_sid).should be_false end it "should be able to delete a user" do connection.expects(:Delete).with('user', username) Puppet::Util::Windows::ADSI::User.delete(username) end it "should return an enumeration of IADsUser wrapped objects" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) name = 'Administrator' wmi_users = [stub('WMI', :name => name)] Puppet::Util::Windows::ADSI.expects(:execquery).with('select name from win32_useraccount where localaccount = "TRUE"').returns(wmi_users) native_user = stub('IADsUser') homedir = "C:\\Users\\#{name}" native_user.expects(:Get).with('HomeDirectory').returns(homedir) Puppet::Util::Windows::ADSI.expects(:connect).with("WinNT://./#{name},user").returns(native_user) users = Puppet::Util::Windows::ADSI::User.to_a users.length.should == 1 users[0].name.should == name users[0]['HomeDirectory'].should == homedir end describe "an instance" do let(:adsi_user) { stub('user', :objectSID => []) } let(:sid) { stub(:account => username, :domain => 'testcomputername') } let(:user) { Puppet::Util::Windows::ADSI::User.new(username, adsi_user) } it "should provide its groups as a list of names" do names = ["group1", "group2"] groups = names.map { |name| mock('group', :Name => name) } adsi_user.expects(:Groups).returns(groups) user.groups.should =~ names end it "should be able to test whether a given password is correct" do Puppet::Util::Windows::ADSI::User.expects(:logon).with(username, 'pwdwrong').returns(false) Puppet::Util::Windows::ADSI::User.expects(:logon).with(username, 'pwdright').returns(true) user.password_is?('pwdwrong').should be_false user.password_is?('pwdright').should be_true end it "should be able to set a password" do adsi_user.expects(:SetPassword).with('pwd') adsi_user.expects(:SetInfo).at_least_once flagname = "UserFlags" fADS_UF_DONT_EXPIRE_PASSWD = 0x10000 adsi_user.expects(:Get).with(flagname).returns(0) adsi_user.expects(:Put).with(flagname, fADS_UF_DONT_EXPIRE_PASSWD) user.password = 'pwd' end it "should generate the correct URI" do - Puppet::Util::Windows::Security.stubs(:octet_string_to_sid_object).returns(sid) + Puppet::Util::Windows::SID.stubs(:octet_string_to_sid_object).returns(sid) user.uri.should == "WinNT://testcomputername/#{username},user" end describe "when given a set of groups to which to add the user" do let(:groups_to_set) { 'group1,group2' } before(:each) do - Puppet::Util::Windows::Security.stubs(:octet_string_to_sid_object).returns(sid) + Puppet::Util::Windows::SID.stubs(:octet_string_to_sid_object).returns(sid) user.expects(:groups).returns ['group2', 'group3'] end describe "if membership is specified as inclusive" do it "should add the user to those groups, and remove it from groups not in the list" do group1 = stub 'group1' group1.expects(:Add).with("WinNT://testcomputername/#{username},user") group3 = stub 'group1' group3.expects(:Remove).with("WinNT://testcomputername/#{username},user") Puppet::Util::Windows::ADSI.expects(:sid_uri).with(sid).returns("WinNT://testcomputername/#{username},user").twice Puppet::Util::Windows::ADSI.expects(:connect).with('WinNT://./group1,group').returns group1 Puppet::Util::Windows::ADSI.expects(:connect).with('WinNT://./group3,group').returns group3 user.set_groups(groups_to_set, false) end end describe "if membership is specified as minimum" do it "should add the user to the specified groups without affecting its other memberships" do group1 = stub 'group1' group1.expects(:Add).with("WinNT://testcomputername/#{username},user") Puppet::Util::Windows::ADSI.expects(:sid_uri).with(sid).returns("WinNT://testcomputername/#{username},user") Puppet::Util::Windows::ADSI.expects(:connect).with('WinNT://./group1,group').returns group1 user.set_groups(groups_to_set, true) end end end end end describe Puppet::Util::Windows::ADSI::Group do let(:groupname) { 'testgroup' } describe "an instance" do let(:adsi_group) { stub 'group' } let(:group) { Puppet::Util::Windows::ADSI::Group.new(groupname, adsi_group) } let(:someone_sid){ stub(:account => 'someone', :domain => 'testcomputername')} it "should be able to add a member (deprecated)" do Puppet.expects(:deprecation_warning).with('Puppet::Util::Windows::ADSI::Group#add_members is deprecated; please use Puppet::Util::Windows::ADSI::Group#add_member_sids') - Puppet::Util::Windows::Security.expects(:name_to_sid_object).with('someone').returns(someone_sid) + Puppet::Util::Windows::SID.expects(:name_to_sid_object).with('someone').returns(someone_sid) Puppet::Util::Windows::ADSI.expects(:sid_uri).with(someone_sid).returns("WinNT://testcomputername/someone,user") adsi_group.expects(:Add).with("WinNT://testcomputername/someone,user") group.add_member('someone') end it "should raise when adding a member that can't resolve to a SID (deprecated)" do expect { group.add_member('foobar') }.to raise_error(Puppet::Error, /Could not resolve username: foobar/) end it "should be able to remove a member (deprecated)" do Puppet.expects(:deprecation_warning).with('Puppet::Util::Windows::ADSI::Group#remove_members is deprecated; please use Puppet::Util::Windows::ADSI::Group#remove_member_sids') - Puppet::Util::Windows::Security.expects(:name_to_sid_object).with('someone').returns(someone_sid) + Puppet::Util::Windows::SID.expects(:name_to_sid_object).with('someone').returns(someone_sid) Puppet::Util::Windows::ADSI.expects(:sid_uri).with(someone_sid).returns("WinNT://testcomputername/someone,user") adsi_group.expects(:Remove).with("WinNT://testcomputername/someone,user") group.remove_member('someone') end it "should raise when removing a member that can't resolve to a SID (deprecated)" do expect { group.remove_member('foobar') }.to raise_error(Puppet::Error, /Could not resolve username: foobar/) end describe "should be able to use SID objects" do - let(:system) { Puppet::Util::Windows::Security.name_to_sid_object('SYSTEM') } + let(:system) { Puppet::Util::Windows::SID.name_to_sid_object('SYSTEM') } it "to add a member" do adsi_group.expects(:Add).with("WinNT://S-1-5-18") group.add_member_sids(system) end it "to remove a member" do adsi_group.expects(:Remove).with("WinNT://S-1-5-18") group.remove_member_sids(system) end end it "should provide its groups as a list of names" do names = ['user1', 'user2'] users = names.map { |name| mock('user', :Name => name) } adsi_group.expects(:Members).returns(users) group.members.should =~ names end it "should be able to add a list of users to a group" do names = ['DOMAIN\user1', 'user2'] sids = [ stub(:account => 'user1', :domain => 'DOMAIN'), stub(:account => 'user2', :domain => 'testcomputername'), stub(:account => 'user3', :domain => 'DOMAIN2'), ] # use stubbed objectSid on member to return stubbed SID - Puppet::Util::Windows::Security.expects(:octet_string_to_sid_object).with([0]).returns(sids[0]) - Puppet::Util::Windows::Security.expects(:octet_string_to_sid_object).with([1]).returns(sids[1]) + Puppet::Util::Windows::SID.expects(:octet_string_to_sid_object).with([0]).returns(sids[0]) + Puppet::Util::Windows::SID.expects(:octet_string_to_sid_object).with([1]).returns(sids[1]) - Puppet::Util::Windows::Security.expects(:name_to_sid_object).with('user2').returns(sids[1]) - Puppet::Util::Windows::Security.expects(:name_to_sid_object).with('DOMAIN2\user3').returns(sids[2]) + Puppet::Util::Windows::SID.expects(:name_to_sid_object).with('user2').returns(sids[1]) + Puppet::Util::Windows::SID.expects(:name_to_sid_object).with('DOMAIN2\user3').returns(sids[2]) Puppet::Util::Windows::ADSI.expects(:sid_uri).with(sids[0]).returns("WinNT://DOMAIN/user1,user") Puppet::Util::Windows::ADSI.expects(:sid_uri).with(sids[2]).returns("WinNT://DOMAIN2/user3,user") members = names.each_with_index.map{|n,i| stub(:Name => n, :objectSID => [i])} adsi_group.expects(:Members).returns members adsi_group.expects(:Remove).with('WinNT://DOMAIN/user1,user') adsi_group.expects(:Add).with('WinNT://DOMAIN2/user3,user') group.set_members(['user2', 'DOMAIN2\user3']) end it "should raise an error when a username does not resolve to a SID" do expect { adsi_group.expects(:Members).returns [] group.set_members(['foobar']) }.to raise_error(Puppet::Error, /Could not resolve username: foobar/) end it "should generate the correct URI" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) group.uri.should == "WinNT://./#{groupname},group" end end it "should generate the correct URI" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) Puppet::Util::Windows::ADSI::Group.uri("people").should == "WinNT://./people,group" end it "should be able to create a group" do adsi_group = stub("adsi") connection.expects(:Create).with('group', groupname).returns(adsi_group) Puppet::Util::Windows::ADSI::User.expects(:exists?).with(groupname).returns(false) group = Puppet::Util::Windows::ADSI::Group.create(groupname) group.should be_a(Puppet::Util::Windows::ADSI::Group) group.native_group.should == adsi_group end it "should be able to confirm the existence of a group" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) Puppet::Util::Windows::ADSI.expects(:connect).with("WinNT://./#{groupname},group").returns connection Puppet::Util::Windows::ADSI::Group.exists?(groupname).should be_true end it "should be able to confirm the existence of a group with a well-known SID" do service_group = Win32::Security::SID::Service # ensure that the underlying OS is queried here Puppet::Util::Windows::ADSI.unstub(:connect) Puppet::Util::Windows::ADSI::Group.exists?(service_group).should be_true end it "should return nil with an unknown SID" do bogus_sid = 'S-1-2-3-4' # ensure that the underlying OS is queried here Puppet::Util::Windows::ADSI.unstub(:connect) Puppet::Util::Windows::ADSI::Group.exists?(bogus_sid).should be_false end it "should be able to delete a group" do connection.expects(:Delete).with('group', groupname) Puppet::Util::Windows::ADSI::Group.delete(groupname) end it "should return an enumeration of IADsGroup wrapped objects" do Puppet::Util::Windows::ADSI.stubs(:sid_uri_safe).returns(nil) name = 'Administrators' wmi_groups = [stub('WMI', :name => name)] Puppet::Util::Windows::ADSI.expects(:execquery).with('select name from win32_group where localaccount = "TRUE"').returns(wmi_groups) native_group = stub('IADsGroup') native_group.expects(:Members).returns([stub(:Name => 'Administrator')]) Puppet::Util::Windows::ADSI.expects(:connect).with("WinNT://./#{name},group").returns(native_group) groups = Puppet::Util::Windows::ADSI::Group.to_a groups.length.should == 1 groups[0].name.should == name groups[0].members.should == ['Administrator'] end end describe Puppet::Util::Windows::ADSI::UserProfile do it "should be able to delete a user profile" do connection.expects(:Delete).with("Win32_UserProfile.SID='S-A-B-C'") Puppet::Util::Windows::ADSI::UserProfile.delete('S-A-B-C') end it "should warn on 2003" do connection.expects(:Delete).raises(RuntimeError, "Delete (WIN32OLERuntimeError) OLE error code:80041010 in SWbemServicesEx Invalid class HRESULT error code:0x80020009 Exception occurred.") Puppet.expects(:warning).with("Cannot delete user profile for 'S-A-B-C' prior to Vista SP1") Puppet::Util::Windows::ADSI::UserProfile.delete('S-A-B-C') end end end diff --git a/spec/unit/util/windows/sid_spec.rb b/spec/unit/util/windows/sid_spec.rb index 770512188..5a3d788c7 100755 --- a/spec/unit/util/windows/sid_spec.rb +++ b/spec/unit/util/windows/sid_spec.rb @@ -1,169 +1,166 @@ #!/usr/bin/env ruby require 'spec_helper' describe "Puppet::Util::Windows::SID", :if => Puppet.features.microsoft_windows? do if Puppet.features.microsoft_windows? require 'puppet/util/windows' - class SIDTester - include Puppet::Util::Windows::SID - end end - let(:subject) { SIDTester.new } + let(:subject) { Puppet::Util::Windows::SID } let(:sid) { Win32::Security::SID::LocalSystem } let(:invalid_sid) { 'bogus' } let(:unknown_sid) { 'S-0-0-0' } let(:unknown_name) { 'chewbacca' } context "#octet_string_to_sid_object" do it "should properly convert an array of bytes for the local Administrator SID" do host = '.' username = 'Administrator' admin = WIN32OLE.connect("WinNT://#{host}/#{username},user") converted = subject.octet_string_to_sid_object(admin.objectSID) converted.should == Win32::Security::SID.new(username, host) converted.should be_an_instance_of Win32::Security::SID end it "should properly convert an array of bytes for a well-known SID" do bytes = [1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0] converted = subject.octet_string_to_sid_object(bytes) converted.should == Win32::Security::SID.new('SYSTEM') converted.should be_an_instance_of Win32::Security::SID end it "should raise an error for non-array input" do expect { subject.octet_string_to_sid_object(invalid_sid) }.to raise_error(Puppet::Error, /Octet string must be an array of bytes/) end it "should raise an error for an empty byte array" do expect { subject.octet_string_to_sid_object([]) }.to raise_error(Puppet::Error, /Octet string must be an array of bytes/) end it "should raise an error for a malformed byte array" do expect { invalid_octet = [1] subject.octet_string_to_sid_object(invalid_octet) }.to raise_error(Win32::Security::SID::Error, /No mapping between account names and security IDs was done./) end end context "#name_to_sid" do it "should return nil if the account does not exist" do subject.name_to_sid(unknown_name).should be_nil end it "should accept unqualified account name" do subject.name_to_sid('SYSTEM').should == sid end it "should be case-insensitive" do subject.name_to_sid('SYSTEM').should == subject.name_to_sid('system') end it "should be leading and trailing whitespace-insensitive" do subject.name_to_sid('SYSTEM').should == subject.name_to_sid(' SYSTEM ') end it "should accept domain qualified account names" do subject.name_to_sid('NT AUTHORITY\SYSTEM').should == sid end it "should be the identity function for any sid" do subject.name_to_sid(sid).should == sid end end context "#name_to_sid_object" do it "should return nil if the account does not exist" do subject.name_to_sid_object(unknown_name).should be_nil end it "should return a Win32::Security::SID instance for any valid sid" do subject.name_to_sid_object(sid).should be_an_instance_of(Win32::Security::SID) end it "should accept unqualified account name" do subject.name_to_sid_object('SYSTEM').to_s.should == sid end it "should be case-insensitive" do subject.name_to_sid_object('SYSTEM').should == subject.name_to_sid_object('system') end it "should be leading and trailing whitespace-insensitive" do subject.name_to_sid_object('SYSTEM').should == subject.name_to_sid_object(' SYSTEM ') end it "should accept domain qualified account names" do subject.name_to_sid_object('NT AUTHORITY\SYSTEM').to_s.should == sid end end context "#sid_to_name" do it "should return nil if given a sid for an account that doesn't exist" do subject.sid_to_name(unknown_sid).should be_nil end it "should accept a sid" do subject.sid_to_name(sid).should == "NT AUTHORITY\\SYSTEM" end end context "#sid_ptr_to_string" do it "should raise if given an invalid sid" do expect { subject.sid_ptr_to_string(nil) }.to raise_error(Puppet::Error, /Invalid SID/) end it "should yield a valid sid pointer" do string = nil subject.string_to_sid_ptr(sid) do |ptr| string = subject.sid_ptr_to_string(ptr) end string.should == sid end end context "#string_to_sid_ptr" do it "should yield sid_ptr" do ptr = nil subject.string_to_sid_ptr(sid) do |p| ptr = p end ptr.should_not be_nil end it "should raise on an invalid sid" do expect { subject.string_to_sid_ptr(invalid_sid) }.to raise_error(Puppet::Error, /Failed to convert string SID/) end end context "#valid_sid?" do it "should return true for a valid SID" do subject.valid_sid?(sid).should be_true end it "should return false for an invalid SID" do subject.valid_sid?(invalid_sid).should be_false end it "should raise if the conversion fails" do subject.expects(:string_to_sid_ptr).with(sid). raises(Puppet::Util::Windows::Error.new("Failed to convert string SID: #{sid}", Windows::Error::ERROR_ACCESS_DENIED)) expect { subject.string_to_sid_ptr(sid) {|ptr| } }.to raise_error(Puppet::Util::Windows::Error, /Failed to convert string SID: #{sid}/) end end end