Class: ActiveDirectory::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/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



494
495
496
497
498
499
500
501
502
# File 'lib/active_directory/base.rb', line 494

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:



575
576
577
578
579
580
581
582
583
584
585
# File 'lib/active_directory/base.rb', line 575

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)


105
106
107
# File 'lib/active_directory/base.rb', line 105

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



508
509
510
# File 'lib/active_directory/base.rb', line 508

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

.clear_cacheObject

Clears the cache



111
112
113
# File 'lib/active_directory/base.rb', line 111

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)


94
95
96
97
98
99
100
101
# File 'lib/active_directory/base.rb', line 94

def self.connected?
	begin
		@@ldap_connected ||= @@ldap.bind unless @@ldap.nil?
		@@ldap_connected
	rescue Net::LDAP::LdapError => e
		false
	end
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.



420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/active_directory/base.rb', line 420

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:



524
525
526
527
528
529
530
# File 'lib/active_directory/base.rb', line 524

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

.disable_cacheObject

Disable caching



125
126
127
# File 'lib/active_directory/base.rb', line 125

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



119
120
121
# File 'lib/active_directory/base.rb', line 119

def self.enable_cache
	@@caching = true
end

.encode_field(name, value) ⇒ Object

:nodoc:



532
533
534
535
536
537
538
# File 'lib/active_directory/base.rb', line 532

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

.errorObject



73
74
75
# File 'lib/active_directory/base.rb', line 73

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)


87
88
89
# File 'lib/active_directory/base.rb', line 87

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

.error_codeObject

Return the last errorcode that ldap generated



79
80
81
# File 'lib/active_directory/base.rb', line 79

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)


166
167
168
169
# File 'lib/active_directory/base.rb', line 166

def self.exists?(filter_as_hash)
	criteria = make_filter_from_hash(filter_as_hash) & filter
	(@@ldap.search(:filter => criteria).size > 0)
end

.filterObject

:nodoc:



129
130
131
# File 'lib/active_directory/base.rb', line 129

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.



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/active_directory/base.rb', line 235

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

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

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

	options[:in] = [ options[:in].to_s, @@settings[:base] ].delete_if { |part| part.empty? }.join(",")

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

	options[:filter] = options[:filter] & filter unless self.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



296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/active_directory/base.rb', line 296

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



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/active_directory/base.rb', line 267

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 and filters.keys == [:distinguishedname]

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

	if dns.kind_of? 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.kind_of? 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



309
310
311
312
313
314
315
316
# File 'lib/active_directory/base.rb', line 309

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
	return 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



515
516
517
518
519
520
# File 'lib/active_directory/base.rb', line 515

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



183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/active_directory/base.rb', line 183

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

	return filter
end

.make_filter_from_hash(hash) ⇒ Object

:nodoc:



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

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

	return filter
end

.method_missing(name, *args) ⇒ Object

:nodoc:



318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/active_directory/base.rb', line 318

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:



331
332
333
334
335
336
337
338
339
340
341
# File 'lib/active_directory/base.rb', line 331

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 { |s| s.to_sym }

	return find_spec, attribute_spec
end

.required_attributesObject

:nodoc:



133
134
135
# File 'lib/active_directory/base.rb', line 133

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.



67
68
69
70
71
# File 'lib/active_directory/base.rb', line 67

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

Instance Method Details

#==(other) ⇒ Object

:nodoc:



343
344
345
346
# File 'lib/active_directory/base.rb', line 343

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)


175
176
177
# File 'lib/active_directory/base.rb', line 175

def changed?
	!@attributes.empty?
end

#destroyObject

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



439
440
441
442
443
444
445
446
447
448
449
# File 'lib/active_directory/base.rb', line 439

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: []



544
545
546
547
548
549
550
551
552
553
554
555
556
# File 'lib/active_directory/base.rb', line 544

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

	return decode_field(name, @attributes[name.to_sym]) if @attributes.has_key?(name.to_sym)
		
	if @entry.attribute_names.include? name.to_sym
		value = @entry[name.to_sym]
		value = value.first if value.kind_of?(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.



471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
# File 'lib/active_directory/base.rb', line 471

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)


351
352
353
# File 'lib/active_directory/base.rb', line 351

def new_record?
	@entry.nil?
end

#reloadObject

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



359
360
361
362
363
364
# File 'lib/active_directory/base.rb', line 359

def reload
	return false if new_record?

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

#saveObject

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



455
456
457
458
459
460
461
462
# File 'lib/active_directory/base.rb', line 455

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

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



558
559
560
# File 'lib/active_directory/base.rb', line 558

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

#to_aryObject

Weird fluke with flattening, probably because of above attribute



571
572
# File 'lib/active_directory/base.rb', line 571

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.



374
375
376
# File 'lib/active_directory/base.rb', line 374

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



383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'lib/active_directory/base.rb', line 383

def update_attributes(attributes_to_update)
	return true if attributes_to_update.empty?

	operations = []
	attributes_to_update.each do |attribute, values|
		if values.nil? || values.empty?
			operations << [ :delete, attribute, nil ]
		else
			values = [values] unless values.is_a? Array
			values = values.collect { |v| v.to_s }

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

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

	@@ldap.modify(
		:dn => distinguishedName,
		:operations => operations
	) && reload
end

#valid_attribute?(name) ⇒ Boolean

Returns:

  • (Boolean)


540
541
542
# File 'lib/active_directory/base.rb', line 540

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