Class: ActiveDirectory::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/bsb_active_directory/base.rb

Overview

Base class for all Ruby/ActiveDirectory Entry Objects (like User and Group)

Direct Known Subclasses

Computer, Group, User

Constant Summary collapse

NIL_FILTER =

A Net::LDAP::Filter object that doesn't do any filtering (outside of check that the CN attribute is present. This is used internally for specifying a 'no filter' condition for methods that require a filter object.

Net::LDAP::Filter.pres('cn')
@@ldap =
nil
@@ldap_connected =
false
@@caching =
false
@@cache =
{}

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(attributes = {}) ⇒ Base

FIXME: Need to document the Base::new


535
536
537
538
539
540
541
542
543
# File 'lib/bsb_active_directory/base.rb', line 535

def initialize(attributes = {}) # :nodoc:
  if attributes.is_a? Net::LDAP::Entry
    @entry = attributes
    @attributes = {}
  else
    @entry = nil
    @attributes = attributes
  end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, args = []) ⇒ Object

:nodoc:


633
634
635
636
637
638
639
640
641
642
643
# File 'lib/bsb_active_directory/base.rb', line 633

def method_missing(name, args = []) # :nodoc:
  name = name.to_s.downcase

  return set_attr(name.chop, args) if name[-1] == '='

  if valid_attribute? name.to_sym
    get_attr(name)
  else
    super
  end
end

Class Method Details

.cache?Boolean

Check to see if result caching is enabled

Returns:

  • (Boolean)

120
121
122
# File 'lib/bsb_active_directory/base.rb', line 120

def self.cache?
  @@caching
end

.class_nameObject

Pull the class we're in This isn't quite right, as extending the object does funny things to how we lookup objects


549
550
551
# File 'lib/bsb_active_directory/base.rb', line 549

def self.class_name
  @klass ||= (name.include?('::') ? name[/.*::(.*)/, 1] : name)
end

.clear_cacheObject

Clears the cache


126
127
128
# File 'lib/bsb_active_directory/base.rb', line 126

def self.clear_cache
  @@cache = {}
end

.connected?Boolean

Check to see if we are connected to the LDAP server This method will try to connect, if we haven't already

Returns:

  • (Boolean)

111
112
113
114
115
116
# File 'lib/bsb_active_directory/base.rb', line 111

def self.connected?
  @@ldap_connected ||= @@ldap.bind unless @@ldap.nil?
  @@ldap_connected
rescue Net::LDAP::LdapError
  false
end

.create(dn, attributes) ⇒ Object

Create a new entry in the Active Record store.

dn is the Distinguished Name for the new entry. This must be a unique identifier, and can be passed as either a Container or a plain string.

attributes is a symbol-keyed hash of attribute_name => value pairs.


461
462
463
464
465
466
467
468
469
470
471
472
473
474
# File 'lib/bsb_active_directory/base.rb', line 461

def self.create(dn, attributes)
  return nil if dn.nil? || attributes.nil?
  begin
    attributes.merge!(required_attributes)
    if @@ldap.add(dn: dn.to_s, attributes: attributes)
      ldap_obj = @@ldap.search(base: dn.to_s)
      return new(ldap_obj[0])
    else
      return nil
    end
  rescue
    return nil
  end
end

.decode_field(name, value) ⇒ Object

:nodoc:


565
566
567
568
569
570
571
# File 'lib/bsb_active_directory/base.rb', line 565

def self.decode_field(name, value) # :nodoc:
  type = get_field_type name
  if !type.nil? && ::ActiveDirectory::FieldType.const_defined?(type)
    return ::ActiveDirectory::FieldType.const_get(type).decode(value)
  end
  value
end

.disable_cacheObject

Disable caching


140
141
142
# File 'lib/bsb_active_directory/base.rb', line 140

def self.disable_cache
  @@caching = false
end

.enable_cacheObject

Enable caching for queries against the DN only This is to prevent membership lookups from hitting the AD unnecessarilly


134
135
136
# File 'lib/bsb_active_directory/base.rb', line 134

def self.enable_cache
  @@caching = true
