summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/puppet/feature/base.rb3
-rw-r--r--lib/puppet/util/adsi.rb278
-rw-r--r--spec/unit/util/adsi_spec.rb202
3 files changed, 483 insertions, 0 deletions
diff --git a/lib/puppet/feature/base.rb b/lib/puppet/feature/base.rb
index 56782b3b6..2eddadb7a 100644
--- a/lib/puppet/feature/base.rb
+++ b/lib/puppet/feature/base.rb
@@ -46,6 +46,9 @@ Puppet.features.add(:microsoft_windows) do
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
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/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