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
- @@reconnect_attempts =
LDAP connection
0
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
On connect, this is overriden by the :base argument Make the return value the string that is your LDAP base.
-
.can_reconnect? ⇒ Boolean
Determine if we have exceed the retry limit or not.
-
.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(exc = RuntimeError.new('unknown error'), try_reconnect = true) ⇒ Object
Return the LDAP connection object currently in use Alternately execute a command against the connection object “safely” using a given block.
-
.connection=(conn) ⇒ Object
Set the LDAP connection avoiding Base.connect or multiplexing connections.
-
.create_object(config = {}) ⇒ Object
Driver generator.
-
.dnattr ⇒ Object
Base.dnattr.
-
.ldap_scope ⇒ Object
On connect, this is overriden by the :base argument.
-
.reconnect(force = false) ⇒ Object
Attempts to reconnect up to the number of times allowed If forced, try once then fail with ConnectionError if not connected.
-
.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 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 val can be a dn attribute value, a full DN, or a LDAP::Entry. The use with a LDAP::Entry is primarily meant for internal use by find and find_all.
565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 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 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 |
# File 'lib/activeldap/base.rb', line 565 def initialize(val) @exists = false # Make sure we're connected Base.reconnect if Base.connection.nil? and Base.can_reconnect? if val.class == LDAP::Entry # Call import, which is basically initialize # without accessing LDAP. @@logger.debug "initialize: val is a LDAP::Entry - running import." import(val) return end if val.class != String raise TypeError, "Object key must be a String" end @data = {} # where the r/w entry data is stored @ldap_data = {} # original ldap entry data @attr_methods = {} # list of valid method calls for attributes used for dereferencing @last_oc = false # for use in other methods for "caching" if dnattr().empty? raise ConfigurationError, "dnattr() not set for this class: #{self.class}" end # Extract dnattr if val looks like a dn if val.match(/^#{dnattr()}=([^,=]+),/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.class != String or val.empty? raise TypeError, 'a dn attribute String must be supplied ' + 'on initialization' else # Create what should be the authoritative DN @dn = "#{dnattr()}=#{val},#{base()}" # Search for the existing entry Base.connection(ConnectionError.new("Failed in #{self.class}#new(#{val.inspect})")) do |conn| # Get some attributes conn.search(base(), ldap_scope(), "(#{dnattr()}=#{val})") do |m| @exists = true # 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 end end # Do the actual object setup work. if @exists # Make sure the server uses objectClass and not objectclass unless @ldap_data.has_key?('objectClass') real_objc = @ldap_data.grep(/^objectclass$/i) if real_objc.size == 1 @ldap_data['objectClass'] = @ldap_data[real_objc] @ldap_data.delete(real_objc) else raise AttributeEmpty, 'objectClass was not sent by LDAP server!' end end # 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| real_attr = @attr_methods[pair[0]] @@logger.debug("new: #{pair[0].inspect} method maps to #{real_attr}") if real_attr.nil? @@logger.error("Unable to resolve attribute value #{pair[0].inspect}. " + "Unpredictable behavior likely!") end @data[real_attr] = pair[1].dup @@logger.debug("new: #{real_attr} set to #{pair[1]}") end else send(:apply_objectclass, required_classes()) # Setup dn attribute (later rdn too!) real_dnattr = @attr_methods[dnattr()] @data[real_dnattr] = val @@logger.debug("new: setting dnattr: #{real_dnattr} = #{val}") 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.
940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 |
# File 'lib/activeldap/base.rb', line 940 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.
111 112 113 |
# File 'lib/activeldap/base.rb', line 111 def logger @logger end |
#may ⇒ Object (readonly)
Parsed schema structures
110 111 112 |
# File 'lib/activeldap/base.rb', line 110 def may @may end |
#must ⇒ Object (readonly)
Parsed schema structures
110 111 112 |
# File 'lib/activeldap/base.rb', line 110 def must @must end |
Class Method Details
.base ⇒ Object
On connect, this is overriden by the :base argument Make the return value the string that is your LDAP base
512 513 514 |
# File 'lib/activeldap/base.rb', line 512 def Base.base 'dc=localdomain' end |
.can_reconnect? ⇒ Boolean
Determine if we have exceed the retry limit or not. True is reconnecting is allowed - False if not.
326 327 328 329 330 331 332 333 334 |
# File 'lib/activeldap/base.rb', line 326 def Base.can_reconnect? # Allow connect if we've never connected. return true unless @@config if @@reconnect_attempts < (@@config[:retries] - 1) or @@config[:retries] < 0 return true end return false end |
.close ⇒ Object
Base.close This method deletes the LDAP connection object. This does NOT reset any overridden values from a Base.connect call.
251 252 253 254 255 256 257 258 259 260 261 |
# File 'lib/activeldap/base.rb', line 251 def Base.close begin @@conn.unbind unless @@conn.nil? rescue # Doesn't matter. end @@conn = nil # Make sure it is cleaned up # This causes Ruby/LDAP memory corruption. # 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 sets the LDAP server hostname :port sets 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) :retries - indicates the number of attempts to reconnect that will be undertaken when a stale connection occurs. -1 means infinite. :sasl_quiet - if true, sets @sasl_quiet on the Ruby/LDAP connection :method - whether to use :ssl, :tls, or :plain (unencrypted) :retry_wait - seconds to wait before retrying a connection :ldap_scope - dictates how to find objects. ONELEVEL by default to avoid dn_attr collisions across OUs. Think before changing. :return_objects - indicates whether find/find_all will return objects or just the distinguished name attribute value of the matches :timeout - time in seconds - defaults to disabled. This CAN interrupt search() requests. Be warned. :retry_on_timeout - whether to reconnect when timeouts occur. Defaults to true See lib/configuration.rb for defaults for each option
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 |
# File 'lib/activeldap/base.rb', line 208 def Base.connect(config={}) # Process config # Class options ## These will be replace by configuration.rb defaults if defined @@config = DEFAULT_CONFIG.dup config.keys.each do |key| case key when :base # Scrub before inserting base = config[:base].gsub(/['}{#]/, '') Base.class_eval("def Base.base();'#{base}';end") when :ldap_scope if config[:ldap_scope].class != Fixnum raise ConfigurationError, ':ldap_scope must be a Fixnum' end Base.class_eval("def Base.ldap_scope();#{config[:ldap_scope]};end") else @@config[key] = config[key] end end # Assign a easier name for the logger @@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 # Reset for the new connection @@reconnect_attempts = 0 # Make the connection. do_connect() # Make irb users happy with a 'true' return true end |
.connection(exc = RuntimeError.new('unknown error'), try_reconnect = true) ⇒ Object
Return the LDAP connection object currently in use Alternately execute a command against the connection object “safely” using a given block. Use the given “errmsg” for any error conditions.
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 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 316 317 |
# File 'lib/activeldap/base.rb', line 267 def Base.connection(exc=RuntimeError.new('unknown error'), try_reconnect = true) # Block was given! Let's safely provide access. if block_given? begin Timeout.alarm(@@config[:timeout]) do begin yield @@conn rescue => e # Raise an LDAP error instead of RuntimeError or whatever @@logger.debug("Converting '#{e.inspect}' to useful exception") raise *LDAP::err2exception(@@conn.err) if @@conn.err != 0 # Else reraise @@logger.debug('Reraising') raise e end end rescue Timeout::Error => e @@logger.error('Requested action timed out.') retry if try_reconnect and @@config[:retry_on_timeout] and Base.reconnect() = e. = exc. unless exc.nil? @@logger.error() raise TimeoutError, rescue LDAP::ServerDown, LDAP::ResultError, RuntimeError => e @@logger.error("#{e.class} exception occurred in connection block") @@logger.error("Exception message: #{e.}") @@logger.error("Exception backtrace: #{e.backtrace}") @@logger.error(exc.) unless exc.nil? retry if try_reconnect and Base.reconnect() raise exc unless exc.nil? return nil rescue LDAP::UndefinedType => e @@logger.error("#{e.class} exception occurred in connection block") @@logger.error("Exception message: #{e.}") @@logger.error("Exception backtrace: #{e.backtrace}") # Do not retry - not a connection error raise exc unless exc.nil? return nil # Catch all - to be remedied later rescue => e @@logger.error("#{e.class} exception occurred in connection block") @@logger.error("Exception message: #{e.}") @@logger.error("Exception backtrace: #{e.backtrace}") @@logger.error("Error in catch all: please send debug log to ActiveLDAP author") @@logger.error(exc.) unless exc.nil? raise exc unless exc.nil? return nil end end return @@conn end |
.connection=(conn) ⇒ Object
Set the LDAP connection avoiding Base.connect or multiplexing connections
320 321 322 |
# File 'lib/activeldap/base.rb', line 320 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’])
THIS METHOD IS DANGEROUS. INPUT IS NOT SANITIZED.
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 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/activeldap/base.rb', line 127 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.
522 523 524 |
# File 'lib/activeldap/base.rb', line 522 def Base.dnattr '' end |
.ldap_scope ⇒ Object
On connect, this is overriden by the :base argument
Set this to LDAP_SCOPE_SUBTREE if you have a LDAP tree where all objects of the same class living in different parts of the same subtree, but not. LDAP_SCOPE_ONELEVEL is for use when all the objects in your classes live under one shared level (e.g. ou=People,dc=localdomain)
This can be overriden on a per class basis in ldap_mapping :scope
547 548 549 |
# File 'lib/activeldap/base.rb', line 547 def Base.ldap_scope LDAP::LDAP_SCOPE_ONELEVEL end |
.reconnect(force = false) ⇒ Object
Attempts to reconnect up to the number of times allowed If forced, try once then fail with ConnectionError if not connected.
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 |
# File 'lib/activeldap/base.rb', line 338 def Base.reconnect(force=false) unless @@config @@logger.error('Ignoring force: Base.reconnect called before Base.connect') if force @@logger.debug('Base.reconnect called before Base.connect - calling Base.connect') Base.connect return true end not_connected = true while not_connected if Base.can_reconnect? @@logger.debug('Attempting to reconnect') Base.close() # Reset the attempts if this was forced. @@reconnect_attempts = 0 if force @@reconnect_attempts += 1 if @@config[:retries] >= 0 begin do_connect() not_connected = false rescue => detail @@logger.error("Reconnect to server failed: #{detail.exception}") @@logger.error("Reconnect to server failed backtrace: #{detail.backtrace}") # Do not loop if forced raise ConnectionError, detail. if force end else # Raise a warning raise ConnectionError, 'Giving up trying to reconnect to LDAP server.' end # Sleep before looping sleep @@config[:retry_wait] end return true 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 [].
536 537 538 |
# File 'lib/activeldap/base.rb', line 536 def Base.required_classes [] end |
.schema ⇒ Object
Return the schema object
375 376 377 |
# File 'lib/activeldap/base.rb', line 375 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
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 |
# File 'lib/activeldap/base.rb', line 383 def Base.search(config={}) Base.reconnect if Base.connection.nil? and Base.can_reconnect? 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 result = Base.connection() do |conn| 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 end if result.nil? # Do nothing on failure @@logger.debug "No matches for #{config[:filter]} and attrs #{config[:attrs]}" end return values end |
Instance Method Details
#__methods ⇒ Object
Add available attributes to the methods
970 |
# File 'lib/activeldap/base.rb', line 970 alias_method :__methods, :methods |
#attributes ⇒ Object
attributes
Return attribute methods so that a program can determine available attributes dynamically without schema awareness
676 677 678 679 680 |
# File 'lib/activeldap/base.rb', line 676 def attributes @@logger.debug("stub: attributes called") send(:apply_objectclass, @data['objectClass']) if @data['objectClass'] != @last_oc return @attr_methods.keys.map {|x|x.downcase}.uniq end |
#delete ⇒ Object
delete
Delete this entry from LDAP
746 747 748 749 750 751 752 753 |
# File 'lib/activeldap/base.rb', line 746 def delete @@logger.debug("stub: delete called") Base.connection(DeleteError.new( "Failed to delete LDAP entry: '#{@dn}'")) do |conn| conn.delete(@dn) @exists = false end end |
#dn ⇒ Object
dn
Return the authoritative dn
693 694 695 696 |
# File 'lib/activeldap/base.rb', line 693 def dn @@logger.debug("stub: dn called") return @dn.dup end |
#exists? ⇒ Boolean
exists?
Return whether the entry exists in LDAP or not
685 686 687 688 |
# File 'lib/activeldap/base.rb', line 685 def exists? @@logger.debug("stub: exists? called") return @exists end |
#methods ⇒ Object
971 972 973 |
# File 'lib/activeldap/base.rb', line 971 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
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 |
# File 'lib/activeldap/base.rb', line 704 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| # Downcase to ensure we catch schema problems deref = @attr_methods[req_attr.downcase] # Set default if it wasn't yet set. @data[deref] = [] if @data[deref].nil? # Check for missing requirements. if @data[deref].empty? 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
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 801 802 803 804 805 806 807 808 809 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 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 |
# File 'lib/activeldap/base.rb', line 762 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 = Marshal.load(Marshal.dump(@ldap_data)) @@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: duping @data') data = Marshal.load(Marshal.dump(@data)) @@logger.debug('#write: finished duping @data') @@logger.debug('#write: removing disallowed attributes from @data') bad_attrs = @data.keys - (@must+@may) bad_attrs.each do |removeme| data.delete(removeme) end @@logger.debug('#write: finished removing disallowed attributes from @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] # If it doesn't exist, don't freak out. value = [] if value.nil? # 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] # Make sure to change this to an Array if there was mistake earlier. value = [] if value.nil? 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') Base.connection(WriteError.new( "Failed to modify: '#{entry}'")) do |conn| @@logger.debug("#write: modifying #{@dn}") conn.modify(@dn, entry) @@logger.debug('#write: modify successful') 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 Base.connection(WriteError.new( "Failed to add: '#{entry}'")) do |conn| @@logger.debug("#write: adding #{@dn}") conn.add(@dn, entry) @@logger.debug("#write: add successful") @exists = true end end @@logger.debug("#write: resetting @ldap_data to a dup of @data") @ldap_data = Marshal.load(Marshal.dump(data)) # Delete items disallowed by objectclasses. # They should have been removed from ldap. @@logger.debug('#write: removing attributes from @ldap_data not sent in data') bad_attrs.each do |removeme| @ldap_data.delete(removeme) end @@logger.debug('#write: @ldap_data reset complete') @@logger.debug('stub: write exitted') end |