ruby-activeldap 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
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