bsb_active_directory 8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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