23 VALID_SEARCH_OPTIONS = [:attribute, :value, :filter, :prefix,
24 :classes, :scope, :limit, :attributes,
25 :sort_by, :order, :connection, :base]
27 def search(options={}, &block)
28 validate_search_options(options)
29 attr = options[:attribute]
30 value = options[:value] || '*'
31 filter = options[:filter]
32 prefix = options[:prefix]
33 classes = options[:classes]
35 value = value.first if value.is_a?(Array) and value.first.size == 1
39 if attr.nil? or attr == dn_attribute
40 _attr, value, _prefix = split_search_value(value)
42 attr ||= _attr || ensure_search_attribute
44 filter ||= [attr, value]
45 filter = [:and, filter, *object_class_filters(classes)]
46 _base = options[:base] ? [options[:base]] : [prefix, base]
47 _base = prepare_search_base(_base)
48 if options.has_key?(:ldap_scope)
49 message = _(":ldap_scope search option is deprecated. " \
50 "Use :scope instead.")
51 ActiveSupport::Deprecation.warn(message)
52 options[:scope] ||= options[:ldap_scope]
56 :scope => options[:scope] || scope,
58 :limit => options[:limit],
59 :attributes => options[:attributes],
60 :sort_by => options[:sort_by] || sort_by,
61 :order => options[:order] || order,
64 options[:connection] ||= connection
65 values = options[:connection].search(search_options) do |dn, attrs|
67 attrs.each do |key, _value|
68 normalized_attr, normalized_value =
69 normalize_attribute_options(key, _value)
70 attributes[normalized_attr] ||= []
71 attributes[normalized_attr].concat(normalized_value)
75 values = values.collect {|_value| yield(_value)} if block_given?
79 def exist?(dn, options={})
80 attr, value, prefix = split_search_value(dn)
88 attribute = attr || ensure_search_attribute
89 options_for_non_leaf = {
92 :prefix => ["#{attribute}=#{value}", prefix].compact.join(","),
96 !search(options_for_leaf.merge(options)).empty? or
97 !search(options_for_non_leaf.merge(options)).empty?
99 alias_method :exists?, :exist?
101 def count(options={})
106 def validate_search_options(options)
107 options.assert_valid_keys(VALID_SEARCH_OPTIONS)
110 def extract_options_from_args!(args)
111 args.last.is_a?(Hash) ? args.pop : {}
114 def ensure_search_attribute(*candidates)
115 default_search_attribute || "objectClass"
118 def ensure_dn_attribute(target)
120 target.gsub(/^\s*#{Regexp.escape(dn_attribute)}\s*=\s*/i, '')
123 def ensure_base(target)
124 [truncate_base(target), base.to_s].reject do |component|
129 def truncate_base(target)
130 return nil if target.blank?
131 return target if base.nil?
135 parsed_target = target
138 parsed_target = DN.parse(target)
139 rescue DistinguishedNameInvalid
143 return target if parsed_target.nil?
145 (parsed_target - base).to_s
151 def prepare_search_base(components)
152 components.compact.collect do |component|
159 DN.new(*component).to_s
161 end.reject{|x| x.empty?}.join(",")
164 def object_class_filters(classes=nil)
165 expected_classes = (classes || required_classes).collect do |name|
166 Escape.ldap_filter_escape(name)
168 unexpected_classes = excluded_classes.collect do |name|
169 Escape.ldap_filter_escape(name)
172 unless expected_classes.empty?
173 filters << ["objectClass", "=", *expected_classes]
175 unless unexpected_classes.empty?
176 filters << [:not, [:or, ["objectClass", "=", *unexpected_classes]]]
181 def split_search_value(value)
186 attr, value = dn.rdns.first.to_a.first
187 rest = dn.rdns[1..-1]
188 prefix = DN.new(*rest).to_s unless rest.empty?
189 rescue DistinguishedNameInputInvalid
190 return [attr, value, prefix]
191 rescue DistinguishedNameInvalid
193 dn = DN.parse("DUMMY=#{value}")
194 _, value = dn.rdns.first.to_a.first
195 rest = dn.rdns[1..-1]
196 prefix = DN.new(*rest).to_s unless rest.empty?
197 rescue DistinguishedNameInvalid
201 prefix = nil if prefix == base
202 prefix = truncate_base(prefix) if prefix
203 [attr, value, prefix]
210 # Finds the first match for value where |value| is the value of some
211 # |field|, or the wildcard match. This is only useful for derived classes.
212 # usage: Subclass.find(:all, :attribute => "cn", :value => "some*val")
213 # Subclass.find(:all, 'some*val')
215 options = extract_options_from_args!(args)
216 args = [:first] if args.empty? and !options.empty?
219 options[:value] ||= args[1]
220 find_initial(options)
222 options[:value] ||= args[1]
225 options[:value] ||= args[1]
228 find_from_dns(args, options)
232 # A convenience wrapper for <tt>find(:first,
233 # *args)</tt>. You can pass in all the same arguments
234 # to this method as you can to <tt>find(:first)</tt>.
239 # A convenience wrapper for <tt>find(:last,
240 # *args)</tt>. You can pass in all the same arguments
241 # to this method as you can to <tt>find(:last)</tt>.
246 # This is an alias for find(:all). You can pass in
247 # all the same arguments to this method as you can
254 def find_initial(options)
255 find_every(options.merge(:limit => 1)).first
258 def find_last(options)
259 order = options[:order] || self.order || 'ascend'
260 order = normalize_sort_order(order) == :ascend ? :descend : :ascend
261 find_initial(options.merge(:order => order))
264 def normalize_sort_order(value)
266 when /\Aasc(?:end)?\z/i
268 when /\Adesc(?:end)?\z/i
271 raise ArgumentError, _("Invalid order: %s") % value.inspect
275 def find_every(options)
276 options = options.dup
277 sort_by = options.delete(:sort_by) || self.sort_by
278 order = options.delete(:order) || self.order
279 limit = options.delete(:limit) if sort_by or order
280 options[:attributes] |= ["objectClass"] if options[:attributes]
282 results = search(options).collect do |dn, attrs|
283 instantiate([dn, attrs, {:connection => options[:connection]}])
285 return results if sort_by.nil? and order.nil?
288 if sort_by.downcase == "dn"
289 results = results.sort_by {|result| DN.parse(result.dn)}
291 results = results.sort_by {|result| result.send(sort_by)}
294 results.reverse! if normalize_sort_order(order || "ascend") == :descend
295 results = results[0, limit] if limit
299 def find_from_dns(dns, options)
300 expects_array = dns.first.is_a?(Array)
301 return [] if expects_array and dns.first.empty?
303 dns = dns.flatten.compact.uniq
307 raise EntryNotFound, _("Couldn't find %s without a DN") % name
309 result = find_one(dns.first, options)
310 expects_array ? [result] : result
312 find_some(dns, options)
316 def find_one(dn, options)
317 attr, value, prefix = split_search_value(dn)
318 filter = [attr || ensure_search_attribute,
319 Escape.ldap_filter_escape(value)]
320 filter = [:and, filter, options[:filter]] if options[:filter]
321 options = {:prefix => prefix}.merge(options.merge(:filter => filter))
322 result = find_initial(options)
326 args = [self.is_a?(Class) ? name : self.class.name,
329 format = _("Couldn't find %s: DN: %s: filter: %s")
330 args << options[:filter].inspect
332 format = _("Couldn't find %s: DN: %s")
334 raise EntryNotFound, format % args
338 def find_some(dns, options)
339 dn_filters = dns.collect do |dn|
340 attr, value, prefix = split_search_value(dn)
341 attr ||= ensure_search_attribute
342 filter = [attr, value]
346 [dn, "*,#{Escape.ldap_filter_escape(prefix)},#{base}"]]
350 filter = [:or, *dn_filters]
351 filter = [:and, filter, options[:filter]] if options[:filter]
352 result = find_every(options.merge(:filter => filter))
353 if result.size == dns.size
356 args = [self.is_a?(Class) ? name : self.class.name,
359 format = _("Couldn't find all %s: DNs (%s): filter: %s")
360 args << options[:filter].inspect
362 format = _("Couldn't find all %s: DNs (%s)")
364 raise EntryNotFound, format % args
368 def ensure_dn(target)
369 attr, value, prefix = split_search_value(target)
370 "#{attr || dn_attribute}=#{value},#{prefix || base}"
377 options = {:base => base, :scope => scope}.merge(options)
378 options[:connection] ||= connection
379 options[:connection].search(options) do |dn, attributes|
380 ldif << Ldif::Record.new(dn, attributes)
382 return "" if ldif.records.empty?
386 def to_ldif_record(dn, attributes)
387 Ldif::Record.new(dn, attributes)
390 def to_ldif(dn, attributes)
391 Ldif.new([to_ldif_record(dn, attributes)]).to_s
394 def load(ldif, options={})
395 return if ldif.blank?
396 Ldif.parse(ldif).each do |record|
397 record.load(self, options)
401 module ContentRecordLoadable
402 def load(operator, options)
403 operator.add_entry(dn, attributes, options)
406 Ldif::ContentRecord.send(:include, ContentRecordLoadable)
408 module AddRecordLoadable
409 def load(operator, options)
410 entries = attributes.collect do |key, value|
413 options = {:controls => controls}.merge(options)
414 operator.modify_entry(dn, entries, options)
417 Ldif::AddRecord.send(:include, AddRecordLoadable)
419 module DeleteRecordLoadable
420 def load(operator, options)
421 operator.delete_entry(dn, {:controls => controls}.merge(options))
424 Ldif::DeleteRecord.send(:include, DeleteRecordLoadable)
426 module ModifyNameRecordLoadable
427 def load(operator, options)
428 operator.modify_rdn_entry(dn, new_rdn, delete_old_rdn?, new_superior,
429 {:controls => controls}.merge(options))
432 Ldif::ModifyNameRecord.send(:include, ModifyNameRecordLoadable)
434 module ModifyRecordLoadable
435 def load(operator, options)
436 modify_entries = operations.inject([]) do |result, operation|
437 result + operation.to_modify_entries
439 return if modify_entries.empty?
440 operator.modify_entry(dn, modify_entries,
441 {:controls => controls}.merge(options))
444 module AddOperationModifiable
445 def to_modify_entries
446 attributes.collect do |key, value|
451 Ldif::ModifyRecord::AddOperation.send(:include, AddOperationModifiable)
453 module DeleteOperationModifiable
454 def to_modify_entries
455 return [[:delete, full_attribute_name, []]] if attributes.empty?
456 attributes.collect do |key, value|
457 [:delete, key, value]
461 Ldif::ModifyRecord::DeleteOperation.send(:include,
462 DeleteOperationModifiable)
464 module ReplaceOperationModifiable
465 def to_modify_entries
466 return [[:replace, full_attribute_name, []]] if attributes.empty?
467 attributes.collect do |key, value|
468 [:replace, key, value]
472 Ldif::ModifyRecord::ReplaceOperation.send(:include,
473 ReplaceOperationModifiable)
475 Ldif::ModifyRecord.send(:include, ModifyRecordLoadable)
479 def destroy(targets, options={})
480 targets = [targets] unless targets.is_a?(Array)
481 targets.each do |target|
482 find(target, options).destroy
486 def destroy_all(options_or_filter=nil, deprecated_options=nil)
487 if deprecated_options.nil?
488 if options_or_filter.is_a?(String)
489 options = {:filter => options_or_filter}
491 options = (options_or_filter || {}).dup
494 options = deprecated_options.merge(:filter => options_or_filter)
497 find(:all, options).sort_by do |target|
504 def delete(targets, options={})
505 targets = [targets] unless targets.is_a?(Array)
506 targets = targets.collect do |target|
507 ensure_dn_attribute(ensure_base(target))
509 delete_entry(targets, options)
512 def delete_entry(dn, options={})
513 options[:connection] ||= connection
515 options[:connection].delete(dn, options)
517 format = _("Failed to delete LDAP entry: <%s>: %s")
518 raise DeleteError.new(format % [dn.inspect, $!.message])
522 def delete_all(options_or_filter=nil, deprecated_options=nil)
523 if deprecated_options.nil?
524 if options_or_filter.is_a?(String)
525 options = {:filter => options_or_filter}
527 options = (options_or_filter || {}).dup
530 options = deprecated_options.merge(:filter => options_or_filter)
532 targets = search(options).collect do |dn, attributes|
538 delete_entry(targets, options)
543 def add_entry(dn, attributes, options={})
544 unnormalized_attributes = attributes.collect do |key, value|
545 [:add, key, unnormalize_attribute(key, value)]
547 options[:connection] ||= connection
548 options[:connection].add(dn, unnormalized_attributes, options)
551 def modify_entry(dn, attributes, options={})
552 return if attributes.empty?
553 unnormalized_attributes = attributes.collect do |type, key, value|
554 [type, key, unnormalize_attribute(key, value)]
556 options[:connection] ||= connection
557 options[:connection].modify(dn, unnormalized_attributes, options)
560 def modify_rdn_entry(dn, new_rdn, delete_old_rdn, new_superior, options={})
561 options[:connection] ||= connection
562 options[:connection].modify_rdn(dn, new_rdn, delete_old_rdn,
563 new_superior, options)
566 def update(dn, attributes, options={})
572 update(_dn, attributes[i], options)
575 object = find(dn, options)
576 object.update_attributes(attributes)
581 def update_all(attributes, filter=nil, options={})
582 search_options = options.dup
584 if filter.is_a?(String) and /[=\(\)&\|]/ !~ filter
585 search_options = search_options.merge(:value => filter)
587 search_options = search_options.merge(:filter => filter)
590 targets = search(search_options).collect do |dn, attrs|
594 unnormalized_attributes = attributes.collect do |name, value|
595 normalized_name, normalized_value = normalize_attribute(name, value)
596 [:replace, normalized_name,
597 unnormalize_attribute(normalized_name, normalized_value)]
599 options[:connection] ||= connection
600 conn = options[:connection]
602 conn.modify(dn, unnormalized_attributes, options)