end

.encode_field(name, value) ⇒ Object

:nodoc:


573
574
575
576
577
578
579
# File 'lib/bsb_active_directory/base.rb', line 573

def self.encode_field(name, value) # :nodoc:
  type = get_field_type name
  if !type.nil? && ::ActiveDirectory::FieldType.const_defined?(type)
    return ::ActiveDirectory::FieldType.const_get(type).encode(value)
  end
  value
end

.errorObject


90
91
92
# File 'lib/bsb_active_directory/base.rb', line 90

def self.error
  "#{@@ldap.get_operation_result.code}: #{@@ldap.get_operation_result.message}"
end

.error?Boolean

Check to see if the last query produced an error Note: Invalid username/password combinations will not produce errors

Returns:

  • (Boolean)

104
105
106
# File 'lib/bsb_active_directory/base.rb', line 104

def self.error?
  @@ldap.nil? ? false : @@ldap.get_operation_result.code != 0
end

.error_codeObject

Return the last errorcode that ldap generated


96
97
98
# File 'lib/bsb_active_directory/base.rb', line 96

def self.error_code
  @@ldap.get_operation_result.code
end

.exists?(filter_as_hash) ⇒ Boolean

Check to see if any entries matching the passed criteria exists.

Filters should be passed as a hash of attribute_name => expected_value, like:

User.exists?(
  :sn => 'Hunt',
  :givenName => 'James'
)

which will return true if one or more User entries have an sn (surname) of exactly 'Hunt' and a givenName (first name) of exactly 'James'.

Partial attribute matches are available. For instance,

Group.exists?(
  :description => 'OldGroup_*'
)

would return true if there are any Group objects in Active Directory whose descriptions start with OldGroup_, like OldGroup_Reporting, or OldGroup_Admins.

Note that the * wildcard matches zero or more characters, so the above query would also return true if a group named 'OldGroup_' exists.

Returns:

  • (Boolean)

181
182
183
184
# File 'lib/bsb_active_directory/base.rb', line 181

def self.exists?(filter_as_hash)
  criteria = make_filter_from_hash(filter_as_hash) & filter
  !@@ldap.search(filter: criteria).empty?
end

.filterObject

:nodoc:


144
145
146
# File 'lib/bsb_active_directory/base.rb', line 144

def self.filter # :nodoc:
  NIL_FILTER
end

.find(*args) ⇒ Object

Performs a search on the Active Directory store, with similar syntax to the Rails ActiveRecord#find method.

The first argument passed should be either :first or :all, to indicate that we want only one (:first) or all (:all) results back from the resultant set.

The second argument should be a hash of attribute_name => expected_value pairs.

User.find(:all, :sn => 'Hunt')

would find all of the User objects in Active Directory that have a surname of exactly 'Hunt'. As with the Base.exists? method, partial searches are allowed.

This method always returns an array if the caller specifies :all for the search e (first argument). If no results are found, the array will be empty.

If you call find(:first, …), you will either get an object (a User or a Group) back, or nil, if there were no entries matching your filter.


259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/bsb_active_directory/base.rb', line 259

def self.find(*args)
  return false unless connected?

  options = {
    filter: args[1].nil? ? NIL_FILTER : args[1],
    in: args[1].nil? ? '' : (args[1][:in] || '')
  }
  options[:filter].delete(:in)

  cached_results = find_cached_results(args[1])
  return cached_results if cached_results || cached_results.nil?

  options[:in] = [options[:in].to_s, @@settings[:base]].delete_if(&:empty?).join(',')

  if options[:filter].is_a? Hash
    options[:filter] = make_filter_from_hash(options[:filter])
  end

  options[:filter] = options[:filter] & filter unless filter == NIL_FILTER

  if args.first == :all
    find_all(options)
  elsif args.first == :first
    find_first(options)
  else
    raise ArgumentError,
          'Invalid specifier (not :all, and not :first) passed to find()'
  end
end

.find_all(options) ⇒ Object


322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/bsb_active_directory/base.rb', line 322

