ruby-activeldap-debug 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1244 @@
1
+ # === ActiveLDAP - an OO-interface to LDAP objects inspired by ActiveRecord
2
+ # Author: Will Drewry <will@alum.bu.edu>
3
+ # License: See LICENSE and COPYING.txt
4
+ # Copyright 2004 Will Drewry <will@alum.bu.edu>
5
+ #
6
+ # == Summary
7
+ # ActiveLDAP lets you read and update LDAP entries in a completely object
8
+ # oriented fashion, even handling attributes with multiple names seamlessly.
9
+ # It was inspired by ActiveRecord so extending it to deal with custom
10
+ # LDAP schemas is as effortless as knowing the 'ou' of the objects, and the
11
+ # primary key. (fix this up some)
12
+ #
13
+ # == Example
14
+ # irb> require 'activeldap'
15
+ # > true
16
+ # irb> user = ActiveLDAP::User.new("drewry")
17
+ # > #<ActiveLDAP::User:0x402e...
18
+ # irb> user.cn
19
+ # > "foo"
20
+ # irb> user.commonname
21
+ # > "foo"
22
+ # irb> user.cn = "Will Drewry"
23
+ # > "Will Drewry"
24
+ # irb> user.cn
25
+ # > "Will Drewry"
26
+ # irb> user.validate
27
+ # > nil
28
+ # irb> user.write
29
+ #
30
+ #
31
+
32
+ require 'ldap'
33
+ require 'ldap/schema'
34
+ require 'log4r'
35
+
36
+ module ActiveLDAP
37
+ # OO-interface to LDAP assuming pam/nss_ldap-style comanization with Active specifics
38
+ # Each subclass does a ldapsearch for the matching entry.
39
+ # If no exact match, raise an error.
40
+ # If match, change all LDAP attributes in accessor attributes on the object.
41
+ # -- these are ACTUALLY populated from schema - see subschema.rb example
42
+ # -- @conn.schema().each{|k,vs| vs.each{|v| print("#{k}: #{v}\n")}}
43
+ # -- extract objectClasses from match and populate
44
+ # Multiple entries become lists.
45
+ # If this isn't read-only then lists become multiple entries, etc.
46
+
47
+ # AttributeEmpty
48
+ #
49
+ # An exception raised when a required attribute is found to be empty
50
+ class AttributeEmpty < RuntimeError
51
+ end
52
+
53
+ # DeleteError
54
+ #
55
+ # An exception raised when an ActiveLDAP delete action fails
56
+ class DeleteError < RuntimeError
57
+ end
58
+
59
+ # WriteError
60
+ #
61
+ # An exception raised when an ActiveLDAP write action fails
62
+ class WriteError < RuntimeError
63
+ end
64
+
65
+ # AuthenticationError
66
+ #
67
+ # An exception raised when user authentication fails
68
+ class AuthenticationError < RuntimeError
69
+ end
70
+
71
+ # ConnectionError
72
+ #
73
+ # An exception raised when the LDAP conenction fails
74
+ class ConnectionError < RuntimeError
75
+ end
76
+
77
+ # ObjectClassError
78
+ #
79
+ # An exception raised when an objectClass is not defined in the schema
80
+ class ObjectClassError < RuntimeError
81
+ end
82
+
83
+
84
+ # Base
85
+ #
86
+ # Base is the primary class which contains all of the core
87
+ # ActiveLDAP functionality. It is meant to only ever be subclassed
88
+ # by extension classes.
89
+ class Base
90
+ # Parsed schema structures
91
+ attr_reader :must, :may
92
+ attr_accessor :logger
93
+
94
+ # All class-wide variables
95
+ @@config = nil # Container for current connection settings
96
+ @@schema = nil # LDAP server's schema
97
+ @@conn = nil # LDAP connection
98
+
99
+ # Driver generator
100
+ #
101
+ # TODO add type checking
102
+ # This let's you call this method to create top-level extension object. This
103
+ # is really just a proof of concept and has not truly useful purpose.
104
+ # example: Base.create_object(:class => "user", :dnattr => "uid", :classes => ['top'])
105
+ #
106
+ def Base.create_object(config={})
107
+ # Just upcase the first letter of the new class name
108
+ str = config[:class]
109
+ class_name = str[0].chr.upcase + str[1..-1]
110
+
111
+ attr = config[:dnattr] # "uid"
112
+ prefix = config[:base] # "ou=People"
113
+ # [ 'top', 'posixAccount' ]
114
+ classes_array = config[:classes] || []
115
+ # [ [ :groups, {:class_name => "Group", :foreign_key => "memberUid"}] ]
116
+ belongs_to_array = config[:belongs_to] || []
117
+ # [ [ :members, {:class_name => "User", :foreign_key => "uid", :local_key => "memberUid"}] ]
118
+ has_many_array = config[:has_many] || []
119
+
120
+ raise TypeError, ":objectclasses must be an array" unless classes_array.respond_to? :size
121
+ raise TypeError, ":belongs_to must be an array" unless belongs_to_array.respond_to? :size
122
+ raise TypeError, ":has_many must be an array" unless has_many_array.respond_to? :size
123
+
124
+ # Build classes array
125
+ classes = '['
126
+ classes_array.map! {|x| x = "'#{x}'"}
127
+ classes << classes_array.join(', ')
128
+ classes << ']'
129
+
130
+ # Build belongs_to
131
+ belongs_to = []
132
+ if belongs_to_array.size > 0
133
+ belongs_to_array.each do |bt|
134
+ line = [ "belongs_to :#{bt[0]}" ]
135
+ bt[1].keys.each do |key|
136
+ line << ":#{key} => '#{bt[1][key]}'"
137
+ end
138
+ belongs_to << line.join(', ')
139
+ end
140
+ end
141
+
142
+ # Build has_many
143
+ has_many = []
144
+ if has_many_array.size > 0
145
+ has_many_array.each do |hm|
146
+ line = [ "has_many :#{hm[0]}" ]
147
+ hm[1].keys.each do |key|
148
+ line << ":#{key} => '#{hm[1][key]}'"
149
+ end
150
+ has_many << line.join(', ')
151
+ end
152
+ end
153
+
154
+ self.class.module_eval <<-"end_eval"
155
+ class ::#{class_name} < ActiveLDAP::Base
156
+ ldap_mapping :dnattr => "#{attr}, :prefix => "#{prefix}", :classes => "#{classes}"
157
+ #{belongs_to.join("\n")}
158
+ #{has_many.join("\n")}
159
+ end
160
+ end_eval
161
+ end
162
+
163
+ # Connect and bind to LDAP creating a class variable for use by all ActiveLDAP
164
+ # objects.
165
+ #
166
+ # == +config+
167
+ # +config+ must be a hash that may contain any of the following fields:
168
+ # :user, :password_block, :logger, :host, :port, :base, :bind_format, :try_sasl, :allow_anonymous
169
+ # :user specifies the username to bind with.
170
+ # :bind_format specifies the string to substitute the username into on bind. e.g. uid=%s,ou=People,dc=example,dc=com. Overrides @@bind_format.
171
+ # :password_block specifies a Proc object that will yield a String to be used as the password when called.
172
+ # :logger specifies a preconfigured Log4r::Logger to be used for all logging
173
+ # :host overrides the configuration.rb @@host setting with the LDAP server hostname
174
+ # :port overrides the configuration.rb @@port setting for the LDAP server port
175
+ # :base overwrites Base.base - this affects EVERYTHING
176
+ # :try_sasl indicates that a SASL bind should be attempted when binding to the server (default: false)
177
+ # :allow_anonymous indicates that a true anonymous bind is allowed when trying to bind to the server (default: true)
178
+ def Base.connect(config={}) # :user, :password_block, :logger
179
+ # Process config
180
+ # Class options
181
+ ## These will be replace by configuration.rb defaults if defined
182
+ @@config = {}
183
+ @@config[:host] = config[:host] || @@host
184
+ @@config[:port] = config[:port] || @@port
185
+ if config[:base]
186
+ Base.class_eval <<-"end_eval"
187
+ def Base.base
188
+ '#{config[:base]}'
189
+ end
190
+ end_eval
191
+ end
192
+ @@config[:bind_format] = config[:bind_format] || @@bind_format
193
+
194
+ @@logger = config[:logger] || nil
195
+ # Setup default logger to console
196
+ if @@logger.nil?
197
+ @@logger = Log4r::Logger.new('activeldap')
198
+ @@logger.level = Log4r::OFF
199
+ Log4r::StderrOutputter.new 'console'
200
+ @@logger.add('console')
201
+ end
202
+
203
+ # Method options
204
+ user = nil
205
+ password_block = nil
206
+ @@config[:allow_anonymous] = true
207
+ @@config[:try_sasl] = false
208
+
209
+ @@config[:user] = config[:user] || user
210
+ @@config[:allow_anonymous] = config[:allow_anonymous] if config.has_key? :allow_anonymous
211
+ @@config[:try_sasl] = config[:try_sasl]
212
+ @@config[:password_block] = config[:password_block] if config.has_key? :password_block
213
+
214
+ # Setup bind credentials
215
+ @@config[:user] = ENV['USER'] unless @@config[:user]
216
+
217
+ # Connect to LDAP
218
+ begin
219
+ # SSL using START_TLS
220
+ @@conn = LDAP::SSLConn.new(@@config[:host], @@config[:port], true)
221
+ rescue
222
+ @@logger.warn "Warning: Failed to connect using TLS!"
223
+ begin
224
+ @@logger.warn "Warning: Attempting SSL connection . . ."
225
+ @@conn = LDAP::SSLConn.new(@@config[:host], @@config[:port], false)
226
+ # HACK: Load the schema here because otherwise you can't tell if the
227
+ # HACK: SSLConn is a real SSL connection.
228
+ @@schema = @@conn.schema() if @@schema.nil?
229
+ rescue
230
+ @@logger.warn "Warning: Attempting unencrypted connection . . ."
231
+ @@conn = LDAP::Conn.new(@@config[:host], @@config[:port])
232
+ end
233
+ end
234
+ @@logger.debug "Connected to #{@@config[:host]}:#{@@config[:port]}."
235
+
236
+ # Enforce LDAPv3
237
+ @@conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
238
+
239
+ # Authenticate
240
+ do_bind
241
+
242
+ # Load Schema (if not straight SSL...)
243
+ begin
244
+ @@schema = @@conn.schema() if @@schema.nil?
245
+ rescue => detail
246
+ raise ConnectionError, "#{detail.exception} - LDAP connection failure, or server does not support schema queries."
247
+ end
248
+
249
+ # Cleanly return
250
+ return true
251
+ end # Base.connect
252
+
253
+ # Base.close
254
+ # This method deletes the LDAP connection object.
255
+ # This does NOT reset any overridden values from a Base.connect call.
256
+ def Base.close
257
+ @@conn.unbind unless @@conn.nil?
258
+ @@conn = nil
259
+ # Make sure it is cleaned up
260
+ ObjectSpace.garbage_collect
261
+ end
262
+
263
+ # Return the LDAP connection object currently in use
264
+ def Base.connection
265
+ return @@conn
266
+ end
267
+
268
+ # Set the LDAP connection avoiding Base.connect or multiplexing connections
269
+ def Base.connection=(conn)
270
+ @@conn = conn
271
+ end
272
+
273
+ # Return the schema object
274
+ def Base.schema
275
+ @@schema
276
+ end
277
+
278
+ # search
279
+ #
280
+ # Wraps Ruby/LDAP connection.search to make it easier to search for specific
281
+ # data without cracking open Base.connection
282
+ def Base.search(config={})
283
+ unless Base.connection
284
+ if @@config
285
+ ActiveLDAP::Base.connect(@@config)
286
+ else
287
+ ActiveLDAP::Base.connect
288
+ end
289
+ end
290
+
291
+ config[:filter] = 'objectClass=*' unless config.has_key? :filter
292
+ config[:attrs] = [] unless config.has_key? :attrs
293
+ config[:scope] = LDAP::LDAP_SCOPE_SUBTREE unless config.has_key? :scope
294
+ config[:base] = base() unless config.has_key? :base
295
+
296
+ values = []
297
+ config[:attrs] = config[:attrs].to_a # just in case
298
+
299
+ begin
300
+ @@conn.search(config[:base], config[:scope], config[:filter], config[:attrs]) do |m|
301
+ res = {}
302
+ res['dn'] = [m.dn.dup] # For consistency with the below
303
+ m.attrs.each do |attr|
304
+ if config[:attrs].member? attr or config[:attrs].empty?
305
+ res[attr] = m.vals(attr).dup
306
+ end
307
+ end
308
+ values.push(res)
309
+ end
310
+ rescue RuntimeError => detail
311
+ @@logger.debug "No matches for #{config[:filter]} and attrs #{config[:attrs]}"
312
+ # Do nothing on failure
313
+ end
314
+ return values
315
+ end
316
+
317
+ # find
318
+ #
319
+ # Finds the first match for value where |value| is the value of some
320
+ # |field|, or the wildcard match. This is only useful for derived classes.
321
+ # usage: Subclass.find(:attribute => "cn", :value => "some*val", :objects => true)
322
+ # Subclass.find('some*val')
323
+ #
324
+ def Base.find(config = {})
325
+ unless Base.connection
326
+ if @@config
327
+ ActiveLDAP::Base.connect(@@config)
328
+ else
329
+ ActiveLDAP::Base.connect
330
+ end
331
+ end
332
+
333
+ if self.class == Class
334
+ klass = self.ancestors[0].to_s.split(':').last
335
+ real_klass = self.ancestors[0]
336
+ else
337
+ klass = self.class.to_s.split(':').last
338
+ real_klass = self.class
339
+ end
340
+
341
+ # Allow a single string argument
342
+ attr = dnattr()
343
+ objects = false
344
+ val = config
345
+ # Or a hash
346
+ if config.respond_to?"has_key?"
347
+ attr = config[:attribute] || dnattr()
348
+ val = config[:value] || '*'
349
+ objects = config[:objects]
350
+ end
351
+
352
+ matches = []
353
+
354
+ begin
355
+ # Get some attributes
356
+ @@conn.search(base(), LDAP::LDAP_SCOPE_SUBTREE, "(#{attr}=#{val})") do |m|
357
+ # Extract the dnattr value
358
+ dnval = m.dn.split(/,/)[0].split(/=/)[1]
359
+
360
+ if objects
361
+ return eval("#{real_klass}.new(m)")
362
+ else
363
+ return dnval
364
+ end
365
+ end
366
+ rescue RuntimeError => detail
367
+ @@logger.debug "No matches for #{attr}=#{val}"
368
+ # Do nothing on failure
369
+ end
370
+ return nil
371
+ end
372
+ private_class_method :find
373
+
374
+
375
+ # find_all
376
+ #
377
+ # Finds all matches for value where |value| is the value of some
378
+ # |field|, or the wildcard match. This is only useful for derived classes.
379
+ def Base.find_all(config = {})
380
+ unless Base.connection
381
+ if @@config
382
+ ActiveLDAP::Base.connect(@@config)
383
+ else
384
+ ActiveLDAP::Base.connect
385
+ end
386
+ end
387
+
388
+ if self.class == Class
389
+ real_klass = self.ancestors[0]
390
+ else
391
+ real_klass = self.class
392
+ end
393
+
394
+ # Allow a single string argument
395
+ val = config
396
+ objects = false
397
+ # Or a hash
398
+ if config.respond_to?"has_key?"
399
+ attr = config[:attribute] || dnattr()
400
+ val = config[:value] || '*'
401
+ objects = config[:objects]
402
+ end
403
+
404
+ matches = []
405
+
406
+ begin
407
+ # Get some attributes
408
+ @@conn.search(base(), LDAP::LDAP_SCOPE_SUBTREE, "(#{attr}=#{val})") do |m|
409
+ # Extract the dnattr value
410
+ dnval = m.dn.split(/,/)[0].split(/=/)[1]
411
+
412
+ if objects
413
+ matches.push(eval("#{real_klass}.new(m)"))
414
+ else
415
+ matches.push(dnval)
416
+ end
417
+ end
418
+ rescue RuntimeError => detail
419
+ #p @@conn.err2string(@@conn.err)
420
+ @@logger.debug "No matches for #{attr}=#{val}"
421
+ # Do nothing on failure
422
+ end
423
+ return matches
424
+ end
425
+ private_class_method :find_all
426
+
427
+ # Base.base
428
+ #
429
+ # This method when included into Base provides
430
+ # an inheritable, overwritable configuration setting
431
+ #
432
+ # This should be a string with the base of the
433
+ # ldap server such as 'dc=example,dc=com', and
434
+ # it should be overwritten by including
435
+ # configuration.rb into this class.
436
+ # When subclassing, the specified prefix will be concatenated.
437
+ def Base.base
438
+ 'dc=example,dc=com'
439
+ end
440
+
441
+ # Base.dnattr
442
+ #
443
+ # This is a placeholder for the class method that will
444
+ # be overridden on calling ldap_mapping in a subclass.
445
+ # Using a class method allows for clean inheritance from
446
+ # classes that already have a ldap_mapping.
447
+ def Base.dnattr
448
+ ''
449
+ end
450
+
451
+ # Base.required_classes
452
+ #
453
+ # This method when included into Base provides
454
+ # an inheritable, overwritable configuration setting
455
+ #
456
+ # The value should be the minimum required objectClasses
457
+ # to make an object in the LDAP server, or an empty array [].
458
+ # This should be overwritten by configuration.rb.
459
+ # Note that subclassing does not cause concatenation of
460
+ # arrays to occurs.
461
+ def Base.required_classes
462
+ []
463
+ end
464
+
465
+
466
+
467
+ ### All instance methods, etc
468
+
469
+ # new
470
+ #
471
+ # Creates a new instance of Base initializing all class and all
472
+ # initialization. Defines local defaults. See examples If multiple values
473
+ # exist for dnattr, the first one put here will be authoritative
474
+ # TODO: Add # support for relative distinguished names
475
+ # val can be a dn attribute value, a full DN, or a LDAP::Entry. The use
476
+ # with a LDAP::Entry is primarily meant for internal use by find and
477
+ # find_all.
478
+ def initialize(val='')
479
+ # Try a default connection if none made explicitly
480
+ unless Base.connection
481
+ # Use @@config if it has been prepopulated and the conn is down.
482
+ if @@config
483
+ ActiveLDAP::Base.connect(@@config)
484
+ else
485
+ ActiveLDAP::Base.connect
486
+ end
487
+ end
488
+ if val.class == LDAP::Entry
489
+ # Call import, which is basically initialize
490
+ # without accessing LDAP.
491
+ @@logger.debug "initialize: val is a LDAP::Entry - running import."
492
+ import(val)
493
+ return
494
+ end
495
+ if val.class != String
496
+ raise TypeError, "Object key must be a String"
497
+ end
498
+
499
+ @data = {} # where the r/w entry data is stored
500
+ @data.default = []
501
+ @ldap_data = {} # original ldap entry data
502
+ @ldap_data.default = []
503
+ @attr_methods = {} # list of valid method calls for attributes used for dereferencing
504
+
505
+ # Break val apart if it is a dn
506
+ if val.match(/^#{dnattr()}=([^,=]+),#{base()}$/i)
507
+ val = $1
508
+ elsif val.match(/[=,]/)
509
+ @@logger.info "initialize: Changing val from '#{val}' to '' because it doesn't match the DN."
510
+ val = ''
511
+ end
512
+
513
+ # Do a search - if it exists, pull all data and parse schema, if not, just set the hierarchical data
514
+ if val.empty?
515
+ @exists = false
516
+ # Setup what should eb authoritative
517
+ @dn = "#{dnattr()}=#{val},#{base()}"
518
+ send(:apply_objectclass, required_classes())
519
+ else # do a search then
520
+ # Search for the existing entry
521
+ begin
522
+ # Get some attributes
523
+ Base.connection.search("#{dnattr()}=#{val},#{base()}", LDAP::LDAP_SCOPE_SUBTREE, "objectClass=*") do |m|
524
+ # Save DN
525
+ @dn = m.dn
526
+ # Load up data into tmp
527
+ @@logger.debug("loading entry: #{@dn}")
528
+ m.attrs.each do |attr|
529
+ # Load with subtypes just like @data
530
+ @@logger.debug("calling make_subtypes for m.vals(attr).dup")
531
+ safe_attr, value = make_subtypes(attr, m.vals(attr).dup)
532
+ @@logger.debug("finished make_subtypes for #{attr}")
533
+ # Add subtype to any existing values
534
+ if @ldap_data.has_key? safe_attr
535
+ value.each do |v|
536
+ @ldap_data[safe_attr].push(v)
537
+ end
538
+ else
539
+ @ldap_data[safe_attr] = value
540
+ end
541
+ end
542
+ end
543
+ @exists = true
544
+ # Populate schema data
545
+ send(:apply_objectclass, @ldap_data['objectClass'])
546
+
547
+ # Populate real data now that we have the schema with aliases
548
+ @ldap_data.each do |pair|
549
+ send(:attribute_method=, pair[0], pair[1].dup)
550
+ end
551
+
552
+ rescue LDAP::ResultError
553
+ @exists = false
554
+ # Create what should be the authoritative DN
555
+ @dn = "#{dnattr()}=#{val},#{base()}"
556
+ send(:apply_objectclass, required_classes())
557
+
558
+ # Setup dn attribute (later rdn too!)
559
+ attr_sym = "#{dnattr()}=".to_sym
560
+ @@logger.debug("new: setting dnattr: #{dnattr()} = #{val}")
561
+ send(attr_sym, val)
562
+ end
563
+ end
564
+ end # initialize
565
+
566
+ # Hide new in Base
567
+ private_class_method :new
568
+
569
+ # attributes
570
+ #
571
+ # Return attribute methods so that a program can determine available
572
+ # attributes dynamically without schema awareness
573
+ def attributes
574
+ @@logger.debug("stub: attributes called")
575
+ send(:apply_objectclass, @data['objectClass']) if @data['objectClass'] != @last_oc
576
+ return @attr_methods.keys
577
+ end
578
+
579
+ # exists?
580
+ #
581
+ # Return whether the entry exists in LDAP or not
582
+ def exists?
583
+ @@logger.debug("stub: exists? called")
584
+ return @exists
585
+ end
586
+
587
+ # dn
588
+ #
589
+ # Return the authoritative dn
590
+ def dn
591
+ @@logger.debug("stub: dn called")
592
+ return @dn.dup
593
+ end
594
+
595
+ # validate
596
+ #
597
+ # Basic validation:
598
+ # - Verify that every 'MUST' specified in the schema has a value defined
599
+ # - Enforcement of undefined attributes is handled in the objectClass= method
600
+ # Must call enforce_types() first before enforcement can be guaranteed
601
+ def validate
602
+ @@logger.debug("stub: validate called")
603
+ # Clean up attr values, etc
604
+ send(:enforce_types)
605
+
606
+ # Validate objectclass settings
607
+ @data['objectClass'].each do |klass|
608
+ unless klass.class == String
609
+ raise TypeError, "Value in objectClass array is not a String. (#{klass.class}:#{klass.inspect})"
610
+ end
611
+ unless Base.schema.names("objectClasses").member? klass
612
+ raise ObjectClassError, "objectClass '#{klass}' unknown to LDAP server."
613
+ end
614
+ end
615
+
616
+ # make sure this doesn't drop any of the required objectclasses
617
+ required_classes().each do |oc|
618
+ unless @data['objectClass'].member? oc.to_s
619
+ raise ObjectClassError, "'#{oc}' must be a defined objectClass for class '#{self.class}' as set in the ldap_mapping"
620
+ end
621
+ end
622
+
623
+ # Make sure all MUST attributes have a value
624
+ @data['objectClass'].each do |objc|
625
+ @must.each do |req_attr|
626
+ deref = @attr_methods[req_attr]
627
+ if @data[deref] == []
628
+ raise AttributeEmpty,
629
+ "objectClass '#{objc}' requires attribute '#{Base.schema.attribute_aliases(req_attr).join(', ')}'"
630
+ end
631
+ end
632
+ end
633
+ @@logger.debug("stub: validate finished")
634
+ end
635
+
636
+
637
+ # delete
638
+ #
639
+ # Delete this entry from LDAP
640
+ def delete
641
+ @@logger.debug("stub: delete called")
642
+ begin
643
+ @@conn.delete(@dn)
644
+ @exists = false
645
+ rescue LDAP::ResultError => detail
646
+ raise DeleteError, "Failed to delete LDAP entry: '#{@dn}'"
647
+ end
648
+ end
649
+
650
+
651
+ # write
652
+ #
653
+ # Write and validate this object into LDAP
654
+ # either adding or replacing attributes
655
+ # TODO: Binary data support
656
+ # TODO: Relative DN support
657
+ def write
658
+ @@logger.debug("stub: write called")
659
+ # Validate against the objectClass requirements
660
+ validate
661
+
662
+ # Put all changes into one change entry to ensure
663
+ # automatic rollback upon failure.
664
+ entry = []
665
+
666
+
667
+ # Expand subtypes to real ldap_data entries
668
+ # We can't reuse @ldap_data because an exception would leave
669
+ # an object in an unknown state
670
+ @@logger.debug("#write: dup'ing @ldap_data")
671
+ ldap_data = @ldap_data.dup
672
+ @@logger.debug("#write: dup finished @ldap_data")
673
+ @@logger.debug("#write: expanding subtypes in @ldap_data")
674
+ ldap_data.keys.each do |key|
675
+ ldap_data[key].each do |value|
676
+ if value.class == Hash
677
+ suffix, real_value = extract_subtypes(value)
678
+ if ldap_data.has_key? key + suffix
679
+ ldap_data[key + suffix].push(real_value)
680
+ else
681
+ ldap_data[key + suffix] = real_value
682
+ end
683
+ ldap_data[key].delete(value)
684
+ end
685
+ end
686
+ end
687
+ @@logger.debug("#write: subtypes expanded for @ldap_data")
688
+
689
+ # Expand subtypes to real data entries, but leave @data alone
690
+ @@logger.debug("#write: dup'ing @data")
691
+ data = @data.dup
692
+ @@logger.debug("#write: finished dup'ing @data")
693
+ @@logger.debug("#write: expanding subtypes for @data")
694
+ data.keys.each do |key|
695
+ data[key].each do |value|
696
+ if value.class == Hash
697
+ suffix, real_value = extract_subtypes(value)
698
+ if data.has_key? key + suffix
699
+ data[key + suffix].push(real_value)
700
+ else
701
+ data[key + suffix] = real_value
702
+ end
703
+ data[key].delete(value)
704
+ end
705
+ end
706
+ end
707
+ @@logger.debug("#write: subtypes expanded for @data")
708
+
709
+
710
+ if @exists
711
+ # Cycle through all attrs to determine action
712
+ action = {}
713
+
714
+ replaceable = []
715
+ # Now that all the subtypes will be treated as unique attributes
716
+ # we can see what's changed and add anything that is brand-spankin'
717
+ # new.
718
+ @@logger.debug("#write: traversing ldap_data determining replaces and deletes")
719
+ ldap_data.each do |pair|
720
+ suffix = ''
721
+ binary = 0
722
+
723
+ name, *suffix_a = pair[0].split(/;/)
724
+ suffix = ';'+ suffix_a.join(';') if suffix_a.size > 0
725
+ name = @attr_methods[name]
726
+ name = pair[0].split(/;/)[0] if name.nil? # for objectClass, or removed vals
727
+ value = data[name+suffix]
728
+
729
+ # Detect subtypes and account for them
730
+ binary = LDAP::LDAP_MOD_BVALUES if Base.schema.binary? name
731
+
732
+ replaceable.push(name+suffix)
733
+ if pair[1] != value
734
+ # Create mod entries
735
+ if not value.empty?
736
+ # Ditched delete then replace because attribs with no equality match rules
737
+ # will fails
738
+ @@logger.debug("updating attribute of existing entry: #{name+suffix}: #{value.inspect}")
739
+ entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, value))
740
+ else
741
+ # Since some types do not have equality matching rules, delete doesn't work
742
+ # Replacing with nothing is equivalent.
743
+ @@logger.debug("removing attribute from existing entry: #{name+suffix}")
744
+ entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, []))
745
+ end
746
+ end
747
+ end
748
+ @@logger.debug("#write: finished traversing ldap_data")
749
+ @@logger.debug("#write: traversing data determining adds")
750
+ data.each do |pair|
751
+ suffix = ''
752
+ binary = 0
753
+
754
+ name, *suffix_a = pair[0].split(/;/)
755
+ suffix = ';' + suffix_a.join(';') if suffix_a.size > 0
756
+ name = @attr_methods[name]
757
+ name = pair[0].split(/;/)[0] if name.nil? # for obj class or removed vals
758
+ value = pair[1]
759
+
760
+ if not replaceable.member? name+suffix
761
+ # Detect subtypes and account for them
762
+ binary = LDAP::LDAP_MOD_BVALUES if Base.schema.binary? name
763
+ @@logger.debug("adding attribute to existing entry: #{name+suffix}: #{value.inspect}")
764
+ # REPLACE will function like ADD, but doesn't hit EQUALITY problems
765
+ # TODO: Added equality(attr) to Schema2
766
+ entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, value)) unless value.empty?
767
+ end
768
+ end
769
+ @@logger.debug("#write: traversing data complete")
770
+ begin
771
+ @@logger.debug("#write: modifying #{@dn}")
772
+ @@conn.modify(@dn, entry)
773
+ @@logger.debug("#write: modify successful")
774
+ rescue => detail
775
+ raise WriteError, "Could not update LDAP entry: #{detail}"
776
+ end
777
+ else # add everything!
778
+ @@logger.debug("#write: adding all attribute value pairs")
779
+ @@logger.debug("#write: adding #{@attr_methods[dnattr()].inspect} = #{data[@attr_methods[dnattr()]].inspect}")
780
+ entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD, @attr_methods[dnattr()],
781
+ data[@attr_methods[dnattr()]]))
782
+ @@logger.debug("#write: adding objectClass = #{data[@attr_methods['objectClass']].inspect}")
783
+ entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD, 'objectClass',
784
+ data[@attr_methods['objectClass']]))
785
+ data.each do |pair|
786
+ if pair[1].size > 0 and pair[0] != 'objectClass' and pair[0] != @attr_methods[dnattr()]
787
+ # Detect subtypes and account for them
788
+ if Base.schema.binary? pair[0].split(/;/)[0]
789
+ binary = LDAP::LDAP_MOD_BVALUES
790
+ else
791
+ binary = 0
792
+ end
793
+ @@logger.debug("adding attribute to new entry: #{pair[0].inspect}: #{pair[1].inspect}")
794
+ entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD|binary, pair[0], pair[1]))
795
+ end
796
+ end
797
+ begin
798
+ @@logger.debug("#write: adding #{@dn}")
799
+ @@conn.add(@dn, entry)
800
+ @@logger.debug("#write: add successful")
801
+ @exists = true
802
+ rescue LDAP::ResultError => detail
803
+ raise WriteError, "Could not add LDAP entry[#{Base.connection.err2string(Base.connection.err)}]: #{detail}"
804
+ end
805
+ end
806
+ @@logger.debug("#write: resetting @ldap_data to a dup of @data")
807
+ @ldap_data = @data.dup
808
+ @@logger.debug("#write: @ldap_data reset complete")
809
+ @@logger.debug("stub: write exitted")
810
+ end
811
+
812
+
813
+ # method_missing
814
+ #
815
+ # If a given method matches an attribute or an attribute alias
816
+ # then call the appropriate method.
817
+ # TODO: Determine if it would be better to define each allowed method
818
+ # using class_eval instead of using method_missing. This would
819
+ # give tab completion in irb.
820
+ def method_missing(name, *args)
821
+ @@logger.debug("stub: called method_missing(#{name.inspect}, #{args.inspect})")
822
+
823
+ # dynamically update the available attributes without requiring an
824
+ # explicit call. The cache 'last_oc' saves a lot of cpu time.
825
+ if @data['objectClass'] != @last_oc
826
+ @@logger.debug("method_missing(#{name.inspect}, #{args.inspect}): updating apply_objectclass(#{@data['objectClass'].inspect})")
827
+ send(:apply_objectclass, @data['objectClass'])
828
+ end
829
+ key = name.to_s
830
+ case key
831
+ when /^(\S+)=$/
832
+ real_key = $1
833
+ @@logger.debug("method_missing: attr_methods has_key? #{real_key}")
834
+ if @attr_methods.has_key? real_key
835
+ raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" if args.size != 1
836
+ @@logger.debug("method_missing: calling :attribute_method=(#{real_key}, #{args[0]})")
837
+ return send(:attribute_method=, real_key, args[0])
838
+ end
839
+ else
840
+ @@logger.debug("method_missing: attr_methods has_key? #{key}")
841
+ if @attr_methods.has_key? key
842
+ raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" if args.size > 1
843
+ return attribute_method(key, *args)
844
+ end
845
+ end
846
+ raise NoMethodError, "undefined method `#{key}' for #{self}"
847
+ end
848
+
849
+ # Add available attributes to the methods
850
+ alias_method :__methods, :methods
851
+ def methods
852
+ return __methods + attributes()
853
+ end
854
+
855
+
856
+ private
857
+
858
+ # import(LDAP::Entry)
859
+ #
860
+ # Overwrites an existing entry (usually called by new)
861
+ # with the data given in the data given in LDAP::Entry.
862
+ #
863
+ def import(entry=nil)
864
+ @@logger.debug("stub: import called")
865
+ if entry.class != LDAP::Entry
866
+ raise TypeError, "argument must be a LDAP::Entry"
867
+ end
868
+
869
+ @data = {} # where the r/w entry data is stored
870
+ @data.default = []
871
+ @ldap_data = {} # original ldap entry data
872
+ @ldap_data.default = []
873
+ @attr_methods = {} # list of valid method calls for attributes used for dereferencing
874
+
875
+ # Get some attributes
876
+ @dn = entry.dn
877
+ entry.attrs.each do |attr|
878
+ # Load with subtypes just like @data
879
+ @@logger.debug("calling make_subtypes for entry.vals(attr).dup")
880
+ safe_attr, value = make_subtypes(attr, entry.vals(attr).dup)
881
+ @@logger.debug("finished make_subtypes for #{attr}")
882
+ # Add subtype to any existing values
883
+ if @ldap_data.has_key? safe_attr
884
+ value.each do |v|
885
+ @ldap_data[safe_attr].push(v)
886
+ end
887
+ else
888
+ @ldap_data[safe_attr] = value
889
+ end
890
+ end
891
+ # Assume if we are importing it that it exists
892
+ @exists = true
893
+ # Populate schema data
894
+ send(:apply_objectclass, @ldap_data['objectClass'])
895
+
896
+ # Populate real data now that we have the schema with aliases
897
+ @ldap_data.each do |pair|
898
+ send(:attribute_method=, pair[0], pair[1].dup)
899
+ end
900
+ end # import
901
+
902
+ # enforce_types
903
+ #
904
+ # enforce_types applies your changes without attempting to write to LDAP. This means that
905
+ # if you set userCertificate to somebinary value, it will wrap it up correctly.
906
+ def enforce_types
907
+ @@logger.debug("stub: enforce_types called")
908
+ send(:apply_objectclass, @data['objectClass']) if @data['objectClass'] != @last_oc
909
+ # Enforce attribute value formatting
910
+ @data.keys.each do |key|
911
+ @data[key] = attribute_input_handler(key, @data[key])
912
+ end
913
+ @@logger.debug("stub: enforce_types done")
914
+ return true
915
+ end
916
+
917
+ # apply_objectclass
918
+ #
919
+ # objectClass= special case for updating appropriately
920
+ # This updates the objectClass entry in @data. It also
921
+ # updating all required and allowed attributes while
922
+ # removing defined attributes that are no longer valid
923
+ # given the new objectclasses.
924
+ def apply_objectclass(val)
925
+ @@logger.debug("stub: objectClass=(#{val.inspect}) called")
926
+ new_oc = val
927
+ new_oc = [val] if new_oc.class != Array
928
+ return new_oc if @last_oc == new_oc
929
+
930
+ # Store for caching purposes
931
+ @last_oc = new_oc.dup
932
+
933
+ # Set the actual objectClass data
934
+ define_attribute_methods('objectClass')
935
+ @data['objectClass'] = new_oc.uniq
936
+
937
+ # Build |data| from schema
938
+ # clear attr_method mapping first
939
+ @attr_methods = {}
940
+ @must = []
941
+ @may = []
942
+ new_oc.each do |objc|
943
+ # get all attributes for the class
944
+ attributes = Base.schema.class_attributes(objc.to_s)
945
+ @must += attributes[:must]
946
+ @may += attributes[:may]
947
+ end
948
+ @must.uniq!
949
+ @may.uniq!
950
+ (@must+@may).each do |attr|
951
+ # Update attr_method with appropriate
952
+ define_attribute_methods(attr)
953
+ end
954
+
955
+ # Delete all now innew_ocid attributes given the new objectClasses
956
+ @data.keys.each do |key|
957
+ # If it's not a proper aliased attribute, drop it
958
+ unless @attr_methods.has_key? key
959
+ @data.delete(key)
960
+ end
961
+ end
962
+ end
963
+
964
+
965
+
966
+ # Enforce typing:
967
+ # Hashes are for subtypes
968
+ # Arrays are for multiple entries
969
+ def attribute_input_handler(attr, value)
970
+ @@logger.debug("stub: called attribute_input_handler(#{attr.inspect}, #{value.inspect})")
971
+ if attr.nil?
972
+ raise RuntimeError, 'The first argument, attr, must not be nil. Please report this as a bug!'
973
+ end
974
+ binary = Base.schema.binary_required? attr
975
+ single = Base.schema.single_value? attr
976
+ case value.class.to_s
977
+ when 'Array'
978
+ if single and value.size > 1
979
+ raise TypeError, "Attribute #{attr} can only have a single value"
980
+ end
981
+ value.map! do |entry|
982
+ if entry.class != Hash
983
+ @@logger.debug("coercing value for #{attr} into a string because nested values exceeds a useful depth: #{entry.inspect} -> #{entry.to_s}")
984
+ entry = entry.to_s
985
+ end
986
+ entry = attribute_input_handler(attr, entry)[0]
987
+ end
988
+ when 'Hash'
989
+ if value.keys.size > 1
990
+ raise TypeError, "Hashes must have one key-value pair only."
991
+ end
992
+ unless value.keys[0].match(/^(lang-[a-z][a-z]*)|(binary)$/)
993
+ @@logger.warn("unknown subtype did not match lang-* or binary: #{value.keys[0]}")
994
+ end
995
+ # Contents MUST be a String or an Array
996
+ if value.keys[0] != 'binary' and binary
997
+ suffix, real_value = extract_subtypes(value)
998
+ value = make_subtypes(name + suffix + ';binary', real_value)
999
+ end
1000
+ value = [value]
1001
+ when 'String'
1002
+ if binary
1003
+ value = {'binary' => value}
1004
+ end
1005
+ return [value]
1006
+ else
1007
+ value = [value.to_s]
1008
+ end
1009
+ return value
1010
+ end
1011
+
1012
+ # make_subtypes
1013
+ #
1014
+ # Makes the Hashized value from the full attributename
1015
+ # e.g. userCertificate;binary => "some_bin"
1016
+ # becomes userCertificate => {"binary" => "some_bin"}
1017
+ def make_subtypes(attr, value)
1018
+ @@logger.debug("stub: called make_subtypes(#{attr.inspect}, #{value.inspect})")
1019
+ return [attr, value] unless attr.match(/;/)
1020
+
1021
+ ret_attr, *subtypes = attr.split(/;/)
1022
+ return [ret_attr, [make_subtypes_helper(subtypes, value)]]
1023
+ end
1024
+
1025
+ # make_subtypes_helper
1026
+ #
1027
+ # This is a recursive function for building
1028
+ # nested hashed from multi-subtyped values
1029
+ def make_subtypes_helper(subtypes, value)
1030
+ @@logger.debug("stub: called make_subtypes_helper(#{subtypes.inspect}, #{value.inspect})")
1031
+ return value if subtypes.size == 0
1032
+ return {subtypes[0] => make_subtypes_helper(subtypes[1..-1], value)}
1033
+ end
1034
+
1035
+ # extract_subtypes
1036
+ #
1037
+ # Extracts all of the subtypes from a given set of nested hashes
1038
+ # and returns the attribute suffix and the final true value
1039
+ def extract_subtypes(value)
1040
+ @@logger.debug("stub: called extract_subtypes(#{value.inspect})")
1041
+ subtype = ''
1042
+ ret_val = value
1043
+ if value.class == Hash
1044
+ subtype = ';' + value.keys[0]
1045
+ ret_val = value[value.keys[0]]
1046
+ subsubtype = ''
1047
+ if ret_val.class == Hash
1048
+ subsubtype, ret_val = extract_subtypes(ret_val)
1049
+ end
1050
+ subtype += subsubtype
1051
+ end
1052
+ ret_val = [ret_val] unless ret_val.class == Array
1053
+ return subtype, ret_val
1054
+ end
1055
+
1056
+
1057
+ # Wrapper all bind activity
1058
+ def Base.do_bind()
1059
+ bind_dn = @@config[:bind_format] % [@@config[:user]]
1060
+ if @@config[:password_block]
1061
+ password = @@config[:password_block].call
1062
+ @@config[:password_block] = Proc.new { password }
1063
+ end
1064
+
1065
+ # Rough bind loop:
1066
+ # Attempt 1: SASL if available
1067
+ # Attempt 2: SIMPLE with credentials if password block
1068
+ # Attempt 3: SIMPLE ANONYMOUS if 1 and 2 fail (or pwblock returns '')
1069
+ auth = false
1070
+ auth = do_sasl_bind(bind_dn) if @@config[:try_sasl]
1071
+ auth = do_simple_bind(bind_dn) unless auth
1072
+ auth = do_anonymous_bind(bind_dn) if not auth and @@config[:allow_anonymous]
1073
+
1074
+ unless auth
1075
+ raise AuthenticationError, "All authentication mechanisms failed"
1076
+ end
1077
+ return auth
1078
+ end
1079
+
1080
+
1081
+ # Base.do_anonymous_bind
1082
+ #
1083
+ # Bind to LDAP with the given DN, but with no password. (anonymous!)
1084
+ def Base.do_anonymous_bind(bind_dn)
1085
+ @@logger.info "Attempting anonymous authentication"
1086
+ begin
1087
+ @@conn.bind()
1088
+ return true
1089
+ rescue
1090
+ @@logger.debug "LDAP Error: #{@@conn.err2string(@@conn.err)}"
1091
+ @@logger.warn "Warning: Anonymous authentication failed."
1092
+ return false
1093
+ end
1094
+ end
1095
+
1096
+ # Base.do_simple_bind
1097
+ #
1098
+ # Bind to LDAP with the given DN and password_block.call()
1099
+ def Base.do_simple_bind(bind_dn)
1100
+ return false unless @@config[:password_block].respond_to? :call
1101
+ begin
1102
+ @@conn.bind(bind_dn, @@config[:password_block].call())
1103
+ return true
1104
+ rescue
1105
+ @@logger.debug "LDAP Error: #{@@conn.err2string(@@conn.err)}"
1106
+ @@logger.warn "Warning: SIMPLE authentication failed."
1107
+ return false
1108
+ end
1109
+ end
1110
+
1111
+ # Base.do_sasl_bind
1112
+ #
1113
+ # Bind to LDAP with the given DN using any available SASL methods
1114
+ def Base.do_sasl_bind(bind_dn)
1115
+ # Get all SASL mechanisms
1116
+ mechanisms = @@conn.root_dse[0]['supportedSASLMechanisms']
1117
+ # Use GSSAPI if available
1118
+ # Currently only GSSAPI is supported with Ruby/LDAP from
1119
+ # http://caliban.com/files/redhat/RPMS/i386/ruby-ldap-0.8.2-4.i386.rpm
1120
+ # TODO: Investigate further SASL support
1121
+ if mechanisms.respond_to? :member? and mechanisms.member? 'GSSAPI'
1122
+ begin
1123
+ @@conn.sasl_bind(bind_dn, 'GSSAPI')
1124
+ return true
1125
+ rescue
1126
+ @@logger.debug "LDAP Error: #{@@conn.err2string(@@conn.err)}"
1127
+ @@logger.warn "Warning: SASL GSSAPI authentication failed."
1128
+ return false
1129
+ end
1130
+ end
1131
+ return false
1132
+ end
1133
+
1134
+ # base
1135
+ #
1136
+ # Returns the value of self.class.base
1137
+ # This is just syntactic sugar
1138
+ def base
1139
+ @@logger.debug("stub: called base")
1140
+ self.class.base
1141
+ end
1142
+
1143
+ # required_classes
1144
+ #
1145
+ # Returns the value of self.class.required_classes
1146
+ # This is just syntactic sugar
1147
+ def required_classes
1148
+ @@logger.debug("stub: called required_classes")
1149
+ self.class.required_classes
1150
+ end
1151
+
1152
+ # dnattr
1153
+ #
1154
+ # Returns the value of self.class.dnattr
1155
+ # This is just syntactic sugar
1156
+ def dnattr
1157
+ @@logger.debug("stub: called dnattr")
1158
+ self.class.dnattr
1159
+ end
1160
+
1161
+ # attribute_method
1162
+ #
1163
+ # Return the value of the attribute called by method_missing?
1164
+ def attribute_method(method, not_array = false)
1165
+ @@logger.debug("stub: called attribute_method(#{method.inspect}, #{not_array.inspect}")
1166
+ attr = @attr_methods[method]
1167
+
1168
+ # Return a copy of the stored data
1169
+ return array_of(@data[attr].dup, false) if not_array
1170
+ return @data[attr]
1171
+ end
1172
+
1173
+
1174
+ # attribute_method=
1175
+ #
1176
+ # Set the value of the attribute called by method_missing?
1177
+ def attribute_method=(method, value)
1178
+ @@logger.debug("stub: called attribute_method=(#{method.inspect}, #{value.inspect})")
1179
+ # Get the attr and clean up the input
1180
+ attr = @attr_methods[method]
1181
+ @@logger.debug("attribute_method=(#{method.inspect}, #{value.inspect}): method maps to #{attr}")
1182
+
1183
+ # Assign the value
1184
+ @data[attr] = value
1185
+
1186
+ # Return the passed in value
1187
+ @@logger.debug("stub: exitting attribute_method=")
1188
+ return @data[attr]
1189
+ end
1190
+
1191
+
1192
+ # define_attribute_methods
1193
+ #
1194
+ # Make a method entry for _every_ alias of a valid attribute and map it
1195
+ # onto the first attribute passed in.
1196
+ def define_attribute_methods(attr)
1197
+ @@logger.debug("stub: called define_attribute_methods(#{attr.inspect})")
1198
+ if @attr_methods.has_key? attr
1199
+ return
1200
+ end
1201
+ aliases = Base.schema.attribute_aliases(attr)
1202
+ aliases.each do |ali|
1203
+ @@logger.debug("associating #{ali} --> #{attr}")
1204
+ @attr_methods[ali] = attr
1205
+ end
1206
+ @@logger.debug("stub: leaving define_attribute_methods(#{attr.inspect})")
1207
+ end
1208
+
1209
+ # array_of
1210
+ #
1211
+ # Returns the array form of a value, or not an array if
1212
+ # false is passed in.
1213
+ def array_of(value, to_a = true)
1214
+ @@logger.debug("stub: called array_of(#{value.inspect}, #{to_a.inspect})")
1215
+ if to_a
1216
+ case value.class.to_s
1217
+ when 'Array'
1218
+ return value
1219
+ when 'Hash'
1220
+ return [value]
1221
+ else
1222
+ return [value.to_s]
1223
+ end
1224
+ else
1225
+ case value.class.to_s
1226
+ when 'Array'
1227
+ return nil if value.size == 0
1228
+ return value[0] if value.size == 1
1229
+ return value
1230
+ when 'Hash'
1231
+ return value
1232
+ else
1233
+ return value.to_s
1234
+ end
1235
+ end
1236
+ end
1237
+
1238
+ end # Base
1239
+
1240
+ end # ActiveLDAP
1241
+
1242
+
1243
+
1244
+