Class | ActiveLDAP::Base |
In: |
lib/activeldap/base.rb
|
Parent: | Object |
Base is the primary class which contains all of the core ActiveLDAP functionality. It is meant to only ever be subclassed by extension classes.
methods | -> | __methods |
Add available attributes to the methods |
logger | [RW] | |
may | [R] | Parsed schema structures |
must | [R] | Parsed schema structures |
This method when included into Base provides an inheritable, overwritable configuration setting
This should be a string with the base of the ldap server such as ‘dc=example,dc=com’, and it should be overwritten by including configuration.rb into this class. When subclassing, the specified prefix will be concatenated.
# File lib/activeldap/base.rb, line 513 513: def Base.base 514: 'dc=localdomain' 515: end
Determine if we have exceed the retry limit or not. True is reconnecting is allowed - False if not.
# File lib/activeldap/base.rb, line 327 327: def Base.can_reconnect? 328: # Allow connect if we've never connected. 329: return true unless @@config 330: if @@reconnect_attempts < (@@config[:retries] - 1) or 331: @@config[:retries] < 0 332: return true 333: end 334: return false 335: end
Base.close This method deletes the LDAP connection object. This does NOT reset any overridden values from a Base.connect call.
# File lib/activeldap/base.rb, line 252 252: def Base.close 253: begin 254: @@conn.unbind unless @@conn.nil? 255: rescue 256: # Doesn't matter. 257: end 258: @@conn = nil 259: # Make sure it is cleaned up 260: # This causes Ruby/LDAP memory corruption. 261: # ObjectSpace.garbage_collect 262: end
Connect and bind to LDAP creating a class variable for use by all ActiveLDAP objects.
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
# File lib/activeldap/base.rb, line 209 209: def Base.connect(config={}) 210: # Process config 211: # Class options 212: ## These will be replace by configuration.rb defaults if defined 213: @@config = DEFAULT_CONFIG.dup 214: config.keys.each do |key| 215: case key 216: when :base 217: # Scrub before inserting 218: base = config[:base].gsub(/['}{#]/, '') 219: Base.class_eval("def Base.base();'#{base}';end") 220: when :ldap_scope 221: if config[:ldap_scope].class != Fixnum 222: raise ConfigurationError, ':ldap_scope must be a Fixnum' 223: end 224: Base.class_eval("def Base.ldap_scope();#{config[:ldap_scope]};end") 225: else 226: @@config[key] = config[key] 227: end 228: end 229: # Assign a easier name for the logger 230: @@logger = @@config[:logger] || nil 231: # Setup default logger to console 232: if @@logger.nil? 233: @@logger = Log4r::Logger.new('activeldap') 234: @@logger.level = Log4r::OFF 235: Log4r::StderrOutputter.new 'console' 236: @@logger.add('console') 237: end 238: 239: # Reset for the new connection 240: @@reconnect_attempts = 0 241: 242: # Make the connection. 243: do_connect() 244: 245: # Make irb users happy with a 'true' 246: return true 247: end
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.
# File lib/activeldap/base.rb, line 268 268: def Base.connection(exc=RuntimeError.new('unknown error'), try_reconnect = true) 269: # Block was given! Let's safely provide access. 270: if block_given? 271: begin 272: Timeout.alarm(@@config[:timeout]) do 273: begin 274: yield @@conn 275: rescue => e 276: # Raise an LDAP error instead of RuntimeError or whatever 277: 278: raise *LDAP::err2exception(@@conn.err) if @@conn.err != 0 279: # Else reraise 280: 281: raise e 282: end 283: end 284: rescue Timeout::Error => e 285: @@logger.error('Requested action timed out.') 286: retry if try_reconnect and @@config[:retry_on_timeout] and Base.reconnect() 287: message = e.message 288: message = exc.message unless exc.nil? 289: @@logger.error(message) 290: raise TimeoutError, message 291: rescue LDAP::ServerDown, LDAP::ResultError, RuntimeError => e 292: @@logger.error("#{e.class} exception occurred in connection block") 293: @@logger.error("Exception message: #{e.message}") 294: @@logger.error("Exception backtrace: #{e.backtrace}") 295: @@logger.error(exc.message) unless exc.nil? 296: retry if try_reconnect and Base.reconnect() 297: raise exc unless exc.nil? 298: return nil 299: rescue LDAP::UndefinedType => e 300: @@logger.error("#{e.class} exception occurred in connection block") 301: @@logger.error("Exception message: #{e.message}") 302: @@logger.error("Exception backtrace: #{e.backtrace}") 303: # Do not retry - not a connection error 304: raise exc unless exc.nil? 305: return nil 306: # Catch all - to be remedied later 307: rescue => e 308: @@logger.error("#{e.class} exception occurred in connection block") 309: @@logger.error("Exception message: #{e.message}") 310: @@logger.error("Exception backtrace: #{e.backtrace}") 311: @@logger.error("Error in catch all: please send debug log to ActiveLDAP author") 312: @@logger.error(exc.message) unless exc.nil? 313: raise exc unless exc.nil? 314: return nil 315: end 316: end 317: return @@conn 318: end
Set the LDAP connection avoiding Base.connect or multiplexing connections
# File lib/activeldap/base.rb, line 321 321: def Base.connection=(conn) 322: @@conn = conn 323: end
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.
# File lib/activeldap/base.rb, line 127 127: def Base.create_object(config={}) 128: # Just upcase the first letter of the new class name 129: str = config[:class] 130: class_name = str[0].chr.upcase + str[1..-1] 131: 132: attr = config[:dnattr] # "uid" 133: prefix = config[:base] # "ou=People" 134: # [ 'top', 'posixAccount' ] 135: classes_array = config[:classes] || [] 136: # [ [ :groups, {:class_name => "Group", :foreign_key => "memberUid"}] ] 137: belongs_to_array = config[:belongs_to] || [] 138: # [ [ :members, {:class_name => "User", :foreign_key => "uid", :local_key => "memberUid"}] ] 139: has_many_array = config[:has_many] || [] 140: 141: raise TypeError, ":objectclasses must be an array" unless classes_array.respond_to? :size 142: raise TypeError, ":belongs_to must be an array" unless belongs_to_array.respond_to? :size 143: raise TypeError, ":has_many must be an array" unless has_many_array.respond_to? :size 144: 145: # Build classes array 146: classes = '[' 147: classes_array.map! {|x| x = "'#{x}'"} 148: classes << classes_array.join(', ') 149: classes << ']' 150: 151: # Build belongs_to 152: belongs_to = [] 153: if belongs_to_array.size > 0 154: belongs_to_array.each do |bt| 155: line = [ "belongs_to :#{bt[0]}" ] 156: bt[1].keys.each do |key| 157: line << ":#{key} => '#{bt[1][key]}'" 158: end 159: belongs_to << line.join(', ') 160: end 161: end 162: 163: # Build has_many 164: has_many = [] 165: if has_many_array.size > 0 166: has_many_array.each do |hm| 167: line = [ "has_many :#{hm[0]}" ] 168: hm[1].keys.each do |key| 169: line << ":#{key} => '#{hm[1][key]}'" 170: end 171: has_many << line.join(', ') 172: end 173: end 174: 175: self.class.module_eval "class ::\#{class_name} < ActiveLDAP::Base\nldap_mapping :dnattr => \"\#{attr}\", :prefix => \"\#{prefix}\", :classes => \#{classes}\n\#{belongs_to.join(\"\\n\")}\n\#{has_many.join(\"\\n\")}\nend\n" 176: end
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.
# File lib/activeldap/base.rb, line 523 523: def Base.dnattr 524: '' 525: end
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
# File lib/activeldap/configuration.rb, line 42 42: def Base.ldap_scope 43: LDAP::LDAP_SCOPE_ONELEVEL 44: end
Attempts to reconnect up to the number of times allowed If forced, try once then fail with ConnectionError if not connected.
# File lib/activeldap/base.rb, line 339 339: def Base.reconnect(force=false) 340: unless @@config 341: @@logger.error('Ignoring force: Base.reconnect called before Base.connect') if force 342: 343: Base.connect 344: return true 345: end 346: not_connected = true 347: while not_connected 348: if Base.can_reconnect? 349: 350: Base.close() 351: 352: # Reset the attempts if this was forced. 353: @@reconnect_attempts = 0 if force 354: @@reconnect_attempts += 1 if @@config[:retries] >= 0 355: begin 356: do_connect() 357: not_connected = false 358: rescue => detail 359: @@logger.error("Reconnect to server failed: #{detail.exception}") 360: @@logger.error("Reconnect to server failed backtrace: #{detail.backtrace}") 361: # Do not loop if forced 362: raise ConnectionError, detail.message if force 363: end 364: else 365: # Raise a warning 366: raise ConnectionError, 'Giving up trying to reconnect to LDAP server.' 367: end 368: 369: # Sleep before looping 370: sleep @@config[:retry_wait] 371: end 372: return true 373: end
This method when included into Base provides an inheritable, overwritable configuration setting
The value should be the minimum required objectClasses to make an object in the LDAP server, or an empty array []. This should be overwritten by configuration.rb. Note that subclassing does not cause concatenation of arrays to occurs.
# File lib/activeldap/base.rb, line 537 537: def Base.required_classes 538: [] 539: end
Return the schema object
# File lib/activeldap/base.rb, line 376 376: def Base.schema 377: @@schema 378: end
search
Wraps Ruby/LDAP connection.search to make it easier to search for specific data without cracking open Base.connection
# File lib/activeldap/base.rb, line 384 384: def Base.search(config={}) 385: Base.reconnect if Base.connection.nil? and Base.can_reconnect? 386: 387: config[:filter] = 'objectClass=*' unless config.has_key? :filter 388: config[:attrs] = [] unless config.has_key? :attrs 389: config[:scope] = LDAP::LDAP_SCOPE_SUBTREE unless config.has_key? :scope 390: config[:base] = base() unless config.has_key? :base 391: 392: values = [] 393: config[:attrs] = config[:attrs].to_a # just in case 394: 395: result = Base.connection() do |conn| 396: conn.search(config[:base], config[:scope], config[:filter], config[:attrs]) do |m| 397: res = {} 398: res['dn'] = [m.dn.dup] # For consistency with the below 399: m.attrs.each do |attr| 400: if config[:attrs].member? attr or config[:attrs].empty? 401: res[attr] = m.vals(attr).dup 402: end 403: end 404: values.push(res) 405: end 406: end 407: if result.nil? 408: # Do nothing on failure 409: 410: end 411: return values 412: end
Bind to LDAP with the given DN, but with no password. (anonymous!)
# File lib/activeldap/base.rb, line 1237 1237: def Base.do_anonymous_bind(bind_dn) 1238: @@logger.info "Attempting anonymous authentication" 1239: Base.connection(nil, false) do |conn| 1240: conn.bind() 1241: return true 1242: end 1243: return false 1244: end
Wrapper all bind activity
# File lib/activeldap/base.rb, line 1213 1213: def Base.do_bind() 1214: bind_dn = @@config[:bind_format] % [@@config[:user]] 1215: # Rough bind loop: 1216: # Attempt 1: SASL if available 1217: # Attempt 2: SIMPLE with credentials if password block 1218: # Attempt 3: SIMPLE ANONYMOUS if 1 and 2 fail (or pwblock returns '') 1219: if @@config[:try_sasl] and do_sasl_bind(bind_dn) 1220: @@logger.info('Bound SASL') 1221: elsif do_simple_bind(bind_dn) 1222: @@logger.info('Bound simple') 1223: elsif @@config[:allow_anonymous] and do_anonymous_bind(bind_dn) 1224: @@logger.info('Bound anonymous') 1225: else 1226: raise *LDAP::err2exception(@@conn.err) if @@conn.err != 0 1227: raise AuthenticationError, 'All authentication methods exhausted.' 1228: end 1229: 1230: return @@conn.bound? 1231: end
Performs the actually connection. This separate so that it may be called to refresh stale connections.
# File lib/activeldap/base.rb, line 1176 1176: def Base.do_connect() 1177: begin 1178: case @@config[:method] 1179: when :ssl 1180: @@conn = LDAP::SSLConn.new(@@config[:host], @@config[:port], false) 1181: when :tls 1182: @@conn = LDAP::SSLConn.new(@@config[:host], @@config[:port], true) 1183: when :plain 1184: @@conn = LDAP::Conn.new(@@config[:host], @@config[:port]) 1185: else 1186: raise ConfigurationError,"#{@@config[:method]} is not one of the available connect methods :ssl, :tls, or :plain" 1187: end 1188: rescue ConfigurationError => e 1189: # Pass through 1190: raise e 1191: rescue => e 1192: @@logger.error("Failed to connect using #{@@config[:method]}") 1193: raise e 1194: end 1195: 1196: # Enforce LDAPv3 1197: Base.connection(nil, false) do |conn| 1198: conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3) 1199: end 1200: 1201: # Authenticate 1202: do_bind 1203: 1204: # Retrieve the schema. We need this to automagically determine attributes 1205: exc = ConnectionError.new("Unable to retrieve schema from server (#{@@config[:method]})") 1206: Base.connection(exc, false) do |conn| 1207: @@schema = @@conn.schema2() if @@schema.nil? 1208: end 1209: 1210: end
Bind to LDAP with the given DN using any available SASL methods
# File lib/activeldap/base.rb, line 1292 1292: def Base.do_sasl_bind(bind_dn) 1293: # Get all SASL mechanisms 1294: # 1295: mechanisms = [] 1296: exc = ConnectionError.new('Root DSE query failed') 1297: Base.connection(exc, false) do |conn| 1298: mechanisms = conn.root_dse[0]['supportedSASLMechanisms'] 1299: end 1300: # Use GSSAPI if available 1301: # Currently only GSSAPI is supported with Ruby/LDAP from 1302: # http://caliban.org/files/redhat/RPMS/i386/ruby-ldap-0.8.2-4.i386.rpm 1303: # TODO: Investigate further SASL support 1304: if mechanisms.respond_to? :member? and mechanisms.member? 'GSSAPI' 1305: Base.connection(nil, false) do |conn| 1306: conn.sasl_quiet = @@config[:sasl_quiet] if @@config.has_key?(:sasl_quiet) 1307: conn.sasl_bind(bind_dn, 'GSSAPI') 1308: return true 1309: end 1310: end 1311: return false 1312: end
Bind to LDAP with the given DN and password
# File lib/activeldap/base.rb, line 1250 1250: def Base.do_simple_bind(bind_dn) 1251: # Bail if we have no password or password block 1252: if @@config[:password_block].nil? and @@config[:password].nil? 1253: return false 1254: end 1255: 1256: # TODO: Give a warning to reconnect users with password clearing 1257: # Get the passphrase for the first time, or anew if we aren't storing 1258: password = '' 1259: if not @@config[:password].nil? 1260: password = @@config[:password] 1261: elsif not @@config[:password_block].nil? 1262: unless @@config[:password_block].respond_to?(:call) 1263: @@logger.error('Skipping simple bind: ' + 1264: ':password_block not nil or Proc object. Ignoring.') 1265: return false 1266: end 1267: password = @@config[:password_block].call 1268: else 1269: @@logger.error('Skipping simple bind: ' + 1270: ':password_block and :password options are empty.') 1271: return false 1272: end 1273: 1274: # Store the password for quick reference later 1275: if @@config[:store_password] 1276: @@config[:password] = password 1277: elsif @@config[:store_password] == false 1278: @@config[:password] = nil 1279: end 1280: 1281: Base.connection(nil, false) do |conn| 1282: conn.bind(bind_dn, password) 1283: return true 1284: end 1285: return false 1286: end
find
Finds the first match for value where |value| is the value of some |field|, or the wildcard match. This is only useful for derived classes. usage: Subclass.find(:attribute => "cn", :value => "some*val", :objects => true)
Subclass.find('some*val')
# File lib/activeldap/base.rb, line 421 421: def Base.find(config='*') 422: Base.reconnect if Base.connection.nil? and Base.can_reconnect? 423: 424: if self.class == Class 425: klass = self.ancestors[0].to_s.split(':').last 426: real_klass = self.ancestors[0] 427: else 428: klass = self.class.to_s.split(':').last 429: real_klass = self.class 430: end 431: 432: # Allow a single string argument 433: attr = dnattr() 434: objects = @@config[:return_objects] 435: val = config 436: # Or a hash 437: if config.respond_to?(:has_key?) 438: attr = config[:attribute] || dnattr() 439: val = config[:value] || '*' 440: objects = config[:objects] unless config[:objects].nil? 441: end 442: 443: Base.connection(ConnectionError.new("Failed in #{self.class}#find(#{config.inspect})")) do |conn| 444: # Get some attributes 445: conn.search(base(), ldap_scope(), "(#{attr}=#{val})") do |m| 446: # Extract the dnattr value 447: dnval = m.dn.split(/,/)[0].split(/=/)[1] 448: 449: if objects 450: return real_klass.new(m) 451: else 452: return dnval 453: end 454: end 455: end 456: # If we're here, there were no results 457: return nil 458: end
Finds all matches for value where |value| is the value of some |field|, or the wildcard match. This is only useful for derived classes.
# File lib/activeldap/base.rb, line 465 465: def Base.find_all(config='*') 466: Base.reconnect if Base.connection.nil? and Base.can_reconnect? 467: 468: if self.class == Class 469: real_klass = self.ancestors[0] 470: else 471: real_klass = self.class 472: end 473: 474: # Allow a single string argument 475: val = config 476: attr = dnattr() 477: objects = @@config[:return_objects] 478: # Or a hash 479: if config.respond_to?(:has_key?) 480: val = config[:value] || '*' 481: attr = config[:attribute] || dnattr() 482: objects = config[:objects] unless config[:objects].nil? 483: end 484: 485: matches = [] 486: Base.connection(ConnectionError.new("Failed in #{self.class}#find_all(#{config.inspect})")) do |conn| 487: # Get some attributes 488: conn.search(base(), ldap_scope(), "(#{attr}=#{val})") do |m| 489: # Extract the dnattr value 490: dnval = m.dn.split(/,/)[0].split(/=/)[1] 491: 492: if objects 493: matches.push(real_klass.new(m)) 494: else 495: matches.push(dnval) 496: end 497: end 498: end 499: return matches 500: end
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.
# File lib/activeldap/base.rb, line 566 566: def initialize(val) 567: @exists = false 568: # Make sure we're connected 569: Base.reconnect if Base.connection.nil? and Base.can_reconnect? 570: 571: if val.class == LDAP::Entry 572: # Call import, which is basically initialize 573: # without accessing LDAP. 574: 575: import(val) 576: return 577: end 578: if val.class != String 579: raise TypeError, "Object key must be a String" 580: end 581: 582: @data = {} # where the r/w entry data is stored 583: @ldap_data = {} # original ldap entry data 584: @attr_methods = {} # list of valid method calls for attributes used for dereferencing 585: @last_oc = false # for use in other methods for "caching" 586: if dnattr().empty? 587: raise ConfigurationError, "dnattr() not set for this class: #{self.class}" 588: end 589: 590: # Extract dnattr if val looks like a dn 591: if val.match(/^#{dnattr()}=([^,=]+),/i) 592: val = $1 593: elsif val.match(/[=,]/) 594: @@logger.info "initialize: Changing val from '#{val}' to '' because it doesn't match the DN." 595: val = '' 596: end 597: 598: # Do a search - if it exists, pull all data and parse schema, if not, just set the hierarchical data 599: if val.class != String or val.empty? 600: raise TypeError, 'a dn attribute String must be supplied ' + 601: 'on initialization' 602: else 603: # Create what should be the authoritative DN 604: @dn = "#{dnattr()}=#{val},#{base()}" 605: 606: # Search for the existing entry 607: Base.connection(ConnectionError.new("Failed in #{self.class}#new(#{val.inspect})")) do |conn| 608: # Get some attributes 609: conn.search(base(), ldap_scope(), "(#{dnattr()}=#{val})") do |m| 610: @exists = true 611: # Save DN 612: @dn = m.dn 613: # Load up data into tmp 614: 615: m.attrs.each do |attr| 616: # Load with subtypes just like @data 617: 618: safe_attr, value = make_subtypes(attr, m.vals(attr).dup) 619: 620: # Add subtype to any existing values 621: if @ldap_data.has_key? safe_attr 622: value.each do |v| 623: @ldap_data[safe_attr].push(v) 624: end 625: else 626: @ldap_data[safe_attr] = value 627: end 628: end 629: end 630: end 631: end 632: 633: # Do the actual object setup work. 634: if @exists 635: # Make sure the server uses objectClass and not objectclass 636: unless @ldap_data.has_key?('objectClass') 637: real_objc = @ldap_data.grep(/^objectclass$/i) 638: if real_objc.size == 1 639: @ldap_data['objectClass'] = @ldap_data[real_objc] 640: @ldap_data.delete(real_objc) 641: else 642: raise AttributeEmpty, 'objectClass was not sent by LDAP server!' 643: end 644: end 645: 646: # Populate schema data 647: send(:apply_objectclass, @ldap_data['objectClass']) 648: 649: # Populate real data now that we have the schema with aliases 650: @ldap_data.each do |pair| 651: real_attr = @attr_methods[pair[0]] 652: 653: if real_attr.nil? 654: @@logger.error("Unable to resolve attribute value #{pair[0].inspect}. " + 655: "Unpredictable behavior likely!") 656: end 657: @data[real_attr] = pair[1].dup 658: 659: end 660: else 661: send(:apply_objectclass, required_classes()) 662: 663: # Setup dn attribute (later rdn too!) 664: real_dnattr = @attr_methods[dnattr()] 665: @data[real_dnattr] = val 666: 667: end 668: end
attributes
Return attribute methods so that a program can determine available attributes dynamically without schema awareness
# File lib/activeldap/base.rb, line 677 677: def attributes 678: 679: send(:apply_objectclass, @data['objectClass']) if @data['objectClass'] != @last_oc 680: return @attr_methods.keys.map {|x|x.downcase}.uniq 681: end
dn
Return the authoritative dn
# File lib/activeldap/base.rb, line 694 694: def dn 695: 696: return @dn.dup 697: end
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.
# File lib/activeldap/base.rb, line 941 941: def method_missing(name, *args) 942: 943: 944: # dynamically update the available attributes without requiring an 945: # explicit call. The cache 'last_oc' saves a lot of cpu time. 946: if @data['objectClass'] != @last_oc 947: 948: send(:apply_objectclass, @data['objectClass']) 949: end 950: key = name.to_s 951: case key 952: when /^(\S+)=$/ 953: real_key = $1 954: 955: if @attr_methods.has_key? real_key 956: raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" if args.size != 1 957: 958: return send(:attribute_method=, real_key, args[0]) 959: end 960: else 961: 962: if @attr_methods.has_key? key 963: raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" if args.size > 1 964: return attribute_method(key, *args) 965: end 966: end 967: raise NoMethodError, "undefined method `#{key}' for #{self}" 968: end
# File lib/activeldap/base.rb, line 972 972: def methods 973: return __methods + attributes() 974: end
validate
Basic validation:
Must call enforce_types() first before enforcement can be guaranteed
# File lib/activeldap/base.rb, line 705 705: def validate 706: 707: # Clean up attr values, etc 708: send(:enforce_types) 709: 710: # Validate objectclass settings 711: @data['objectClass'].each do |klass| 712: unless klass.class == String 713: raise TypeError, "Value in objectClass array is not a String. (#{klass.class}:#{klass.inspect})" 714: end 715: unless Base.schema.names("objectClasses").member? klass 716: raise ObjectClassError, "objectClass '#{klass}' unknown to LDAP server." 717: end 718: end 719: 720: # make sure this doesn't drop any of the required objectclasses 721: required_classes().each do |oc| 722: unless @data['objectClass'].member? oc.to_s 723: raise ObjectClassError, "'#{oc}' must be a defined objectClass for class '#{self.class}' as set in the ldap_mapping" 724: end 725: end 726: 727: # Make sure all MUST attributes have a value 728: @data['objectClass'].each do |objc| 729: @must.each do |req_attr| 730: # Downcase to ensure we catch schema problems 731: deref = @attr_methods[req_attr.downcase] 732: # Set default if it wasn't yet set. 733: @data[deref] = [] if @data[deref].nil? 734: # Check for missing requirements. 735: if @data[deref].empty? 736: raise AttributeEmpty, 737: "objectClass '#{objc}' requires attribute '#{Base.schema.attribute_aliases(req_attr).join(', ')}'" 738: end 739: end 740: end 741: 742: end
write
Write and validate this object into LDAP either adding or replacing attributes TODO: Binary data support TODO: Relative DN support
# File lib/activeldap/base.rb, line 763 763: def write 764: 765: # Validate against the objectClass requirements 766: validate 767: 768: # Put all changes into one change entry to ensure 769: # automatic rollback upon failure. 770: entry = [] 771: 772: 773: # Expand subtypes to real ldap_data entries 774: # We can't reuse @ldap_data because an exception would leave 775: # an object in an unknown state 776: 777: ldap_data = Marshal.load(Marshal.dump(@ldap_data)) 778: 779: 780: ldap_data.keys.each do |key| 781: ldap_data[key].each do |value| 782: if value.class == Hash 783: suffix, real_value = extract_subtypes(value) 784: if ldap_data.has_key? key + suffix 785: ldap_data[key + suffix].push(real_value) 786: else 787: ldap_data[key + suffix] = real_value 788: end 789: ldap_data[key].delete(value) 790: end 791: end 792: end 793: 794: 795: # Expand subtypes to real data entries, but leave @data alone 796: 797: data = Marshal.load(Marshal.dump(@data)) 798: 799: 800: 801: bad_attrs = @data.keys - (@must+@may) 802: bad_attrs.each do |removeme| 803: data.delete(removeme) 804: end 805: 806: 807: 808: 809: data.keys.each do |key| 810: data[key].each do |value| 811: if value.class == Hash 812: suffix, real_value = extract_subtypes(value) 813: if data.has_key? key + suffix 814: data[key + suffix].push(real_value) 815: else 816: data[key + suffix] = real_value 817: end 818: data[key].delete(value) 819: end 820: end 821: end 822: 823: 824: if @exists 825: # Cycle through all attrs to determine action 826: action = {} 827: 828: replaceable = [] 829: # Now that all the subtypes will be treated as unique attributes 830: # we can see what's changed and add anything that is brand-spankin' 831: # new. 832: 833: ldap_data.each do |pair| 834: suffix = '' 835: binary = 0 836: 837: name, *suffix_a = pair[0].split(/;/) 838: suffix = ';'+ suffix_a.join(';') if suffix_a.size > 0 839: name = @attr_methods[name] 840: name = pair[0].split(/;/)[0] if name.nil? # for objectClass, or removed vals 841: value = data[name+suffix] 842: # If it doesn't exist, don't freak out. 843: value = [] if value.nil? 844: 845: # Detect subtypes and account for them 846: binary = LDAP::LDAP_MOD_BVALUES if Base.schema.binary? name 847: 848: replaceable.push(name+suffix) 849: if pair[1] != value 850: # Create mod entries 851: if not value.empty? 852: # Ditched delete then replace because attribs with no equality match rules 853: # will fails 854: 855: entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, value)) 856: else 857: # Since some types do not have equality matching rules, delete doesn't work 858: # Replacing with nothing is equivalent. 859: 860: entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, [])) 861: end 862: end 863: end 864: 865: 866: data.each do |pair| 867: suffix = '' 868: binary = 0 869: 870: name, *suffix_a = pair[0].split(/;/) 871: suffix = ';' + suffix_a.join(';') if suffix_a.size > 0 872: name = @attr_methods[name] 873: name = pair[0].split(/;/)[0] if name.nil? # for obj class or removed vals 874: value = pair[1] 875: # Make sure to change this to an Array if there was mistake earlier. 876: value = [] if value.nil? 877: 878: if not replaceable.member? name+suffix 879: # Detect subtypes and account for them 880: binary = LDAP::LDAP_MOD_BVALUES if Base.schema.binary? name 881: 882: # REPLACE will function like ADD, but doesn't hit EQUALITY problems 883: # TODO: Added equality(attr) to Schema2 884: entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, value)) unless value.empty? 885: end 886: end 887: 888: Base.connection(WriteError.new( 889: "Failed to modify: '#{entry}'")) do |conn| 890: 891: conn.modify(@dn, entry) 892: 893: end 894: else # add everything! 895: 896: 897: entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD, @attr_methods[dnattr()], 898: data[@attr_methods[dnattr()]])) 899: 900: entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD, 'objectClass', 901: data[@attr_methods['objectClass']])) 902: data.each do |pair| 903: if pair[1].size > 0 and pair[0] != 'objectClass' and pair[0] != @attr_methods[dnattr()] 904: # Detect subtypes and account for them 905: if Base.schema.binary? pair[0].split(/;/)[0] 906: binary = LDAP::LDAP_MOD_BVALUES 907: else 908: binary = 0 909: end 910: 911: entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD|binary, pair[0], pair[1])) 912: end 913: end 914: Base.connection(WriteError.new( 915: "Failed to add: '#{entry}'")) do |conn| 916: 917: conn.add(@dn, entry) 918: 919: @exists = true 920: end 921: end 922: 923: @ldap_data = Marshal.load(Marshal.dump(data)) 924: # Delete items disallowed by objectclasses. 925: # They should have been removed from ldap. 926: 927: bad_attrs.each do |removeme| 928: @ldap_data.delete(removeme) 929: end 930: 931: 932: end
objectClass= special case for updating appropriately This updates the objectClass entry in @data. It also updating all required and allowed attributes while removing defined attributes that are no longer valid given the new objectclasses.
# File lib/activeldap/base.rb, line 1046 1046: def apply_objectclass(val) 1047: 1048: new_oc = val 1049: new_oc = [val] if new_oc.class != Array 1050: if defined?(@last_oc).nil? 1051: @last_oc = false 1052: end 1053: return new_oc if @last_oc == new_oc 1054: 1055: # Store for caching purposes 1056: @last_oc = new_oc.dup 1057: 1058: # Set the actual objectClass data 1059: define_attribute_methods('objectClass') 1060: @data['objectClass'] = new_oc.uniq 1061: 1062: # Build |data| from schema 1063: # clear attr_method mapping first 1064: @attr_methods = {} 1065: @must = [] 1066: @may = [] 1067: new_oc.each do |objc| 1068: # get all attributes for the class 1069: attributes = Base.schema.class_attributes(objc.to_s) 1070: @must += attributes[:must] 1071: @may += attributes[:may] 1072: end 1073: @must.uniq! 1074: @may.uniq! 1075: (@must+@may).each do |attr| 1076: # Update attr_method with appropriate 1077: define_attribute_methods(attr) 1078: end 1079: end
Returns the array form of a value, or not an array if false is passed in.
# File lib/activeldap/base.rb, line 1426 1426: def array_of(value, to_a = true) 1427: 1428: if to_a 1429: case value.class.to_s 1430: when 'Array' 1431: return value 1432: when 'Hash' 1433: return [value] 1434: else 1435: return [value.to_s] 1436: end 1437: else 1438: case value.class.to_s 1439: when 'Array' 1440: return nil if value.size == 0 1441: return value[0] if value.size == 1 1442: return value 1443: when 'Hash' 1444: return value 1445: else 1446: return value.to_s 1447: end 1448: end 1449: end
Enforce typing: Hashes are for subtypes Arrays are for multiple entries
# File lib/activeldap/base.rb, line 1086 1086: def attribute_input_handler(attr, value) 1087: 1088: if attr.nil? 1089: raise RuntimeError, 'The first argument, attr, must not be nil. Please report this as a bug!' 1090: end 1091: binary = Base.schema.binary_required? attr 1092: single = Base.schema.single_value? attr 1093: case value.class.to_s 1094: when 'Array' 1095: if single and value.size > 1 1096: raise TypeError, "Attribute #{attr} can only have a single value" 1097: end 1098: value.map! do |entry| 1099: if entry.class != Hash 1100: 1101: entry = entry.to_s 1102: end 1103: entry = attribute_input_handler(attr, entry)[0] 1104: end 1105: when 'Hash' 1106: if value.keys.size > 1 1107: raise TypeError, "Hashes must have one key-value pair only." 1108: end 1109: unless value.keys[0].match(/^(lang-[a-z][a-z]*)|(binary)$/) 1110: @@logger.warn("unknown subtype did not match lang-* or binary: #{value.keys[0]}") 1111: end 1112: # Contents MUST be a String or an Array 1113: if value.keys[0] != 'binary' and binary 1114: suffix, real_value = extract_subtypes(value) 1115: value = make_subtypes(name + suffix + ';binary', real_value) 1116: end 1117: value = [value] 1118: when 'String' 1119: if binary 1120: value = {'binary' => value} 1121: end 1122: return [value] 1123: else 1124: value = [value.to_s] 1125: end 1126: return value 1127: end
Return the value of the attribute called by method_missing?
# File lib/activeldap/base.rb, line 1354 1354: def attribute_method(method, not_array = false) 1355: 1356: attr = @attr_methods[method] 1357: 1358: # Set the default value to empty if attr is not set. 1359: @data[attr] = [] if @data[attr].nil? 1360: 1361: # Return a copy of the stored data 1362: return array_of(@data[attr].dup, false) if not_array 1363: return @data[attr] 1364: end
Set the value of the attribute called by method_missing?
# File lib/activeldap/base.rb, line 1370 1370: def attribute_method=(method, value) 1371: 1372: # Get the attr and clean up the input 1373: attr = @attr_methods[method] 1374: 1375: 1376: # Check if it is the DN attribute 1377: if dnattr() == attr 1378: raise AttributeAssignmentError, 'cannot modify the DN attribute value' 1379: end 1380: 1381: # Enforce LDAP-pleasing values 1382: 1383: real_value = value 1384: # Squash empty values 1385: if value.class == Array 1386: real_value = value.collect {|c| if c == ''; []; else c; end }.flatten 1387: end 1388: real_value = [] if real_value.nil? 1389: real_value = [] if real_value == '' 1390: real_value = [real_value] if real_value.class == String 1391: real_value = [real_value.to_s] if real_value.class == Fixnum 1392: # NOTE: Hashes are allowed for subtyping. 1393: 1394: # Assign the value 1395: @data[attr] = real_value 1396: 1397: # Return the passed in value 1398: 1399: return @data[attr] 1400: end
base
Returns the value of self.class.base This is just syntactic sugar
# File lib/activeldap/base.rb, line 1319 1319: def base 1320: 1321: self.class.base 1322: end
Make a method entry for every alias of a valid attribute and map it onto the first attribute passed in.
# File lib/activeldap/base.rb, line 1407 1407: def define_attribute_methods(attr) 1408: 1409: if @attr_methods.has_key? attr 1410: return 1411: end 1412: aliases = Base.schema.attribute_aliases(attr) 1413: aliases.each do |ali| 1414: 1415: @attr_methods[ali] = attr 1416: 1417: @attr_methods[ali.downcase] = attr 1418: end 1419: 1420: end
dnattr
Returns the value of self.class.dnattr This is just syntactic sugar
# File lib/activeldap/base.rb, line 1346 1346: def dnattr 1347: 1348: self.class.dnattr 1349: end
enforce_types applies your changes without attempting to write to LDAP. This means that if you set userCertificate to somebinary value, it will wrap it up correctly.
# File lib/activeldap/base.rb, line 1028 1028: def enforce_types 1029: 1030: send(:apply_objectclass, @data['objectClass']) if @data['objectClass'] != @last_oc 1031: # Enforce attribute value formatting 1032: @data.keys.each do |key| 1033: @data[key] = attribute_input_handler(key, @data[key]) 1034: end 1035: 1036: return true 1037: end
Extracts all of the subtypes from a given set of nested hashes and returns the attribute suffix and the final true value
# File lib/activeldap/base.rb, line 1156 1156: def extract_subtypes(value) 1157: 1158: subtype = '' 1159: ret_val = value 1160: if value.class == Hash 1161: subtype = ';' + value.keys[0] 1162: ret_val = value[value.keys[0]] 1163: subsubtype = '' 1164: if ret_val.class == Hash 1165: subsubtype, ret_val = extract_subtypes(ret_val) 1166: end 1167: subtype += subsubtype 1168: end 1169: ret_val = [ret_val] unless ret_val.class == Array 1170: return subtype, ret_val 1171: end
import(LDAP::Entry)
Overwrites an existing entry (usually called by new) with the data given in the data given in LDAP::Entry.
# File lib/activeldap/base.rb, line 984 984: def import(entry=nil) 985: 986: if entry.class != LDAP::Entry 987: raise TypeError, "argument must be a LDAP::Entry" 988: end 989: 990: @data = {} # where the r/w entry data is stored 991: @ldap_data = {} # original ldap entry data 992: @attr_methods = {} # list of valid method calls for attributes used for dereferencing 993: 994: # Get some attributes 995: @dn = entry.dn 996: entry.attrs.each do |attr| 997: # Load with subtypes just like @data 998: 999: safe_attr, value = make_subtypes(attr, entry.vals(attr).dup) 1000: 1001: # Add subtype to any existing values 1002: if @ldap_data.has_key? safe_attr 1003: value.each do |v| 1004: @ldap_data[safe_attr].push(v) 1005: end 1006: else 1007: @ldap_data[safe_attr] = value 1008: end 1009: end 1010: # Assume if we are importing it that it exists 1011: @exists = true 1012: # Populate schema data 1013: send(:apply_objectclass, @ldap_data['objectClass']) 1014: 1015: # Populate real data now that we have the schema with aliases 1016: @ldap_data.each do |pair| 1017: real_attr = @attr_methods[pair[0]] 1018: 1019: @data[real_attr] = pair[1].dup 1020: 1021: end 1022: end
Returns the value of self.class.ldap_scope This is just syntactic sugar
# File lib/activeldap/base.rb, line 1328 1328: def ldap_scope 1329: 1330: self.class.ldap_scope 1331: end
Makes the Hashized value from the full attributename e.g. userCertificate;binary => "some_bin"
becomes userCertificate => {"binary" => "some_bin"}
# File lib/activeldap/base.rb, line 1134 1134: def make_subtypes(attr, value) 1135: 1136: return [attr, value] unless attr.match(/;/) 1137: 1138: ret_attr, *subtypes = attr.split(/;/) 1139: return [ret_attr, [make_subtypes_helper(subtypes, value)]] 1140: end
This is a recursive function for building nested hashed from multi-subtyped values
# File lib/activeldap/base.rb, line 1146 1146: def make_subtypes_helper(subtypes, value) 1147: 1148: return value if subtypes.size == 0 1149: return {subtypes[0] => make_subtypes_helper(subtypes[1..-1], value)} 1150: end