commit b52f0f7dd2e38270b9d123a28907deeba34dd249
Author: Marc Dequènes (Duck) <Duck@DuckCorp.org>
Date:   Sun Aug 2 21:16:43 2009 +0200

    [evol] move all activeldap workarounds away, and properly manage operational attributes (using schema instead of a hardcoded list)

diff --git a/bin/shadowwalker b/bin/shadowwalker
index de09b4d..c4d1b65 100755
--- a/bin/shadowwalker
+++ b/bin/shadowwalker
@@ -58,6 +58,7 @@ cmdparser.program_version = VERSION.split(".")
 
 cmdparser.options = CmdParse::OptionParserWrapper.new do |opt|
   opt.separator "Global options:"
+  opt.on("-a", "--admin", "Output extra info for administrators") {|t| $program_options[:admin] = true }
   opt.on("-d", "--debug", "Output debug info without being formated") {|t| $program_options[:debug] = true }
   opt.on("-e", "--expert", "Output extra info for expert users") {|t| $program_options[:expert] = true }
   opt.on("-H", "--handles", "Output with handles (objects/field/... keys used for manipulations)") {|t| $program_options[:handles] = true }
@@ -124,11 +125,11 @@ class Command < CmdParse::Command
     obj_klass
   end
 
-  def params_shift_item(obj_klass, args, with_expert = false)
+  def params_shift_item(obj_klass, args)
     raise SyntaxError, _("no item name given") if args.empty?
 
     attr_list = ["*"]
-    attr_list << "+" if $program_options[:expert] and with_expert # add operational attributes if expert
+    attr_list << "+" if $program_options[:admin]
 
     item_hdl = args.shift.downcase
     begin
@@ -225,7 +226,7 @@ class ShowCommand < Command
 
   def execute (args)
     obj_klass = params_shift_object(args)
-    item = params_shift_item(obj_klass, args, true)
+    item = params_shift_item(obj_klass, args)
 
     if args.empty?
       $program_options[:skip_binary] = true
diff --git a/conf/ldap_shadows/test.conf b/conf/ldap_shadows/test.conf
index 996bac3..8a460d7 100644
--- a/conf/ldap_shadows/test.conf
+++ b/conf/ldap_shadows/test.conf
@@ -1,6 +1,6 @@
 ---
 presentation:
-  hidden_attributes: ['objectClass', 'userPassword']
+  hidden_attributes: ['objectClass', 'userPassword', 'hasSubordinates', 'entryUUID', 'entryDN', 'structuralObjectClass', 'subschemaSubentry']
   tree_objects: ['unit', 'entity']
 objects:
   bot:
