Issue #13871 has been reported by Andreas Schuster.

----------------------------------------
Feature #13871: LDAP - Referrals, SeachBase, multiValued-Attributes and 
multiParentnode Feature
https://projects.puppetlabs.com/issues/13871

Author: Andreas Schuster
Status: Unreviewed
Priority: Normal
Assignee: 
Category: LDAP
Target version: 
Affected Puppet version: 2.7.9
Keywords: 
Branch: 


Hi all,

within our copmany, we are currently evaluating puppet 2.7.9 for our 
config-managment. Main focus is currently on Solaris Operating Systm. But 
Linux, AIX and HP-UX are also of interrest.
First experiences are fine, but to rollout Puppet into our existing environment 
it was necessary to implement following features:

1. modification of ldapsearch to follow referrals and modification of 
LDAP-SearchBase (`ldapbase`) to look for Full-Qulified-Domain-Name of client
1. provision for multi-valued LDAP-Attributes. A Multivalued Attribute must be 
also collected from parentnode(s) if it already exists.
1. implementation of multi-parentnode feature to support our existing 
LDAP-Structure
1. introduction of LDAP-Certifcate-Directory variable `ldapcrtdir` for use of 
an SSL encrypted connection to the LDAP Server


I hope you loke our implementation. And if it is possible, we are lucky if you 
will implement our features/changes into official puppet-source-code.

Regards
Andreas



# Our LDAP-Directory and requirements
To understand our environment, i will give a short introduction to our 
LDAP-Structure.

The PuppetClients are applied for example within the LDAP directory at 
cn=systemX,ou=hostconfig,dc=network_L,dc=customerA, dc=ourcompany. The 
domainname is here dc=customerA,dc=ourcompany  => customerA.ourcompmany.
But it is possible that also another client with the same name (cn=systemX) 
exists at a different customer-Tree. 
If the search base (`Puppet::ldapbase`) is only dc=ourcompany, 2 or more 
PuppetClients with name cn=systemX will be found. To ensure that only one 
PuppetClient is found, the search base must be set to the domain of the 
PuppetClients to look for.

Likewise, the parentnodes (eg. cn=HostConfig) exists across multiple LDAP 
branches. So the parentnode must be given as a FQDN. 
See the picture "LDAP Directory Tree":

![LDAP-Directory-Tree](LDAP_Trees.png)

The client "cn=systemZ,ou=HostConfig,dc=network_L,dc=customerA,dc=ourcompany" 
has following parentnodes:

1. cn=HostConfig,dc=network_L,dc=customerA,dc=ourcompany
1. cn=HostConfig,ou=buildingX,ou=cityA,ou=location,dc=ourcompany
1. cn=HostConfig,ou=cityA,ou=location,dc=ourcompany
1. cn=HostConfig,dc=customerA,dc=ourcompany
1. cn=HostConfig,dc=ourcompany

As you can see, the parentnode 
"cn=HostConfig,dc=network_L,dc=customerA,dc=ourcompany" has 2 parentnodes. And 
the parentnodes must be evaluated in the given order! 





# Follow Referrals and use Domain of client as searchbase
To follow referrals and to search only within Domain of the client or the 
parentnode, we have implemented following changes:

<pre><code class="ruby">
Index: lib/puppet/indirector/ldap.rb
--- lib/puppet/indirector/ldap.rb Base (BASE)
+++ lib/puppet/indirector/ldap.rb Locally Modified (Based On LOCAL)
@@ -18,10 +18,16 @@
     nil
   end
 
-  def search_base
-    Puppet[:ldapbase]
+  # Setting LDAP SearchBase
+  def search_base(base=nil)
+    base.nil? ? Puppet[:ldapbase] : base
   end
 
+  # Set LDAP SearchScope
+  def search_scope(scope=nil)
+    scope.nil? ? LDAP::LDAP_SCOPE_SUBTREE : scope
+  end
+
   # The ldap search filter to use.
   def search_filter(name)
     raise Puppet::DevError, "No search string set for LDAP terminus for 
#{self.name}"
@@ -29,14 +35,14 @@
 
   # Find the ldap node, return the class list and parent node specially,
   # and everything else in a parameter hash.
