diff --git a/lib/puppet/provider/group/windows_adsi.rb b/lib/puppet/provider/group/windows_adsi.rb index 4468d0071..7d67367f2 100644 --- a/lib/puppet/provider/group/windows_adsi.rb +++ b/lib/puppet/provider/group/windows_adsi.rb @@ -1,48 +1,55 @@ require 'puppet/util/adsi' Puppet::Type.type(:group).provide :windows_adsi do desc "Group management for Windows" defaultfor :operatingsystem => :windows confine :operatingsystem => :windows confine :feature => :microsoft_windows has_features :manages_members def group @group ||= Puppet::Util::ADSI::Group.new(@resource[:name]) end def members group.members end def members=(members) group.set_members(members) end def create @group = Puppet::Util::ADSI::Group.create(@resource[:name]) + @group.commit + self.members = @resource[:members] end def exists? Puppet::Util::ADSI::Group.exists?(@resource[:name]) end def delete Puppet::Util::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 nil end def gid=(value) warning "No support for managing property gid of group #{@resource[:name]} on Windows" end def self.instances Puppet::Util::ADSI::Group.map { |g| new(:ensure => :present, :name => g.name) } end end diff --git a/lib/puppet/provider/user/windows_adsi.rb b/lib/puppet/provider/user/windows_adsi.rb index 9250def59..06ffe445f 100644 --- a/lib/puppet/provider/user/windows_adsi.rb +++ b/lib/puppet/provider/user/windows_adsi.rb @@ -1,71 +1,73 @@ require 'puppet/util/adsi' Puppet::Type.type(:user).provide :windows_adsi do desc "User management for Windows" defaultfor :operatingsystem => :windows confine :operatingsystem => :windows confine :feature => :microsoft_windows has_features :manages_homedir def user @user ||= Puppet::Util::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::ADSI::User.create(@resource[:name]) + @user.commit + [:comment, :home, :groups].each do |prop| send("#{prop}=", @resource[prop]) if @resource[prop] end end def exists? Puppet::Util::ADSI::User.exists?(@resource[:name]) end def delete Puppet::Util::ADSI::User.delete(@resource[:name]) 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 [:uid, :gid, :shell].each do |prop| define_method(prop) { nil } define_method("#{prop}=") do |v| warning "No support for managing property #{prop} of user #{@resource[:name]} on Windows" end end def self.instances Puppet::Util::ADSI::User.map { |u| new(:ensure => :present, :name => u.name) } end end diff --git a/lib/puppet/util/adsi.rb b/lib/puppet/util/adsi.rb index f865743e2..dfa8a04ce 100644 --- a/lib/puppet/util/adsi.rb +++ b/lib/puppet/util/adsi.rb @@ -1,278 +1,282 @@ module Puppet::Util::ADSI class << self 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}" ) end end def create(name, resource_type) Puppet::Util::ADSI.connect(computer_uri).Create(resource_type, name) end def delete(name, resource_type) Puppet::Util::ADSI.connect(computer_uri).Delete(resource_type, name) end def computer_name unless @computer_name buf = " " * 128 Win32API.new('kernel32', 'GetComputerName', ['P','P'], 'I').call(buf, buf.length.to_s) @computer_name = buf.unpack("A*") end @computer_name end def computer_uri "WinNT://#{computer_name}" end def wmi_resource_uri( host = '.' ) "winmgmts:{impersonationLevel=impersonate}!//#{host}/root/cimv2" end def uri(resource_name, resource_type) "#{computer_uri}/#{resource_name},#{resource_type}" end def execquery(query) connect(wmi_resource_uri).execquery(query) end end class User extend Enumerable attr_accessor :native_user attr_reader :name def initialize(name, native_user = nil) @name = name @native_user = native_user end def native_user @native_user ||= Puppet::Util::ADSI.connect(uri) end def self.uri(name) Puppet::Util::ADSI.uri(name, 'user') end def uri self.class.uri(name) end def self.logon(name, password) fLOGON32_LOGON_NETWORK = 3 fLOGON32_PROVIDER_DEFAULT = 0 logon_user = Win32API.new("advapi32", "LogonUser", ['P', 'P', 'P', 'L', 'L', 'P'], 'L') close_handle = Win32API.new("kernel32", "CloseHandle", ['P'], 'V') token = ' ' * 4 if logon_user.call(name, "", password, fLOGON32_LOGON_NETWORK, fLOGON32_PROVIDER_DEFAULT, token) != 0 close_handle.call(token.unpack('L')[0]) true else false end 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}" ) 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} + 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::ADSI::Group.new(group_name).add_member(@name) end end alias add_to_group add_to_groups def remove_from_groups(*group_names) group_names.each do |group_name| Puppet::Util::ADSI::Group.new(group_name).remove_member(@name) 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::ADSI::Group.exists? name new(name, Puppet::Util::ADSI.create(name, 'user')) end def self.exists?(name) Puppet::Util::ADSI::connectable?(User.uri(name)) end def self.delete(name) Puppet::Util::ADSI.delete(name, 'user') end def self.each(&block) wql = Puppet::Util::ADSI.execquery("select * from win32_useraccount") users = [] wql.each do |u| users << new(u.name, u) end users.each(&block) 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) Puppet::Util::ADSI.uri(name, 'group') end def native_group @native_group ||= Puppet::Util::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}" ) end self end def add_members(*names) names.each do |name| native_group.Add(Puppet::Util::ADSI::User.uri(name)) end end alias add_member add_members def remove_members(*names) names.each do |name| native_group.Remove(Puppet::Util::ADSI::User.uri(name)) end end alias remove_member remove_members def members # WIN32OLE objects aren't enumerable, so no map members = [] native_group.Members.each {|m| members << m.Name} members end def set_members(desired_members) return if desired_members.nil? or desired_members.empty? current_members = self.members # First we add all missing members members_to_add = desired_members - current_members add_members(*members_to_add) # Then we remove all extra members members_to_remove = current_members - desired_members remove_members(*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::ADSI::User.exists? name new(name, Puppet::Util::ADSI.create(name, 'group')) end def self.exists?(name) Puppet::Util::ADSI.connectable?(Group.uri(name)) end def self.delete(name) Puppet::Util::ADSI.delete(name, 'group') end def self.each(&block) wql = Puppet::Util::ADSI.execquery( "select * from win32_group" ) groups = [] wql.each do |g| groups << new(g.name, g) 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 7faaa1a8c..61f290202 100644 --- a/spec/unit/provider/group/windows_adsi_spec.rb +++ b/spec/unit/provider/group/windows_adsi_spec.rb @@ -1,79 +1,95 @@ #!/usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:group).provider(:windows_adsi) 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::ADSI.stubs(:computer_name).returns('testcomputername') Puppet::Util::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 * from win32_group").returns stub_groups described_class.instances.map(&:name).should =~ names 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'] provider.group.expects(:remove_members).with('user1') provider.group.expects(:add_members).with('user3') provider.members = ['user2', 'user3'] end end - it "should be able to create a group" do - resource[:members] = ['user1', 'user2'] + describe 'when creating groups' do + it "should be able to create a group" do + resource[:members] = ['user1', 'user2'] - group = stub 'group' - Puppet::Util::ADSI::Group.expects(:create).with('testers').returns group + group = stub 'group' + Puppet::Util::ADSI::Group.expects(:create).with('testers').returns group - group.expects(:set_members).with(['user1', 'user2']) + create = sequence('create') + group.expects(:commit).in_sequence(create) + group.expects(:set_members).with(['user1', 'user2']).in_sequence(create) - provider.create + provider.create + end + + it 'should not create a group if a user by the same name exists' do + Puppet::Util::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::ADSI.stubs(:connect).returns stub('connection') provider.should be_exists Puppet::Util::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 warn when trying to manage the gid property" do provider.expects(:warning).with { |msg| msg =~ /No support for managing property gid/ } provider.send(:gid=, 500) end end diff --git a/spec/unit/provider/user/windows_adsi_spec.rb b/spec/unit/provider/user/windows_adsi_spec.rb index 073a3d328..68f1c134a 100644 --- a/spec/unit/provider/user/windows_adsi_spec.rb +++ b/spec/unit/provider/user/windows_adsi_spec.rb @@ -1,110 +1,118 @@ #!/usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:user).provider(:windows_adsi) 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::ADSI.stubs(:computer_name).returns('testcomputername') Puppet::Util::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 * from win32_useraccount").returns(stub_users) described_class.instances.map(&:name).should =~ names end end it "should provide access to a Puppet::Util::ADSI::User object" do provider.user.should be_a(Puppet::Util::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::ADSI::User.expects(:create).with('testuser').returns user user.stubs(:groups).returns(['group2', 'group3']) - user.expects(:set_groups).with('group1,group2', false) + create = 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 not create a user if a group by the same name exists' do + Puppet::Util::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::ADSI.stubs(:connect).returns stub('connection') provider.should be_exists Puppet::Util::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 commit the user when flushed" do provider.user.expects(:commit) provider.flush end [:uid, :gid, :shell].each do |prop| it "should warn when trying to manage the #{prop} property" do provider.expects(:warning).with { |msg| msg =~ /No support for managing property #{prop}/ } provider.send("#{prop}=", 'foo') end end end diff --git a/spec/unit/util/adsi_spec.rb b/spec/unit/util/adsi_spec.rb index 3a27cdaef..a14845f7b 100755 --- a/spec/unit/util/adsi_spec.rb +++ b/spec/unit/util/adsi_spec.rb @@ -1,206 +1,208 @@ #!/usr/bin/env ruby require 'spec_helper' require 'puppet/util/adsi' describe Puppet::Util::ADSI do let(:connection) { stub 'connection' } before(:each) do Puppet::Util::ADSI.instance_variable_set(:@computer_name, 'testcomputername') Puppet::Util::ADSI.stubs(:connect).returns connection end after(:each) do Puppet::Util::ADSI.instance_variable_set(:@computer_name, nil) end it "should generate the correct URI for a resource" do Puppet::Util::ADSI.uri('test', 'user').should == "WinNT://testcomputername/test,user" end it "should be able to get the name of the computer" do Puppet::Util::ADSI.computer_name.should == 'testcomputername' end it "should be able to provide the correct WinNT base URI for the computer" do Puppet::Util::ADSI.computer_uri.should == "WinNT://testcomputername" end describe Puppet::Util::ADSI::User do let(:username) { 'testuser' } it "should generate the correct URI" do Puppet::Util::ADSI::User.uri(username).should == "WinNT://testcomputername/#{username},user" 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::ADSI::Group.expects(:exists?).with(username).returns(false) user = Puppet::Util::ADSI::User.create(username) user.should be_a(Puppet::Util::ADSI::User) user.native_user.should == adsi_user end it "should be able to check the existence of a user" do Puppet::Util::ADSI.expects(:connect).with("WinNT://testcomputername/#{username},user").returns connection Puppet::Util::ADSI::User.exists?(username).should be_true end it "should be able to delete a user" do connection.expects(:Delete).with('user', username) Puppet::Util::ADSI::User.delete(username) end describe "an instance" do let(:adsi_user) { stub 'user' } let(:user) { Puppet::Util::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::ADSI::User.expects(:logon).with(username, 'pwdwrong').returns(false) Puppet::Util::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 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 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::ADSI.expects(:connect).with('WinNT://testcomputername/group1,group').returns group1 Puppet::Util::ADSI.expects(:connect).with('WinNT://testcomputername/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::ADSI.expects(:connect).with('WinNT://testcomputername/group1,group').returns group1 user.set_groups(groups_to_set, true) end end end end end describe Puppet::Util::ADSI::Group do let(:groupname) { 'testgroup' } describe "an instance" do let(:adsi_group) { stub 'group' } let(:group) { Puppet::Util::ADSI::Group.new(groupname, adsi_group) } it "should be able to add a member" do adsi_group.expects(:Add).with("WinNT://testcomputername/someone,user") group.add_member('someone') end it "should be able to remove a member" do adsi_group.expects(:Remove).with("WinNT://testcomputername/someone,user") group.remove_member('someone') 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 = ['user1', 'user2'] adsi_group.expects(:Members).returns names.map{|n| stub(:Name => n)} adsi_group.expects(:Remove).with('WinNT://testcomputername/user1,user') adsi_group.expects(:Add).with('WinNT://testcomputername/user3,user') group.set_members(['user2', 'user3']) end it "should generate the correct URI" do group.uri.should == "WinNT://testcomputername/#{groupname},group" end end it "should generate the correct URI" do Puppet::Util::ADSI::Group.uri("people").should == "WinNT://testcomputername/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::ADSI::User.expects(:exists?).with(groupname).returns(false) group = Puppet::Util::ADSI::Group.create(groupname) group.should be_a(Puppet::Util::ADSI::Group) group.native_group.should == adsi_group end it "should be able to confirm the existence of a group" do Puppet::Util::ADSI.expects(:connect).with("WinNT://testcomputername/#{groupname},group").returns connection Puppet::Util::ADSI::Group.exists?(groupname).should be_true end it "should be able to delete a group" do connection.expects(:Delete).with('group', groupname) Puppet::Util::ADSI::Group.delete(groupname) end end end