diff --git a/lib/ldap_shadows/activeldap_fixes.rb b/lib/ldap_shadows/activeldap_fixes.rb
new file mode 100644
index 0000000..0f42256
--- /dev/null
+++ b/lib/ldap_shadows/activeldap_fixes.rb
@@ -0,0 +1,137 @@
+module ActiveLdap
+  # patch for activeldap#26824
+  class Schema
+    class Attribute
+      # operational?
+      #
+      # Returns true if an attribute is operational
+      # USAGE contains directoryOperation
+      # Duck: new method
+      def operational?
+        @operational
+      end
+
+      def collect_info
+        @description = attribute("DESC")[0]
+        @super_attribute = attribute("SUP")[0]
+        if @super_attribute
+          @super_attribute = @schema.attribute(@super_attribute)
+          @super_attribute = nil if @super_attribute.id.nil?
+        end
+        @read_only = attribute('NO-USER-MODIFICATION')[0] == 'TRUE'
+        @single_value = attribute('SINGLE-VALUE')[0] == 'TRUE'
+        @syntax = attribute("SYNTAX")[0]
+        @syntax = @schema.ldap_syntax(@syntax) if @syntax
+        if @syntax
+          @binary_required = @syntax.binary_transfer_required?
+          @binary = (@binary_required or !@syntax.human_readable?)
+          @derived_syntax = @syntax
+        else
+          @binary_required = false
+          @binary = false
+          @derived_syntax = nil
+          @derived_syntax = @super_attribute.syntax if @super_attribute
+        end
+        # Duck: newly collected data
+        @operational = attribute("USAGE").include?("directoryOperation")
+      end
+    end
+  end
+
+  # patch for activeldap#26745
+  class Base
+    def collect_modified_attributes(ldap_data, data)
+      attributes = []
+      # Now that all the options will be treated as unique attributes
+      # we can see what's changed and add anything that is brand-spankin'
+      # new.
+      ldap_data.each do |k, v|
+        next if schema.attribute(k).read_only?
+
+        value = data[k] || []
+
+        next if v == value
+
+        x = value
+        value = self.class.remove_blank_value(value) || []
+        next if v == value
+
+        # Create mod entries
+        if self.class.blank_value?(value)
+          # Since some types do not have equality matching rules,
+          # delete doesn't work
+          # Replacing with nothing is equivalent.
+          if !data.has_key?(k) and schema.attribute(k).binary_required?
+            value = [{'binary' => []}]
+          end
+        else
+          # Ditched delete then replace because attribs with no equality
+          # match rules will fails
+        end
+        attributes.push([:replace, k, value])
+      end
+      data.each do |k, v|
+        value = v || []
+        next if ldap_data.has_key?(k)
+
+        value = self.class.remove_blank_value(value) || []
+        next if self.class.blank_value?(value)
+
+
+        # Detect subtypes and account for them
+        # REPLACE will function like ADD, but doesn't hit EQUALITY problems
+        # TODO: Added equality(attr) to Schema
+        attributes.push([:replace, k, value])
+      end
+
+      attributes
+    end
+  end
+
+  # unfinished workaround for activeldap#26720 (not sure it would work in all cases)
+  module Validations
+    def validate_required_ldap_values
+      _schema = nil
+      # Make sure all MUST attributes have a value
+      entry_attribute.object_classes.each do |object_class|
+        object_class.must.each do |required_attribute|
+          # Normalize to ensure we catch schema problems
+          # needed?
+          real_name = to_real_attribute_name(required_attribute.name, true)
+          raise UnknownAttribute.new(required_attribute) if real_name.nil?
+
+          next if required_attribute.read_only?
+
+          value = @data[real_name] || []
+          next unless self.class.blank_value?(value)
+
+          # Duck: ignore attributes which were not present, as they may not
+          # be available because of ACL
+          next unless @initial_attribute_list.nil? or @initial_attribute_list.include?(real_name)
+
+          _schema ||= schema
+          aliases = required_attribute.aliases.collect do |name|
+            self.class.human_attribute_name(name)
+          end
+          args = [self.class.human_object_class_name(object_class)]
+          if aliases.empty?
+            format = _("%{fn} is required attribute by objectClass '%s'")
+          else
+            format = _("%{fn} is required attribute by objectClass " \
+                              "'%s': aliases: %s")
+            args << aliases.join(', ')
+          end
+          unless ActiveLdap.get_text_supported?
+            format = format.sub(/^%\{fn\} /, '')
+          end
+          errors.add(real_name, format % args)
+          end
+      end  
+    end
+
+    # Duck: new method, should be hooked somewhere
+    def load_initial_attribute_list
+      @initial_attribute_list ||= self.nonempty_attributes
+    end
+  end
+end
diff --git a/lib/ldap_shadows/controller.rb b/lib/ldap_shadows/controller.rb
index b2b59fa..03bc2ee 100755
--- a/lib/ldap_shadows/controller.rb
+++ b/lib/ldap_shadows/controller.rb
@@ -20,6 +20,7 @@
 $KCODE = 'UTF8'
 require 'jcode'
 require 'active_ldap'
+require 'ldap_shadows/activeldap_fixes'
 require 'ldap_shadows/object'
 
 
diff --git a/lib/ldap_shadows/display_utils.rb b/lib/ldap_shadows/display_utils.rb
index 0aff71a..505ea18 100644
--- a/lib/ldap_shadows/display_utils.rb
+++ b/lib/ldap_shadows/display_utils.rb
@@ -52,6 +52,7 @@ module LdapShadows
     def self.display_fields(attr_data, options = {})
       attr_data.each_pair do |key, val|
         next if val[:expert] and not options[:expert]
+        next if val[:admin] and not options[:admin]
 
         field_name = Translator.translate_field_name(key)
 
diff --git a/lib/ldap_shadows/object.rb b/lib/ldap_shadows/object.rb
index bf4dae2..0e16043 100755
--- a/lib/ldap_shadows/object.rb
+++ b/lib/ldap_shadows/object.rb
@@ -23,8 +23,6 @@ module LdapShadows
 
     ldap_mapping :prefix => '', :classes => ['top'], :scope => :sub
 
-    OPERATIONAL_ATTRIBUTES = ['creatorsName', 'createTimestamp', 'modifiersName', 'modifyTimestamp', 'structuralObjectClass', 'entryUUID']
-
     # temporary method until active_ldap is fixed: return a DN object (see activeldap#23932)
     def dn_obj
       ActiveLdap::DistinguishedName.parse(self.dn)
@@ -45,44 +43,6 @@ module LdapShadows
       has_attribute?(field)
     end
 
