summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNick Lewis <nick@puppetlabs.com>2011-08-09 13:04:10 -0700
committerJacob Helwig <jacob@puppetlabs.com>2011-08-19 13:52:59 -0700
commitf19a0ea8c12c06bb01ddfe53e47e3a02ae87bdbb (patch)
tree4e5b26d8250b583ba502139a5e304e3e31f224ce
parent6919d2c082ad2bdf1bcc19cd1b62c1befc75133c (diff)
downloadpuppet-f19a0ea8c12c06bb01ddfe53e47e3a02ae87bdbb.tar.gz
puppet-f19a0ea8c12c06bb01ddfe53e47e3a02ae87bdbb.tar.xz
puppet-f19a0ea8c12c06bb01ddfe53e47e3a02ae87bdbb.zip
(#8408/8409) Add a Windows ADSI helper module
This module (Puppet::Util::ADSI) provides access to Active Directory Services Interfaces, using win32ole. The base module has methods for generating resource URIs and connecting to ADSI. It also provides classes Puppet::Util::ADSI::User and Puppet::Util::ADSI::Group for managing Active Directory users and groups, along with their properties and group memberships. This will be used to implement the Windows ADSI user and group providers. Based on work by: Joel Rosario <joel.r@.internal.directi.com> Based on work by: Cameron Thomas <cameron@puppetlabs.com> Reviewed-By: Matt Robinson <matt@puppetlabs.com> (cherry picked from commit b5fd95336e71ad428109cddf6cd2f33bdd31e025)
-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