ruby-activeldap 0.7.4 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. data/CHANGES +375 -0
  2. data/COPYING +340 -0
  3. data/LICENSE +58 -0
  4. data/Manifest.txt +33 -0
  5. data/README +63 -0
  6. data/Rakefile +37 -0
  7. data/TODO +31 -0
  8. data/benchmark/bench-al.rb +152 -0
  9. data/lib/{activeldap.rb → active_ldap.rb} +280 -263
  10. data/lib/active_ldap/adaptor/base.rb +29 -0
  11. data/lib/active_ldap/adaptor/ldap.rb +466 -0
  12. data/lib/active_ldap/association/belongs_to.rb +38 -0
  13. data/lib/active_ldap/association/belongs_to_many.rb +40 -0
  14. data/lib/active_ldap/association/collection.rb +80 -0
  15. data/lib/active_ldap/association/has_many.rb +48 -0
  16. data/lib/active_ldap/association/has_many_wrap.rb +56 -0
  17. data/lib/active_ldap/association/proxy.rb +89 -0
  18. data/lib/active_ldap/associations.rb +162 -0
  19. data/lib/active_ldap/attributes.rb +199 -0
  20. data/lib/active_ldap/base.rb +1343 -0
  21. data/lib/active_ldap/callbacks.rb +19 -0
  22. data/lib/active_ldap/command.rb +46 -0
  23. data/lib/active_ldap/configuration.rb +96 -0
  24. data/lib/active_ldap/connection.rb +137 -0
  25. data/lib/{activeldap → active_ldap}/ldap.rb +1 -1
  26. data/lib/active_ldap/object_class.rb +70 -0
  27. data/lib/active_ldap/schema.rb +258 -0
  28. data/lib/{activeldap → active_ldap}/timeout.rb +0 -0
  29. data/lib/{activeldap → active_ldap}/timeout_stub.rb +0 -0
  30. data/lib/active_ldap/user_password.rb +92 -0
  31. data/lib/active_ldap/validations.rb +78 -0
  32. data/rails/plugin/active_ldap/README +54 -0
  33. data/rails/plugin/active_ldap/init.rb +6 -0
  34. data/test/TODO +2 -0
  35. data/test/al-test-utils.rb +337 -0
  36. data/test/command.rb +62 -0
  37. data/test/config.yaml +8 -0
  38. data/test/config.yaml.sample +6 -0
  39. data/test/run-test.rb +17 -0
  40. data/test/test-unit-ext.rb +2 -0
  41. data/test/test_associations.rb +334 -0
  42. data/test/test_attributes.rb +71 -0
  43. data/test/test_base.rb +345 -0
  44. data/test/test_base_per_instance.rb +32 -0
  45. data/test/test_bind.rb +53 -0
  46. data/test/test_callback.rb +35 -0
  47. data/test/test_connection.rb +38 -0
  48. data/test/test_connection_per_class.rb +50 -0
  49. data/test/test_find.rb +36 -0
  50. data/test/test_groupadd.rb +50 -0
  51. data/test/test_groupdel.rb +46 -0
  52. data/test/test_groupls.rb +107 -0
  53. data/test/test_groupmod.rb +51 -0
  54. data/test/test_lpasswd.rb +75 -0
  55. data/test/test_object_class.rb +32 -0
  56. data/test/test_reflection.rb +173 -0
  57. data/test/test_schema.rb +166 -0
  58. data/test/test_user.rb +209 -0
  59. data/test/test_user_password.rb +93 -0
  60. data/test/test_useradd-binary.rb +59 -0
  61. data/test/test_useradd.rb +55 -0
  62. data/test/test_userdel.rb +48 -0
  63. data/test/test_userls.rb +86 -0
  64. data/test/test_usermod-binary-add-time.rb +62 -0
  65. data/test/test_usermod-binary-add.rb +61 -0
  66. data/test/test_usermod-binary-del.rb +64 -0
  67. data/test/test_usermod-lang-add.rb +57 -0
  68. data/test/test_usermod.rb +56 -0
  69. data/test/test_validation.rb +38 -0
  70. metadata +94 -21
  71. data/lib/activeldap/associations.rb +0 -170
  72. data/lib/activeldap/base.rb +0 -1456
  73. data/lib/activeldap/configuration.rb +0 -59
  74. data/lib/activeldap/schema2.rb +0 -217