-    def validate_required_ldap_values
-      _schema = nil
-      # Make sure all MUST attributes have a value
-      entry_attribute.object_classes.each do |object_class|
-        object_class.must.each do |required_attribute|
-          # Normalize to ensure we catch schema problems
-          # needed?
-          real_name = to_real_attribute_name(required_attribute.name, true)
-          raise UnknownAttribute.new(required_attribute) if real_name.nil?
-
-          next if required_attribute.read_only?
-
-          value = @data[real_name] || []
-          next unless self.class.blank_value?(value)
-
-          # workaround for activeldap#26720 (not sure it works in all cases)
-          next unless @initial_attribute_list.nil? or @initial_attribute_list.include?(real_name)
-
-          _schema ||= schema
-          aliases = required_attribute.aliases.collect do |name|
-            self.class.human_attribute_name(name)
-          end
-          args = [self.class.human_object_class_name(object_class)]
-          if aliases.empty?
-            format = _("%{fn} is required attribute by objectClass '%s'")
-          else
-            format = _("%{fn} is required attribute by objectClass " \
-                       "'%s': aliases: %s")
-            args << aliases.join(', ')
-          end
-          unless ActiveLdap.get_text_supported?
-            format = format.sub(/^%\{fn\} /, '')
-          end
-          errors.add(real_name, format % args)
-        end
-      end
-    end
-
     def human_name
       attr_list = ['displayName', 'cn']
       attr_list.unshift(self.class.presentation[:name_attribute]) if self.class.presentation.has_key?(:name_attribute)
@@ -172,12 +132,15 @@ module LdapShadows
     end
 
     def organized_data
-      expert_attributes = (self.class.presentation[:expert_attributes] || []) + OPERATIONAL_ATTRIBUTES
-
       ignored_attrs = self.mapper.get_global_config[:hidden_attributes] || []
       ignored_attrs += self.class.presentation[:hidden_attributes] || []
       attr_list = self.nonempty_attributes - ignored_attrs
 
+      expert_attributes = (self.class.presentation[:expert_attributes] || [])
+      admin_attributes = attr_list.select do |attr|
+        ActiveLdap::Base.schema.attribute(attr).operational?
+      end
+
       aspects = self.aspects
       rel_list = self.possible_relations
 
@@ -190,7 +153,7 @@ module LdapShadows
           taken_attr_list = aspect_data[:presentation][:associated_attributes] & (attr_list + ignored_attrs)
           unless taken_attr_list.empty?
             obj_aspects[aspect] ||= {}
-            obj_aspects[aspect].merge!(fetch_attributes_data(taken_attr_list, expert_attributes))
+            obj_aspects[aspect].merge!(fetch_attributes_data(taken_attr_list, expert_attributes, admin_attributes))
             attr_list -= taken_attr_list
           end
         end
@@ -210,14 +173,14 @@ module LdapShadows
       if self.class.presentation[:associate_unclaimed_attributes]
         taken_attr_list = attr_list
       else
-        taken_attr_list = []
+        taken_attr_list = admin_attributes
         if self.class.presentation.has_key?(:associated_attributes)
           taken_attr_list += self.class.presentation[:associated_attributes]
         end
         taken_attr_list += self.class.possible_attributes
       end
       taken_attr_list = taken_attr_list.uniq & attr_list
-      obj_info = fetch_attributes_data(taken_attr_list, expert_attributes)
+      obj_info = fetch_attributes_data(taken_attr_list, expert_attributes, admin_attributes)
       attr_list -= taken_attr_list
 
       # manage general relations
@@ -234,7 +197,7 @@ module LdapShadows
         aspects.each do |aspect|
           taken_attr_list = (self.class.possible_attributes_for_aspect(aspect) & attr_list)
           obj_aspects[aspect] ||= {}
-          obj_aspects[aspect].merge!(fetch_attributes_data(taken_attr_list, expert_attributes))
+          obj_aspects[aspect].merge!(fetch_attributes_data(taken_attr_list, expert_attributes, admin_attributes))
           attr_list -= taken_attr_list
 
           break if attr_list.empty?
@@ -508,13 +471,14 @@ module LdapShadows
       end
     end
 
-    def fetch_attributes_data(attr_list, expert_attributes)
+    def fetch_attributes_data(attr_list, expert_attributes, admin_attributes)
       attr_data = self.attributes.collect do |key, val|
         if attr_list.include?(key)
           [key, {
              :value => val,
              :multiple => (val.is_a?(Array) ? val.size : 1),
              :expert => expert_attributes.include?(key),
+             :admin => admin_attributes.include?(key),
              :binary => ActiveLdap::Base.schema.attribute(key).binary?
            }]
         else
@@ -552,16 +516,13 @@ module LdapShadows
              :value => value,
              :multiple => multiple,
              :expert => expert_attributes.include?(rel_key),
+             :admin => false,
              :binary => false
            }]
         end
       end
       Hash[rel_data.compact]
     end
-
-    def load_initial_attribute_list
-      @initial_attribute_list ||= self.nonempty_attributes
-    end
   end
 end
 
