diff --git a/lib/puppet/util/windows/adsi.rb b/lib/puppet/util/windows/adsi.rb index d16bd23e2..6eff9cf00 100644 --- a/lib/puppet/util/windows/adsi.rb +++ b/lib/puppet/util/windows/adsi.rb @@ -1,431 +1,447 @@ 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 + rescue WIN32OLERuntimeError => 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 SystemCallError 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::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 extend FFI::Library 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::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 + rescue WIN32OLERuntimeError => e + # ERROR_BAD_USERNAME 2202L from winerror.h + if e.message =~ /8007089A/m + raise Puppet::Error.new( + "Puppet is not able to create/delete domain users with the user resource.", + e + ) + end + 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 # UNLEN from lmcons.h - http://stackoverflow.com/a/2155176 MAX_USERNAME_LENGTH = 256 def self.current_user_name user_name = '' max_length = MAX_USERNAME_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 GetUserNameW(buffer, buffer_size) == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Failed to get user name") end # buffer_size includes trailing NULL user_name = buffer.read_wide_string(buffer_size.read_dword - 1) end end user_name 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 ffi_convention :stdcall # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724432(v=vs.85).aspx # BOOL WINAPI GetUserName( # _Out_ LPTSTR lpBuffer, # _Inout_ LPDWORD lpnSize # ); ffi_lib :advapi32 attach_function_private :GetUserNameW, [:lpwstr, :lpdword], :win32_bool end class UserProfile def self.delete(sid) begin Puppet::Util::Windows::ADSI.wmi_connection.Delete("Win32_UserProfile.SID='#{sid}'") - rescue => e + rescue WIN32OLERuntimeError => 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, :sid 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 sid @sid ||= Puppet::Util::Windows::SID.octet_string_to_sid_object(native_group.objectSID) end def commit begin native_group.SetInfo unless native_group.nil? - rescue Exception => e + rescue WIN32OLERuntimeError => e + # ERROR_BAD_USERNAME 2202L from winerror.h + if e.message =~ /8007089A/m + raise Puppet::Error.new( + "Puppet is not able to create/delete domain groups with the group resource.", + e + ) + end + 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::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::SID.octet_string_to_sid_object(m.objectSID) end sids end def set_members(desired_members, inclusive = true) 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) if inclusive 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/spec/unit/provider/group/windows_adsi_spec.rb b/spec/unit/provider/group/windows_adsi_spec.rb index 49316665f..a61338ef1 100644 --- a/spec/unit/provider/group/windows_adsi_spec.rb +++ b/spec/unit/provider/group/windows_adsi_spec.rb @@ -1,208 +1,220 @@ #!/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') } let(:user3) { stub(:account => 'user3', :domain => '.', :to_s => 'user3sid') } before :each do 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) Puppet::Util::Windows::SID.stubs(:name_to_sid_object).with('user3').returns(user3) 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 context "when auth_membership => true" do before :each do # this is also the default resource[:auth_membership] = true end it "should return false when should user(s) are not the only items in the current" do provider.members_insync?(['user1', 'user2'], ['user1']).should be_false end end context "when auth_membership => false" do before :each do resource[:auth_membership] = false end it "should return true when current user(s) contains at least the should list" do provider.members_insync?(['user1','user2'], ['user1']).should be_true end it "should return true when current user(s) contains at least the should list, even unordered" do provider.members_insync?(['user3','user1','user2'], ['user2','user1']).should be_true end 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::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'], true).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 fail with an actionable message when trying to create an active directory group" do + resource[:name] = 'DOMAIN\testdomaingroup' + Puppet::Util::Windows::ADSI::User.expects(:exists?).with(resource[:name]).returns(false) + connection.expects(:Create) + connection.expects(:SetInfo).raises( WIN32OLERuntimeError.new("(in OLE method `SetInfo': )\n OLE error code:8007089A in Active Directory\n The specified username is invalid.\r\n\n HRESULT error code:0x80020009\n Exception occurred.")) + + expect{ provider.create }.to raise_error( + Puppet::Error, + /not able to create\/delete domain groups/ + ) + 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 not run commit on a deleted group' do connection.expects(:Delete).with('group', 'testers') connection.expects(:SetInfo).never provider.delete provider.flush end it "should report the group's SID as gid" do 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/user/windows_adsi_spec.rb b/spec/unit/provider/user/windows_adsi_spec.rb index 13f98fe52..c09b17d4c 100755 --- a/spec/unit/provider/user/windows_adsi_spec.rb +++ b/spec/unit/provider/user/windows_adsi_spec.rb @@ -1,181 +1,194 @@ #!/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 + + it "should fail with an actionable message when trying to create an active directory user" do + resource[:name] = 'DOMAIN\testdomainuser' + Puppet::Util::Windows::ADSI::Group.expects(:exists?).with(resource[:name]).returns(false) + connection.expects(:Create) + connection.expects(:SetPassword) + connection.expects(:SetInfo).raises( WIN32OLERuntimeError.new("(in OLE method `SetInfo': )\n OLE error code:8007089A in Active Directory\n The specified username is invalid.\r\n\n HRESULT error code:0x80020009\n Exception occurred.")) + + expect{ provider.create }.to raise_error( + Puppet::Error, + /not able to create\/delete domain users/ + ) + 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 not run commit on a deleted user' do connection.expects(:Delete).with('user', 'testuser') connection.expects(:SetInfo).never provider.delete provider.flush end it 'should delete the profile if managehome is set' do resource[:managehome] = true sid = 'S-A-B-C' 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::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 f569d91e9..f9ab2e2a2 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::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::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::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::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::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::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::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::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::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::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, + connection.expects(:Delete).raises(WIN32OLERuntimeError, "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