File without changes
File without changes
@@ -0,0 +1,92 @@
1
+ require 'English'
2
+ require 'base64'
3
+ require 'md5'
4
+ require 'sha1'
5
+
6
+ module ActiveLdap
7
+ module UserPassword
8
+ module_function
9
+ def valid?(password, hashed_password)
10
+ unless /^\{([A-Z][A-Z\d]+)\}/ =~ hashed_password
11
+ raise ArgumentError, "Invalid hashed password"
12
+ end
13
+ type = $1
14
+ hashed_password_without_type = $POSTMATCH
15
+ normalized_type = type.downcase
16
+ unless respond_to?(normalized_type)
17
+ raise ArgumentError, "Unknown Hash type #{type}"
18
+ end
19
+ salt_extractor = "extract_salt_for_#{normalized_type}"
20
+ if respond_to?(salt_extractor)
21
+ salt = send(salt_extractor, hashed_password_without_type)
22
+ if salt.nil?
23
+ raise ArgumentError, "Can't extract salt from hashed password"
24
+ end
25
+ generated_password = send(normalized_type, password, salt)
26
+ else
27
+ generated_password = send(normalized_type, password)
28
+ end
29
+ hashed_password == generated_password
30
+ end
31
+
32
+ def crypt(password, salt=nil)
33
+ salt ||= "$1$#{Salt.generate(8)}"
34
+ "{CRYPT}#{password.crypt(salt)}"
35
+ end
36
+
37
+ def extract_salt_for_crypt(crypted_password)
38
+ if /^\$1\$/ =~ crypted_password
39
+ $MATCH + $POSTMATCH[0, 8].sub(/\$.*/, '') + "$"
40
+ else
41
+ crypted_password[0, 2]
42
+ end
43
+ end
44
+
45
+ def md5(password)
46
+ "{MD5}#{Base64.encode64(MD5.md5(password).digest).chomp}"
47
+ end
48
+
49
+ def smd5(password, salt=nil)
50
+ if salt and salt.size != 4
51
+ raise ArgumentError.new("salt size must be == 4")
52
+ end
53
+ salt ||= Salt.generate(4)
54
+ md5_hash_with_salt = "#{MD5.md5(password + salt).digest}#{salt}"
55
+ "{SMD5}#{Base64.encode64(md5_hash_with_salt).chomp}"
56
+ end
57
+
58
+ def extract_salt_for_smd5(smd5ed_password)
59
+ Base64.decode64(smd5ed_password)[-4, 4]
60
+ end
61
+
62
+ def sha(password)
63
+ "{SHA}#{Base64.encode64(SHA1.sha1(password).digest).chomp}"
64
+ end
65
+
66
+ def ssha(password, salt=nil)
67
+ if salt and salt.size != 4
68
+ raise ArgumentError.new("salt size must be == 4")
69
+ end
70
+ salt ||= Salt.generate(4)
71
+ sha1_hash_with_salt = "#{SHA1.sha1(password + salt).digest}#{salt}"
72
+ "{SSHA}#{Base64.encode64(sha1_hash_with_salt).chomp}"
73
+ end
74
+
75
+ def extract_salt_for_ssha(sshaed_password)
76
+ extract_salt_for_smd5(sshaed_password)
77
+ end
78
+
79
+ module Salt
80
+ CHARS = ['.', '/', '0'..'9', 'A'..'Z', 'a'..'z'].collect do |x|
81
+ x.to_a
82
+ end.flatten
83
+
84
+ module_function
85
+ def generate(length)
86
+ salt = ""
87
+ length.times {salt << CHARS[rand(CHARS.length)]}
88
+ salt
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,78 @@
1
+ require 'active_record/validations'
2
+
3
+ module ActiveLdap
4
+ module Validations
5
+ def self.append_features(base)
6
+ super
7
+
8
+ base.class_eval do
9
+ alias_method :new_record?, :new_entry?
10
+ include ActiveRecord::Validations
11
+
12
+ validate :validate_required_values
13
+
14
+ class << self
15
+ alias_method :evaluate_condition_for_active_record,
16
+ :evaluate_condition
17
+ def evaluate_condition(condition, entry)
18
+ evaluate_condition_for_active_record(condition, entry)
19
+ rescue ActiveRecord::ActiveRecordError
20
+ raise Error, $!.message
21
+ end
22
+ end
23
+
24
+ alias_method :save_with_validation_for_active_record!,
25
+ :save_with_validation!
26
+ def save_with_validation!
27
+ save_with_validation_for_active_record!
28
+ rescue ActiveRecord::RecordInvalid
29
+ raise EntryInvalid, $!.message
30
+ end
31
+ alias_method :save!, :save_with_validation!
32
+
33
+ def valid?
34
+ ensure_apply_object_class
35
+ super
36
+ end
37
+
38
+ # validate_required_values
39
+ #
40
+ # Basic validation:
41
+ # - Verify that every 'MUST' specified in the schema has a value defined
42
+ def validate_required_values
43
+ logger.debug {"stub: validate_required_values called"}
44
+
45
+ # Make sure all MUST attributes have a value
46
+ @musts.each do |object_class, attributes|
47
+ attributes.each do |required_attribute|
48
+ # Normalize to ensure we catch schema problems
49
+ real_name = to_real_attribute_name(required_attribute)
50
+ # # Set default if it wasn't yet set.
51
+ # @data[real_name] ||= [] # need?
52
+ value = @data[real_name] || []
53
+ # Check for missing requirements.
54
+ if value.empty?
55
+ aliases = schema.attribute_aliases(real_name) - [real_name]
56
+ message = "is required attribute "
57
+ unless aliases.empty?
58
+ message << "(aliases: #{aliases.join(', ')}) "
59
+ end
60
+ message << "by objectClass '#{object_class}'"
61
+ errors.add(real_name, message)
62
+ end
63
+ end
64
+ end
65
+ logger.debug {"stub: validate_required_values finished"}
66
+ end
67
+
68
+ private
69
+ alias_method :run_validations_for_active_record, :run_validations
70
+ def run_validations(validation_method)
71
+ run_validations_for_active_record(validation_method)
72
+ rescue ActiveRecord::ActiveRecordError
73
+ raise Error, $!.message
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,54 @@
1
+ = ActiveLdap plugin for Ruby on Rails
2
+
3
+ == Setup
4
+
5
+ You need to write RAILS_ROOT/config/ldap.yml like the following:
6
+
7
+ development:
8
+ host: 127.0.0.1
9
+ port: 389
10
+ base: dc=devel,dc=local,dc=net
11
+ bind_dn: cn=admin,dc=local,dc=net
12
+ password: secret
13
+
14
+ test:
15
+ host: 127.0.0.1
16
+ port: 389
17
+ base: dc=test,dc=local,dc=net
18
+ bind_dn: cn=admin,dc=local,dc=net
19
+ password: secret
20
+
21
+ production:
22
+ host: 127.0.0.1
23
+ port: 389
24
+ base: dc=production,dc=local,dc=net
25
+ bind_dn: cn=admin,dc=local,dc=net
26
+ password: secret
27
+
28
+ == Model
29
+
30
+ Here is some examples.
31
+
32
+ app/model/member.rb:
33
+ class Member < ActiveLdap::Base
34
+ ldap_mapping :dn_attribute => 'uid',
35
+ :classes => ['person', 'posixAccount']
36
+ belongs_to :primary_group, :class => "Group",
37
+ :foreign_key => "gidNumber", :primary_key => "gidNumber"
38
+ belongs_to :groups, :many => 'memberUid'
39
+ end
40
+
41
+ app/model/group.rb:
42
+ class Group < ActiveLdap::Base
43
+ ldap_mapping :dn_attribute => "cn", :classes => ['posixGroup']
44
+ has_many :members, :wrap => "memberUid"
45
+ has_many :primary_members,
46
+ :foreign_key => 'gidNumber',
47
+ :primary_key => 'gidNumber'
48
+ end
49
+
50
+ app/model/ou.rb:
51
+ class Ou < ActiveLdap::Base
52
+ ldap_mapping :prefix => "",
53
+ :classes => ["top", "organizationalUnit"]
54
+ end
@@ -0,0 +1,6 @@
1
+ require_dependency 'active_ldap'
2
+ ActiveLdap::Base.logger ||= RAILS_DEFAULT_LOGGER
3
+ ldap_configuration_file = File.join(RAILS_ROOT, 'config', 'ldap.yml')
4
+ configurations = YAML::load(ERB.new(IO.read(ldap_configuration_file)).result)
5
+ ActiveLdap::Base.configurations = configurations
6
+ ActiveLdap::Base.establish_connection
data/test/TODO ADDED
@@ -0,0 +1,2 @@
1
+
2
+ - Perform explicit testing of Schema2, ActiveLdap::Base, etc
@@ -0,0 +1,337 @@
1
+ require 'test/unit'
2
+ require 'test-unit-ext'
3
+
4
+ require 'erb'
5
+ require 'yaml'
6
+ require 'socket'
7
+ require 'openssl'
8
+ require 'rbconfig'
9
+
10
+ require 'active_ldap'
11
+
12
+ require File.join(File.expand_path(File.dirname(__FILE__)), "command")
13
+
14
+ LDAP_ENV = "test" unless defined?(LDAP_ENV)
15
+
16
+ module AlTestUtils
17
+ def self.included(base)
18
+ base.class_eval do
19
+ include Config
20
+ include Connection
21
+ include Populate
22
+ include TemporaryEntry
23
+ include CommandSupport
24
+ end
25
+ end
26
+
27
+ module Config
28
+ def setup
29
+ super
30
+ @base_dir = File.expand_path(File.dirname(__FILE__))
31
+ @top_dir = File.expand_path(File.join(@base_dir, ".."))
32
+ @example_dir = File.join(@top_dir, "examples")
33
+ @config_file = File.join(File.dirname(__FILE__), "config.yaml")
34
+ ActiveLdap::Base.configurations = read_config
35
+ end
36
+
37
+ def teardown
38
+ super
39
+ end
40
+
41
+ def current_configuration
42
+ ActiveLdap::Base.configurations[LDAP_ENV]
43
+ end
44
+
45
+ def read_config
46
+ unless File.exist?(@config_file)
47
+ raise "config file for testing doesn't exist: #{@config_file}"
48
+ end
49
+ YAML.load(ERB.new(File.read(@config_file)).result)
50
+ end
51
+ end
52
+
53
+ module Connection
54
+ def setup
55
+ super
56
+ ActiveLdap::Base.establish_connection
57
+ end
58
+
59
+ def teardown
60
+ ActiveLdap::Base.clear_active_connections!
61
+ super
62
+ end
63
+ end
64
+
65
+ module Populate
66
+ def setup
67
+ @dumped_data = nil
68
+ super
69
+ begin
70
+ @dumped_data = ActiveLdap::Base.dump(:scope => :sub)
71
+ rescue ActiveLdap::ConnectionError
72
+ end
73
+ ActiveLdap::Base.delete_all(nil, :scope => :sub)
74
+ populate
75
+ end
76
+
77
+ def teardown
78
+ if @dumped_data
79
+ ActiveLdap::Base.establish_connection
80
+ ActiveLdap::Base.delete_all(nil, :scope => :sub)
81
+ ActiveLdap::Base.load(@dumped_data)
82
+ end
83
+ super
84
+ end
85
+
86
+ def populate
87
+ populate_base
88
+ populate_ou
89
+ populate_user_class
90
+ populate_group_class
91
+ populate_associations
92
+ end
93
+
94
+ def populate_base
95
+ unless ActiveLdap::Base.search(:scope => :base).empty?
96
+ return
97
+ end
98
+
99
+ suffixes = []
100
+ ActiveLdap::Base.base.split(/,/).reverse_each do |suffix|
101
+ prefix = suffixes.join(",")
102
+ suffixes.unshift(suffix)
103
+ name, value = suffix.split(/=/, 2)
104
+ next unless name == "dc"
105
+ dc_class = Class.new(ActiveLdap::Base)
106
+ dc_class.ldap_mapping :dn_attribute => "dc",
107
+ :prefix => "",
108
+ :scope => :base,
109
+ :classes => ["top", "dcObject", "organization"]
110
+ dc_class.base = prefix
111
+ next if dc_class.exists?(value, :prefix => "dc=#{value}")
112
+ dc = dc_class.new(value)
113
+ dc.o = dc.dc
114
+ dc.save
115
+ end
116
+ end
117
+
118
+ def ou_class(prefix="")
119
+ ou_class = Class.new(ActiveLdap::Base)
120
+ ou_class.ldap_mapping :dn_attribute => "ou",
121
+ :prefix => prefix,
122
+ :classes => ["top", "organizationalUnit"]
123
+ ou_class
124
+ end
125
+
126
+ def populate_ou
127
+ %w(Users Groups).each do |name|
128
+ make_ou(name)
129
+ end
130
+ end
131
+
132
+ def make_ou(name)
133
+ ou_class.new(name).save
134
+ end
135
+
136
+ def populate_user_class
137
+ @user_class = Class.new(ActiveLdap::Base)
138
+ @user_class.ldap_mapping :dn_attribute => "uid",
139
+ :prefix => "ou=Users",
140
+ :scope => :sub,
141
+ :classes => ["posixAccount", "person"]
142
+ end
143
+
144
+ def populate_group_class
145
+ @group_class = Class.new(ActiveLdap::Base)
146
+ @group_class.ldap_mapping :prefix => "ou=Groups",
147
+ :scope => :sub,
148
+ :classes => ["posixGroup"]
149
+ end
150
+
151
+ def populate_associations
152
+ @user_class.belongs_to :groups, :many => "memberUid"
153
+ @user_class.belongs_to :primary_group,
154
+ :foreign_key => "gidNumber",
155
+ :primary_key => "gidNumber"
156
+ @group_class.has_many :members, :wrap => "memberUid"
157
+ @group_class.has_many :primary_members,
158
+ :foreign_key => "gidNumber",
159
+ :primary_key => "gidNumber"
160
+ @user_class.set_associated_class(:groups, @group_class)
161
+ @user_class.set_associated_class(:primary_group, @group_class)
162
+ @group_class.set_associated_class(:members, @user_class)
163
+ @group_class.set_associated_class(:primary_members, @user_class)
164
+ end
165
+ end
166
+
167
+ module TemporaryEntry
168
+ @@certificate = nil
169
+ def setup
170
+ super
171
+ @user_index = 0
172
+ @group_index = 0
173
+ end
174
+
175
+ def make_temporary_user(config={})
176
+ @user_index += 1
177
+ uid = config[:uid] || "temp-user#{@user_index}"
178
+ ensure_delete_user(uid) do
179
+ password = config[:password] || "password"
180
+ uid_number = config[:uid_number] || default_uid
181
+ gid_number = config[:gid_number] || default_gid
182
+ home_directory = config[:home_directory] || "/nonexistent"
183
+ _wrap_assertion do
184
+ assert(!@user_class.exists?(uid))
185
+ assert_raise(ActiveLdap::EntryNotFound) do
186
+ @user_class.find(uid).dn
187
+ end
188
+ user = @user_class.new(uid)
189
+ assert(user.new_entry?)
190
+ user.cn = user.uid
191
+ user.sn = user.uid
192
+ user.uid_number = uid_number
193
+ user.gid_number = gid_number
194
+ user.home_directory = home_directory
195
+ user.user_password = ActiveLdap::UserPassword.ssha(password)
196
+ unless config[:simple]
197
+ user.add_class('shadowAccount', 'inetOrgPerson',
198
+ 'organizationalPerson')
199
+ user.user_certificate = certificate
200
+ user.jpeg_photo = jpeg_photo
201
+ end
202
+ user.save
203
+ assert(!user.new_entry?)
204
+ yield(@user_class.find(user.uid), password)
205
+ end
206
+ end
207
+ end
208
+
209
+ def make_temporary_group(config={})
210
+ @group_index += 1
211
+ cn = config[:cn] || "temp-group#{@group_index}"
212
+ ensure_delete_group(cn) do
213
+ gid_number = config[:gid_number] || default_gid
214
+ _wrap_assertion do
215
+ assert(!@group_class.exists?(cn))
216
+ assert_raise(ActiveLdap::EntryNotFound) do
217
+ @group_class.find(cn)
218
+ end
219
+ group = @group_class.new(cn)
220
+ assert(group.new_entry?)
221
+ group.gid_number = gid_number
222
+ assert(group.save)
223
+ assert(!group.new_entry?)
224
+ yield(@group_class.find(group.cn))
225
+ end
226
+ end
227
+ end
228
+
229
+ def ensure_delete_user(uid)
230
+ yield(uid)
231
+ ensure
232
+ @user_class.delete(uid) if @user_class.exists?(uid)
233
+ end
234
+
235
+ def ensure_delete_group(cn)
236
+ yield(cn)
237
+ ensure
238
+ @group_class.delete(cn) if @group_class.exists?(cn)
239
+ end
240
+
241
+ def default_uid
242
+ "10000#{@user_index}"
243
+ end
244
+
245
+ def default_gid
246
+ "10000#{@group_index}"
247
+ end
248
+
249
+ def certificate_path
250
+ File.join(@example_dir, 'example.der')
251
+ end
252
+
253
+ def certificate
254
+ return @@certificate if @@certificate
255
+ if File.exists?(certificate_path)
256
+ @@certificate = File.read(certificate_path)
257
+ return @@certificate
258
+ end
259
+
260
+ rsa = OpenSSL::PKey::RSA.new(512)
261
+ comment = "Generated by Ruby/OpenSSL"
262
+
263
+ cert = OpenSSL::X509::Certificate.new
264
+ cert.version = 3
265
+ cert.serial = 0
266
+ subject = [["OU", "test"],
267
+ ["CN", Socket.gethostname]]
268
+ name = OpenSSL::X509::Name.new(subject)
269
+ cert.subject = name
270
+ cert.issuer = name
271
+ cert.not_before = Time.now
272
+ cert.not_after = Time.now + (365*24*60*60)
273
+ cert.public_key = rsa.public_key
274
+
275
+ ef = OpenSSL::X509::ExtensionFactory.new(nil, cert)
276
+ ef.issuer_certificate = cert
277
+ cert.extensions = [
278
+ ef.create_extension("basicConstraints","CA:FALSE"),
279
+ ef.create_extension("keyUsage", "keyEncipherment"),
280
+ ef.create_extension("subjectKeyIdentifier", "hash"),
281
+ ef.create_extension("extendedKeyUsage", "serverAuth"),
282
+ ef.create_extension("nsComment", comment),
283
+ ]
284
+ aki = ef.create_extension("authorityKeyIdentifier",
285
+ "keyid:always,issuer:always")
286
+ cert.add_extension(aki)
287
+ cert.sign(rsa, OpenSSL::Digest::SHA1.new)
288
+
289
+ @@certificate = cert.to_der
290
+ @@certificate
291
+ end
292
+
293
+ def jpeg_photo_path
294
+ File.join(@example_dir, 'example.jpg')
295
+ end
296
+
297
+ def jpeg_photo
298
+ File.read(jpeg_photo_path)
299
+ end
300
+ end
301
+
302
+ module CommandSupport
303
+ def setup
304
+ super
305
+ @fakeroot = "fakeroot"
306
+ @ruby = File.join(::Config::CONFIG["bindir"],
307
+ ::Config::CONFIG["RUBY_INSTALL_NAME"])
308
+ @top_dir = File.expand_path(File.join(File.dirname(__FILE__), ".."))
309
+ @examples_dir = File.join(@top_dir, "examples")
310
+ @lib_dir = File.join(@top_dir, "lib")
311
+ @ruby_args = [
312
+ "-I", @examples_dir,
313
+ "-I", @lib_dir,
314
+ ]
315
+ end
316
+
317
+ def run_command(*args, &block)
318
+ file = Tempfile.new("al-command-support")
319
+ file.open
320
+ file.puts(ActiveLdap::Base.configurations["test"].to_yaml)
321
+ file.close
322
+ run_ruby(*[@command, "--config", file.path, *args], &block)
323
+ end
324
+
325
+ def run_ruby(*ruby_args, &block)
326
+ args = [@ruby, *@ruby_args]
327
+ args.concat(ruby_args)
328
+ Command.run(*args, &block)
329
+ end
330
+
331
+ def run_ruby_with_fakeroot(*ruby_args, &block)
332
+ args = [@fakeroot, @ruby, *@ruby_args]
333
+ args.concat(ruby_args)
334
+ Command.run(*args, &block)
335
+ end
336
+ end
337
+ end