Class: ActiveLDAP::Base
- Inherits:
-
Object
- Object
- ActiveLDAP::Base
- Defined in:
- lib/activeldap/base.rb
Overview
Base
Base is the primary class which contains all of the core ActiveLDAP functionality. It is meant to only ever be subclassed by extension classes.
Constant Summary collapse
- @@config =
All class-wide variables
nil
- @@schema =
Container for current connection settings
nil
- @@conn =
LDAP server’s schema
nil
Instance Attribute Summary collapse
-
#logger ⇒ Object
Returns the value of attribute logger.
-
#may ⇒ Object
readonly
Parsed schema structures.
-
#must ⇒ Object
readonly
Parsed schema structures.
Class Method Summary collapse
-
.base ⇒ Object
Make the return value the string that is your LDAP base.
-
.close ⇒ Object
Base.close This method deletes the LDAP connection object.
-
.connect(config = {}) ⇒ Object
Connect and bind to LDAP creating a class variable for use by all ActiveLDAP objects.
-
.connection ⇒ Object
Return the LDAP connection object currently in use.
-
.connection=(conn) ⇒ Object
Set the LDAP connection avoiding Base.connect or multiplexing connections.
-
.create_object(config = {}) ⇒ Object
Driver generator.
-
.dnattr ⇒ Object
Base.dnattr.
-
.required_classes ⇒ Object
This is optionally set to the array of objectClass names that are minimally required for EVERY object on your LDAP server.
-
.schema ⇒ Object
Return the schema object.
-
.search(config = {}) ⇒ Object
search.
Instance Method Summary collapse
-
#__methods ⇒ Object
Add available attributes to the methods.
-
#attributes ⇒ Object
attributes.
-
#delete ⇒ Object
delete.
-
#dn ⇒ Object
dn.
-
#exists? ⇒ Boolean
exists?.
-
#initialize(val = '') ⇒ Base
constructor
new.
-
#method_missing(name, *args) ⇒ Object
method_missing.
- #methods ⇒ Object
-
#validate ⇒ Object
validate.
-
#write ⇒ Object
write.
Constructor Details
#initialize(val = '') ⇒ Base
new
Creates a new instance of Base initializing all class and instance variables. initialize2() must be called to complete all initialization. defines local defaults. See examples If multiple values exist for dnattr, the first one put here will be authoritative TODO: Add support for relative distinguished names
475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 |
# File 'lib/activeldap/base.rb', line 475 def initialize(val='') if val.class != String raise TypeError, "Object key must be a String" end # Try a default connection if none made explicitly unless Base.connection # Use @@config if it has been prepopulated and the conn is down. if @@config ActiveLDAP::Base.connect(@@config) else ActiveLDAP::Base.connect end end @data = {} # where the r/w entry data is stored @data.default = [] @ldap_data = {} # original ldap entry data @ldap_data.default = [] @attr_methods = {} # list of valid method calls for attributes used for dereferencing # Break val apart if it is a dn if val.match(/^#{dnattr()}=([^,=]+),#{base()}$/i) val = $1 elsif val.match(/[=,]/) @@logger.info "initialize: Changing val from '#{val}' to '' because it doesn't match the DN." val = '' end # Do a search - if it exists, pull all data and parse schema, if not, just set the hierarchical data if val.empty? @exists = false # Setup what should eb authoritative @dn = "#{dnattr()}=#{val},#{base()}" send(:apply_objectclass, required_classes()) else # do a search then # Search for the existing entry begin # Get some attributes Base.connection.search("#{dnattr()}=#{val},#{base()}", LDAP::LDAP_SCOPE_SUBTREE, "objectClass=*") do |m| # Save DN @dn = m.dn # Load up data into tmp @@logger.debug("loading entry: #{@dn}") m.attrs.each do |attr| # Load with subtypes just like @data @@logger.debug("calling make_subtypes for m.vals(attr).dup") safe_attr, value = make_subtypes(attr, m.vals(attr).dup) @@logger.debug("finished make_subtypes for #{attr}") # Add subtype to any existing values if @ldap_data.has_key? safe_attr value.each do |v| @ldap_data[safe_attr].push(v) end else @ldap_data[safe_attr] = value end end end @exists = true # Populate schema data send(:apply_objectclass, @ldap_data['objectClass']) # Populate real data now that we have the schema with aliases @ldap_data.each do |pair| send(:attribute_method=, pair[0], pair[1].dup) end rescue LDAP::ResultError @exists = false # Create what should be the authoritative DN @dn = "#{dnattr()}=#{val},#{base()}" send(:apply_objectclass, required_classes()) # Setup dn attribute (later rdn too!) attr_sym = "#{dnattr()}=".to_sym @@logger.debug("new: setting dnattr: #{dnattr()} = #{val}") send(attr_sym, val) end end end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(name, *args) ⇒ Object
method_missing
If a given method matches an attribute or an attribute alias then call the appropriate method. TODO: Determine if it would be better to define each allowed method
using class_eval instead of using method_missing. This would
give tab completion in irb.
810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 |
# File 'lib/activeldap/base.rb', line 810 def method_missing(name, *args) @@logger.debug("stub: called method_missing(#{name.inspect}, #{args.inspect})") # dynamically update the available attributes without requiring an # explicit call. The cache 'last_oc' saves a lot of cpu time. if @data['objectClass'] != @last_oc @@logger.debug("method_missing(#{name.inspect}, #{args.inspect}): updating apply_objectclass(#{@data['objectClass'].inspect})") send(:apply_objectclass, @data['objectClass']) end key = name.to_s case key when /^(\S+)=$/ real_key = $1 @@logger.debug("method_missing: attr_methods has_key? #{real_key}") if @attr_methods.has_key? real_key raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" if args.size != 1 @@logger.debug("method_missing: calling :attribute_method=(#{real_key}, #{args[0]})") return send(:attribute_method=, real_key, args[0]) end else @@logger.debug("method_missing: attr_methods has_key? #{key}") if @attr_methods.has_key? key raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" if args.size > 1 return attribute_method(key, *args) end end raise NoMethodError, "undefined method `#{key}' for #{self}" end |
Instance Attribute Details
#logger ⇒ Object
Returns the value of attribute logger.
92 93 94 |
# File 'lib/activeldap/base.rb', line 92 def logger @logger end |
#may ⇒ Object (readonly)
Parsed schema structures
91 92 93 |
# File 'lib/activeldap/base.rb', line 91 def may @may end |
#must ⇒ Object (readonly)
Parsed schema structures
91 92 93 |
# File 'lib/activeldap/base.rb', line 91 def must @must end |
Class Method Details
.base ⇒ Object
Make the return value the string that is your LDAP base
437 438 439 |
# File 'lib/activeldap/base.rb', line 437 def Base.base 'dc=example,dc=com' end |
.close ⇒ Object
Base.close This method deletes the LDAP connection object. This does NOT reset any overridden values from a Base.connect call.
256 257 258 259 260 261 |
# File 'lib/activeldap/base.rb', line 256 def Base.close @@conn.unbind unless @@conn.nil? @@conn = nil # Make sure it is cleaned up ObjectSpace.garbage_collect end |
.connect(config = {}) ⇒ Object
Connect and bind to LDAP creating a class variable for use by all ActiveLDAP objects.
config
config
must be a hash that may contain any of the following fields: :user, :password_block, :logger, :host, :port, :base, :bind_format, :try_sasl, :allow_anonymous :user specifies the username to bind with. :bind_format specifies the string to substitute the username into on bind. e.g. uid=%s,ou=People,dc=dataspill,dc=org. Overrides @@bind_format. :password_block specifies a Proc object that will yield a String to be used as the password when called. :logger specifies a preconfigured Log4r::Logger to be used for all logging :host overrides the configuration.rb @@host setting with the LDAP server hostname :port overrides the configuration.rb @@port setting for the LDAP server port :base overwrites Base.base - this affects EVERYTHING :try_sasl indicates that a SASL bind should be attempted when binding to the server (default: false) :allow_anonymous indicates that a true anonymous bind is allowed when trying to bind to the server (default: true)
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 |
# File 'lib/activeldap/base.rb', line 178 def Base.connect(config={}) # :user, :password_block, :logger # Process config # Class options ## These will be replace by configuration.rb defaults if defined @@config = {} @@config[:host] = config[:host] || @@host @@config[:port] = config[:port] || @@port if config[:base] Base.class_eval <<-"end_eval" def Base.base '#{config[:base]}' end end_eval end @@config[:bind_format] = config[:bind_format] || @@bind_format @@logger = config[:logger] || nil # Setup default logger to console if @@logger.nil? @@logger = Log4r::Logger.new('activeldap') @@logger.level = Log4r::OFF Log4r::StderrOutputter.new 'console' @@logger.add('console') end # Method options user = nil password_block = nil @@config[:allow_anonymous] = true @@config[:try_sasl] = false @@config[:user] = config[:user] || user @@config[:allow_anonymous] = config[:allow_anonymous] if config.has_key? :allow_anonymous @@config[:try_sasl] = config[:try_sasl] @@config[:password_block] = config[:password_block] if config.has_key? :password_block # Setup bind credentials @@config[:user] = ENV['USER'] unless @@config[:user] # Connect to LDAP begin # SSL using START_TLS @@conn = LDAP::SSLConn.new(@@config[:host], @@config[:port], true) rescue @@logger.warn "Warning: Failed to connect using TLS!" begin @@logger.warn "Warning: Attempting SSL connection . . ." @@conn = LDAP::SSLConn.new(@@config[:host], @@config[:port], false) # HACK: Load the schema here because otherwise you can't tell if the # HACK: SSLConn is a real SSL connection. @@schema = @@conn.schema() if @@schema.nil? rescue @@logger.warn "Warning: Attempting unencrypted connection . . ." @@conn = LDAP::Conn.new(@@config[:host], @@config[:port]) end end @@logger.debug "Connected to #{@@config[:host]}:#{@@config[:port]}." # Enforce LDAPv3 @@conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3) # Authenticate do_bind # Load Schema (if not straight SSL...) begin @@schema = @@conn.schema() if @@schema.nil? rescue => detail raise ConnectionError, "#{detail.exception} - LDAP connection failure, or server does not support schema queries." end # Cleanly return return true end |
.connection ⇒ Object
Return the LDAP connection object currently in use
264 265 266 |
# File 'lib/activeldap/base.rb', line 264 def Base.connection return @@conn end |
.connection=(conn) ⇒ Object
Set the LDAP connection avoiding Base.connect or multiplexing connections
269 270 271 |
# File 'lib/activeldap/base.rb', line 269 def Base.connection=(conn) @@conn = conn end |
.create_object(config = {}) ⇒ Object
Driver generator
TODO add type checking This let’s you call this method to create top-level extension object. This is really just a proof of concept and has not truly useful purpose. example: Base.create_object(:class => “user”, :dnattr => “uid”, :classes => [‘top’])
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/activeldap/base.rb', line 106 def Base.create_object(config={}) # Just upcase the first letter of the new class name str = config[:class] class_name = str[0].chr.upcase + str[1..-1] attr = config[:dnattr] # "uid" prefix = config[:base] # "ou=People" # [ 'top', 'posixAccount' ] classes_array = config[:classes] || [] # [ [ :groups, {:class_name => "Group", :foreign_key => "memberUid"}] ] belongs_to_array = config[:belongs_to] || [] # [ [ :members, {:class_name => "User", :foreign_key => "uid", :local_key => "memberUid"}] ] has_many_array = config[:has_many] || [] raise TypeError, ":objectclasses must be an array" unless classes_array.respond_to? :size raise TypeError, ":belongs_to must be an array" unless belongs_to_array.respond_to? :size raise TypeError, ":has_many must be an array" unless has_many_array.respond_to? :size # Build classes array classes = '[' classes_array.map! {|x| x = "'#{x}'"} classes << classes_array.join(', ') classes << ']' # Build belongs_to belongs_to = [] if belongs_to_array.size > 0 belongs_to_array.each do |bt| line = [ "belongs_to :#{bt[0]}" ] bt[1].keys.each do |key| line << ":#{key} => '#{bt[1][key]}'" end belongs_to << line.join(', ') end end # Build has_many has_many = [] if has_many_array.size > 0 has_many_array.each do |hm| line = [ "has_many :#{hm[0]}" ] hm[1].keys.each do |key| line << ":#{key} => '#{hm[1][key]}'" end has_many << line.join(', ') end end self.class.module_eval <<-"end_eval" class ::#{class_name} < ActiveLDAP::Base ldap_mapping :dnattr => "#{attr}, :prefix => "#{prefix}", :classes => "#{classes}" #{belongs_to.join("\n")} #{has_many.join("\n")} end end_eval end |
.dnattr ⇒ Object
Base.dnattr
This is a placeholder for the class method that will be overridden on calling ldap_mapping in a subclass. Using a class method allows for clean inheritance from classes that already have a ldap_mapping.
447 448 449 |
# File 'lib/activeldap/base.rb', line 447 def Base.dnattr '' end |
.required_classes ⇒ Object
This is optionally set to the array of objectClass names that are minimally required for EVERY object on your LDAP server. If you don’t want one, set this to [].
461 462 463 |
# File 'lib/activeldap/base.rb', line 461 def Base.required_classes [] end |
.schema ⇒ Object
Return the schema object
274 275 276 |
# File 'lib/activeldap/base.rb', line 274 def Base.schema @@schema end |
.search(config = {}) ⇒ Object
search
Wraps Ruby/LDAP connection.search to make it easier to search for specific data without cracking open Base.connection
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/activeldap/base.rb', line 282 def Base.search(config={}) unless Base.connection if @@config ActiveLDAP::Base.connect(@@config) else ActiveLDAP::Base.connect end end config[:filter] = 'objectClass=*' unless config.has_key? :filter config[:attrs] = [] unless config.has_key? :attrs config[:scope] = LDAP::LDAP_SCOPE_SUBTREE unless config.has_key? :scope config[:base] = base() unless config.has_key? :base values = [] config[:attrs] = config[:attrs].to_a # just in case begin @@conn.search(config[:base], config[:scope], config[:filter], config[:attrs]) do |m| res = {} res['dn'] = [m.dn.dup] # For consistency with the below m.attrs.each do |attr| if config[:attrs].member? attr or config[:attrs].empty? res[attr] = m.vals(attr).dup end end values.push(res) end rescue RuntimeError => detail @@logger.debug "No matches for #{config[:filter]} and attrs #{config[:attrs]}" # Do nothing on failure end return values end |
Instance Method Details
#__methods ⇒ Object
Add available attributes to the methods
840 |
# File 'lib/activeldap/base.rb', line 840 alias_method :__methods, :methods |
#attributes ⇒ Object
attributes
Return attribute methods so that a program can determine available attributes dynamically without schema awareness
563 564 565 566 567 |
# File 'lib/activeldap/base.rb', line 563 def attributes @@logger.debug("stub: attributes called") send(:apply_objectclass, @data['objectClass']) if @data['objectClass'] != @last_oc return @attr_methods.keys end |
#delete ⇒ Object
delete
Delete this entry from LDAP
630 631 632 633 634 635 636 637 638 |
# File 'lib/activeldap/base.rb', line 630 def delete @@logger.debug("stub: delete called") begin @@conn.delete(@dn) @exists = false rescue LDAP::ResultError => detail raise DeleteError, "Failed to delete LDAP entry: '#{@dn}'" end end |
#dn ⇒ Object
dn
Return the authoritative dn
580 581 582 583 |
# File 'lib/activeldap/base.rb', line 580 def dn @@logger.debug("stub: dn called") return @dn.dup end |
#exists? ⇒ Boolean
exists?
Return whether the entry exists in LDAP or not
572 573 574 575 |
# File 'lib/activeldap/base.rb', line 572 def exists? @@logger.debug("stub: exists? called") return @exists end |
#methods ⇒ Object
841 842 843 |
# File 'lib/activeldap/base.rb', line 841 def methods return __methods + attributes() end |
#validate ⇒ Object
validate
Basic validation:
-
Verify that every ‘MUST’ specified in the schema has a value defined
-
Enforcement of undefined attributes is handled in the objectClass= method
Must call enforce_types() first before enforcement can be guaranteed
591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 |
# File 'lib/activeldap/base.rb', line 591 def validate @@logger.debug("stub: validate called") # Clean up attr values, etc send(:enforce_types) # Validate objectclass settings @data['objectClass'].each do |klass| unless klass.class == String raise TypeError, "Value in objectClass array is not a String. (#{klass.class}:#{klass.inspect})" end unless Base.schema.names("objectClasses").member? klass raise ObjectClassError, "objectClass '#{klass}' unknown to LDAP server." end end # make sure this doesn't drop any of the required objectclasses required_classes().each do |oc| unless @data['objectClass'].member? oc.to_s raise ObjectClassError, "'#{oc}' must be a defined objectClass for class '#{self.class}' as set in the ldap_mapping" end end # Make sure all MUST attributes have a value @data['objectClass'].each do |objc| @must.each do |req_attr| deref = @attr_methods[req_attr] if @data[deref] == [] raise AttributeEmpty, "objectClass '#{objc}' requires attribute '#{Base.schema.attribute_aliases(req_attr).join(', ')}'" end end end @@logger.debug("stub: validate finished") end |
#write ⇒ Object
write
Write and validate this object into LDAP either adding or replacing attributes TODO: Binary data support TODO: Relative DN support
647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 |
# File 'lib/activeldap/base.rb', line 647 def write @@logger.debug("stub: write called") # Validate against the objectClass requirements validate # Put all changes into one change entry to ensure # automatic rollback upon failure. entry = [] # Expand subtypes to real ldap_data entries # We can't reuse @ldap_data because an exception would leave # an object in an unknown state @@logger.debug("#write: dup'ing @ldap_data") ldap_data = @ldap_data.dup @@logger.debug("#write: dup finished @ldap_data") @@logger.debug("#write: expanding subtypes in @ldap_data") ldap_data.keys.each do |key| ldap_data[key].each do |value| if value.class == Hash suffix, real_value = extract_subtypes(value) if ldap_data.has_key? key + suffix ldap_data[key + suffix].push(real_value) else ldap_data[key + suffix] = real_value end ldap_data[key].delete(value) end end end @@logger.debug("#write: subtypes expanded for @ldap_data") # Expand subtypes to real data entries, but leave @data alone @@logger.debug("#write: dup'ing @data") data = @data.dup @@logger.debug("#write: finished dup'ing @data") @@logger.debug("#write: expanding subtypes for @data") data.keys.each do |key| data[key].each do |value| if value.class == Hash suffix, real_value = extract_subtypes(value) if data.has_key? key + suffix data[key + suffix].push(real_value) else data[key + suffix] = real_value end data[key].delete(value) end end end @@logger.debug("#write: subtypes expanded for @data") if @exists # Cycle through all attrs to determine action action = {} replaceable = [] # Now that all the subtypes will be treated as unique attributes # we can see what's changed and add anything that is brand-spankin' # new. @@logger.debug("#write: traversing ldap_data determining replaces and deletes") ldap_data.each do |pair| suffix = '' binary = 0 name, *suffix_a = pair[0].split(/;/) suffix = ';'+ suffix_a.join(';') if suffix_a.size > 0 name = @attr_methods[name] name = pair[0].split(/;/)[0] if name.nil? # for objectClass, or removed vals value = data[name+suffix] # Detect subtypes and account for them binary = LDAP::LDAP_MOD_BVALUES if Base.schema.binary? name replaceable.push(name+suffix) if pair[1] != value # Create mod entries if not value.empty? # Ditched delete then replace because attribs with no equality match rules # will fails @@logger.debug("updating attribute of existing entry: #{name+suffix}: #{value.inspect}") entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, value)) else # Since some types do not have equality matching rules, delete doesn't work # Replacing with nothing is equivalent. @@logger.debug("removing attribute from existing entry: #{name+suffix}") entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, [])) end end end @@logger.debug("#write: finished traversing ldap_data") @@logger.debug("#write: traversing data determining adds") data.each do |pair| suffix = '' binary = 0 name, *suffix_a = pair[0].split(/;/) suffix = ';' + suffix_a.join(';') if suffix_a.size > 0 name = @attr_methods[name] name = pair[0].split(/;/)[0] if name.nil? # for obj class or removed vals value = pair[1] if not replaceable.member? name+suffix # Detect subtypes and account for them binary = LDAP::LDAP_MOD_BVALUES if Base.schema.binary? name @@logger.debug("adding attribute to existing entry: #{name+suffix}: #{value.inspect}") # REPLACE will function like ADD, but doesn't hit EQUALITY problems # TODO: Added equality(attr) to Schema2 entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, value)) unless value.empty? end end @@logger.debug("#write: traversing data complete") begin @@logger.debug("#write: modifying #{@dn}") @@conn.modify(@dn, entry) @@logger.debug("#write: modify successful") rescue => detail raise WriteError, "Could not update LDAP entry: #{detail}" end else # add everything! @@logger.debug("#write: adding all attribute value pairs") @@logger.debug("#write: adding #{@attr_methods[dnattr()].inspect} = #{data[@attr_methods[dnattr()]].inspect}") entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD, @attr_methods[dnattr()], data[@attr_methods[dnattr()]])) @@logger.debug("#write: adding objectClass = #{data[@attr_methods['objectClass']].inspect}") entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD, 'objectClass', data[@attr_methods['objectClass']])) data.each do |pair| if pair[1].size > 0 and pair[0] != 'objectClass' and pair[0] != @attr_methods[dnattr()] # Detect subtypes and account for them if Base.schema.binary? pair[0].split(/;/)[0] binary = LDAP::LDAP_MOD_BVALUES else binary = 0 end @@logger.debug("adding attribute to new entry: #{pair[0].inspect}: #{pair[1].inspect}") entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD|binary, pair[0], pair[1])) end end begin @@logger.debug("#write: adding #{@dn}") @@conn.add(@dn, entry) @@logger.debug("#write: add successful") @exists = true rescue LDAP::ResultError => detail raise WriteError, "Could not add LDAP entry[#{Base.connection.err2string(Base.connection.err)}]: #{detail}" end end @@logger.debug("#write: resetting @ldap_data to a dup of @data") @ldap_data = @data.dup @@logger.debug("#write: @ldap_data reset complete") @@logger.debug("stub: write exitted") end |