ruby-activeldap 0.8.1 → 0.8.2

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.
Files changed (65) hide show
  1. data/CHANGES +5 -0
  2. data/Manifest.txt +91 -25
  3. data/README +22 -0
  4. data/Rakefile +41 -8
  5. data/TODO +1 -6
  6. data/examples/config.yaml.example +5 -0
  7. data/examples/example.der +0 -0
  8. data/examples/example.jpg +0 -0
  9. data/examples/groupadd +41 -0
  10. data/examples/groupdel +35 -0
  11. data/examples/groupls +49 -0
  12. data/examples/groupmod +42 -0
  13. data/examples/lpasswd +55 -0
  14. data/examples/objects/group.rb +13 -0
  15. data/examples/objects/ou.rb +4 -0
  16. data/examples/objects/user.rb +20 -0
  17. data/examples/ouadd +38 -0
  18. data/examples/useradd +45 -0
  19. data/examples/useradd-binary +50 -0
  20. data/examples/userdel +34 -0
  21. data/examples/userls +50 -0
  22. data/examples/usermod +42 -0
  23. data/examples/usermod-binary-add +47 -0
  24. data/examples/usermod-binary-add-time +51 -0
  25. data/examples/usermod-binary-del +48 -0
  26. data/examples/usermod-lang-add +43 -0
  27. data/lib/active_ldap.rb +213 -214
  28. data/lib/active_ldap/adapter/base.rb +461 -0
  29. data/lib/active_ldap/adapter/ldap.rb +232 -0
  30. data/lib/active_ldap/adapter/ldap_ext.rb +69 -0
  31. data/lib/active_ldap/adapter/net_ldap.rb +288 -0
  32. data/lib/active_ldap/adapter/net_ldap_ext.rb +29 -0
  33. data/lib/active_ldap/association/belongs_to.rb +3 -1
  34. data/lib/active_ldap/association/belongs_to_many.rb +5 -6
  35. data/lib/active_ldap/association/has_many.rb +9 -17
  36. data/lib/active_ldap/association/has_many_wrap.rb +4 -5
  37. data/lib/active_ldap/attributes.rb +4 -0
  38. data/lib/active_ldap/base.rb +201 -56
  39. data/lib/active_ldap/configuration.rb +11 -1
  40. data/lib/active_ldap/connection.rb +15 -9
  41. data/lib/active_ldap/distinguished_name.rb +246 -0
  42. data/lib/active_ldap/ldap_error.rb +74 -0
  43. data/lib/active_ldap/object_class.rb +9 -5
  44. data/lib/active_ldap/schema.rb +50 -9
  45. data/lib/active_ldap/validations.rb +11 -13
  46. data/rails/plugin/active_ldap/generators/scaffold_al/scaffold_al_generator.rb +7 -0
  47. data/rails/plugin/active_ldap/generators/scaffold_al/templates/ldap.yml +21 -0
  48. data/rails/plugin/active_ldap/init.rb +10 -4
  49. data/test/al-test-utils.rb +46 -3
  50. data/test/run-test.rb +16 -4
  51. data/test/test-unit-ext/always-show-result.rb +28 -0
  52. data/test/test-unit-ext/priority.rb +163 -0
  53. data/test/test_adapter.rb +81 -0
  54. data/test/test_attributes.rb +8 -1
  55. data/test/test_base.rb +132 -3
  56. data/test/test_base_per_instance.rb +14 -3
  57. data/test/test_connection.rb +19 -0
  58. data/test/test_dn.rb +161 -0
  59. data/test/test_find.rb +24 -0
  60. data/test/test_object_class.rb +15 -2
  61. data/test/test_schema.rb +108 -1
  62. metadata +111 -41
  63. data/lib/active_ldap/adaptor/base.rb +0 -29
  64. data/lib/active_ldap/adaptor/ldap.rb +0 -466
  65. data/lib/active_ldap/ldap.rb +0 -113