-  def ldapsearch(filter)
+  def ldapsearch(filter, base=nil, scope=nil)
     raise ArgumentError.new("You must pass a block to ldapsearch") unless 
block_given?
 
     found = false
     count = 0
 
     begin
-      connection.search(search_base, 2, filter, search_attributes) do |entry|
+      connection.search(search_base(base), search_scope(scope), filter, 
search_attributes) do |entry|
         found = true
         yield entry
       end
Index: lib/puppet/indirector/node/ldap.rb
--- lib/puppet/indirector/node/ldap.rb Base (BASE)
+++ lib/puppet/indirector/node/ldap.rb Locally Modified (Based On LOCAL)
@@ -19,9 +19,21 @@
   # LAK:NOTE Unfortunately, the ldap support is too stupid to throw anything
   # but LDAP::ResultError, even on bad connections, so we are rough handed
   # with our error handling.
-  def name2hash(name)
+  # Added 'parentmode' to use different ldapsearchscope for 'normal' clients 
and parentnode
+  def name2hash(name, parentmode=false)
     info = nil
-    ldapsearch(search_filter(name)) { |entry| info = entry2hash(entry) }
+    base = nil
+    scope = nil
+
+    if parentmode
+      base = name
+      name = "*"
+      scope = LDAP::LDAP_SCOPE_BASE
+    else
+      name, base = name.gsub('.', ",dc=").split(',', 2)
+      scope = LDAP::LDAP_SCOPE_SUBTREE
+    end
+    ldapsearch(search_filter(name), base, scope) { |entry| info = 
entry2hash(entry) }
     info
   end
 
@@ -171,7 +183,7 @@
 
   # Find information for our parent and merge it into the current info.
   def find_and_merge_parent(parent, information)
