bsb_active_directory 8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,645 @@
1
+ #-- license
2
+ #
3
+ # Based on original code by Justin Mecham and James Hunt
4
+ # at http://rubyforge.org/projects/activedirectory
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ #
19
+ #++ license
20
+
21
+ require 'bindata'
22
+
23
+ module ActiveDirectory
24
+ #
25
+ # Create a SID from the binary string in the directory
26
+ #
27
+ class SID < BinData::Record
28
+ endian :little
29
+ uint8 :revision
30
+ uint8 :dashes
31
+ uint48be :nt_authority
32
+ uint32 :nt_non_unique
33
+ array :uuids, type: :uint32, initial_length: -> { dashes - 1 }
34
+ def to_s
35
+ ['S', revision, nt_authority, nt_non_unique, uuids].join('-')
36
+ end
37
+ end
38
+
39
+ #
40
+ # Base class for all Ruby/ActiveDirectory Entry Objects (like User and Group)
41
+ #
42
+ class Base
43
+ #
44
+ # A Net::LDAP::Filter object that doesn't do any filtering
45
+ # (outside of check that the CN attribute is present. This
46
+ # is used internally for specifying a 'no filter' condition
47
+ # for methods that require a filter object.
48
+ #
49
+ NIL_FILTER = Net::LDAP::Filter.pres('cn')
50
+
51
+ @@ldap = nil
52
+ @@ldap_connected = false
53
+ @@caching = false
54
+ @@cache = {}
55
+
56
+ #
57
+ # Configures the connection for the Ruby/ActiveDirectory library.
58
+ #
59
+ # For example:
60
+ #
61
+ # ActiveDirectory::Base.setup(
62
+ # :host => 'domain_controller1.example.org',
63
+ # :port => 389,
64
+ # :base => 'dc=example,dc=org',
65
+ # :auth => {
66
+ # :method => :simple,
67
+ # :username => 'querying_user@example.org',
68
+ # :password => 'querying_users_password'
69
+ # }
70
+ # )
71
+ #
72
+ # This will configure Ruby/ActiveDirectory to connect to the domain
73
+ # controller at domain_controller1.example.org, using port 389. The
74
+ # domain's base LDAP dn is expected to be 'dc=example,dc=org', and
75
+ # Ruby/ActiveDirectory will try to bind as the
76
+ # querying_user@example.org user, using the supplied password.
77
+ #
78
+ # Currently, there can be only one active connection per
79
+ # execution context.
80
+ #
81
+ # For more advanced options, refer to the Net::LDAP.new
82
+ # documentation.
83
+ #
84
+ def self.setup(settings)
85
+ @@settings = settings
86
+ @@ldap_connected = false
87
+ @@ldap = Net::LDAP.new(settings)
88
+ end
89
+
90
+ def self.error
91
+ "#{@@ldap.get_operation_result.code}: #{@@ldap.get_operation_result.message}"
92
+ end
93
+
94
+ ##
95
+ # Return the last errorcode that ldap generated
96
+ def self.error_code
97
+ @@ldap.get_operation_result.code
98
+ end
99
+
100
+ ##
101
+ # Check to see if the last query produced an error
102
+ # Note: Invalid username/password combinations will not
103
+ # produce errors
104
+ def self.error?
105
+ @@ldap.nil? ? false : @@ldap.get_operation_result.code != 0
106
+ end
107
+
108
+ ##
109
+ # Check to see if we are connected to the LDAP server
110
+ # This method will try to connect, if we haven't already
111
+ def self.connected?
112
+ @@ldap_connected ||= @@ldap.bind unless @@ldap.nil?
113
+ @@ldap_connected
114
+ rescue Net::LDAP::LdapError
115
+ false
116
+ end
117
+
118
+ ##
119
+ # Check to see if result caching is enabled
120
+ def self.cache?
121
+ @@caching
122
+ end
123
+
124
+ ##
125
+ # Clears the cache
126
+ def self.clear_cache
127
+ @@cache = {}
128
+ end
129
+
130
+ ##
131
+ # Enable caching for queries against the DN only
132
+ # This is to prevent membership lookups from hitting the
133
+ # AD unnecessarilly
134
+ def self.enable_cache
135
+ @@caching = true
136
+ end
137
+
138
+ ##
139
+ # Disable caching
140
+ def self.disable_cache
141
+ @@caching = false
142
+ end
143
+
144
+ def self.filter # :nodoc:
145
+ NIL_FILTER
146
+ end
147
+
148
+ def self.required_attributes # :nodoc:
149
+ {}
150
+ end
151
+
152
+ #
153
+ # Check to see if any entries matching the passed criteria exists.
154
+ #
155
+ # Filters should be passed as a hash of
156
+ # attribute_name => expected_value, like:
157
+ #
158
+ # User.exists?(
159
+ # :sn => 'Hunt',
160
+ # :givenName => 'James'
161
+ # )
162
+ #
163
+ # which will return true if one or more User entries have an
164
+ # sn (surname) of exactly 'Hunt' and a givenName (first name)
165
+ # of exactly 'James'.
166
+ #
167
+ # Partial attribute matches are available. For instance,
168
+ #
169
+ # Group.exists?(
170
+ # :description => 'OldGroup_*'
171
+ # )
172
+ #
173
+ # would return true if there are any Group objects in
174
+ # Active Directory whose descriptions start with OldGroup_,
175
+ # like OldGroup_Reporting, or OldGroup_Admins.
176
+ #
177
+ # Note that the * wildcard matches zero or more characters,
178
+ # so the above query would also return true if a group named
179
+ # 'OldGroup_' exists.
180
+ #
181
+ def self.exists?(filter_as_hash)
182
+ criteria = make_filter_from_hash(filter_as_hash) & filter
183
+ @@ldap.search(filter: criteria).!empty?
184
+ end
185
+
186
+ #
187
+ # Whether or not the entry has local changes that have not yet been
188
+ # replicated to the Active Directory server via a call to Base#save
189
+ #
190
+ def changed?
191
+ !@attributes.empty?
192
+ end
193
+
194
+ ##
195
+ # Makes a single filter from a given key and value
196
+ # It will try to encode an array if there is a process for it
197
+ # Otherwise, it will treat it as an or condition
198
+ def self.make_filter(key, value)
199
+ # Join arrays using OR condition
200
+ if value.is_a? Array
201
+ filter = ~NIL_FILTER
202
+
203
+ value.each do |v|
204
+ filter |= Net::LDAP::Filter.eq(key, encode_field(key, v).to_s)
205
+ end
206
+ else
207
+ filter = Net::LDAP::Filter.eq(key, encode_field(key, value).to_s)
208
+ end
209
+
210
+ filter
211
+ end
212
+
213
+ def self.make_filter_from_hash(hash) # :nodoc:
214
+ return NIL_FILTER if hash.nil? || hash.empty?
215
+
216
+ filter = NIL_FILTER
217
+
218
+ hash.each do |key, value|
219
+ filter &= make_filter(key, value)
220
+ end
221
+
222
+ filter
223
+ end
224
+
225
+ def self.from_dn(dn)
226
+ ldap_result = @@ldap.search(filter: '(objectClass=*)', base: dn)
227
+ return nil unless ldap_result
228
+
229
+ ad_obj = new(ldap_result[0])
230
+ @@cache[ad_obj.dn] = ad_obj unless ad_obj.instance_of? Base
231
+ ad_obj
232
+ end
233
+
234
+ #
235
+ # Performs a search on the Active Directory store, with similar
236
+ # syntax to the Rails ActiveRecord#find method.
237
+ #
238
+ # The first argument passed should be
239
+ # either :first or :all, to indicate that we want only one
240
+ # (:first) or all (:all) results back from the resultant set.
241
+ #
242
+ # The second argument should be a hash of attribute_name =>
243
+ # expected_value pairs.
244
+ #
245
+ # User.find(:all, :sn => 'Hunt')
246
+ #
247
+ # would find all of the User objects in Active Directory that
248
+ # have a surname of exactly 'Hunt'. As with the Base.exists?
249
+ # method, partial searches are allowed.
250
+ #
251
+ # This method always returns an array if the caller specifies
252
+ # :all for the search e (first argument). If no results
253
+ # are found, the array will be empty.
254
+ #
255
+ # If you call find(:first, ...), you will either get an object
256
+ # (a User or a Group) back, or nil, if there were no entries
257
+ # matching your filter.
258
+ #
259
+ def self.find(*args)
260
+ return false unless connected?
261
+
262
+ options = {
263
+ filter: args[1].nil? ? NIL_FILTER : args[1],
264
+ in: args[1].nil? ? '' : (args[1][:in] || '')
265
+ }
266
+ options[:filter].delete(:in)
267
+
268
+ cached_results = find_cached_results(args[1])
269
+ return cached_results if cached_results || cached_results.nil?
270
+
271
+ options[:in] = [options[:in].to_s, @@settings[:base]].delete_if(&:empty?).join(',')
272
+
273
+ if options[:filter].is_a? Hash
274
+ options[:filter] = make_filter_from_hash(options[:filter])
275
+ end
276
+
277
+ options[:filter] = options[:filter] & filter unless filter == NIL_FILTER
278
+
279
+ if args.first == :all
280
+ find_all(options)
281
+ elsif args.first == :first
282
+ find_first(options)
283
+ else
284
+ raise ArgumentError,
285
+ 'Invalid specifier (not :all, and not :first) passed to find()'
286
+ end
287
+ end
288
+
289
+ ##
290
+ # Searches the cache and returns the result
291
+ # Returns false on failure, nil on wrong object type
292
+ #
293
+ def self.find_cached_results(filters)
294
+ return false unless cache?
295
+
296
+ # Check to see if we're only looking for :distinguishedname
297
+ return false unless filters.is_a?(Hash) && filters.keys == [:distinguishedname]
298
+
299
+ # Find keys we're looking up
300
+ dns = filters[:distinguishedname]
301
+
302
+ if dns.is_a? Array
303
+ result = []
304
+
305
+ dns.each do |dn|
306
+ entry = @@cache[dn]
307
+
308
+ # If the object isn't in the cache just run the query
309
+ return false if entry.nil?
310
+
311
+ # Only permit objects of the type we're looking for
312
+ result << entry if entry.is_a? self
313
+ end
314
+
315
+ return result
316
+ else
317
+ return false unless @@cache.key? dns
318
+ return @@cache[dns] if @@cache[dns].is_a? self
319
+ end
320
+ end
321
+
322
+ def self.find_all(options)
323
+ results = []
324
+ ldap_objs = @@ldap.search(filter: options[:filter], base: options[:in]) || []
325
+
326
+ ldap_objs.each do |entry|
327
+ ad_obj = new(entry)
328
+ @@cache[entry.dn] = ad_obj unless ad_obj.instance_of? Base
329
+ results << ad_obj
330
+ end
331
+
332
+ results
333
+ end
334
+
335
+ def self.find_first(options)
336
+ ldap_result = @@ldap.search(filter: options[:filter], base: options[:in])
337
+ return nil if ldap_result.empty?
338
+
339
+ ad_obj = new(ldap_result[0])
340
+ @@cache[ad_obj.dn] = ad_obj unless ad_obj.instance_of? Base
341
+ ad_obj
342
+ end
343
+
344
+ def self.method_missing(name, *args) # :nodoc:
345
+ name = name.to_s
346
+ if name[0, 5] == 'find_'
347
+ find_spec, attribute_spec = parse_finder_spec(name)
348
+ raise ArgumentError, "find: Wrong number of arguments (#{args.size} for #{attribute_spec.size})" unless args.size == attribute_spec.size
349
+ filters = {}
350
+ [attribute_spec, args].transpose.each { |pr| filters[pr[0]] = pr[1] }
351
+ find(find_spec, filter: filters)
352
+ else
353
+ super name.to_sym, args
354
+ end
355
+ end
356
+
357
+ def self.parse_finder_spec(method_name) # :nodoc:
358
+ # FIXME: This is a prime candidate for a
359
+ # first-class object, FinderSpec
360
+
361
+ method_name = method_name.gsub(/^find_/, '').gsub(/^by_/, 'first_by_')
362
+ find_spec, attribute_spec = *method_name.split('_by_')
363
+ find_spec = find_spec.to_sym
364
+ attribute_spec = attribute_spec.split('_and_').collect(&:to_sym)
365
+
366
+ [find_spec, attribute_spec]
367
+ end
368
+
369
+ def ==(other) # :nodoc:
370
+ return false if other.nil?
371
+ other[:objectguid] == get_attr(:objectguid)
372
+ end
373
+
374
+ #
375
+ # Returns true if this entry does not yet exist in Active Directory.
376
+ #
377
+ def new_record?
378
+ @entry.nil?
379
+ end
380
+
381
+ #
382
+ # Refreshes the attributes for the entry with updated data from the
383
+ # domain controller.
384
+ #
385
+ def reload
386
+ return false if new_record?
387
+
388
+ @entry = @@ldap.search(filter: Net::LDAP::Filter.eq('distinguishedName', distinguishedName))[0]
389
+ !@entry.nil?
390
+ end
391
+
392
+ #
393
+ # Updates a single attribute (name) with one or more values
394
+ # (value), by immediately contacting the Active Directory
395
+ # server and initiating the update remotely.
396
+ #
397
+ # Entries are always reloaded (via Base.reload) after calling
398
+ # this method.
399
+ #
400
+ def update_attribute(name, value)
401
+ update_attributes(name.to_s => value)
402
+ end
403
+
404
+ #
405
+ # Updates multiple attributes, like ActiveRecord#update_attributes.
406
+ # The updates are immediately sent to the server for processing,
407
+ # and the entry is reloaded after the update (if all went well).
408
+ #
409
+ def update_attributes(attributes_to_update)
410
+ return true if attributes_to_update.empty?
411
+ rename = false
412
+
413
+ operations = []
414
+ attributes_to_update.each do |attribute, values|
415
+ if attribute == :cn
416
+ rename = true
417
+ else
418
+ if values.nil? || values.empty?
419
+ operations << [:delete, attribute, nil]
420
+ else
421
+ values = [values] unless values.is_a? Array
422
+ values = values.collect(&:to_s)
423
+
424
+ current_value = begin
425
+ @entry[attribute]
426
+ rescue NoMethodError
427
+ nil
428
+ end
429
+
430
+ operations << [(current_value.nil? ? :add : :replace), attribute, values]
431
+ end
432
+ end
433
+ end
434
+
435
+ unless operations.empty?
436
+ @@ldap.modify(
437
+ dn: distinguishedName,
438
+ operations: operations
439
+ )
440
+ end
441
+ if rename
442
+ @@ldap.modify(
443
+ dn: distinguishedName,
444
+ operations: [[(name.nil? ? :add : :replace), 'samaccountname', attributes_to_update[:cn]]]
445
+ )
446
+ @@ldap.rename(olddn: distinguishedName, newrdn: 'cn=' + attributes_to_update[:cn], delete_attributes: true)
447
+ end
448
+ reload
449
+ end
450
+
451
+ #
452
+ # Create a new entry in the Active Record store.
453
+ #
454
+ # dn is the Distinguished Name for the new entry. This must be
455
+ # a unique identifier, and can be passed as either a Container
456
+ # or a plain string.
457
+ #
458
+ # attributes is a symbol-keyed hash of attribute_name => value
459
+ # pairs.
460
+ #
461
+ def self.create(dn, attributes)
462
+ return nil if dn.nil? || attributes.nil?
463
+ begin
464
+ attributes.merge!(required_attributes)
465
+ if @@ldap.add(dn: dn.to_s, attributes: attributes)
466
+ ldap_obj = @@ldap.search(base: dn.to_s)
467
+ return new(ldap_obj[0])
468
+ else
469
+ return nil
470
+ end
471
+ rescue
472
+ return nil
473
+ end
474
+ end
475
+
476
+ #
477
+ # Deletes the entry from the Active Record store and returns true
478
+ # if the operation was successfully.
479
+ #
480
+ def destroy
481
+ return false if new_record?
482
+
483
+ if @@ldap.delete(dn: distinguishedName)
484
+ @entry = nil
485
+ @attributes = {}
486
+ return true
487
+ else
488
+ return false
489
+ end
490
+ end
491
+
492
+ #
493
+ # Saves any pending changes to the entry by updating the remote
494
+ # entry.
495
+ #
496
+ def save
497
+ if update_attributes(@attributes)
498
+ @attributes = {}
499
+ true
500
+ else
501
+ false
502
+ end
503
+ end
504
+
505
+ #
506
+ # This method may one day provide the ability to move entries from
507
+ # container to container. Currently, it does nothing, as we are
508
+ # waiting on the Net::LDAP folks to either document the
509
+ # Net::LDAP#modrdn method, or provide a similar method for
510
+ # moving / renaming LDAP entries.
511
+ #
512
+ def move(new_rdn)
513
+ return false if new_record?
514
+ puts "Moving #{distinguishedName} to RDN: #{new_rdn}"
515
+
516
+ settings = @@settings.dup
517
+ settings[:port] = 636
518
+ settings[:encryption] = { method: :simple_tls }
519
+
520
+ ldap = Net::LDAP.new(settings)
521
+
522
+ if ldap.rename(
523
+ olddn: distinguishedName,
524
+ newrdn: new_rdn,
525
+ delete_attributes: false
526
+ )
527
+ return true
528
+ else
529
+ puts Base.error
530
+ return false
531
+ end
532
+ end
533
+
534
+ # FIXME: Need to document the Base::new
535
+ def initialize(attributes = {}) # :nodoc:
536
+ if attributes.is_a? Net::LDAP::Entry
537
+ @entry = attributes
538
+ @attributes = {}
539
+ else
540
+ @entry = nil
541
+ @attributes = attributes
542
+ end
543
+ end
544
+
545
+ ##
546
+ # Pull the class we're in
547
+ # This isn't quite right, as extending the object does funny things to how we
548
+ # lookup objects
549
+ def self.class_name
550
+ @klass ||= (name.include?('::') ? name[/.*::(.*)/, 1] : name)
551
+ end
552
+
553
+ ##
554
+ # Grabs the field type depending on the class it is called from
555
+ # Takes the field name as a parameter
556
+ def self.get_field_type(name)
557
+ # Extract class name
558
+ throw 'Invalid field name' if name.nil?
559
+ type = ::ActiveDirectory.special_fields[class_name.to_sym][name.to_s.downcase.to_sym]
560
+ type.to_s unless type.nil?
561
+ end
562
+
563
+ @types = {}
564
+
565
+ def self.decode_field(name, value) # :nodoc:
566
+ type = get_field_type name
567
+ if !type.nil? && ::ActiveDirectory::FieldType.const_defined?(type)
568
+ return ::ActiveDirectory::FieldType.const_get(type).decode(value)
569
+ end
570
+ value
571
+ end
572
+
573
+ def self.encode_field(name, value) # :nodoc:
574
+ type = get_field_type name
575
+ if !type.nil? && ::ActiveDirectory::FieldType.const_defined?(type)
576
+ return ::ActiveDirectory::FieldType.const_get(type).encode(value)
577
+ end
578
+ value
579
+ end
580
+
581
+ def valid_attribute?(name)
582
+ @attributes.key?(name) || @entry.attribute_names.include?(name)
583
+ end
584
+
585
+ def get_attr(name)
586
+ name = name.to_s.downcase
587
+
588
+ return self.class.decode_field(name, @attributes[name.to_sym]) if @attributes.key?(name.to_sym)
589
+
590
+ if @entry.attribute_names.include? name.to_sym
591
+ value = @entry[name.to_sym]
592
+ value = value.first if value.is_a?(Array) && value.size == 1
593
+ value = value.to_s if value.nil? || value.size == 1
594
+ value = nil.to_s if value.empty?
595
+ return self.class.decode_field(name, value)
596
+ end
597
+ end
598
+
599
+ def set_attr(name, value)
600
+ @attributes[name.to_sym] = self.class.encode_field(name, value)
601
+ end
602
+
603
+ ##
604
+ # Reads the array of values for the provided attribute. The attribute name
605
+ # is canonicalized prior to reading. Returns an empty array if the
606
+ # attribute does not exist.
607
+ alias [] get_attr
608
+ alias []= set_attr
609
+
610
+ ##
611
+ # Weird fluke with flattening, probably because of above attribute
612
+ def to_ary; end
613
+
614
+ def sid
615
+ unless @sid
616
+ raise 'Object has no sid' unless valid_attribute? :objectsid
617
+ # SID is stored as a binary in the directory
618
+ # however, Net::LDAP returns an hex string
619
+ #
620
+ # As per [1], there seems to be 2 ways to get back binary data.
621
+ #
622
+ # [str].pack("H*")
623
+ # str.gsub(/../) { |b| b.hex.chr }
624
+ #
625
+ # [1] :
626
+ # http://stackoverflow.com/questions/22957688/convert-string-with-hex-ascii-codes-to-characters
627
+ #
628
+ @sid = SID.read([get_attr(:objectsid)].pack('H*'))
629
+ end
630
+ @sid.to_s
631
+ end
632
+
633
+ def method_missing(name, args = []) # :nodoc:
634
+ name = name.to_s.downcase
635
+
636
+ return set_attr(name.chop, args) if name[-1] == '='
637
+
638
+ if valid_attribute? name.to_sym
639
+ get_attr(name)
640
+ else
641
+ super
642
+ end
643
+ end
644
+ end
645
+ end
@@ -0,0 +1,35 @@
1
+ #-- license
2
+ #
3
+ # Based on original code by Justin Mecham and James Hunt
4
+ # at http://rubyforge.org/projects/activedirectory
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ #
19
+ #++ license
20
+
21
+ module ActiveDirectory
22
+ class Computer < Base
23
+ def self.filter # :nodoc:
24
+ Net::LDAP::Filter.eq(:objectClass, 'computer')
25
+ end
26
+
27
+ def self.required_attributes # :nodoc:
28
+ { objectClass: %w[top person organizationalPerson user computer] }
29
+ end
30
+
31
+ def hostname
32
+ dNSHostName || name
33
+ end
34
+ end
35
+ end