@@ -0,0 +1,232 @@
1
+ require 'active_ldap/adapter/base'
2
+
3
+ module ActiveLdap
4
+ module Adapter
5
+ class Base
6
+ class << self
7
+ def ldap_connection(options)
8
+ unless defined?(::LDAP)
9
+ require 'active_ldap/adapter/ldap_ext'
10
+ end
11
+ Ldap.new(options)
12
+ end
13
+ end
14
+ end
15
+
16
+ class Ldap < Base
17
+ module Method
18
+ class SSL
19
+ def connect(host, port)
20
+ LDAP::SSLConn.new(host, port, false)
21
+ end
22
+ end
23
+
24
+ class TLS
25
+ def connect(host, port)
26
+ LDAP::SSLConn.new(host, port, true)
27
+ end
28
+ end
29
+
30
+ class Plain
31
+ def connect(host, port)
32
+ LDAP::Conn.new(host, port)
33
+ end
34
+ end
35
+ end
36
+
37
+ def connect(options={})
38
+ super do |host, port, method|
39
+ method.connect(host, port)
40
+ end
41
+ end
42
+
43
+ def unbind(options={})
44
+ return unless bound?
45
+ operation(options) do
46
+ execute(:unbind)
47
+ end
48
+ end
49
+
50
+ def bind(options={})
51
+ super do
52
+ @connection.error_message
53
+ end
54
+ end
55
+
56
+ def bind_as_anonymous(options={})
57
+ super do
58
+ execute(:bind)
59
+ true
60
+ end
61
+ end
62
+
63
+ def bound?
64
+ connecting? and @connection.bound?
65
+ end
66
+
67
+ def search(options={}, &block)
68
+ super(options) do |base, scope, filter, attrs, limit, callback|
69
+ begin
70
+ i = 0
71
+ execute(:search, base, scope, filter, attrs) do |entry|
72
+ i += 1
73
+ attributes = {}
74
+ entry.attrs.each do |attr|
75
+ attributes[attr] = entry.vals(attr)
76
+ end
77
+ callback.call([entry.dn, attributes], block)
78
+ break if limit and limit >= i
79
+ end
80
+ rescue RuntimeError
81
+ if $!.message == "no result returned by search"
82
+ @logger.debug {"No matches for #{filter} and attrs " +
83
+ "#{attrs.inspect}"}
84
+ else
85
+ raise
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ def to_ldif(dn, attributes)
92
+ ldif = LDAP::LDIF.to_ldif("dn", [dn.dup])
93
+ attributes.sort_by do |key, value|
94
+ key
95
+ end.each do |key, values|
96
+ ldif << LDAP::LDIF.to_ldif(key, values)
97
+ end
98
+ ldif
99
+ end
100
+
101
+ def load(ldifs, options={})
102
+ super do |ldif|
103
+ LDAP::LDIF.parse_entry(ldif).send(@connection)
104
+ end
105
+ end
106
+
107
+ def delete(targets, options={})
108
+ super do |target|
109
+ execute(:delete, target)
110
+ end
111
+ end
112
+
113
+ def add(dn, entries, options={})
114
+ super do |dn, entries|
115
+ execute(:add, dn, parse_entries(entries))
116
+ end
117
+ end
118
+
119
+ def modify(dn, entries, options={})
120
+ super do |dn, entries|
121
+ execute(:modify, dn, parse_entries(entries))
122
+ end
123
+ end
124
+
125
+ private
126
+ def prepare_connection(options={})
127
+ operation(options) do
128
+ @connection.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
129
+ end
130
+ end
131
+
132
+ def root_dse(attributes, options={})
133
+ sec = options[:sec] || 0
134
+ usec = options[:usec] || 0
135
+ @connection.root_dse(attributes, sec, usec)
136
+ end
137
+
138
+ def execute(method, *args, &block)
139
+ begin
140
+ @connection.send(method, *args, &block)
141
+ rescue LDAP::ResultError
142
+ @connection.assert_error_code
143
+ raise $!.message
144
+ end
145
+ end
146
+
147
+ def with_timeout(try_reconnect=true, options={}, &block)
148
+ begin
149
+ super
150
+ rescue LDAP::ServerDown => e
151
+ @logger.error {"LDAP server is down: #{e.message}"}
152
+ retry if try_reconnect and reconnect(options)
153
+ raise ConnectionError.new(e.message)
154
+ end
155
+ end
156
+
157
+ def ensure_method(method)
158
+ Method.constants.each do |name|
159
+ if method.to_s.downcase == name.downcase
160
+ return Method.const_get(name).new
161
+ end
162
+ end
163
+
164
+ available_methods = Method.constants.collect do |name|
165
+ name.downcase.to_sym.inspect
166
+ end.join(", ")
167
+ raise ConfigurationError,
168
+ "#{method.inspect} is not one of the available connect " +
169
+ "methods #{available_methods}"
170
+ end
171
+
172
+ def ensure_scope(scope)
173
+ scope_map = {
174
+ :base => LDAP::LDAP_SCOPE_BASE,
175
+ :sub => LDAP::LDAP_SCOPE_SUBTREE,
176
+ :one => LDAP::LDAP_SCOPE_ONELEVEL,
177
+ }
178
+ value = scope_map[scope || :sub]
179
+ if value.nil?
180
+ available_scopes = scope_map.keys.inspect
181
+ raise ArgumentError, "#{scope.inspect} is not one of the available " +
182
+ "LDAP scope #{available_scopes}"
183
+ end
184
+ value
185
+ end
186
+
187
+ def sasl_bind(bind_dn, options={})
188
+ super do |bind_dn, mechanism, quiet|
189
+ begin
190
+ sasl_quiet = @connection.sasl_quiet
191
+ @connection.sasl_quiet = quiet unless quiet.nil?
192
+ args = [bind_dn, mechanism]
193
+ if need_credential_sasl_mechanism?(mechanism)
194
+ args << password(bind_dn, options)
195
+ end
196
+ execute(:sasl_bind, *args)
197
+ ensure
198
+ @connection.sasl_quiet = sasl_quiet
199
+ end
200
+ end
201
+ end
202
+
203
+ def simple_bind(bind_dn, options={})
204
+ super do |bind_dn, passwd|
205
+ execute(:bind, bind_dn, passwd)
206
+ end
207
+ end
208
+
209
+ def parse_entries(entries)
210
+ result = []
211
+ entries.each do |type, key, attributes|
212
+ mod_type = ensure_mod_type(type)
213
+ binary = schema.binary?(key)
214
+ mod_type |= LDAP::LDAP_MOD_BVALUES if binary
215
+ attributes.each do |name, values|
216
+ result << LDAP.mod(mod_type, name, values)
217
+ end
218
+ end
219
+ result
220
+ end
221
+
222
+ def ensure_mod_type(type)
223
+ case type
224
+ when :replace, :add
225
+ LDAP.const_get("LDAP_MOD_#{type.to_s.upcase}")
226
+ else
227
+ raise ArgumentError, "unknown type: #{type}"
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,69 @@
1
+ require_library_or_gem 'ldap'
2
+ require 'ldap/ldif'
3
+ require 'ldap/schema'
4
+
5
+ module LDAP
6
+ class Mod
7
+ unless instance_method(:to_s).arity.zero?
8
+ alias_method :original_to_s, :to_s
9
+ def to_s
10
+ inspect
11
+ end
12
+ end
13
+
14
+ alias_method :_initialize, :initialize
15
+ def initialize(op, type, vals)
16
+ if (LDAP::VERSION.split(/\./).collect {|x| x.to_i} <=> [0, 9, 7]) <= 0
17
+ @op, @type, @vals = op, type, vals # to protect from GC
18
+ end
19
+ _initialize(op, type, vals)
20
+ end
21
+ end
22
+
23
+ IMPLEMENT_SPECIFIC_ERRORS = {}
24
+ {
25
+ 0x51 => "SERVER_DOWN",
26
+ 0x52 => "LOCAL_ERROR",
27
+ 0x53 => "ENCODING_ERROR",
28
+ 0x54 => "DECODING_ERROR",
29
+ 0x55 => "TIMEOUT",
30
+ 0x56 => "AUTH_UNKNOWN",
31
+ 0x57 => "FILTER_ERROR",
32
+ 0x58 => "USER_CANCELLED",
33
+ 0x59 => "PARAM_ERROR",
34
+ 0x5a => "NO_MEMORY",
35
+
36
+ 0x5b => "CONNECT_ERROR",
37
+ 0x5c => "NOT_SUPPORTED",
38
+ 0x5d => "CONTROL_NOT_FOUND",
39
+ 0x5e => "NO_RESULTS_RETURNED",
40
+ 0x5f => "MORE_RESULTS_TO_RETURN",
41
+ 0x60 => "CLIENT_LOOP",
42
+ 0x61 => "REFERRAL_LIMIT_EXCEEDED",
43
+ }.each do |code, name|
44
+ IMPLEMENT_SPECIFIC_ERRORS[code] =
45
+ ActiveLdap::LdapError.define(code, name, self)
46
+ end
47
+
48
+ class Conn
49
+ def failed?
50
+ not err.zero?
51
+ end
52
+
53
+ def error_message
54
+ if failed?
55
+ LDAP.err2string(err)
56
+ else
57
+ nil
58
+ end
59
+ end
60
+
61
+ def assert_error_code
62
+ return unless failed?
63
+ klass = ActiveLdap::LdapError::ERRORS[err]
64
+ klass ||= IMPLEMENT_SPECIFIC_ERRORS[err]
65
+ klass ||= ActiveLdap::LdapError
66
+ raise klass, LDAP.err2string(err)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,288 @@
1
+ require 'digest/md5'
2
+
3
+ require 'active_ldap/adapter/base'
4
+
5
+ module ActiveLdap
6
+ module Adapter
7
+ class Base
8
+ class << self
9
+ def net_ldap_connection(options)
10
+ unless defined?(::Net::LDAP)
11
+ require 'active_ldap/adapter/net_ldap_ext'
12
+ end
13
+ NetLdap.new(options)
14
+ end
15
+ end
16
+ end
17
+
18
+ class NetLdap < Base
19
+ METHOD = {
20
+ :ssl => :simple_tls,
21
+ :tls => :start_tls,
22
+ :plain => nil,
23
+ }
24
+
25
+ def connect(options={})
26
+ @bound = false
27
+ super do |host, port, method|
28
+ config = {
29
+ :host => host,
30
+ :port => port,
31
+ }
32
+ config[:encryption] = {:method => method} if method
33
+ Net::LDAP::Connection.new(config)
34
+ end
35
+ end
36
+
37
+ def unbind(options={})
38
+ @bound = false
39
+ end
40
+
41
+ def bind(options={})
42
+ @bound = false
43
+ begin
44
+ super
45
+ rescue Net::LDAP::LdapError
46
+ raise AuthenticationError, $!.message
47
+ end
48
+ end
49
+
50
+ def bind_as_anonymous(options={})
51
+ super do
52
+ @bound = false
53
+ execute(:bind, :method => :anonymous)
54
+ @bound = true
55
+ end
56
+ end
57
+
58
+ def bound?
59
+ connecting? and @bound
60
+ end
61
+
62
+ def search(options={}, &block)
63
+ super(options) do |base, scope, filter, attrs, limit, callback|
64
+ args = {
65
+ :base => base,
66
+ :scope => scope,
67
+ :filter => filter,
68
+ :attributes => attrs,
69
+ :size => limit,
70
+ }
71
+ execute(:search, args) do |entry|
72
+ attributes = {}
73
+ entry.original_attribute_names.each do |name|
74
+ attributes[name] = entry[name]
75
+ end
76
+ callback.call([entry.dn, attributes], block)
77
+ end
78
+ end
79
+ end
80
+
81
+ def to_ldif(dn, attributes)
82
+ entry = Net::LDAP::Entry.new(dn.dup)
83
+ attributes.each do |key, values|
84
+ entry[key] = values.flatten
85
+ end
86
+ entry.to_ldif
87
+ end
88
+
89
+ def load(ldifs, options={})
90
+ super do |ldif|
91
+ entry = Net::LDAP::Entry.from_single_ldif_string(ldif)
92
+ attributes = {}
93
+ entry.each do |name, values|
94
+ attributes[name] = values
95
+ end
96
+ attributes.delete(:dn)
97
+ execute(:add,
98
+ :dn => entry.dn,
99
+ :attributes => attributes)
100
+ end
101
+ end
102
+
103
+ def delete(targets, options={})
104
+ super do |target|
105
+ execute(:delete, :dn => target)
106
+ end
107
+ end
108
+
109
+ def add(dn, entries, options={})
110
+ super do |dn, entries|
111
+ attributes = {}
112
+ entries.each do |type, key, attrs|
113
+ attrs.each do |name, values|
114
+ attributes[name] = values
115
+ end
116
+ end
117
+ execute(:add, :dn => dn, :attributes => attributes)
118
+ end
119
+ end
120
+
121
+ def modify(dn, entries, options={})
122
+ super do |dn, entries|
123
+ execute(:modify,
124
+ :dn => dn,
125
+ :operations => parse_entries(entries))
126
+ end
127
+ end
128
+
129
+ private
130
+ def execute(method, *args, &block)
131
+ result = @connection.send(method, *args, &block)
132
+ message = nil
133
+ if result.is_a?(Hash)
134
+ message = result[:errorMessage]
135
+ result = result[:resultCode]
136
+ end
137
+ unless result.zero?
138
+ klass = LdapError::ERRORS[result]
139
+ klass ||= LdapError
140
+ raise klass,
141
+ [Net::LDAP.result2string(result), message].compact.join(": ")
142
+ end
143
+ end
144
+
145
+ def root_dse(attrs, options={})
146
+ search(:base => "",
147
+ :scope => :base,
148
+ :attributes => attrs).collect do |dn, attributes|
149
+ attributes
150
+ end
151
+ end
152
+
153
+ def ensure_method(method)
154
+ method ||= "plain"
155
+ normalized_method = method.to_s.downcase.to_sym
156
+ return METHOD[normalized_method] if METHOD.has_key?(normalized_method)
157
+
158
+ available_methods = METHOD.keys.collect {|m| m.inspect}.join(", ")
159
+ raise ConfigurationError,
160
+ "#{method.inspect} is not one of the available connect " +
161
+ "methods #{available_methods}"
162
+ end
163
+
164
+ def ensure_scope(scope)
165
+ scope_map = {
166
+ :base => Net::LDAP::SearchScope_BaseObject,
167
+ :sub => Net::LDAP::SearchScope_WholeSubtree,
168
+ :one => Net::LDAP::SearchScope_SingleLevel,
169
+ }
170
+ value = scope_map[scope || :sub]
171
+ if value.nil?
172
+ available_scopes = scope_map.keys.inspect
173
+ raise ArgumentError, "#{scope.inspect} is not one of the available " +
174
+ "LDAP scope #{available_scopes}"
175
+ end
176
+ value
177
+ end
178
+
179
+ def sasl_bind(bind_dn, options={})
180
+ super do |bind_dn, mechanism, quiet|
181
+ normalized_mechanism = mechanism.downcase.gsub(/-/, '_')
182
+ sasl_bind_setup = "sasl_bind_setup_#{normalized_mechanism}"
183
+ next unless respond_to?(sasl_bind_setup, true)
184
+ initial_credential, challenge_response =
185
+ send(sasl_bind_setup, bind_dn, options)
186
+ args = {
187
+ :method => :sasl,
188
+ :initial_credential => initial_credential,
189
+ :mechanism => mechanism,
190
+ :challenge_response => challenge_response,
191
+ }
192
+ @bound = false
193
+ execute(:bind, args)
194
+ @bound = true
195
+ end
196
+ end
197
+
198
+ def sasl_bind_setup_digest_md5(bind_dn, options)
199
+ initial_credential = ""
200
+ nonce_count = 1
201
+ challenge_response = Proc.new do |cred|
202
+ params = parse_sasl_digest_md5_credential(cred)
203
+ qops = params["qop"].split(/,/)
204
+ return "unsupported qops: #{qops.inspect}" unless qops.include?("auth")
205
+ qop = "auth"
206
+ server = @connection.instance_variable_get("@conn").addr[2]
207
+ realm = params['realm']
208
+ uri = "ldap/#{server}"
209
+ nc = "%08x" % nonce_count
210
+ nonce = params["nonce"]
211
+ cnonce = generate_client_nonce
212
+ requests = {
213
+ :username => bind_dn.inspect,
214
+ :realm => realm.inspect,
215
+ :nonce => nonce.inspect,
216
+ :cnonce => cnonce.inspect,
217
+ :nc => nc,
218
+ :qop => qop,
219
+ :maxbuf => "65536",
220
+ "digest-uri" => uri.inspect,
221
+ }
222
+ a1 = "#{bind_dn}:#{realm}:#{password(cred, options)}"
223
+ a1 = "#{Digest::MD5.digest(a1)}:#{nonce}:#{cnonce}"
224
+ ha1 = Digest::MD5.hexdigest(a1)
225
+ a2 = "AUTHENTICATE:#{uri}"
226
+ ha2 = Digest::MD5.hexdigest(a2)
227
+ response = "#{ha1}:#{nonce}:#{nc}:#{cnonce}:#{qop}:#{ha2}"
228
+ requests["response"] = Digest::MD5.hexdigest(response)
229
+ nonce_count += 1
230
+ requests.collect do |key, value|
231
+ "#{key}=#{value}"
232
+ end.join(",")
233
+ end
234
+ [initial_credential, challenge_response]
235
+ end
236
+
237
+ def parse_sasl_digest_md5_credential(cred)
238
+ params = {}
239
+ cred.scan(/(\w+)=(\"?)(.+?)\2(?:,|$)/) do |name, sep, value|
240
+ params[name] = value
241
+ end
242
+ params
243
+ end
244
+
245
+ CHARS = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
246
+ def generate_client_nonce(size=32)
247
+ nonce = ""
248
+ size.times do |i|
249
+ nonce << CHARS[rand(CHARS.size)]
250
+ end
251
+ nonce
252
+ end
253
+
254
+ def simple_bind(bind_dn, options={})
255
+ super do |bind_dn, passwd|
256
+ args = {
257
+ :method => :simple,
258
+ :username => bind_dn,
259
+ :password => passwd,
260
+ }
261
+ @bound = false
262
+ execute(:bind, args)
263
+ @bound = true
264
+ end
265
+ end
266
+
267
+ def parse_entries(entries)
268
+ result = []
269
+ entries.each do |type, key, attributes|
270
+ mod_type = ensure_mod_type(type)
271
+ attributes.each do |name, values|
272
+ result << [mod_type, name, values]
273
+ end
274
+ end
275
+ result
276
+ end
277
+
278
+ def ensure_mod_type(type)
279
+ case type
280
+ when :replace, :add
281
+ type
282
+ else
283
+ raise ArgumentError, "unknown type: #{type}"
284
+ end
285
+ end
286
+ end
287
+ end
288
+ end