def self.find_all(options)
  results = []
  ldap_objs = @@ldap.search(filter: options[:filter], base: options[:in]) || []

  ldap_objs.each do |entry|
    ad_obj = new(entry)
    @@cache[entry.dn] = ad_obj unless ad_obj.instance_of? Base
    results << ad_obj
  end

  results
end

.find_cached_results(filters) ⇒ Object

Searches the cache and returns the result Returns false on failure, nil on wrong object type


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
318
319
320
# File 'lib/bsb_active_directory/base.rb', line 293

def self.find_cached_results(filters)
  return false unless cache?

  # Check to see if we're only looking for :distinguishedname
  return false unless filters.is_a?(Hash) && filters.keys == [:distinguishedname]

  # Find keys we're looking up
  dns = filters[:distinguishedname]

  if dns.is_a? Array
    result = []

    dns.each do |dn|
      entry = @@cache[dn]

      # If the object isn't in the cache just run the query
      return false if entry.nil?

      # Only permit objects of the type we're looking for
      result << entry if entry.is_a? self
    end

    return result
  else
    return false unless @@cache.key? dns
    return @@cache[dns] if @@cache[dns].is_a? self
  end
end

.find_first(options) ⇒ Object


335
336
337
338
339
340
341
342
# File 'lib/bsb_active_directory/base.rb', line 335

def self.find_first(options)
  ldap_result = @@ldap.search(filter: options[:filter], base: options[:in])
  return nil if ldap_result.empty?

  ad_obj = new(ldap_result[0])
  @@cache[ad_obj.dn] = ad_obj unless ad_obj.instance_of? Base
  ad_obj
end

.from_dn(dn) ⇒ Object


225
226
227
228
229
230
231
232
# File 'lib/bsb_active_directory/base.rb', line 225

def self.from_dn(dn)
  ldap_result = @@ldap.search(filter: '(objectClass=*)', base: dn)
  return nil unless ldap_result

  ad_obj = new(ldap_result[0])
  @@cache[ad_obj.dn] = ad_obj unless ad_obj.instance_of? Base
  ad_obj
end

.get_field_type(name) ⇒ Object

Grabs the field type depending on the class it is called from Takes the field name as a parameter


556
557
558
559
560
561
# File 'lib/bsb_active_directory/base.rb', line 556

def self.get_field_type(name)
  # Extract class name
  throw 'Invalid field name' if name.nil?
  type = ::ActiveDirectory.special_fields[class_name.to_sym][name.to_s.downcase.to_sym]
  type.to_s unless type.nil?
end

.make_filter(key, value) ⇒ Object

Makes a single filter from a given key and value It will try to encode an array if there is a process for it Otherwise, it will treat it as an or condition


198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/bsb_active_directory/base.rb', line 198

def self.make_filter(key, value)
  # Join arrays using OR condition
  if value.is_a? Array
    filter = ~NIL_FILTER

    value.each do |v|
      filter |= Net::LDAP::Filter.eq(key, encode_field(key, v).to_s)
    end
  else
    filter = Net::LDAP::Filter.eq(key, encode_field(key, value).to_s)
  end

  filter
end

.make_filter_from_hash(hash) ⇒ Object

:nodoc:


213
214
215
216
217
218
219
220
221
222
223
# File 'lib/bsb_active_directory/base.rb', line 213

def self.make_filter_from_hash(hash) # :nodoc:
  return NIL_FILTER if hash.nil? || hash.empty?

  filter = NIL_FILTER

  hash.each do |key, value|
    filter &= make_filter(key, value)
  end

  filter
end

.method_missing(name, *args) ⇒ Object

:nodoc:


344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/bsb_active_directory/base.rb', line 344

def self.method_missing(name, *args) # :nodoc:
  name = name.to_s
  if name[0, 5] == 'find_'
    find_spec, attribute_spec = parse_finder_spec(name)
    raise ArgumentError, "find: Wrong number of arguments (#{args.size} for #{attribute_spec.size})" unless args.size == attribute_spec.size
    filters = {}
    [attribute_spec, args].transpose.each { |pr| filters[pr[0]] = pr[1] }
    find(find_spec, filter: filters)
  else
    super name.to_sym, args
  end
