diff options
-rw-r--r-- | lib/puppet/provider/group/ldap.rb | 37 | ||||
-rw-r--r-- | lib/puppet/provider/ldap.rb | 137 | ||||
-rw-r--r-- | lib/puppet/provider/user/ldap.rb | 115 | ||||
-rw-r--r-- | lib/puppet/util/ldap.rb | 5 | ||||
-rw-r--r-- | lib/puppet/util/ldap/connection.rb | 57 | ||||
-rw-r--r-- | lib/puppet/util/ldap/generator.rb | 45 | ||||
-rw-r--r-- | lib/puppet/util/ldap/manager.rb | 281 | ||||
-rwxr-xr-x | spec/unit/provider/group/ldap.rb | 66 | ||||
-rwxr-xr-x | spec/unit/provider/ldap.rb | 248 | ||||
-rwxr-xr-x | spec/unit/provider/user/ldap.rb | 252 | ||||
-rwxr-xr-x | spec/unit/util/ldap/connection.rb | 114 | ||||
-rwxr-xr-x | spec/unit/util/ldap/generator.rb | 54 | ||||
-rwxr-xr-x | spec/unit/util/ldap/manager.rb | 654 |
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 |