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

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(attributes = {}) ⇒ Base

FIXME: Need to document the Base::new



391
392
393
394
395
396
397
398
399
# File 'lib/active_directory/base.rb', line 391

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:



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
450
# File 'lib/active_directory/base.rb', line 425

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

	if name[-1,1] == '='
		name.chop!
		@attributes[name.to_sym] = encode_field(name, args)
	end

	return decode_field(name, @attributes[name.to_sym]) if @attributes.has_key?(name.to_sym)
		
	if @entry
		# begin
			value = @entry.send(name.to_sym)
			value = value.first if value.kind_of?(Array) && value.size == 1
			value = value.to_s if value.nil? || value.size == 1
			::Rails.logger.add 0, "Decoded as #{decode_field(name, value)}\n"
			::Rails.logger.add 0, ""
			return decode_field(name, value)
		# rescue NoMethodError => e
		# 	::Rails.logger.add 0, "#{e.inspect}"
		# 	return nil
		# end
	end

	super
end

Class Method Details

.connected?Boolean

Returns:

  • (Boolean)


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

def self.connected?
	@@ldap.bind
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.



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

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)
			return find_by_distinguishedName(dn.to_s)
		else
			return nil
		end
	rescue
		return nil
	end
end

.errorObject



71
72
73
# File 'lib/active_directory/base.rb', line 71

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

.error?Boolean

Returns:

  • (Boolean)


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

def self.error?
	!success?
end

.error_codeObject



75
76
77
# File 'lib/active_directory/base.rb', line 75

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)


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

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

.filterObject

:nodoc:



91
92
93
# File 'lib/active_directory/base.rb', line 91

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 type (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.



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

def self.find(*args)
	options = {
		:filter => NIL_FILTER,
		:in => ''
	}
	options.merge!(:filter => args[1]) unless args[1].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



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

def self.find_all(options)
	results = []
	@@ldap.search(:filter => options[:filter], :base => options[:in], :return_result => false) do |entry|
		results << new(entry)
	end
	results
end

.find_first(options) ⇒ Object



210
211
212
213
214
# File 'lib/active_directory/base.rb', line 210

def self.find_first(options)
	@@ldap.search(:filter => options[:filter], :base => options[:in], :return_result => false) do |entry|
		return new(entry)
	end
end

.make_filter_from_hash(hash) ⇒ Object

:nodoc:



141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/active_directory/base.rb', line 141

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

	#I'm sure there's a better way to do this
	#Grab the first one, then do the rest
	key, value = hash.shift
	f = Net::LDAP::Filter.eq(key, value.to_s)
	
	hash.each do |key, value|
		f = f & Net::LDAP::Filter.eq(key, value.to_s)
	end

	return f
end

.method_missing(name, *args) ⇒ Object

:nodoc:



216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/active_directory/base.rb', line 216

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:



229
230
231
232
233
234
235
236
237
238
239
# File 'lib/active_directory/base.rb', line 229

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:



95
96
97
# File 'lib/active_directory/base.rb', line 95

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 => {
    :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.



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

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

.success?Boolean

Returns:

  • (Boolean)


83
84
85
# File 'lib/active_directory/base.rb', line 83

def self.success?
	@@ldap.get_operation_result.code == 0
end

Instance Method Details

#==(other) ⇒ Object

:nodoc:



241
242
243
244
# File 'lib/active_directory/base.rb', line 241

def ==(other) # :nodoc:
	return false if other.nil?
	other.objectGUID == 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)


137
138
139
# File 'lib/active_directory/base.rb', line 137

def changed?
	!@attributes.empty?
end

#decode_field(name, value) ⇒ Object



409
410
411
412
413
414
415
# File 'lib/active_directory/base.rb', line 409

def decode_field(name, value)
	::Rails.logger.add 0, "Decoding #{name}, #{value}"
	type = get_field_type name
	::Rails.logger.add 0, "Type: #{type}  Const: #{::ActiveDirectory::FieldType::const_get type unless type.nil?}"
	return ::ActiveDirectory::FieldType::const_get(type).decode(value) if !type.nil? and ::ActiveDirectory::FieldType::const_defined? type
	return value
end

#destroyObject

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



336
337
338
339
340
341
342
343
344
345
346
# File 'lib/active_directory/base.rb', line 336

def destroy
	return false if new_record?

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

#encode_field(name, value) ⇒ Object



417
418
419
420
421
422
423
# File 'lib/active_directory/base.rb', line 417

def encode_field(name, value)
	::Rails.logger.add 0, "Encoding #{name}, #{value}"
	type = get_field_type name
	::Rails.logger.add 0, "Type: #{type}  Const: #{::ActiveDirectory::FieldType::const_get type}"
	return ::ActiveDirectory::FieldType::const_get(type).encode(value) if ::ActiveDirectory::FieldType::const_defined? type
	return value
end

#get_field_type(name) ⇒ Object



401
402
403
404
405
406
407
# File 'lib/active_directory/base.rb', line 401

def get_field_type(name)
	#Extract class name
	klass = self.class.name[/.*::(.*)/, 1]
	::Rails.logger.add 0, "special_fields[#{klass.classify.to_sym}][#{name.downcase.to_sym}] = #{::ActiveDirectory.special_fields[klass.classify.to_sym][name.downcase.to_sym]}"
	type = ::ActiveDirectory.special_fields[klass.classify.to_sym][name.downcase.to_sym]
	type.to_s.classify unless type.nil?
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.



368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/active_directory/base.rb', line 368

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)


249
250
251
# File 'lib/active_directory/base.rb', line 249

def new_record?
	@entry.nil?
end

#reloadObject

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



257
258
259
260
261
262
# File 'lib/active_directory/base.rb', line 257

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.



352
353
354
355
356
357
358
359
# File 'lib/active_directory/base.rb', line 352

def save
	if update_attributes(@attributes)
		@attributes = {}
		return true
	else
		return false
	end
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.



272
273
274
# File 'lib/active_directory/base.rb', line 272

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



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/active_directory/base.rb', line 281

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.send(attribute)
			rescue NoMethodError
				nil
			end

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

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