end

.parse_finder_spec(method_name) ⇒ Object

:nodoc:


357
358
359
360
361
362
363
364
365
366
367
# File 'lib/bsb_active_directory/base.rb', line 357

def self.parse_finder_spec(method_name) # :nodoc:
  # FIXME: This is a prime candidate for a
  # first-class object, FinderSpec

  method_name = method_name.gsub(/^find_/, '').gsub(/^by_/, 'first_by_')
  find_spec, attribute_spec = *method_name.split('_by_')
  find_spec = find_spec.to_sym
  attribute_spec = attribute_spec.split('_and_').collect(&:to_sym)

  [find_spec, attribute_spec]
end

.required_attributesObject

:nodoc:


148
149
150
# File 'lib/bsb_active_directory/base.rb', line 148

def self.required_attributes # :nodoc:
  {}
end

.setup(settings) ⇒ Object

Configures the connection for the Ruby/ActiveDirectory library.

For example:

ActiveDirectory::Base.setup(
  :host => 'domain_controller1.example.org',
  :port => 389,
  :base => 'dc=example,dc=org',
  :auth => {

:method => :simple,

    :username => '[email protected]',
    :password => 'querying_users_password'
  }
)

This will configure Ruby/ActiveDirectory to connect to the domain controller at domain_controller1.example.org, using port 389. The domain's base LDAP dn is expected to be 'dc=example,dc=org', and Ruby/ActiveDirectory will try to bind as the [email protected] user, using the supplied password.

Currently, there can be only one active connection per execution context.

For more advanced options, refer to the Net::LDAP.new documentation.


84
85
86
87
88
# File 'lib/bsb_active_directory/base.rb', line 84

def self.setup(settings)
  @@settings = settings
  @@ldap_connected = false
  @@ldap = Net::LDAP.new(settings)
end

Instance Method Details

#==(other) ⇒ Object

:nodoc:


369
370
371
372
# File 'lib/bsb_active_directory/base.rb', line 369

def ==(other) # :nodoc:
  return false if other.nil?
  other[:objectguid] == get_attr(:objectguid)
end

#changed?Boolean

Whether or not the entry has local changes that have not yet been replicated to the Active Directory server via a call to Base#save

Returns:

  • (Boolean)

190
191
192
# File 'lib/bsb_active_directory/base.rb', line 190

def changed?
  !@attributes.empty?
end

#destroyObject

Deletes the entry from the Active Record store and returns true if the operation was successfully.


480
481
482
483
484
485
486
487
488
489
490
# File 'lib/bsb_active_directory/base.rb', line 480

def destroy
  return false if new_record?

  if @@ldap.delete(dn: distinguishedName)
    @entry = nil
    @attributes = {}
    return true
  else
    return false
  end
end

#get_attr(name) ⇒ Object Also known as: []


585
586
587
588
589
590
591
592
593
594
595
596
597
# File 'lib/bsb_active_directory/base.rb', line 585

def get_attr(name)
  name = name.to_s.downcase

  return self.class.decode_field(name, @attributes[name.to_sym]) if @attributes.key?(name.to_sym)

  if @entry.attribute_names.include? name.to_sym
    value = @entry[name.to_sym]
    value = value.first if value.is_a?(Array) && value.size == 1
    value = value.to_s if value.nil? || value.size == 1
    value = nil.to_s if value.empty?
    return self.class.decode_field(name, value)
  end
end

#move(new_rdn) ⇒ Object

This method may one day provide the ability to move entries from container to container. Currently, it does nothing, as we are waiting on the Net::LDAP folks to either document the Net::LDAP#modrdn method, or provide a similar method for moving / renaming LDAP entries.


512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
# File 'lib/bsb_active_directory/base.rb', line 512

def move(new_rdn)
  return false if new_record?
  puts "Moving #{distinguishedName} to RDN: #{new_rdn}"

  settings = @@settings.dup
  settings[:port] = 636
  settings[:encryption] = { method: :simple_tls }

  ldap = Net::LDAP.new(settings)

  if ldap.rename(
    olddn: distinguishedName,
    newrdn: new_rdn,
    delete_attributes: false
  )
    return true
  else
    puts Base.error
    return false
  end
