diff --git a/lib/puppet/provider/group/windows_adsi.rb b/lib/puppet/provider/group/windows_adsi.rb index c6db2af92..76cd65805 100644 --- a/lib/puppet/provider/group/windows_adsi.rb +++ b/lib/puppet/provider/group/windows_adsi.rb @@ -1,86 +1,93 @@ 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) + current_users = Puppet::Util::Windows::ADSI::Group.name_sid_hash(current) + specified_users = Puppet::Util::Windows::ADSI::Group.name_sid_hash(should) + + if @resource[:auth_membership] + current_users == specified_users + else + (specified_users.keys.to_a & current_users.keys.to_a) == specified_users.keys.to_a + end 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::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) + group.set_members(members, @resource[:auth_membership]) 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::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/type/group.rb b/lib/puppet/type/group.rb index d5adaf455..66752bd7a 100644 --- a/lib/puppet/type/group.rb +++ b/lib/puppet/type/group.rb @@ -1,188 +1,188 @@ require 'etc' require 'facter' require 'puppet/property/keyvalue' require 'puppet/parameter/boolean' module Puppet newtype(:group) do @doc = "Manage groups. On most platforms this can only create groups. Group membership must be managed on individual users. On some platforms such as OS X, group membership is managed as an attribute of the group, not the user record. Providers must have the feature 'manages_members' to manage the 'members' property of a group record." feature :manages_members, "For directories where membership is an attribute of groups not users." feature :manages_aix_lam, "The provider can manage AIX Loadable Authentication Module (LAM) system." feature :system_groups, "The provider allows you to create system groups with lower GIDs." feature :libuser, "Allows local groups to be managed on systems that also use some other remote NSS method of managing accounts." ensurable do desc "Create or remove the group." newvalue(:present) do provider.create end newvalue(:absent) do provider.delete end end newproperty(:gid) do desc "The group ID. Must be specified numerically. If no group ID is specified when creating a new group, then one will be chosen automatically according to local system standards. This will likely result in the same group having different GIDs on different systems, which is not recommended. On Windows, this property is read-only and will return the group's security identifier (SID)." def retrieve provider.gid end def sync if self.should == :absent raise Puppet::DevError, "GID cannot be deleted" else provider.gid = self.should end end munge do |gid| case gid when String if gid =~ /^[-0-9]+$/ gid = Integer(gid) else self.fail "Invalid GID #{gid}" end when Symbol unless gid == :absent self.devfail "Invalid GID #{gid}" end end return gid end end newproperty(:members, :array_matching => :all, :required_features => :manages_members) do desc "The members of the group. For directory services where group membership is stored in the group objects, not the users." def change_to_s(currentvalue, newvalue) currentvalue = currentvalue.join(",") if currentvalue != :absent newvalue = newvalue.join(",") super(currentvalue, newvalue) end def insync?(current) if provider.respond_to?(:members_insync?) return provider.members_insync?(current, @should) end super(current) end def is_to_s(currentvalue) if provider.respond_to?(:members_to_s) currentvalue = '' if currentvalue.nil? return provider.members_to_s(currentvalue.split(',')) end super(currentvalue) end alias :should_to_s :is_to_s end - newparam(:auth_membership) do + newparam(:auth_membership, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "whether the provider is authoritative for group membership." defaultto true end newparam(:name) do desc "The group name. While naming limitations vary by operating system, it is advisable to restrict names to the lowest common denominator, which is a maximum of 8 characters beginning with a letter. Note that Puppet considers group names to be case-sensitive, regardless of the platform's own rules; be sure to always use the same case when referring to a given group." isnamevar end newparam(:allowdupe, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Whether to allow duplicate GIDs. Defaults to `false`." defaultto false end newparam(:ia_load_module, :required_features => :manages_aix_lam) do desc "The name of the I&A module to use to manage this user" end newproperty(:attributes, :parent => Puppet::Property::KeyValue, :required_features => :manages_aix_lam) do desc "Specify group AIX attributes in an array of `key=value` pairs." def membership :attribute_membership end def delimiter " " end validate do |value| raise ArgumentError, "Attributes value pairs must be separated by an =" unless value.include?("=") end end newparam(:attribute_membership) do desc "Whether specified attribute value pairs should be treated as the only attributes of the user or whether they should merely be treated as the minimum list." newvalues(:inclusive, :minimum) defaultto :minimum end newparam(:system, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Whether the group is a system group with lower GID." defaultto false end newparam(:forcelocal, :boolean => true, :required_features => :libuser, :parent => Puppet::Parameter::Boolean) do desc "Forces the management of local accounts when accounts are also being managed by some other NSS" defaultto false end # This method has been exposed for puppet to manage users and groups of # files in its settings and should not be considered available outside of # puppet. # # (see Puppet::Settings#service_group_available?) # # @return [Boolean] if the group exists on the system # @api private def exists? provider.exists? end end end diff --git a/lib/puppet/util/windows/adsi.rb b/lib/puppet/util/windows/adsi.rb index 041be7765..d16bd23e2 100644 --- a/lib/puppet/util/windows/adsi.rb +++ b/lib/puppet/util/windows/adsi.rb @@ -1,430 +1,431 @@ 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 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 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 # 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 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) + 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) + + 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 7c9366f72..3c9329ee1 100644 --- a/spec/unit/provider/group/windows_adsi_spec.rb +++ b/spec/unit/provider/group/windows_adsi_spec.rb @@ -1,168 +1,200 @@ #!/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']).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 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::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