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 self end # Set the offset from the search base and return the config. def at(location) @location = location 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 @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 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}=#{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(objectclasses.length == 1 ? "objectclass=#{objectclasses[0]}" : "(&(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) connect do |conn| begin conn.search2(dn(name), 0, "objectclass=*") do |result| # Convert to puppet-appropriate attributes return entry2provider(result) end rescue 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, "#{generator.source} must be defined to generate #{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 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 } 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(result.empty? ? nil : result) end # Update the ldap entry with the desired state. def update(name, is, should) if should[:ensure] == :absent Puppet.info "Removing #{dn(name)} from ldap" delete(name) return end # We're creating a new entry if is.empty? or is[:ensure] == :absent Puppet.info "Creating #{dn(name)} in ldap" # 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