summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLuke Kanies <luke@madstop.com>2008-05-12 17:00:48 -0500
committerLuke Kanies <luke@madstop.com>2008-05-12 17:00:48 -0500
commit17e8158e35336291c551da03067b55dd815ab539 (patch)
tree7783b1f3d08ea9eeea7116d522018acabf438f10
parentc56e9a6a0a9491270e22363e750046f284ee2793 (diff)
downloadpuppet-17e8158e35336291c551da03067b55dd815ab539.tar.gz
puppet-17e8158e35336291c551da03067b55dd815ab539.tar.xz
puppet-17e8158e35336291c551da03067b55dd815ab539.zip
Adding ldap providers for the user and group type.
These providers use posixAccount and posixGroup. This is a collapsed merge, fwiw.
-rw-r--r--lib/puppet/provider/group/ldap.rb37
-rw-r--r--lib/puppet/provider/ldap.rb137
-rw-r--r--lib/puppet/provider/user/ldap.rb115
-rw-r--r--lib/puppet/util/ldap.rb5
-rw-r--r--lib/puppet/util/ldap/connection.rb57
-rw-r--r--lib/puppet/util/ldap/generator.rb45
-rw-r--r--lib/puppet/util/ldap/manager.rb281
-rwxr-xr-xspec/unit/provider/group/ldap.rb66
-rwxr-xr-xspec/unit/provider/ldap.rb248
-rwxr-xr-xspec/unit/provider/user/ldap.rb252
-rwxr-xr-xspec/unit/util/ldap/connection.rb114
-rwxr-xr-xspec/unit/util/ldap/generator.rb54
-rwxr-xr-xspec/unit/util/ldap/manager.rb654
13 files changed, 2065 insertions, 0 deletions
diff --git a/lib/puppet/provider/group/ldap.rb b/lib/puppet/provider/group/ldap.rb
new file mode 100644
index 000000000..632358ff1
--- /dev/null
+++ b/lib/puppet/provider/group/ldap.rb
@@ -0,0 +1,37 @@
+require 'puppet/provider/ldap'
+
+Puppet::Type.type(:group).provide :ldap, :parent => Puppet::Provider::Ldap do
+ desc "Group management via ``ldap``. This provider requires that you
+ have valid values for all of the ldap-related settings,
+ including ``ldapbase``. You will also almost definitely need settings
+ for ``ldapuser`` and ``ldappassword``, so that your clients can write
+ to ldap.
+
+ Note that this provider will automatically generate a GID for you if
+ you do not specify one, but it is a potentially expensive operation,
+ as it iterates across all existing groups to pick the appropriate next
+ one."
+
+ confine :true => Puppet.features.ldap?
+
+ # We're mapping 'members' here because we want to make it
+ # easy for the ldap user provider to manage groups. This
+ # way it can just use the 'update' method in the group manager,
+ # whereas otherwise it would need to replicate that code.
+ manages(:posixGroup).at("ou=Groups").and.maps :name => :cn, :gid => :gidNumber, :members => :memberUid
+
+ # Find the next gid after the current largest gid.
+ provider = self
+ manager.generates(:gidNumber).with do
+ largest = 0
+ provider.manager.search.each do |hash|
+ next unless value = hash[:gid]
+ num = value[0].to_i
+ if num > largest
+ largest = num
+ end
+ end
+ largest + 1
+ end
+
+end
diff --git a/lib/puppet/provider/ldap.rb b/lib/puppet/provider/ldap.rb
new file mode 100644
index 000000000..76834f94d
--- /dev/null
+++ b/lib/puppet/provider/ldap.rb
@@ -0,0 +1,137 @@
+require 'puppet/provider'
+
+# The base class for LDAP providers.
+class Puppet::Provider::Ldap < Puppet::Provider
+ require 'puppet/util/ldap/manager'
+
+ class << self
+ attr_reader :manager
+ end
+
+ # Look up all instances at our location. Yay.
+ def self.instances
+ return [] unless list = manager.search
+
+ list.collect { |entry| new(entry) }
+ end
+
+ # Specify the ldap manager for this provider, which is
+ # used to figure out how we actually interact with ldap.
+ def self.manages(*args)
+ @manager = Puppet::Util::Ldap::Manager.new
+ @manager.manages(*args)
+
+ # Set up our getter/setter methods.
+ mk_resource_methods
+ return @manager
+ end
+
+ # Query all of our resources from ldap.
+ def self.prefetch(resources)
+ resources.each do |name, resource|
+ if result = manager.find(name)
+ result[:ensure] = :present
+ resource.provider = new(result)
+ else
+ resource.provider = new(:ensure => :absent)
+ end
+ end
+ end
+
+ attr_reader :ldap_properties
+
+ def manager
+ self.class.manager
+ end
+
+ def create
+ @property_hash[:ensure] = :present
+ self.class.resource_type.validproperties.each do |property|
+ if val = resource.should(property)
+ @property_hash[property] = val
+ end
+ end
+ end
+
+ def delete
+ @property_hash[:ensure] = :absent
+ end
+
+ def exists?
+ @property_hash[:ensure] != :absent
+ end
+
+ # Apply our changes to ldap, yo.
+ def flush
+ # Just call the manager's update() method.
+ @property_hash.delete(:groups)
+ @ldap_properties.delete(:groups)
+ manager.update(name, ldap_properties, properties)
+ @property_hash.clear
+ @ldap_properties.clear
+ end
+
+ def initialize(*args)
+ raise(Puppet::DevError, "No LDAP Configuration defined for %s" % self.class) unless self.class.manager
+ raise(Puppet::DevError, "Invalid LDAP Configuration defined for %s" % self.class) unless self.class.manager.valid?
+ super
+
+ @property_hash = @property_hash.inject({}) do |result, ary|
+ param, values = ary
+
+ # Skip any attributes we don't manage.
+ next result unless self.class.resource_type.validattr?(param)
+
+ paramclass = self.class.resource_type.attrclass(param)
+
+ unless values.is_a?(Array)
+ result[param] = values
+ next result
+ end
+
+ # Only use the first value if the attribute class doesn't manage
+ # arrays of values.
+ if paramclass.superclass == Puppet::Parameter or paramclass.array_matching == :first
+ result[param] = values[0]
+ else
+ result[param] = values
+ end
+ result
+ end
+
+ # Make a duplicate, so that we have a copy for comparison
+ # at the end.
+ @ldap_properties = @property_hash.dup
+ end
+
+ # Return the current state of ldap.
+ def ldap_properties
+ @ldap_properties.dup
+ end
+
+ # Return (and look up if necessary) the desired state.
+ def properties
+ if @property_hash.empty?
+ @property_hash = query || {:ensure => :absent}
+ if @property_hash.empty?
+ @property_hash[:ensure] = :absent
+ end
+ end
+ @property_hash.dup
+ end
+
+ # Collect the current attributes from ldap. Returns
+ # the results, but also stores the attributes locally,
+ # so we have something to compare against when we update.
+ # LAK:NOTE This is normally not used, because we rely on prefetching.
+ def query
+ # Use the module function.
+ unless attributes = manager.find(name)
+ @ldap_properties = {}
+ return nil
+ end
+
+ @ldap_properties = attributes
+ return @ldap_properties.dup
+ end
+end
diff --git a/lib/puppet/provider/user/ldap.rb b/lib/puppet/provider/user/ldap.rb
new file mode 100644
index 000000000..ba91a871e
--- /dev/null
+++ b/lib/puppet/provider/user/ldap.rb
@@ -0,0 +1,115 @@
+require 'puppet/provider/ldap'
+
+Puppet::Type.type(:user).provide :ldap, :parent => Puppet::Provider::Ldap do
+ desc "User management via ``ldap``. This provider requires that you
+ have valid values for all of the ldap-related settings,
+ including ``ldapbase``. You will also almost definitely need settings
+ for ``ldapuser`` and ``ldappassword``, so that your clients can write
+ to ldap.
+
+ Note that this provider will automatically generate a UID for you if
+ you do not specify one, but it is a potentially expensive operation,
+ as it iterates across all existing users to pick the appropriate next
+ one."
+
+ confine :true => Puppet.features.ldap?
+
+ manages(:posixAccount, :person).at("ou=People").named_by(:uid).and.maps :name => :uid,
+ :password => :userPassword,
+ :comment => :cn,
+ :uid => :uidNumber,
+ :gid => :gidNumber,
+ :home => :homeDirectory,
+ :shell => :loginShell
+
+ # Use the last field of a space-separated array as
+ # the sn. LDAP requires a surname, for some stupid reason.
+ manager.generates(:sn).from(:cn).with do |cn|
+ x = 1
+ cn[0].split(/\s+/)[-1]
+ end
+
+ # Find the next uid after the current largest uid.
+ provider = self
+ manager.generates(:uidNumber).with do
+ largest = 0
+ provider.manager.search.each do |hash|
+ next unless value = hash[:uid]
+ num = value[0].to_i
+ if num > largest
+ largest = num
+ end
+ end
+ largest + 1
+ end
+
+ # Find all groups this user is a member of in ldap.
+ def groups
+ # We want to cache the current result, so we know if we
+ # have to remove old values.
+ unless @property_hash[:groups]
+ unless result = group_manager.search("memberUid=%s" % name)
+ return @property_hash[:groups] = :absent
+ end
+
+ return @property_hash[:groups] = result.collect { |r| r[:name] }.join(",")
+ end
+ return @property_hash[:groups]
+ end
+
+ # Manage the list of groups this user is a member of.
+ def groups=(values)
+ should = values.split(",")
+
+ if groups() == :absent
+ is = []
+ else
+ is = groups().split(",")
+ end
+
+ modes = {}
+ [is, should].flatten.uniq.each do |group|
+ # Skip it when they're in both
+ next if is.include?(group) and should.include?(group)
+
+ # We're adding a group.
+ modes[group] = :add and next unless is.include?(group)
+
+ # We're removing a group.
+ modes[group] = :remove and next unless should.include?(group)
+ end
+
+ modes.each do |group, form|
+ self.fail "Could not find ldap group %s" % group unless ldap_group = group_manager.find(group)
+
+ current = ldap_group[:members]
+
+ if form == :add
+ if current.is_a?(Array) and ! current.empty?
+ new = current + [name]
+ else
+ new = [name]
+ end
+ else
+ new = current - [name]
+ new = :absent if new.empty?
+ end
+
+ group_manager.update(group, {:ensure => :present, :members => current}, {:ensure => :present, :members => new})
+ end
+ end
+
+ private
+
+ def group_manager
+ Puppet::Type.type(:group).provider(:ldap).manager
+ end
+
+ def group_properties(values)
+ if values.empty? or values == :absent
+ {:ensure => :present}
+ else
+ {:ensure => :present, :members => values}
+ end
+ end
+end
diff --git a/lib/puppet/util/ldap.rb b/lib/puppet/util/ldap.rb
new file mode 100644
index 000000000..33f01f789
--- /dev/null
+++ b/lib/puppet/util/ldap.rb
@@ -0,0 +1,5 @@
+#
+# Created by Luke Kanies on 2008-3-23.
+# Copyright (c) 2008. All rights reserved.
+module Puppet::Util::Ldap
+end
diff --git a/lib/puppet/util/ldap/connection.rb b/lib/puppet/util/ldap/connection.rb
new file mode 100644
index 000000000..abcc07ecb
--- /dev/null
+++ b/lib/puppet/util/ldap/connection.rb
@@ -0,0 +1,57 @@
+#
+# Created by Luke Kanies on 2008-3-23.
+# Copyright (c) 2008. All rights reserved.
+require 'puppet/util/ldap'
+
+class Puppet::Util::Ldap::Connection
+ attr_accessor :host, :port, :user, :password, :reset, :ssl
+
+ attr_reader :connection
+
+ def close
+ connection.unbind if connection.bound?
+ end
+
+ def initialize(host, port, options = {})
+ raise Puppet::Error, "Could not set up LDAP Connection: Missing ruby/ldap libraries" unless Puppet.features.ldap?
+
+ @host, @port = host, port
+
+ options.each do |param, value|
+ begin
+ send(param.to_s + "=", value)
+ rescue
+ raise ArgumentError, "LDAP connections do not support %s parameters" % param
+ end
+ end
+ end
+
+ # Create a per-connection unique name.
+ def name
+ [host, port, user, password, ssl].collect { |p| p.to_s }.join("/")
+ end
+
+ # Should we reset the connection?
+ def reset?
+ reset
+ end
+
+ # Start our ldap connection.
+ def start
+ begin
+ case ssl
+ when :tls:
+ @connection = LDAP::SSLConn.new(host, port, true)
+ when true:
+ @connection = LDAP::SSLConn.new(host, port)
+ else
+ @connection = LDAP::Conn.new(host, port)
+ end
+ @connection.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
+ @connection.set_option(LDAP::LDAP_OPT_REFERRALS, LDAP::LDAP_OPT_ON)
+ @connection.simple_bind(user, password)
+ rescue => detail
+ raise Puppet::Error, "Could not connect to LDAP: %s" % detail
+ end
+ end
+end
diff --git a/lib/puppet/util/ldap/generator.rb b/lib/puppet/util/ldap/generator.rb
new file mode 100644
index 000000000..2a868b0d9
--- /dev/null
+++ b/lib/puppet/util/ldap/generator.rb
@@ -0,0 +1,45 @@
+#
+# Created by Luke Kanies on 2008-3-28.
+# Copyright (c) 2008. All rights reserved.
+require 'puppet/util/ldap'
+
+class Puppet::Util::Ldap::Generator
+ # Declare the attribute we'll use to generate the value.
+ def from(source)
+ @source = source
+ return self
+ end
+
+ # Actually do the generation.
+ def generate(value = nil)
+ if value.nil?
+ @generator.call
+ else
+ @generator.call(value)
+ end
+ end
+
+ # Initialize our generator with the name of the parameter
+ # being generated.
+ def initialize(name)
+ @name = name
+ end
+
+ def name
+ @name.to_s
+ end
+
+ def source
+ if defined?(@source) and @source
+ @source.to_s
+ else
+ nil
+ end
+ end
+
+ # Provide the code that does the generation.
+ def with(&block)
+ @generator = block
+ return self
+ end
+end
diff --git a/lib/puppet/util/ldap/manager.rb b/lib/puppet/util/ldap/manager.rb
new file mode 100644
index 000000000..9761fc753
--- /dev/null
+++ b/lib/puppet/util/ldap/manager.rb
@@ -0,0 +1,281 @@
+require 'puppet/util/ldap'
+require 'puppet/util/ldap/connection'
+require 'puppet/util/ldap/generator'
+
+# The configuration class for LDAP providers, plus
+# connection handling for actually interacting with ldap.
+class Puppet::Util::Ldap::Manager
+ attr_reader :objectclasses, :puppet2ldap, :location, :rdn
+
+ # A null-op that just returns the config.
+ def and
+ return self
+ end
+
+ # Set the offset from the search base and return the config.
+ def at(location)
+ @location = location
+ return self
+ end
+
+ # The basic search base.
+ def base
+ [location, Puppet[:ldapbase]].join(",")
+ end
+
+ # Convert the name to a dn, then pass the args along to
+ # our connection.
+ def create(name, attributes)
+ attributes = attributes.dup
+
+ # Add the objectclasses
+ attributes["objectClass"] = objectclasses.collect { |o| o.to_s }
+ attributes["objectClass"] << "top" unless attributes["objectClass"].include?("top")
+
+ attributes[rdn.to_s] = [name]
+
+ # Generate any new values we might need.
+ generate(attributes)
+
+ # And create our resource.
+ connect { |conn| conn.add dn(name), attributes }
+ end
+
+ # Open, yield, and close the connection. Cannot be left
+ # open, at this point.
+ def connect
+ raise ArgumentError, "You must pass a block to #connect" unless block_given?
+
+ unless defined?(@connection) and @connection
+ if Puppet[:ldaptls]
+ ssl = :tls
+ elsif Puppet[:ldapssl]
+ ssl = true
+ else
+ ssl = false
+ end
+ options = {:ssl => ssl}
+ if user = Puppet[:ldapuser] and user != ""
+ options[:user] = user
+ end
+ if password = Puppet[:ldappassword] and password != ""
+ options[:password] = password
+ end
+ @connection = Puppet::Util::Ldap::Connection.new(Puppet[:ldapserver], Puppet[:ldapport], options)
+ end
+ @connection.start
+ begin
+ yield @connection.connection
+ ensure
+ @connection.close
+ end
+ return nil
+ end
+
+ # Convert the name to a dn, then pass the args along to
+ # our connection.
+ def delete(name)
+ connect { |connection| connection.delete dn(name) }
+ end
+
+ # Calculate the dn for a given resource.
+ def dn(name)
+ ["#{rdn.to_s}=%s" % name, base].join(",")
+ end
+
+ # Convert an ldap-style entry hash to a provider-style hash.
+ def entry2provider(entry)
+ raise ArgumentError, "Could not get dn from ldap entry" unless entry["dn"]
+
+ # DN is always a single-entry array. Strip off the bits before the
+ # first comma, then the bits after the remaining equal sign. This is the
+ # name.
+ name = entry["dn"].dup.pop.split(",").shift.split("=").pop
+
+ result = {:name => name}
+
+ @ldap2puppet.each do |ldap, puppet|
+ result[puppet] = entry[ldap.to_s] || :absent
+ end
+
+ result
+ end
+
+ # Create our normal search filter.
+ def filter
+ return "objectclass=%s" % objectclasses[0] if objectclasses.length == 1
+ return "(&(objectclass=" + objectclasses.join(")(objectclass=") + "))"
+ end
+
+ # Find the associated entry for a resource. Returns a hash, minus
+ # 'dn', or nil if the entry cannot be found.
+ def find(name)
+ result = nil
+ connect do |conn|
+ begin
+ conn.search2(dn(name), 0, "objectclass=*") do |result|
+ # Convert to puppet-appropriate attributes
+ return entry2provider(result)
+ end
+ rescue => detail
+ return nil
+ end
+ end
+ end
+
+ # Declare a new attribute generator.
+ def generates(parameter)
+ @generators << Puppet::Util::Ldap::Generator.new(parameter)
+ @generators[-1]
+ end
+
+ # Generate any extra values we need to make the ldap entry work.
+ def generate(values)
+ return unless @generators.length > 0
+
+ @generators.each do |generator|
+ # Don't override any values that might exist.
+ next if values[generator.name]
+
+ if generator.source
+ unless value = values[generator.source]
+ raise ArgumentError, "%s must be defined to generate %s" % [generator.source, generator.name]
+ end
+ result = generator.generate(value)
+ else
+ result = generator.generate
+ end
+
+ result = [result] unless result.is_a?(Array)
+ result = result.collect { |r| r.to_s }
+
+ values[generator.name] = result
+ end
+ end
+
+ def initialize
+ @rdn = :cn
+ @generators = []
+ end
+
+ # Specify what classes this provider models.
+ def manages(*classes)
+ @objectclasses = classes
+ return self
+ end
+
+ # Specify the attribute map. Assumes the keys are the puppet
+ # attributes, and the values are the ldap attributes, and creates a map
+ # for each direction.
+ def maps(attributes)
+ # The map with the puppet attributes as the keys
+ @puppet2ldap = attributes
+
+ # and the ldap attributes as the keys.
+ @ldap2puppet = attributes.inject({}) { |map, ary| map[ary[1]] = ary[0]; map }
+
+ return self
+ end
+
+ # Return the ldap name for a puppet attribute.
+ def ldap_name(attribute)
+ @puppet2ldap[attribute].to_s
+ end
+
+ # Convert the name to a dn, then pass the args along to
+ # our connection.
+ def modify(name, mods)
+ connect { |connection| connection.modify dn(name), mods }
+ end
+
+ # Specify the rdn that we use to build up our dn.
+ def named_by(attribute)
+ @rdn = attribute
+ self
+ end
+
+ # Return the puppet name for an ldap attribute.
+ def puppet_name(attribute)
+ @ldap2puppet[attribute]
+ end
+
+ # Search for all entries at our base. A potentially expensive search.
+ def search(sfilter = nil)
+ sfilter ||= filter()
+
+ result = []
+ connect do |conn|
+ conn.search2(base, 1, sfilter) do |entry|
+ result << entry2provider(entry)
+ end
+ end
+ return nil if result.empty?
+ return result
+ end
+
+ # Update the ldap entry with the desired state.
+ def update(name, is, should)
+ if should[:ensure] == :absent
+ Puppet.info "Removing %s from ldap" % dn(name)
+ delete(name)
+ return
+ end
+
+ # We're creating a new entry
+ if is.empty? or is[:ensure] == :absent
+ Puppet.info "Creating %s in ldap" % dn(name)
+ # Remove any :absent params and :ensure, then convert the names to ldap names.
+ attrs = ldap_convert(should)
+ create(name, attrs)
+ return
+ end
+
+ # We're modifying an existing entry. Yuck.
+
+ mods = []
+ # For each attribute we're deleting that is present, create a
+ # modify instance for deletion.
+ [is.keys, should.keys].flatten.uniq.each do |property|
+ # They're equal, so do nothing.
+ next if is[property] == should[property]
+
+ attributes = ldap_convert(should)
+
+ prop_name = ldap_name(property).to_s
+
+ # We're creating it.
+ if is[property] == :absent or is[property].nil?
+ mods << LDAP::Mod.new(LDAP::LDAP_MOD_ADD, prop_name, attributes[prop_name])
+ next
+ end
+
+ # We're deleting it
+ if should[property] == :absent or should[property].nil?
+ mods << LDAP::Mod.new(LDAP::LDAP_MOD_DELETE, prop_name, [])
+ next
+ end
+
+ # We're replacing an existing value
+ mods << LDAP::Mod.new(LDAP::LDAP_MOD_REPLACE, prop_name, attributes[prop_name])
+ end
+
+ modify(name, mods)
+ end
+
+ # Is this a complete ldap configuration?
+ def valid?
+ location and objectclasses and ! objectclasses.empty? and puppet2ldap
+ end
+
+ private
+
+ # Convert a hash of attributes to ldap-like forms. This mostly means
+ # getting rid of :ensure and making sure everything's an array of strings.
+ def ldap_convert(attributes)
+ attributes.reject { |param, value| value == :absent or param == :ensure }.inject({}) do |result, ary|
+ value = (ary[1].is_a?(Array) ? ary[1] : [ary[1]]).collect { |v| v.to_s }
+ result[ldap_name(ary[0])] = value
+ result
+ end
+ end
+end
diff --git a/spec/unit/provider/group/ldap.rb b/spec/unit/provider/group/ldap.rb
new file mode 100755
index 000000000..3f12d74e3
--- /dev/null
+++ b/spec/unit/provider/group/ldap.rb
@@ -0,0 +1,66 @@
+#!/usr/bin/env ruby
+#
+# Created by Luke Kanies on 2008-3-10.
+# Copyright (c) 2006. All rights reserved.
+
+require File.dirname(__FILE__) + '/../../../spec_helper'
+
+provider_class = Puppet::Type.type(:group).provider(:ldap)
+
+describe provider_class do
+ it "should have the Ldap provider class as its baseclass" do
+ provider_class.superclass.should equal(Puppet::Provider::Ldap)
+ end
+
+ it "should manage :posixGroup objectclass" do
+ provider_class.manager.objectclasses.should == [:posixGroup]
+ end
+
+ it "should use 'ou=Groups' as its relative base" do
+ provider_class.manager.location.should == "ou=Groups"
+ end
+
+ it "should use :cn as its rdn" do
+ provider_class.manager.rdn.should == :cn
+ end
+
+ it "should map :name to 'cn'" do
+ provider_class.manager.ldap_name(:name).should == 'cn'
+ end
+
+ it "should map :gid to 'gidNumber'" do
+ provider_class.manager.ldap_name(:gid).should == 'gidNumber'
+ end
+
+ it "should map :members to 'memberUid', to be used by the user ldap provider" do
+ provider_class.manager.ldap_name(:members).should == 'memberUid'
+ end
+
+ describe "when being created" do
+ before do
+ # So we don't try to actually talk to ldap
+ @connection = mock 'connection'
+ provider_class.manager.stubs(:connect).yields @connection
+ end
+
+ describe "with no gid specified" do
+ it "should pick the first available GID after the largest existing GID" do
+ low = {:name=>["luke"], :gid=>["100"]}
+ high = {:name=>["testing"], :gid=>["140"]}
+ provider_class.manager.expects(:search).returns([low, high])
+
+ resource = stub 'resource', :should => %w{whatever}
+ resource.stubs(:should).with(:gid).returns nil
+ resource.stubs(:should).with(:ensure).returns :present
+ instance = provider_class.new(:name => "luke", :ensure => :absent)
+ instance.stubs(:resource).returns resource
+
+ @connection.expects(:add).with { |dn, attrs| attrs["gidNumber"] == ["141"] }
+
+ instance.create
+ instance.flush
+ end
+ end
+ end
+
+end
diff --git a/spec/unit/provider/ldap.rb b/spec/unit/provider/ldap.rb
new file mode 100755
index 000000000..fd5d1bdc3
--- /dev/null
+++ b/spec/unit/provider/ldap.rb
@@ -0,0 +1,248 @@
+#!/usr/bin/env ruby
+#
+# Created by Luke Kanies on 2008-3-21.
+# Copyright (c) 2006. All rights reserved.
+
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+require 'puppet/provider/ldap'
+
+describe Puppet::Provider::Ldap do
+ before do
+ @class = Class.new(Puppet::Provider::Ldap)
+ end
+
+ it "should be able to define its manager" do
+ manager = mock 'manager'
+ Puppet::Util::Ldap::Manager.expects(:new).returns manager
+ @class.stubs :mk_resource_methods
+ manager.expects(:manages).with(:one)
+ @class.manages(:one).should equal(manager)
+ @class.manager.should equal(manager)
+ end
+
+ it "should be able to prefetch instances from ldap" do
+ @class.should respond_to(:prefetch)
+ end
+
+ it "should create its resource getter/setter methods when the manager is defined" do
+ manager = mock 'manager'
+ Puppet::Util::Ldap::Manager.expects(:new).returns manager
+ @class.expects :mk_resource_methods
+ manager.stubs(:manages)
+ @class.manages(:one).should equal(manager)
+ end
+
+ it "should have an instances method" do
+ @class.should respond_to(:instances)
+ end
+
+ describe "when providing a list of instances" do
+ it "should convert all results returned from the manager's :search method into provider instances" do
+ manager = mock 'manager'
+ @class.stubs(:manager).returns manager
+
+ manager.expects(:search).returns %w{one two three}
+
+ @class.expects(:new).with("one").returns(1)
+ @class.expects(:new).with("two").returns(2)
+ @class.expects(:new).with("three").returns(3)
+
+ @class.instances.should == [1,2,3]
+ end
+ end
+
+ it "should have a prefetch method" do
+ @class.should respond_to(:prefetch)
+ end
+
+ describe "when prefetching" do
+ before do
+ @manager = mock 'manager'
+ @class.stubs(:manager).returns @manager
+
+ @resource = mock 'resource'
+
+ @resources = {"one" => @resource}
+ end
+
+ it "should find an entry for each passed resource" do
+ @manager.expects(:find).with("one").returns nil
+
+ @class.stubs(:new)
+ @resource.stubs(:provider=)
+ @class.prefetch(@resources)
+ end
+
+ describe "resources that do not exist" do
+ it "should create a provider with :ensure => :absent" do
+ result = mock 'result'
+ @manager.expects(:find).with("one").returns nil
+
+ @class.expects(:new).with(:ensure => :absent).returns "myprovider"
+
+ @resource.expects(:provider=).with("myprovider")
+
+ @class.prefetch(@resources)
+ end
+ end
+
+ describe "resources that exist" do
+ it "should create a provider with the results of the find" do
+ @manager.expects(:find).with("one").returns("one" => "two")
+
+ @class.expects(:new).with("one" => "two", :ensure => :present).returns "myprovider"
+
+ @resource.expects(:provider=).with("myprovider")
+
+ @class.prefetch(@resources)
+ end
+
+ it "should set :ensure to :present in the returned values" do
+ @manager.expects(:find).with("one").returns("one" => "two")
+
+ @class.expects(:new).with("one" => "two", :ensure => :present).returns "myprovider"
+
+ @resource.expects(:provider=).with("myprovider")
+
+ @class.prefetch(@resources)
+ end
+ end
+ end
+
+ describe "when being initialized" do
+ it "should fail if no manager has been defined" do
+ lambda { @class.new }.should raise_error(Puppet::DevError)
+ end
+
+ it "should fail if the manager is invalid" do
+ manager = stub "manager", :valid? => false
+ @class.stubs(:manager).returns manager
+ lambda { @class.new }.should raise_error(Puppet::DevError)
+ end
+
+ describe "with a hash" do
+ before do
+ @manager = stub "manager", :valid? => true
+ @class.stubs(:manager).returns @manager
+
+ @resource_class = mock 'resource_class'
+ @class.stubs(:resource_type).returns @resource_class
+
+ @property_class = stub 'property_class', :array_matching => :all, :superclass => Puppet::Property
+ @resource_class.stubs(:attrclass).with(:one).returns(@property_class)
+ @resource_class.stubs(:validattr?).returns true
+ end
+
+ it "should store a copy of the hash as its ldap_properties" do
+ instance = @class.new(:one => :two)
+ instance.ldap_properties.should == {:one => :two}
+ end
+
+ it "should only store the first value of each value array for those attributes that do not match all values" do
+ @property_class.expects(:array_matching).returns :first
+ instance = @class.new(:one => %w{two three})
+ instance.properties.should == {:one => "two"}
+ end
+
+ it "should store the whole value array for those attributes that match all values" do
+ @property_class.expects(:array_matching).returns :all
+ instance = @class.new(:one => %w{two three})
+ instance.properties.should == {:one => %w{two three}}
+ end
+
+ it "should only use the first value for attributes that are not properties" do
+ # Yay. hackish, but easier than mocking everything.
+ @resource_class.expects(:attrclass).with(:a).returns Puppet::Type.type(:user).attrclass(:name)
+ @property_class.stubs(:array_matching).returns :all
+
+ instance = @class.new(:one => %w{two three}, :a => %w{b c})
+ instance.properties.should == {:one => %w{two three}, :a => "b"}
+ end
+
+ it "should discard any properties not valid in the resource class" do
+ @resource_class.expects(:validattr?).with(:a).returns false
+ @property_class.stubs(:array_matching).returns :all
+
+ instance = @class.new(:one => %w{two three}, :a => %w{b})
+ instance.properties.should == {:one => %w{two three}}
+ end
+ end
+ end
+
+ describe "when an instance" do
+ before do
+ @manager = stub "manager", :valid? => true
+ @class.stubs(:manager).returns @manager
+ @instance = @class.new
+
+ @property_class = stub 'property_class', :array_matching => :all, :superclass => Puppet::Property
+ @resource_class = stub 'resource_class', :attrclass => @property_class, :validattr? => true, :validproperties => [:one, :two]
+ @class.stubs(:resource_type).returns @resource_class
+ end
+
+ it "should have a method for creating the ldap entry" do
+ @instance.should respond_to(:create)
+ end
+
+ it "should have a method for removing the ldap entry" do
+ @instance.should respond_to(:delete)
+ end
+
+ it "should have a method for returning the class's manager" do
+ @instance.manager.should equal(@manager)
+ end
+
+ it "should indicate when the ldap entry already exists" do
+ @instance = @class.new(:ensure => :present)
+ @instance.exists?.should be_true
+ end
+
+ it "should indicate when the ldap entry does not exist" do
+ @instance = @class.new(:ensure => :absent)
+ @instance.exists?.should be_false
+ end
+
+ describe "is being flushed" do
+ it "should call the manager's :update method with its name, current attributes, and desired attributes" do
+ @instance.stubs(:name).returns "myname"
+ @instance.stubs(:ldap_properties).returns(:one => :two)
+ @instance.stubs(:properties).returns(:three => :four)
+ @manager.expects(:update).with(@instance.name, {:one => :two}, {:three => :four})
+ @instance.flush
+ end
+ end
+
+ describe "is being created" do
+ before do
+ @rclass = mock 'resource_class'
+ @rclass.stubs(:validproperties).returns([:one, :two])
+ @resource = mock 'resource'
+ @resource.stubs(:class).returns @rclass
+ @resource.stubs(:should).returns nil
+ @instance.stubs(:resource).returns @resource
+ end
+
+ it "should set its :ensure value to :present" do
+ @instance.create
+ @instance.properties[:ensure].should == :present
+ end
+
+ it "should set all of the other attributes from the resource" do
+ @resource.expects(:should).with(:one).returns "oneval"
+ @resource.expects(:should).with(:two).returns "twoval"
+
+ @instance.create
+ @instance.properties[:one].should == "oneval"
+ @instance.properties[:two].should == "twoval"
+ end
+ end
+
+ describe "is being deleted" do
+ it "should set its :ensure value to :absent" do
+ @instance.delete
+ @instance.properties[:ensure].should == :absent
+ end
+ end
+ end
+end
diff --git a/spec/unit/provider/user/ldap.rb b/spec/unit/provider/user/ldap.rb
new file mode 100755
index 000000000..c4731cbbb
--- /dev/null
+++ b/spec/unit/provider/user/ldap.rb
@@ -0,0 +1,252 @@
+#!/usr/bin/env ruby
+#
+# Created by Luke Kanies on 2008-3-10.
+# Copyright (c) 2006. All rights reserved.
+
+require File.dirname(__FILE__) + '/../../../spec_helper'
+
+provider_class = Puppet::Type.type(:user).provider(:ldap)
+
+describe provider_class do
+ it "should have the Ldap provider class as its baseclass" do
+ provider_class.superclass.should equal(Puppet::Provider::Ldap)
+ end
+
+ it "should manage :posixAccount and :person objectclasses" do
+ provider_class.manager.objectclasses.should == [:posixAccount, :person]
+ end
+
+ it "should use 'ou=People' as its relative base" do
+ provider_class.manager.location.should == "ou=People"
+ end
+
+ it "should use :uid as its rdn" do
+ provider_class.manager.rdn.should == :uid
+ end
+
+ {:name => "uid",
+ :password => "userPassword",
+ :comment => "cn",
+ :uid => "uidNumber",
+ :gid => "gidNumber",
+ :home => "homeDirectory",
+ :shell => "loginShell"
+ }.each do |puppet, ldap|
+ it "should map :#{puppet.to_s} to '#{ldap}'" do
+ provider_class.manager.ldap_name(puppet).should == ldap
+ end
+ end
+
+ describe "when being created" do
+ before do
+ # So we don't try to actually talk to ldap
+ @connection = mock 'connection'
+ provider_class.manager.stubs(:connect).yields @connection
+ end
+
+ it "should generate the sn as the last field of the cn" do
+ resource = stub 'resource', :should => %w{whatever}
+ resource.stubs(:should).with(:comment).returns ["Luke Kanies"]
+ resource.stubs(:should).with(:ensure).returns :present
+ instance = provider_class.new(:name => "luke", :ensure => :absent)
+ instance.stubs(:resource).returns resource
+
+ @connection.expects(:add).with { |dn, attrs| attrs["sn"] == ["Kanies"] }
+
+ instance.create
+ instance.flush
+ end
+
+ describe "with no uid specified" do
+ it "should pick the first available UID after the largest existing UID" do
+ low = {:name=>["luke"], :shell=>:absent, :uid=>["100"], :home=>["/h"], :gid=>["1000"], :password=>["blah"], :comment=>["l k"]}
+ high = {:name=>["testing"], :shell=>:absent, :uid=>["140"], :home=>["/h"], :gid=>["1000"], :password=>["blah"], :comment=>["t u"]}
+ provider_class.manager.expects(:search).returns([low, high])
+
+ resource = stub 'resource', :should => %w{whatever}
+ resource.stubs(:should).with(:uid).returns nil
+ resource.stubs(:should).with(:ensure).returns :present
+ instance = provider_class.new(:name => "luke", :ensure => :absent)
+ instance.stubs(:resource).returns resource
+
+ @connection.expects(:add).with { |dn, attrs| attrs["uidNumber"] == ["141"] }
+
+ instance.create
+ instance.flush
+ end
+ end
+ end
+
+ describe "when flushing" do
+ before do
+ provider_class.stubs(:suitable?).returns true
+
+ @instance = provider_class.new(:name => "myname", :groups => %w{whatever}, :uid => "400")
+ end
+
+ it "should remove the :groups value before updating" do
+ @instance.class.manager.expects(:update).with { |name, ldap, puppet| puppet[:groups].nil? }
+
+ @instance.flush
+ end
+
+ it "should empty the property hash" do
+ @instance.class.manager.stubs(:update)
+
+ @instance.flush
+
+ @instance.uid.should == :absent
+ end
+
+ it "should empty the ldap property hash" do
+ @instance.class.manager.stubs(:update)
+
+ @instance.flush
+
+ @instance.ldap_properties[:uid].should be_nil
+ end
+ end
+
+ describe "when checking group membership" do
+ before do
+ @groups = Puppet::Type.type(:group).provider(:ldap)
+ @group_manager = @groups.manager
+ provider_class.stubs(:suitable?).returns true
+
+ @instance = provider_class.new(:name => "myname")
+ end
+
+ it "should show its group membership as the list of all groups returned by an ldap query of group memberships" do
+ one = {:name => "one"}
+ two = {:name => "two"}
+ @group_manager.expects(:search).with("memberUid=myname").returns([one, two])
+
+ @instance.groups.should == "one,two"
+ end
+
+ it "should show its group membership as :absent if no matching groups are found in ldap" do
+ @group_manager.expects(:search).with("memberUid=myname").returns(nil)
+
+ @instance.groups.should == :absent
+ end
+
+ it "should cache the group value" do
+ @group_manager.expects(:search).with("memberUid=myname").once.returns nil
+
+ @instance.groups
+ @instance.groups.should == :absent
+ end
+ end
+
+ describe "when modifying group membership" do
+ before do
+ @groups = Puppet::Type.type(:group).provider(:ldap)
+ @group_manager = @groups.manager
+ provider_class.stubs(:suitable?).returns true
+
+ @one = {:name => "one", :gid => "500"}
+ @group_manager.stubs(:find).with("one").returns(@one)
+
+ @two = {:name => "one", :gid => "600"}
+ @group_manager.stubs(:find).with("two").returns(@two)
+
+ @instance = provider_class.new(:name => "myname")
+
+ @instance.stubs(:groups).returns :absent
+ end
+
+ it "should fail if the group does not exist" do
+ @group_manager.expects(:find).with("mygroup").returns nil
+
+ lambda { @instance.groups = "mygroup" }.should raise_error(Puppet::Error)
+ end
+
+ it "should only pass the attributes it cares about to the group manager" do
+ @group_manager.expects(:update).with { |name, attrs| attrs[:gid].nil? }
+
+ @instance.groups = "one"
+ end
+
+ it "should always include :ensure => :present in the current values" do
+ @group_manager.expects(:update).with { |name, is, should| is[:ensure] == :present }
+
+ @instance.groups = "one"
+ end
+
+ it "should always include :ensure => :present in the desired values" do
+ @group_manager.expects(:update).with { |name, is, should| should[:ensure] == :present }
+
+ @instance.groups = "one"
+ end
+
+ it "should always pass the group's original member list" do
+ @one[:members] = %w{yay ness}
+ @group_manager.expects(:update).with { |name, is, should| is[:members] == %w{yay ness} }
+
+ @instance.groups = "one"
+ end
+
+ it "should find the group again when resetting its member list, so it has the full member list" do
+ @group_manager.expects(:find).with("one").returns(@one)
+
+ @group_manager.stubs(:update)
+
+ @instance.groups = "one"
+ end
+
+ describe "for groups that have no members" do
+ it "should create a new members attribute with its value being the user's name" do
+ @group_manager.expects(:update).with { |name, is, should| should[:members] == %w{myname} }
+
+ @instance.groups = "one"
+ end
+ end
+
+ describe "for groups it is being removed from" do
+ it "should replace the group's member list with one missing the user's name" do
+ @one[:members] = %w{myname a}
+ @two[:members] = %w{myname b}
+
+ @group_manager.expects(:update).with { |name, is, should| name == "two" and should[:members] == %w{b} }
+
+ @instance.stubs(:groups).returns "one,two"
+ @instance.groups = "one"
+ end
+
+ it "should mark the member list as empty if there are no remaining members" do
+ @one[:members] = %w{myname}
+ @two[:members] = %w{myname b}
+
+ @group_manager.expects(:update).with { |name, is, should| name == "one" and should[:members] == :absent }
+
+ @instance.stubs(:groups).returns "one,two"
+ @instance.groups = "two"
+ end
+ end
+
+ describe "for groups that already have members" do
+ it "should replace each group's member list with a new list including the user's name" do
+ @one[:members] = %w{a b}
+ @group_manager.expects(:update).with { |name, is, should| should[:members] == %w{a b myname} }
+ @two[:members] = %w{b c}
+ @group_manager.expects(:update).with { |name, is, should| should[:members] == %w{b c myname} }
+
+ @instance.groups = "one,two"
+ end
+ end
+
+ describe "for groups of which it is a member" do
+ it "should do nothing" do
+ @one[:members] = %w{a b}
+ @group_manager.expects(:update).with { |name, is, should| should[:members] == %w{a b myname} }
+
+ @two[:members] = %w{c myname}
+ @group_manager.expects(:update).with { |name, *other| name == "two" }.never
+
+ @instance.stubs(:groups).returns "two"
+
+ @instance.groups = "one,two"
+ end
+ end
+ end
+end
diff --git a/spec/unit/util/ldap/connection.rb b/spec/unit/util/ldap/connection.rb
new file mode 100755
index 000000000..212f3ca54
--- /dev/null
+++ b/spec/unit/util/ldap/connection.rb
@@ -0,0 +1,114 @@
+#!/usr/bin/env ruby
+#
+# Created by Luke Kanies on 2008-3-19.
+# Copyright (c) 2006. All rights reserved.
+
+require File.dirname(__FILE__) + '/../../../spec_helper'
+
+require 'puppet/util/ldap/connection'
+
+# So our mocks and such all work, even when ldap isn't available.
+unless defined?(LDAP::Conn)
+ class LDAP
+ class Conn
+ def initialize(*args)
+ end
+ end
+ class SSLConn < Conn; end
+
+ LDAP_OPT_PROTOCOL_VERSION = 1
+ LDAP_OPT_REFERRALS = 2
+ LDAP_OPT_ON = 3
+ end
+end
+
+describe Puppet::Util::Ldap::Connection do
+ before do
+ Puppet.features.stubs(:ldap?).returns true
+
+ @ldapconn = mock 'ldap'
+ LDAP::Conn.stubs(:new).returns(@ldapconn)
+ LDAP::SSLConn.stubs(:new).returns(@ldapconn)
+
+ @ldapconn.stub_everything
+
+ @connection = Puppet::Util::Ldap::Connection.new("host", "port")
+ end
+
+
+ describe "when creating connections" do
+ it "should require the host and port" do
+ lambda { Puppet::Util::Ldap::Connection.new("myhost") }.should raise_error(ArgumentError)
+ end
+
+ it "should allow specification of a user and password" do
+ lambda { Puppet::Util::Ldap::Connection.new("myhost", "myport", :user => "blah", :password => "boo") }.should_not raise_error
+ end
+
+ it "should allow specification of ssl" do
+ lambda { Puppet::Util::Ldap::Connection.new("myhost", "myport", :ssl => :tsl) }.should_not raise_error
+ end
+
+ it "should support requiring a new connection" do
+ lambda { Puppet::Util::Ldap::Connection.new("myhost", "myport", :reset => true) }.should_not raise_error
+ end
+
+ it "should fail if ldap is unavailable" do
+ Puppet.features.expects(:ldap?).returns(false)
+
+ lambda { Puppet::Util::Ldap::Connection.new("host", "port") }.should raise_error(Puppet::Error)
+ end
+
+ it "should use neither ssl nor tls by default" do
+ LDAP::Conn.expects(:new).with("host", "port").returns(@ldapconn)
+
+ @connection.start
+ end
+
+ it "should use LDAP::SSLConn if ssl is requested" do
+ LDAP::SSLConn.expects(:new).with("host", "port").returns(@ldapconn)
+
+ @connection.ssl = true
+
+ @connection.start
+ end
+
+ it "should use LDAP::SSLConn and tls if tls is requested" do
+ LDAP::SSLConn.expects(:new).with("host", "port", true).returns(@ldapconn)
+
+ @connection.ssl = :tls
+
+ @connection.start
+ end
+
+ it "should set the protocol version to 3 and enable referrals" do
+ @ldapconn.expects(:set_option).with(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
+ @ldapconn.expects(:set_option).with(LDAP::LDAP_OPT_REFERRALS, LDAP::LDAP_OPT_ON)
+ @connection.start
+ end
+
+ it "should bind with the provided user and password" do
+ @connection.user = "myuser"
+ @connection.password = "mypassword"
+ @ldapconn.expects(:simple_bind).with("myuser", "mypassword")
+
+ @connection.start
+ end
+
+ it "should bind with no user and password if none has been provided" do
+ @ldapconn.expects(:simple_bind).with(nil, nil)
+ @connection.start
+ end
+ end
+
+ describe "when closing connections" do
+ it "should not close connections that are not open" do
+ @connection.stubs(:connection).returns(@ldapconn)
+
+ @ldapconn.expects(:bound?).returns false
+ @ldapconn.expects(:unbind).never
+
+ @connection.close
+ end
+ end
+end
diff --git a/spec/unit/util/ldap/generator.rb b/spec/unit/util/ldap/generator.rb
new file mode 100755
index 000000000..a6c69de83
--- /dev/null
+++ b/spec/unit/util/ldap/generator.rb
@@ -0,0 +1,54 @@
+#!/usr/bin/env ruby
+#
+# Created by Luke Kanies on 2008-3-28.
+# Copyright (c) 2008. All rights reserved.
+
+require File.dirname(__FILE__) + '/../../../spec_helper'
+
+require 'puppet/util/ldap/generator'
+
+describe Puppet::Util::Ldap::Generator do
+ before do
+ @generator = Puppet::Util::Ldap::Generator.new(:uno)
+ end
+
+ it "should require a parameter name at initialization" do
+ lambda { Puppet::Util::Ldap::Generator.new }.should raise_error
+ end
+
+ it "should always return its name as a string" do
+ g = Puppet::Util::Ldap::Generator.new(:myname)
+ g.name.should == "myname"
+ end
+
+ it "should provide a method for declaring the source parameter" do
+ @generator.from(:dos)
+ end
+
+ it "should always return a set source as a string" do
+ @generator.from(:dos)
+ @generator.source.should == "dos"
+ end
+
+ it "should return the source as nil if there is no source" do
+ @generator.source.should be_nil
+ end
+
+ it "should return itself when declaring the source" do
+ @generator.from(:dos).should equal(@generator)
+ end
+
+ it "should run the provided block when asked to generate the value" do
+ @generator.with { "yayness" }
+ @generator.generate().should == "yayness"
+ end
+
+ it "should pass in any provided value to the block" do
+ @generator.with { |value| value.upcase }
+ @generator.generate("myval").should == "MYVAL"
+ end
+
+ it "should return itself when declaring the code used for generating" do
+ @generator.with { |value| value.upcase }.should equal(@generator)
+ end
+end
diff --git a/spec/unit/util/ldap/manager.rb b/spec/unit/util/ldap/manager.rb
new file mode 100755
index 000000000..b18b1b933
--- /dev/null
+++ b/spec/unit/util/ldap/manager.rb
@@ -0,0 +1,654 @@
+#!/usr/bin/env ruby
+#
+# Created by Luke Kanies on 2008-3-19.
+# Copyright (c) 2006. All rights reserved.
+
+require File.dirname(__FILE__) + '/../../../spec_helper'
+
+require 'puppet/util/ldap/manager'
+
+# If the ldap classes aren't available, go ahead and
+# create some, so our tests will pass.
+unless defined?(LDAP::Mod)
+ class LDAP
+ LDAP_MOD_ADD = :adding
+ LDAP_MOD_REPLACE = :replacing
+ LDAP_MOD_DELETE = :deleting
+ class ResultError < RuntimeError; end
+ class Mod
+ def initialize(*args)
+ end
+ end
+ end
+end
+
+describe Puppet::Util::Ldap::Manager do
+ before do
+ @manager = Puppet::Util::Ldap::Manager.new
+ end
+
+ it "should return self when specifying objectclasses" do
+ @manager.manages(:one, :two).should equal(@manager)
+ end
+
+ it "should allow specification of what objectclasses are managed" do
+ @manager.manages(:one, :two).objectclasses.should == [:one, :two]
+ end
+
+ it "should return self when specifying the relative base" do
+ @manager.at("yay").should equal(@manager)
+ end
+
+ it "should allow specification of the relative base" do
+ @manager.at("yay").location.should == "yay"
+ end
+
+ it "should return self when specifying the attribute map" do
+ @manager.maps(:one => :two).should equal(@manager)
+ end
+
+ it "should allow specification of the rdn attribute" do
+ @manager.named_by(:uid).rdn.should == :uid
+ end
+
+ it "should allow specification of the attribute map" do
+ @manager.maps(:one => :two).puppet2ldap.should == {:one => :two}
+ end
+
+ it "should have a no-op 'and' method that just returns self" do
+ @manager.and.should equal(@manager)
+ end
+
+ it "should allow specification of generated attributes" do
+ @manager.generates(:thing).should be_instance_of(Puppet::Util::Ldap::Generator)
+ end
+
+ describe "when generating attributes" do
+ before do
+ @generator = stub 'generator', :source => "one", :name => "myparam"
+
+ Puppet::Util::Ldap::Generator.stubs(:new).with(:myparam).returns @generator
+ end
+
+ it "should create a generator to do the parameter generation" do
+ Puppet::Util::Ldap::Generator.expects(:new).with(:myparam).returns @generator
+ @manager.generates(:myparam)
+ end
+
+ it "should return the generator from the :generates method" do
+ @manager.generates(:myparam).should equal(@generator)
+ end
+
+ it "should not replace already present values" do
+ @manager.generates(:myparam)
+
+ attrs = {"myparam" => "testing"}
+ @generator.expects(:generate).never
+
+ @manager.generate attrs
+
+ attrs["myparam"].should == "testing"
+ end
+
+ it "should look for the parameter as a string, not a symbol" do
+ @manager.generates(:myparam)
+ @generator.expects(:generate).with("yay").returns %w{double yay}
+ attrs = {"one" => "yay"}
+ @manager.generate attrs
+
+ attrs["myparam"].should == %w{double yay}
+ end
+
+ it "should fail if a source is specified and no source value is not defined" do
+ @manager.generates(:myparam)
+ lambda { @manager.generate "two" => "yay" }.should raise_error(ArgumentError)
+ end
+
+ it "should use the source value to generate the new value if a source attribute is specified" do
+ @manager.generates(:myparam)
+ @generator.expects(:generate).with("yay").returns %w{double yay}
+ @manager.generate "one" => "yay"
+ end
+
+ it "should not pass in any value if no source attribute is specified" do
+ @generator.stubs(:source).returns nil
+ @manager.generates(:myparam)
+ @generator.expects(:generate).with().returns %w{double yay}
+ @manager.generate "one" => "yay"
+ end
+
+ it "should convert any results to arrays of strings if necessary" do
+ @generator.expects(:generate).returns :test
+ @manager.generates(:myparam)
+
+ attrs = {"one" => "two"}
+ @manager.generate(attrs)
+ attrs["myparam"].should == ["test"]
+ end
+
+ it "should add the result to the passed-in attribute hash" do
+ @generator.expects(:generate).returns %w{test}
+ @manager.generates(:myparam)
+
+ attrs = {"one" => "two"}
+ @manager.generate(attrs)
+ attrs["myparam"].should == %w{test}
+ end
+ end
+
+ it "should be considered invalid if it is missing a location" do
+ @manager.manages :me
+ @manager.maps :me => :you
+ @manager.should_not be_valid
+ end
+
+ it "should be considered invalid if it is missing an objectclass list" do
+ @manager.maps :me => :you
+ @manager.at "ou=yayness"
+ @manager.should_not be_valid
+ end
+
+ it "should be considered invalid if it is missing an attribute map" do
+ @manager.manages :me
+ @manager.at "ou=yayness"
+ @manager.should_not be_valid
+ end
+
+ it "should be considered valid if it has an attribute map, location, and objectclass list" do
+ @manager.maps :me => :you
+ @manager.manages :me
+ @manager.at "ou=yayness"
+ @manager.should be_valid
+ end
+
+ it "should calculate an instance's dn using the :ldapbase setting and the relative base" do
+ Puppet.settings.expects(:value).with(:ldapbase).returns "dc=testing"
+ @manager.at "ou=mybase"
+ @manager.dn("me").should == "cn=me,ou=mybase,dc=testing"
+ end
+
+ it "should use the specified rdn when calculating an instance's dn" do
+ Puppet.settings.expects(:value).with(:ldapbase).returns "dc=testing"
+ @manager.named_by :uid
+ @manager.at "ou=mybase"
+ @manager.dn("me").should =~ /^uid=me/
+ end
+
+ it "should calculate its base using the :ldapbase setting and the relative base" do
+ Puppet.settings.expects(:value).with(:ldapbase).returns "dc=testing"
+ @manager.at "ou=mybase"
+ @manager.base.should == "ou=mybase,dc=testing"
+ end
+
+ describe "when generating its search filter" do
+ it "should using a single 'objectclass=<name>' filter if a single objectclass is specified" do
+ @manager.manages("testing")
+ @manager.filter.should == "objectclass=testing"
+ end
+
+ it "should create an LDAP AND filter if multiple objectclasses are specified" do
+ @manager.manages "testing", "okay", "done"
+ @manager.filter.should == "(&(objectclass=testing)(objectclass=okay)(objectclass=done))"
+ end
+ end
+
+ it "should have a method for converting a Puppet attribute name to an LDAP attribute name as a string" do
+ @manager.maps :puppet_attr => :ldap_attr
+ @manager.ldap_name(:puppet_attr).should == "ldap_attr"
+ end
+
+ it "should have a method for converting an LDAP attribute name to a Puppet attribute name" do
+ @manager.maps :puppet_attr => :ldap_attr
+ @manager.puppet_name(:ldap_attr).should == :puppet_attr
+ end
+
+ it "should have a :create method for creating ldap entries" do
+ @manager.should respond_to(:create)
+ end
+
+ it "should have a :delete method for deleting ldap entries" do
+ @manager.should respond_to(:delete)
+ end
+
+ it "should have a :modify method for modifying ldap entries" do
+ @manager.should respond_to(:modify)
+ end
+
+ it "should have a method for finding an entry by name in ldap" do
+ @manager.should respond_to(:find)
+ end
+
+ describe "when converting ldap entries to hashes for providers" do
+ before do
+ @manager.maps :uno => :one, :dos => :two
+
+ @result = @manager.entry2provider("dn" => ["cn=one,ou=people,dc=madstop"], "one" => ["two"], "three" => %w{four}, "objectclass" => %w{yay ness})
+ end
+
+ it "should set the name to the short portion of the dn" do
+ @result[:name].should == "one"
+ end
+
+ it "should remove the objectclasses" do
+ @result["objectclass"].should be_nil
+ end
+
+ it "should remove any attributes that are not mentioned in the map" do
+ @result["three"].should be_nil
+ end
+
+ it "should rename convert to symbols all attributes to their puppet names" do
+ @result[:uno].should == %w{two}
+ end
+
+ it "should set the value of all unset puppet attributes as :absent" do
+ @result[:dos].should == :absent
+ end
+ end
+
+ describe "when using an ldap connection" do
+ before do
+ @ldapconn = mock 'ldapconn'
+ @conn = stub 'connection', :connection => @ldapconn, :start => nil, :close => nil
+ Puppet::Util::Ldap::Connection.stubs(:new).returns(@conn)
+ end
+
+ it "should fail unless a block is given" do
+ lambda { @manager.connect }.should raise_error(ArgumentError)
+ end
+
+ it "should open the connection with its server set to :ldapserver" do
+ Puppet.settings.stubs(:value).returns(false)
+ Puppet.settings.expects(:value).with(:ldapserver).returns("myserver")
+ Puppet::Util::Ldap::Connection.expects(:new).with { |*args| args[0] == "myserver" }.returns @conn
+
+ @manager.connect { |c| }
+ end
+
+ it "should open the connection with its port set to the :ldapport" do
+ Puppet.settings.stubs(:value).returns(false)
+ Puppet.settings.expects(:value).with(:ldapport).returns("28")
+ Puppet::Util::Ldap::Connection.expects(:new).with { |*args| args[1] == "28" }.returns @conn
+
+ @manager.connect { |c| }
+ end
+
+ it "should open the connection with no user if :ldapuser is not set" do
+ Puppet.settings.stubs(:value).returns(false)
+ Puppet.settings.expects(:value).with(:ldapuser).returns("")
+ Puppet::Util::Ldap::Connection.expects(:new).with { |*args| args[2][:user].nil? }.returns @conn
+
+ @manager.connect { |c| }
+ end
+
+ it "should open the connection with its user set to the :ldapuser if it is set" do
+ Puppet.settings.stubs(:value).returns(false)
+ Puppet.settings.expects(:value).with(:ldapuser).returns("mypass")
+ Puppet::Util::Ldap::Connection.expects(:new).with { |*args| args[2][:user] == "mypass" }.returns @conn
+
+ @manager.connect { |c| }
+ end
+
+ it "should open the connection with no password if :ldappassword is not set" do
+ Puppet.settings.stubs(:value).returns(false)
+ Puppet.settings.expects(:value).with(:ldappassword).returns("")
+ Puppet::Util::Ldap::Connection.expects(:new).with { |*args| args[2][:password].nil? }.returns @conn
+
+ @manager.connect { |c| }
+ end
+
+ it "should open the connection with its password set to the :ldappassword if it is set" do
+ Puppet.settings.stubs(:value).returns(false)
+ Puppet.settings.expects(:value).with(:ldappassword).returns("mypass")
+ Puppet::Util::Ldap::Connection.expects(:new).with { |*args| args[2][:password] == "mypass" }.returns @conn
+
+ @manager.connect { |c| }
+ end
+
+ it "should set ssl to :tls if ldaptls is enabled" do
+ Puppet.settings.stubs(:value).returns(false)
+ Puppet.settings.expects(:value).with(:ldaptls).returns(true)
+ Puppet::Util::Ldap::Connection.expects(:new).with { |*args| args[2][:ssl] == :tls }.returns @conn
+
+ @manager.connect { |c| }
+ end
+
+ it "should set ssl to true if ldapssl is enabled" do
+ Puppet.settings.stubs(:value).returns(false)
+ Puppet.settings.expects(:value).with(:ldapssl).returns(true)
+ Puppet::Util::Ldap::Connection.expects(:new).with { |*args| args[2][:ssl] == true }.returns @conn
+
+ @manager.connect { |c| }
+ end
+
+ it "should set ssl to false if neither ldaptls nor ldapssl is enabled" do
+ Puppet.settings.stubs(:value).returns(false)
+ Puppet.settings.expects(:value).with(:ldapssl).returns(false)
+ Puppet::Util::Ldap::Connection.expects(:new).with { |*args| args[2][:ssl] == false }.returns @conn
+
+ @manager.connect { |c| }
+ end
+
+ it "should open, yield, and then close the connection" do
+ @conn.expects(:start)
+ @conn.expects(:close)
+ Puppet::Util::Ldap::Connection.expects(:new).returns(@conn)
+ @ldapconn.expects(:test)
+ @manager.connect { |c| c.test }
+ end
+
+ it "should close the connection even if there's an exception in the passed block" do
+ @conn.expects(:close)
+ lambda { @manager.connect { |c| raise ArgumentError } }.should raise_error(ArgumentError)
+ end
+ end
+
+ describe "when using ldap" do
+ before do
+ @conn = mock 'connection'
+ @manager.stubs(:connect).yields @conn
+ @manager.stubs(:objectclasses).returns [:oc1, :oc2]
+ @manager.maps :one => :uno, :two => :dos, :three => :tres, :four => :quatro
+ end
+
+ describe "to create entries" do
+ it "should convert the first argument to its :create method to a full dn and pass the resulting argument list to its connection" do
+ @manager.expects(:dn).with("myname").returns "mydn"
+ @conn.expects(:add).with { |name, attrs| name == "mydn" }
+
+ @manager.create("myname", {"attr" => "myattrs"})
+ end
+
+ it "should add the objectclasses to the attributes" do
+ @manager.expects(:dn).with("myname").returns "mydn"
+ @conn.expects(:add).with { |name, attrs| attrs["objectClass"].include?("oc1") and attrs["objectClass"].include?("oc2") }
+
+ @manager.create("myname", {:one => :testing})
+ end
+
+ it "should add the rdn to the attributes" do
+ @manager.expects(:dn).with("myname").returns "mydn"
+ @conn.expects(:add).with { |name, attrs| attrs["cn"] == %w{myname} }
+
+ @manager.create("myname", {:one => :testing})
+ end
+
+ it "should add 'top' to the objectclasses if it is not listed" do
+ @manager.expects(:dn).with("myname").returns "mydn"
+ @conn.expects(:add).with { |name, attrs| attrs["objectClass"].include?("top") }
+
+ @manager.create("myname", {:one => :testing})
+ end
+
+ it "should add any generated values that are defined" do
+ generator = stub 'generator', :source => :one, :name => "myparam"
+
+ Puppet::Util::Ldap::Generator.expects(:new).with(:myparam).returns generator
+
+ @manager.generates(:myparam)
+
+ @manager.stubs(:dn).with("myname").returns "mydn"
+
+ generator.expects(:generate).with(:testing).returns ["generated value"]
+ @conn.expects(:add).with { |name, attrs| attrs["myparam"] == ["generated value"] }
+
+ @manager.create("myname", {:one => :testing})
+ end
+
+ it "should convert any generated values to arrays of strings if necessary" do
+ generator = stub 'generator', :source => :one, :name => "myparam"
+
+ Puppet::Util::Ldap::Generator.expects(:new).with(:myparam).returns generator
+
+ @manager.generates(:myparam)
+
+ @manager.stubs(:dn).returns "mydn"
+
+ generator.expects(:generate).returns :generated
+ @conn.expects(:add).with { |name, attrs| attrs["myparam"] == ["generated"] }
+
+ @manager.create("myname", {:one => :testing})
+ end
+ end
+
+ describe "do delete entries" do
+ it "should convert the first argument to its :delete method to a full dn and pass the resulting argument list to its connection" do
+ @manager.expects(:dn).with("myname").returns "mydn"
+ @conn.expects(:delete).with("mydn")
+
+ @manager.delete("myname")
+ end
+ end
+
+ describe "to modify entries" do
+ it "should convert the first argument to its :modify method to a full dn and pass the resulting argument list to its connection" do
+ @manager.expects(:dn).with("myname").returns "mydn"
+ @conn.expects(:modify).with("mydn", :mymods)
+
+ @manager.modify("myname", :mymods)
+ end
+ end
+
+ describe "to find a single entry" do
+ it "should use the dn of the provided name as the search base, a scope of 0, and 'objectclass=*' as the filter for a search2 call" do
+ @manager.expects(:dn).with("myname").returns "mydn"
+ @conn.expects(:search2).with("mydn", 0, "objectclass=*")
+
+ @manager.find("myname")
+ end
+
+ it "should return nil if an exception is thrown because no result is found" do
+ @manager.expects(:dn).with("myname").returns "mydn"
+ @conn.expects(:search2).raises LDAP::ResultError
+
+ @manager.find("myname").should be_nil
+ end
+
+ it "should return a converted provider hash if the result is found" do
+ @manager.expects(:dn).with("myname").returns "mydn"
+ result = {"one" => "two"}
+ @conn.expects(:search2).yields result
+
+ @manager.expects(:entry2provider).with(result).returns "myprovider"
+
+ @manager.find("myname").should == "myprovider"
+ end
+ end
+
+ describe "to search for multiple entries" do
+ before do
+ @manager.stubs(:filter).returns "myfilter"
+ end
+
+ it "should use the manager's search base as the dn of the provided name as the search base" do
+ @manager.expects(:base).returns "mybase"
+ @conn.expects(:search2).with { |base, scope, filter| base == "mybase" }
+
+ @manager.search
+ end
+
+ it "should use a scope of 1" do
+ @conn.expects(:search2).with { |base, scope, filter| scope == 1 }
+
+ @manager.search
+ end
+
+ it "should use any specified search filter" do
+ @manager.expects(:filter).never
+ @conn.expects(:search2).with { |base, scope, filter| filter == "boo" }
+
+ @manager.search("boo")
+ end
+
+ it "should turn its objectclass list into its search filter if one is not specified" do
+ @manager.expects(:filter).returns "yay"
+ @conn.expects(:search2).with { |base, scope, filter| filter == "yay" }
+
+ @manager.search
+ end
+
+ it "should return nil if no result is found" do
+ @conn.expects(:search2)
+
+ @manager.search.should be_nil
+ end
+
+ it "should return an array of the found results converted to provider hashes" do
+ # LAK: AFAICT, it's impossible to yield multiple times in an expectation.
+ one = {"dn" => "cn=one,dc=madstop,dc=com", "one" => "two"}
+ @conn.expects(:search2).yields(one)
+
+ @manager.expects(:entry2provider).with(one).returns "myprov"
+
+ @manager.search.should == ["myprov"]
+ end
+ end
+ end
+
+ describe "when an instance" do
+ before do
+ @name = "myname"
+ @manager.maps :one => :uno, :two => :dos, :three => :tres, :four => :quatro
+ end
+
+ describe "is being updated" do
+ it "should get created if the current attribute list is empty and the desired attribute list has :ensure == :present" do
+ @manager.expects(:create)
+ @manager.update(@name, {}, {:ensure => :present})
+ end
+
+ it "should get created if the current attribute list has :ensure == :absent and the desired attribute list has :ensure == :present" do
+ @manager.expects(:create)
+ @manager.update(@name, {:ensure => :absent}, {:ensure => :present})
+ end
+
+ it "should get deleted if the current attribute list has :ensure == :present and the desired attribute list has :ensure == :absent" do
+ @manager.expects(:delete)
+ @manager.update(@name, {:ensure => :present}, {:ensure => :absent})
+ end
+
+ it "should get modified if both attribute lists have :ensure == :present" do
+ @manager.expects(:modify)
+ @manager.update(@name, {:ensure => :present, :one => :two}, {:ensure => :present, :one => :three})
+ end
+ end
+
+ describe "is being deleted" do
+ it "should call the :delete method with its name and manager" do
+ @manager.expects(:delete).with(@name)
+
+ @manager.update(@name, {}, {:ensure => :absent})
+ end
+ end
+
+ describe "is being created" do
+ before do
+ @is = {}
+ @should = {:ensure => :present, :one => :yay, :two => :absent}
+ end
+
+ it "should call the :create method with its name" do
+ @manager.expects(:create).with { |name, attrs| name == @name }
+ @manager.update(@name, @is, @should)
+ end
+
+ it "should call the :create method with its property hash converted to ldap attribute names" do
+ @manager.expects(:create).with { |name, attrs| attrs["uno"] == ["yay"] }
+ @manager.update(@name, @is, @should)
+ end
+
+ it "should convert the property names to strings" do
+ @manager.expects(:create).with { |name, attrs| attrs["uno"] == ["yay"] }
+ @manager.update(@name, @is, @should)
+ end
+
+ it "should convert the property values to arrays if necessary" do
+ @manager.expects(:create).with { |name, attrs| attrs["uno"] == ["yay"] }
+ @manager.update(@name, @is, @should)
+ end
+
+ it "should convert the property values to strings if necessary" do
+ @manager.expects(:create).with { |name, attrs| attrs["uno"] == ["yay"] }
+ @manager.update(@name, @is, @should)
+ end
+
+ it "should not include :ensure in the properties sent" do
+ @manager.expects(:create).with { |*args| args[1][:ensure].nil? }
+ @manager.update(@name, @is, @should)
+ end
+
+ it "should not include attributes set to :absent in the properties sent" do
+ @manager.expects(:create).with { |*args| args[1][:dos].nil? }
+ @manager.update(@name, @is, @should)
+ end
+ end
+
+ describe "is being modified" do
+ it "should call the :modify method with its name and an array of LDAP::Mod instances" do
+ LDAP::Mod.stubs(:new).returns "whatever"
+
+ @is = {:one => :yay}
+ @should = {:one => :yay, :two => :foo}
+
+ @manager.expects(:modify).with { |name, mods| name == @name }
+ @manager.update(@name, @is, @should)
+ end
+
+ it "should create the LDAP::Mod with the property name converted to the ldap name as a string" do
+ @is = {:one => :yay}
+ @should = {:one => :yay, :two => :foo}
+ mod = mock 'module'
+ LDAP::Mod.expects(:new).with { |form, name, value| name == "dos" }.returns mod
+
+ @manager.stubs(:modify)
+
+ @manager.update(@name, @is, @should)
+ end
+
+ it "should create an LDAP::Mod instance of type LDAP_MOD_ADD for each attribute being added, with the attribute value converted to a string of arrays" do
+ @is = {:one => :yay}
+ @should = {:one => :yay, :two => :foo}
+ mod = mock 'module'
+ LDAP::Mod.expects(:new).with(LDAP::LDAP_MOD_ADD, "dos", ["foo"]).returns mod
+
+ @manager.stubs(:modify)
+
+ @manager.update(@name, @is, @should)
+ end
+
+ it "should create an LDAP::Mod instance of type LDAP_MOD_DELETE for each attribute being deleted" do
+ @is = {:one => :yay, :two => :foo}
+ @should = {:one => :yay, :two => :absent}
+ mod = mock 'module'
+ LDAP::Mod.expects(:new).with(LDAP::LDAP_MOD_DELETE, "dos", []).returns mod
+
+ @manager.stubs(:modify)
+
+ @manager.update(@name, @is, @should)
+ end
+
+ it "should create an LDAP::Mod instance of type LDAP_MOD_REPLACE for each attribute being modified, with the attribute converted to a string of arrays" do
+ @is = {:one => :yay, :two => :four}
+ @should = {:one => :yay, :two => :five}
+ mod = mock 'module'
+ LDAP::Mod.expects(:new).with(LDAP::LDAP_MOD_REPLACE, "dos", ["five"]).returns mod
+
+ @manager.stubs(:modify)
+
+ @manager.update(@name, @is, @should)
+ end
+
+ it "should pass all created Mod instances to the modify method" do
+ @is = {:one => :yay, :two => :foo, :three => :absent}
+ @should = {:one => :yay, :two => :foe, :three => :fee, :four => :fie}
+ LDAP::Mod.expects(:new).times(3).returns("mod1").then.returns("mod2").then.returns("mod3")
+
+ @manager.expects(:modify).with do |name, mods|
+ mods.sort == %w{mod1 mod2 mod3}.sort
+ end
+
+ @manager.update(@name, @is, @should)
+ end
+ end
+ end
+end