Class ActiveLDAP::Base
In: lib/activeldap/base.rb
Parent: Object
RuntimeError DeleteError AttributeAssignmentError ConnectionError AuthenticationError TimeoutError ConfigurationError ObjectClassError WriteError AttributeEmpty Base lib/activeldap/base.rb ClassMethods Associations Configuration ActiveLDAP Module: ActiveLDAP

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.

Methods

External Aliases

methods -> __methods
  Add available attributes to the methods

Attributes

logger  [RW] 
may  [R]  Parsed schema structures
must  [R]  Parsed schema structures

Public Class methods

On connect, this is overriden by the :base argument Make the return value the string that is your LDAP base

[Source]

    # File lib/activeldap/configuration.rb, line 48
48:     def Base.base
49:       'dc=localdomain'
50:     end

Base.base

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.

[Source]

     # 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.

[Source]

     # 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.

[Source]

     # 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

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

[Source]

     # 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.

[Source]

     # 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

[Source]

     # 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.

[Source]

     # 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

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.

[Source]

     # 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

[Source]

    # File lib/activeldap/configuration.rb, line 42
42:     def Base.ldap_scope
43:       LDAP::LDAP_SCOPE_ONELEVEL
44:     end

Base.ldap_scope

This method when included into Base provides an inheritable, overwritable configuration setting

This value should be the default LDAP scope behavior desired.

[Source]

     # File lib/activeldap/base.rb, line 548
548:     def Base.ldap_scope
549:       LDAP::LDAP_SCOPE_ONELEVEL
550:     end

Attempts to reconnect up to the number of times allowed If forced, try once then fail with ConnectionError if not connected.

[Source]

     # 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 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 [].

[Source]

    # File lib/activeldap/configuration.rb, line 55
55:     def Base.required_classes
56:       ['top']
57:     end

Base.required_classes

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.

[Source]

     # File lib/activeldap/base.rb, line 537
537:     def Base.required_classes
538:       []
539:     end

Return the schema object

[Source]

     # 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

[Source]

     # 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

Private Class methods

Base.do_anonymous_bind

Bind to LDAP with the given DN, but with no password. (anonymous!)

[Source]

      # 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

[Source]

      # 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.

[Source]

      # 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

Base.do_sasl_bind

Bind to LDAP with the given DN using any available SASL methods

[Source]

      # 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

Base.do_simple_bind

Bind to LDAP with the given DN and password

[Source]

      # 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')

[Source]

     # 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

find_all

Finds all matches for value where |value| is the value of some |field|, or the wildcard match. This is only useful for derived classes.

[Source]

     # 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.

[Source]

     # 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

Public Instance methods

attributes

Return attribute methods so that a program can determine available attributes dynamically without schema awareness

[Source]

     # 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

delete

Delete this entry from LDAP

[Source]

     # File lib/activeldap/base.rb, line 747
747:     def delete
748:       
749:       Base.connection(DeleteError.new(
750:                         "Failed to delete LDAP entry: '#{@dn}'")) do |conn|
751:         conn.delete(@dn)
752:         @exists = false
753:       end
754:     end

dn

Return the authoritative dn

[Source]

     # File lib/activeldap/base.rb, line 694
694:     def dn
695:       
696:       return @dn.dup
697:     end

exists?

Return whether the entry exists in LDAP or not

[Source]

     # File lib/activeldap/base.rb, line 686
686:     def exists?
687:       
688:       return @exists
689:     end

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.

[Source]

     # 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

[Source]

     # File lib/activeldap/base.rb, line 972
972:     def methods
973:       return __methods + attributes()
974:     end

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

[Source]

     # 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

[Source]

     # 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

Private Instance methods

apply_objectclass

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.

[Source]

      # 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

array_of

Returns the array form of a value, or not an array if false is passed in.

[Source]

      # 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

[Source]

      # 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

attribute_method

Return the value of the attribute called by method_missing?

[Source]

      # 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

attribute_method=

Set the value of the attribute called by method_missing?

[Source]

      # 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

[Source]

      # File lib/activeldap/base.rb, line 1319
1319:      def base
1320:        
1321:        self.class.base
1322:      end

define_attribute_methods

Make a method entry for every alias of a valid attribute and map it onto the first attribute passed in.

[Source]

      # 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

[Source]

      # File lib/activeldap/base.rb, line 1346
1346:      def dnattr
1347:        
1348:        self.class.dnattr
1349:      end

enforce_types

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.

[Source]

      # 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

extract_subtypes

Extracts all of the subtypes from a given set of nested hashes and returns the attribute suffix and the final true value

[Source]

      # 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.

[Source]

      # 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

ldap_scope

Returns the value of self.class.ldap_scope This is just syntactic sugar

[Source]

      # File lib/activeldap/base.rb, line 1328
1328:      def ldap_scope
1329:        
1330:        self.class.ldap_scope
1331:      end

make_subtypes

Makes the Hashized value from the full attributename e.g. userCertificate;binary => "some_bin"

     becomes userCertificate => {"binary" => "some_bin"}

[Source]

      # 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

make_subtypes_helper

This is a recursive function for building nested hashed from multi-subtyped values

[Source]

      # 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

required_classes

Returns the value of self.class.required_classes This is just syntactic sugar

[Source]

      # File lib/activeldap/base.rb, line 1337
1337:      def required_classes
1338:        
1339:        self.class.required_classes
1340:      end

[Validate]