-    parent_info = name2hash(parent) || raise(Puppet::Error.new("Could not find 
parent node '#{parent}'"))
+    parent_info = name2hash(parent, parentmode=true) || 
raise(Puppet::Error.new("Could not find parent node '#{parent}'"))
     information[:classes] += parent_info[:classes]
     parent_info[:parameters].each do |param, value|
       # Specifically test for whether it's set, so false values are handled 
correctly.
</code></pre>

**Patch File**: *02-01_puppet-2.7.9_ldap-referrals-and-searchbase.patch*

The searchbase will be set in non-parentmode to the domainname ot the 
puppetclient.

If parentmode is true, the seachbase will be set to the parentnode himself. 
Therefore the parentnode must be given as FQDN.
LDAP-Attribute 'parentnode' looks like this:

parentnode: cn=HostConfig,dc=network_L,dc=customerA,dc=ourcompany





# Multi-Valued LDAP-Attributes
A additinal requirement is to collect all multiValued LDAP-Atributes. And not 
only if it is not yet included/collected.


<pre><code class="ruby">
Index: lib/puppet/indirector/ldap.rb
--- lib/puppet/indirector/ldap.rb Base (BASE)
+++ lib/puppet/indirector/ldap.rb Locally Modified (Based On LOCAL)
@@ -1,5 +1,6 @@
 require 'puppet/indirector/terminus'
 require 'puppet/util/ldap/connection'
+require 'ldap/schema'
 
 class Puppet::Indirector::Ldap < Puppet::Indirector::Terminus
   # Perform our ldap search and process the result.
@@ -81,4 +82,11 @@
 
     @connection
   end
+
+  # give configured LDAP-Schema
+  def ldapschema
+    @ldap_schema = @connection.schema() unless defined?(@ldap_schema)
+    @ldap_schema
 end
+  
+end
Index: lib/puppet/indirector/node/ldap.rb
--- lib/puppet/indirector/node/ldap.rb Base (BASE)
+++ lib/puppet/indirector/node/ldap.rb Locally Modified (Based On LOCAL)
@@ -151,10 +151,36 @@
 
   private
 
+  # create hash-table of ldap-attributes and if it is multivalued
+  def is_multivalued?(name)
+    unless defined?(@attr_lut)
+      @attr_lut = {}
+      ldapschema["attributeTypes"].each do |entry|
+        multivalued = entry.include?("SINGLE-VALUE") ? false : true
+        attributes  = entry.sub(/.*NAME (\([\s'\w\-]+\)|['\w\-]+).*/, 
'\1').gsub(/(\( |'| \))/, '').split(' ')
+
+        attributes.each do |attribute|
+          @attr_lut[attribute] = multivalued
+        end
+      end
+    end
+    @attr_lut[name]
+  end
+
   # Add our hash of ldap information to the node instance.
   def add_to_node(node, information)
     node.classes = information[:classes].uniq unless 
information[:classes].nil? or information[:classes].empty?
-    node.parameters = information[:parameters] unless 
information[:parameters].nil? or information[:parameters].empty?
+    #node.parameters = information[:parameters] unless 
information[:parameters].nil? or information[:parameters].empty?
+    unless information[:parameters].nil? or information[:parameters].empty?
+          information[:parameters].each do |key, value|
+            if value.is_a?(Array)
+              information[:parameters][key].flatten!
+              information[:parameters][key].uniq!
+            end
+          end
+          node.parameters = information[:parameters]
+        end
+
     node.environment = information[:environment] if information[:environment]
   end
 
@@ -187,8 +213,20 @@
     information[:classes] += parent_info[:classes]
     parent_info[:parameters].each do |param, value|
       # Specifically test for whether it's set, so false values are handled 
correctly.
-      information[:parameters][param] = value unless 
information[:parameters].include?(param)
+      #information[:parameters][param] = value unless 
information[:parameters].include?(param)
+      if information[:parameters].include?(param)
+        if is_multivalued?(param)
+          if information[:parameters][param].is_a?(Array)
+            information[:parameters][param] << value
+          else
+            information[:parameters][param] = 
[information[:parameters][param], value]
     end
+        end
+      else
+        information[:parameters][param] = value
+      end
+    end
+    
     information[:environment] ||= parent_info[:environment]
     parent_info[:parent]
   end
</code></pre>

Now all multiValued LDAP-Attributes are collected. Even if a Attribute already 
filled and the next parentnode also have additional values it will added to the 
Attribute.

**Patch File**: *02-02_puppet-2.7.9_ldap-multivalued-attributetypes.patch*





# Support of more than one ParentNode
As you can see at out LDAP-Directory-Structure, we need a aolution to support 
more then one parentnode.
Actually we implemented it this way:
<pre><code class="ruby">
Index: lib/puppet/indirector/node/ldap.rb
--- lib/puppet/indirector/node/ldap.rb Base (BASE)
+++ lib/puppet/indirector/node/ldap.rb Locally Modified (Based On LOCAL)
@@ -246,15 +246,22 @@
 
   def merge_parent(info)
     parent_info = nil
-    parent = info[:parent]
 
-    # Preload the parent array with the node name.
+    # load parentnode(s) of client
+    parentnodes = info[:parent]
+
+    # Pre-load the parant array with the node name.
     parents = [info[:name]]
-    while parent
-      raise ArgumentError, "Found loop in LDAP node parents; #{parent} appears 
twice" if parents.include?(parent)
-      parents << parent
-      parent = find_and_merge_parent(parent, info)
+    parentnodes.each do |parentnode|
+      if parents.include?(parentnode)
+        raise ArgumentError, "Found loop in LDAP node parents; #{parentnode} 
appears twice"
     end
+      parents << parentnode
+      parentnode2add = find_and_merge_parent(parentnode, info)
+      unless parentnode2add.nil?
+        parentnodes.insert(parentnodes.index(parentnode)+1 , 
parentnode2add).flatten!
+      end
+    end
 
     info
   end
@@ -288,11 +295,16 @@
 
     return nil unless values = entry.vals(pattr)
 
-    if values.length > 1
+    if values.length > 2
       raise Puppet::Error,
-        "Node entry #{entry.dn} specifies more than one parent: 
#{values.inspect}"
+        "Node entry #{entry.dn} specifies more than two parent nodes: 
#{values.inspect}"
     end
-    return(values.empty? ? nil : values.shift)
+
+    return nil if values.empty?
+
+    # sort values if necessary
+    values = sort_and_remove_delimiter(values, ':')
+    return values
   end
 
   def get_stacked_values_from_entry(entry)
@@ -303,4 +315,28 @@
       result
     end
   end
+  
+  def sort_and_remove_delimiter(array_to_sort, delimiter)
+    new_array = []
+    
+    return nil if array_to_sort.empty?
+    
+    if array_to_sort.length > 1
+      array_to_sort.each do |entry|
+        if entry.include? delimiter
+          new_entry_index = entry.split(delimiter)[1].to_i
+          new_entry_value = entry.split(delimiter)[0]
+          new_array.insert(new_entry_index, new_entry_value)
+        else
+          new_array << entry
+          Puppet.warning "More then one parentnode defined, but no sort index 
given: \n\t#{array_to_sort.inspect}"
 end
+      
+      end
+    else
+        new_array = [array_to_sort.first.split(delimiter)[0]]
+    end
+
+    return new_array.compact
+  end
+end
</code></pre>

Now it is possible to set 2 parentnode LDAP-Attributes to a puppetClient and 
add a index as additional element to this Attribute:

* parentnode: cn=HostConfig,ou=buildingX,ou=cityA,ou=location,dc=ourcompany:2
* parentnode: cn=HostConfig,ou=cityA,ou=location,dc=ourcompany:1

and the parentnodes of this puppetClient will be evaluated in the given order. 
If you set more then one parentnode and not setting the index value, a warning 
will be given.

**Patch File**: *02-03_puppet-2.7.9_ldap-multi-parentnode.patch*




# LDAP-Certificate-Directory
To use the server side certificate for LDAP connection we implemented a new 
variable **ldapcrtdir** to pint to the directory which containes the 
certificate. On Solaris it points to /var/ldap.
Don't know, if this patch is of generall interest...

<pre><code class="ruby">
Index: lib/puppet/defaults.rb
--- lib/puppet/defaults.rb Base (BASE)
+++ lib/puppet/defaults.rb Locally Modified (Based On LOCAL)
@@ -855,6 +855,9 @@
       "Whether SSL should be used when searching for nodes.
       Defaults to false because SSL usually requires certificates
       to be set up on the client side."],
+    :ldapcrtdir => [false,
+      "A path to cerificate directories.
+      Defaults to 'false'."],
     :ldaptls => [false,
       "Whether TLS should be used when searching for nodes.
       Defaults to false because TLS usually requires certificates
Index: lib/puppet/util/ldap/connection.rb
--- lib/puppet/util/ldap/connection.rb Base (BASE)
+++ lib/puppet/util/ldap/connection.rb Locally Modified (Based On LOCAL)
@@ -1,7 +1,7 @@
 require 'puppet/util/ldap'
 
 class Puppet::Util::Ldap::Connection
-  attr_accessor :host, :port, :user, :password, :reset, :ssl
+  attr_accessor :host, :port, :user, :password, :reset, :ssl, :crtdir
 
   attr_reader :connection
 
@@ -15,6 +15,8 @@
           false
         end
 
+    crtdir = Puppet[:ldapcrtdir]
+
     options = {}
     options[:ssl] = ssl
     if user = Puppet.settings[:ldapuser] and user != ""
@@ -47,7 +49,7 @@
 
   # Create a per-connection unique name.
   def name
-    [host, port, user, password, ssl].collect { |p| p.to_s }.join("/")
+    [host, port, user, password, ssl, crtdir].collect { |p| p.to_s }.join("/")
   end
 
   # Should we reset the connection?
@@ -59,10 +61,18 @@
   def start
       case ssl
       when :tls
+      if crtdir
+        @connection = LDAP::SSLConn.new(host, port, true, crtdir);
+      else
         @connection = LDAP::SSLConn.new(host, port, true)
+      end
       when true
-        @connection = LDAP::SSLConn.new(host, port)
+      if crtdir
+        @connection = LDAP::SSLConn.new(host, port, true, crtdir);
       else
+        @connection = LDAP::SSLConn.new(host, port, true)
+      end
+    else
         @connection = LDAP::Conn.new(host, port)
       end
       @connection.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
</code></pre>

**Patch File**: *01-01_puppet-2.7.9_add-ldap-crtdir.patch*








-- 
You have received this notification because you have either subscribed to it, 
or are involved in it.
To change your notification preferences, please click here: 
http://projects.puppetlabs.com/my/account

-- 
You received this message because you are subscribed to the Google Groups 
"Puppet Bugs" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/puppet-bugs?hl=en.

Reply via email to