diff --git a/lib/puppet/feature/base.rb b/lib/puppet/feature/base.rb index b4b1313f8..b1988278e 100644 --- a/lib/puppet/feature/base.rb +++ b/lib/puppet/feature/base.rb @@ -1,63 +1,66 @@ # Created by Luke Kanies on 2006-04-30. # Copyright (c) 2006. All rights reserved. require 'puppet/util/feature' # Add the simple features, all in one file. # We've got LDAP available. Puppet.features.add(:ldap, :libs => ["ldap"]) # We have the Rdoc::Usage library. Puppet.features.add(:usage, :libs => %w{rdoc/ri/ri_paths rdoc/usage}) # We have libshadow, useful for managing passwords. Puppet.features.add(:libshadow, :libs => ["shadow"]) # We're running as root. Puppet.features.add(:root) { require 'puppet/util/suidmanager'; Puppet::Util::SUIDManager.root? } # We've got mongrel available Puppet.features.add(:mongrel, :libs => %w{rubygems mongrel puppet/network/http_server/mongrel}) # We have lcs diff Puppet.features.add :diff, :libs => %w{diff/lcs diff/lcs/hunk} # We have augeas Puppet.features.add(:augeas, :libs => ["augeas"]) # We have RRD available Puppet.features.add(:rrd_legacy, :libs => ["RRDtool"]) Puppet.features.add(:rrd, :libs => ["RRD"]) # We have OpenSSL Puppet.features.add(:openssl, :libs => ["openssl"]) # We have a syslog implementation Puppet.features.add(:syslog, :libs => ["syslog"]) # We can use POSIX user functions Puppet.features.add(:posix) do require 'etc' Etc.getpwuid(0) != nil && Puppet.features.syslog? end # We can use Microsoft Windows functions Puppet.features.add(:microsoft_windows) do begin require 'sys/admin' require 'win32/process' require 'win32/dir' require 'win32/service' + require 'win32ole' + require 'win32/api' + true rescue LoadError => err warn "Cannot run on Microsoft Windows without the sys-admin, win32-process, win32-dir & win32-service gems: #{err}" unless Puppet.features.posix? end end raise Puppet::Error,"Cannot determine basic system flavour" unless Puppet.features.posix? or Puppet.features.microsoft_windows? # We have CouchDB Puppet.features.add(:couchdb, :libs => ["couchrest"]) # We have sqlite Puppet.features.add(:sqlite, :libs => ["sqlite3"]) diff --git a/lib/puppet/provider/group/windows_adsi.rb b/lib/puppet/provider/group/windows_adsi.rb new file mode 100644 index 000000000..4468d0071 --- /dev/null +++ b/lib/puppet/provider/group/windows_adsi.rb @@ -0,0 +1,48 @@ +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]) + 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 + + 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 new file mode 100644 index 000000000..9250def59 --- /dev/null +++ b/lib/puppet/provider/user/windows_adsi.rb @@ -0,0 +1,71 @@ +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]) + [: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 new file mode 100644 index 000000000..f865743e2 --- /dev/null +++ b/lib/puppet/util/adsi.rb @@ -0,0 +1,278 @@ +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} + 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) + 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) + 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 new file mode 100644 index 000000000..7faaa1a8c --- /dev/null +++ b/spec/unit/provider/group/windows_adsi_spec.rb @@ -0,0 +1,79 @@ +#!/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'] + + group = stub 'group' + Puppet::Util::ADSI::Group.expects(:create).with('testers').returns group + + group.expects(:set_members).with(['user1', 'user2']) + + provider.create + 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 new file mode 100644 index 000000000..073a3d328 --- /dev/null +++ b/spec/unit/provider/user/windows_adsi_spec.rb @@ -0,0 +1,110 @@ +#!/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) + user.expects(:[]=).with('Description', 'a test user') + user.expects(:[]=).with('HomeDirectory', 'C:\Users\testuser') + + provider.create + 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 new file mode 100644 index 000000000..b61724405 --- /dev/null +++ b/spec/unit/util/adsi_spec.rb @@ -0,0 +1,202 @@ +#!/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 + + 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) + + 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) + + 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