end

#new_record?Boolean

Returns true if this entry does not yet exist in Active Directory.

Returns:

  • (Boolean)

377
378
379
# File 'lib/bsb_active_directory/base.rb', line 377

def new_record?
  @entry.nil?
end

#reloadObject

Refreshes the attributes for the entry with updated data from the domain controller.


385
386
387
388
389
390
# File 'lib/bsb_active_directory/base.rb', line 385

def reload
  return false if new_record?

  @entry = @@ldap.search(filter: Net::LDAP::Filter.eq('distinguishedName', distinguishedName))[0]
  !@entry.nil?
end

#saveObject

Saves any pending changes to the entry by updating the remote entry.


496
497
498
499
500
501
502
503
# File 'lib/bsb_active_directory/base.rb', line 496

def save
  if update_attributes(@attributes)
    @attributes = {}
    true
  else
    false
  end
end

#set_attr(name, value) ⇒ Object Also known as: []=


599
600
601
# File 'lib/bsb_active_directory/base.rb', line 599

def set_attr(name, value)
  @attributes[name.to_sym] = self.class.encode_field(name, value)
end

#sidObject


614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
# File 'lib/bsb_active_directory/base.rb', line 614

def sid
  unless @sid
    raise 'Object has no sid' unless valid_attribute? :objectsid
    # SID is stored as a binary in the directory
    # however, Net::LDAP returns an hex string
    #
    # As per [1], there seems to be 2 ways to get back binary data.
    #
    # [str].pack("H*")
    # str.gsub(/../) { |b| b.hex.chr }
    #
    # [1] :
    # http://stackoverflow.com/questions/22957688/convert-string-with-hex-ascii-codes-to-characters
    #
    @sid = SID.read([get_attr(:objectsid)].pack('H*'))
  end
  @sid.to_s
end

#to_aryObject

Weird fluke with flattening, probably because of above attribute


612
# File 'lib/bsb_active_directory/base.rb', line 612

def to_ary; end

#update_attribute(name, value) ⇒ Object

Updates a single attribute (name) with one or more values (value), by immediately contacting the Active Directory server and initiating the update remotely.

Entries are always reloaded (via Base.reload) after calling this method.


400
401
402
# File 'lib/bsb_active_directory/base.rb', line 400

def update_attribute(name, value)
  update_attributes(name.to_s => value)
end

#update_attributes(attributes_to_update) ⇒ Object

Updates multiple attributes, like ActiveRecord#update_attributes. The updates are immediately sent to the server for processing, and the entry is reloaded after the update (if all went well).


409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
# File 'lib/bsb_active_directory/base.rb', line 409

def update_attributes(attributes_to_update)
  return true if attributes_to_update.empty?
  rename = false

  operations = []
  attributes_to_update.each do |attribute, values|
    if attribute == :cn
      rename = true
    else
      if values.nil? || values.empty?
        operations << [:delete, attribute, nil]
      else
        values = [values] unless values.is_a? Array
        values = values.collect(&:to_s)

        current_value = begin
                          @entry[attribute]
                        rescue NoMethodError
                          nil
                        end

        operations << [(current_value.nil? ? :add : :replace), attribute, values]
      end
    end
  end

  unless operations.empty?
    @@ldap.modify(
      dn: distinguishedName,
      operations: operations
    )
  end
  if rename
    @@ldap.modify(
      dn: distinguishedName,
      operations: [[(name.nil? ? :add : :replace), 'samaccountname', attributes_to_update[:cn]]]
    )
    @@ldap.rename(olddn: distinguishedName, newrdn: 'cn=' + attributes_to_update[:cn], delete_attributes: true)
  end
  reload
end

#valid_attribute?(name) ⇒ Boolean

Returns:

  • (Boolean)

581
582
583
# File 'lib/bsb_active_directory/base.rb', line 581

def valid_attribute?(name)
  @attributes.key?(name) || @entry.attribute_names.include?(name)
end