ruby-activeldap 0.8.3 → 0.8.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (177) hide show
  1. data/CHANGES +431 -0
  2. data/COPYING +340 -0
  3. data/LICENSE +58 -0
  4. data/README +104 -0
  5. data/Rakefile +165 -0
  6. data/TODO +22 -0
  7. data/benchmark/bench-al.rb +202 -0
  8. data/benchmark/config.yaml.sample +5 -0
  9. data/data/locale/en/LC_MESSAGES/active-ldap.mo +0 -0
  10. data/data/locale/ja/LC_MESSAGES/active-ldap.mo +0 -0
  11. data/examples/al-admin/README +182 -0
  12. data/examples/al-admin/Rakefile +10 -0
  13. data/examples/al-admin/app/controllers/account_controller.rb +50 -0
  14. data/examples/al-admin/app/controllers/application.rb +15 -0
  15. data/examples/al-admin/app/controllers/directory_controller.rb +22 -0
  16. data/examples/al-admin/app/controllers/users_controller.rb +38 -0
  17. data/examples/al-admin/app/controllers/welcome_controller.rb +4 -0
  18. data/examples/al-admin/app/helpers/account_helper.rb +2 -0
  19. data/examples/al-admin/app/helpers/application_helper.rb +6 -0
  20. data/examples/al-admin/app/helpers/directory_helper.rb +2 -0
  21. data/examples/al-admin/app/helpers/users_helper.rb +13 -0
  22. data/examples/al-admin/app/helpers/welcome_helper.rb +2 -0
  23. data/examples/al-admin/app/models/entry.rb +19 -0
  24. data/examples/al-admin/app/models/ldap_user.rb +49 -0
  25. data/examples/al-admin/app/models/user.rb +91 -0
  26. data/examples/al-admin/app/views/account/login.rhtml +12 -0
  27. data/examples/al-admin/app/views/account/sign_up.rhtml +22 -0
  28. data/examples/al-admin/app/views/directory/index.rhtml +5 -0
  29. data/examples/al-admin/app/views/directory/populate.rhtml +2 -0
  30. data/examples/al-admin/app/views/layouts/application.rhtml +41 -0
  31. data/examples/al-admin/app/views/users/_attribute_information.rhtml +22 -0
  32. data/examples/al-admin/app/views/users/_entry.rhtml +12 -0
  33. data/examples/al-admin/app/views/users/_form.rhtml +29 -0
  34. data/examples/al-admin/app/views/users/_object_class_information.rhtml +23 -0
  35. data/examples/al-admin/app/views/users/edit.rhtml +10 -0
  36. data/examples/al-admin/app/views/users/index.rhtml +9 -0
  37. data/examples/al-admin/app/views/users/show.rhtml +3 -0
  38. data/examples/al-admin/app/views/welcome/index.rhtml +16 -0
  39. data/examples/al-admin/config/boot.rb +45 -0
  40. data/examples/al-admin/config/database.yml.example +19 -0
  41. data/examples/al-admin/config/environment.rb +68 -0
  42. data/examples/al-admin/config/environments/development.rb +21 -0
  43. data/examples/al-admin/config/environments/production.rb +18 -0
  44. data/examples/al-admin/config/environments/test.rb +19 -0
  45. data/examples/al-admin/config/ldap.yml.example +21 -0
  46. data/examples/al-admin/config/routes.rb +26 -0
  47. data/examples/al-admin/db/migrate/001_create_users.rb +16 -0
  48. data/examples/al-admin/lib/accept_http_rails_relative_url_root.rb +9 -0
  49. data/examples/al-admin/lib/authenticated_system.rb +131 -0
  50. data/examples/al-admin/lib/authenticated_test_helper.rb +113 -0
  51. data/examples/al-admin/lib/tasks/gettext.rake +35 -0
  52. data/examples/al-admin/po/en/al-admin.po +190 -0
  53. data/examples/al-admin/po/ja/al-admin.po +190 -0
  54. data/examples/al-admin/po/nl/al-admin.po +202 -0
  55. data/examples/al-admin/public/.htaccess +40 -0
  56. data/examples/al-admin/public/404.html +30 -0
  57. data/examples/al-admin/public/500.html +30 -0
  58. data/examples/al-admin/public/dispatch.cgi +10 -0
  59. data/examples/al-admin/public/dispatch.fcgi +24 -0
  60. data/examples/al-admin/public/dispatch.rb +10 -0
  61. data/examples/al-admin/public/favicon.ico +0 -0
  62. data/examples/al-admin/public/images/rails.png +0 -0
  63. data/examples/al-admin/public/javascripts/application.js +2 -0
  64. data/examples/al-admin/public/javascripts/controls.js +833 -0
  65. data/examples/al-admin/public/javascripts/dragdrop.js +942 -0
  66. data/examples/al-admin/public/javascripts/effects.js +1088 -0
  67. data/examples/al-admin/public/javascripts/prototype.js +2515 -0
  68. data/examples/al-admin/public/robots.txt +1 -0
  69. data/examples/al-admin/public/stylesheets/rails.css +35 -0
  70. data/examples/al-admin/public/stylesheets/screen.css +52 -0
  71. data/examples/al-admin/script/about +3 -0
  72. data/examples/al-admin/script/breakpointer +3 -0
  73. data/examples/al-admin/script/console +3 -0
  74. data/examples/al-admin/script/destroy +3 -0
  75. data/examples/al-admin/script/generate +3 -0
  76. data/examples/al-admin/script/performance/benchmarker +3 -0
  77. data/examples/al-admin/script/performance/profiler +3 -0
  78. data/examples/al-admin/script/plugin +3 -0
  79. data/examples/al-admin/script/process/inspector +3 -0
  80. data/examples/al-admin/script/process/reaper +3 -0
  81. data/examples/al-admin/script/process/spawner +3 -0
  82. data/examples/al-admin/script/runner +3 -0
  83. data/examples/al-admin/script/server +3 -0
  84. data/examples/al-admin/test/fixtures/users.yml +9 -0
  85. data/examples/al-admin/test/functional/account_controller_test.rb +24 -0
  86. data/examples/al-admin/test/functional/directory_controller_test.rb +18 -0
  87. data/examples/al-admin/test/functional/users_controller_test.rb +18 -0
  88. data/examples/al-admin/test/functional/welcome_controller_test.rb +18 -0
  89. data/examples/al-admin/test/run-test.sh +3 -0
  90. data/examples/al-admin/test/test_helper.rb +28 -0
  91. data/examples/al-admin/test/unit/user_test.rb +13 -0
  92. data/examples/al-admin/vendor/plugins/exception_notification/README +111 -0
  93. data/examples/al-admin/vendor/plugins/exception_notification/init.rb +1 -0
  94. data/examples/al-admin/vendor/plugins/exception_notification/lib/exception_notifiable.rb +99 -0
  95. data/examples/al-admin/vendor/plugins/exception_notification/lib/exception_notifier.rb +67 -0
  96. data/examples/al-admin/vendor/plugins/exception_notification/lib/exception_notifier_helper.rb +77 -0
  97. data/examples/al-admin/vendor/plugins/exception_notification/test/exception_notifier_helper_test.rb +61 -0
  98. data/examples/al-admin/vendor/plugins/exception_notification/test/test_helper.rb +7 -0
  99. data/examples/al-admin/vendor/plugins/exception_notification/views/exception_notifier/_backtrace.rhtml +1 -0
  100. data/examples/al-admin/vendor/plugins/exception_notification/views/exception_notifier/_environment.rhtml +7 -0
  101. data/examples/al-admin/vendor/plugins/exception_notification/views/exception_notifier/_inspect_model.rhtml +16 -0
  102. data/examples/al-admin/vendor/plugins/exception_notification/views/exception_notifier/_request.rhtml +3 -0
  103. data/examples/al-admin/vendor/plugins/exception_notification/views/exception_notifier/_session.rhtml +2 -0
  104. data/examples/al-admin/vendor/plugins/exception_notification/views/exception_notifier/_title.rhtml +3 -0
  105. data/examples/al-admin/vendor/plugins/exception_notification/views/exception_notifier/exception_notification.rhtml +6 -0
  106. data/examples/config.yaml.example +5 -0
  107. data/examples/example.der +0 -0
  108. data/examples/example.jpg +0 -0
  109. data/examples/groupadd +41 -0
  110. data/examples/groupdel +35 -0
  111. data/examples/groupls +49 -0
  112. data/examples/groupmod +42 -0
  113. data/examples/lpasswd +55 -0
  114. data/examples/objects/group.rb +13 -0
  115. data/examples/objects/ou.rb +4 -0
  116. data/examples/objects/user.rb +20 -0
  117. data/examples/ouadd +38 -0
  118. data/examples/useradd +45 -0
  119. data/examples/useradd-binary +50 -0
  120. data/examples/userdel +34 -0
  121. data/examples/userls +50 -0
  122. data/examples/usermod +42 -0
  123. data/examples/usermod-binary-add +47 -0
  124. data/examples/usermod-binary-add-time +51 -0
  125. data/examples/usermod-binary-del +48 -0
  126. data/examples/usermod-lang-add +43 -0
  127. data/lib/active_ldap.rb +978 -0
  128. data/lib/active_ldap/adapter/base.rb +512 -0
  129. data/lib/active_ldap/adapter/ldap.rb +233 -0
  130. data/lib/active_ldap/adapter/ldap_ext.rb +69 -0
  131. data/lib/active_ldap/adapter/net_ldap.rb +290 -0
  132. data/lib/active_ldap/adapter/net_ldap_ext.rb +29 -0
  133. data/lib/active_ldap/association/belongs_to.rb +47 -0
  134. data/lib/active_ldap/association/belongs_to_many.rb +42 -0
  135. data/lib/active_ldap/association/collection.rb +83 -0
  136. data/lib/active_ldap/association/has_many.rb +31 -0
  137. data/lib/active_ldap/association/has_many_utils.rb +35 -0
  138. data/lib/active_ldap/association/has_many_wrap.rb +46 -0
  139. data/lib/active_ldap/association/proxy.rb +102 -0
  140. data/lib/active_ldap/associations.rb +172 -0
  141. data/lib/active_ldap/attributes.rb +211 -0
  142. data/lib/active_ldap/base.rb +1256 -0
  143. data/lib/active_ldap/callbacks.rb +19 -0
  144. data/lib/active_ldap/command.rb +48 -0
  145. data/lib/active_ldap/configuration.rb +114 -0
  146. data/lib/active_ldap/connection.rb +234 -0
  147. data/lib/active_ldap/distinguished_name.rb +250 -0
  148. data/lib/active_ldap/escape.rb +12 -0
  149. data/lib/active_ldap/get_text/parser.rb +142 -0
  150. data/lib/active_ldap/get_text_fallback.rb +53 -0
  151. data/lib/active_ldap/get_text_support.rb +12 -0
  152. data/lib/active_ldap/helper.rb +23 -0
  153. data/lib/active_ldap/ldap_error.rb +74 -0
  154. data/lib/active_ldap/object_class.rb +93 -0
  155. data/lib/active_ldap/operations.rb +419 -0
  156. data/lib/active_ldap/populate.rb +44 -0
  157. data/lib/active_ldap/schema.rb +427 -0
  158. data/lib/active_ldap/timeout.rb +75 -0
  159. data/lib/active_ldap/timeout_stub.rb +17 -0
  160. data/lib/active_ldap/user_password.rb +93 -0
  161. data/lib/active_ldap/validations.rb +112 -0
  162. data/po/en/active-ldap.po +3011 -0
  163. data/po/ja/active-ldap.po +3044 -0
  164. data/rails/plugin/active_ldap/README +54 -0
  165. data/rails/plugin/active_ldap/generators/scaffold_al/scaffold_al_generator.rb +7 -0
  166. data/rails/plugin/active_ldap/generators/scaffold_al/templates/ldap.yml +21 -0
  167. data/rails/plugin/active_ldap/init.rb +19 -0
  168. data/test/al-test-utils.rb +362 -0
  169. data/test/command.rb +62 -0
  170. data/test/config.yaml.sample +6 -0
  171. data/test/run-test.rb +31 -0
  172. data/test/test-unit-ext.rb +4 -0
  173. data/test/test-unit-ext/always-show-result.rb +28 -0
  174. data/test/test-unit-ext/backtrace-filter.rb +17 -0
  175. data/test/test-unit-ext/long-display-for-emacs.rb +25 -0
  176. data/test/test-unit-ext/priority.rb +163 -0
  177. metadata +211 -4
@@ -0,0 +1,211 @@
1
+ module ActiveLdap
2
+ module Attributes
3
+ def self.included(base)
4
+ base.class_eval do
5
+ extend(ClassMethods)
6
+ extend(Normalize)
7
+ include(Normalize)
8
+ end
9
+ end
10
+
11
+ module ClassMethods
12
+ def attr_protected(*attributes)
13
+ targets = attributes.collect {|attr| attr.to_s} - protected_attributes
14
+ instance_variable_set("@attr_protected", targets)
15
+ end
16
+
17
+ def protected_attributes
18
+ ancestors[0..(ancestors.index(Base))].inject([]) do |result, ancestor|
19
+ result + ancestor.instance_eval {@attr_protected ||= []}
20
+ end
21
+ end
22
+ end
23
+
24
+ module Normalize
25
+ def normalize_attribute_name(name)
26
+ name.to_s.downcase
27
+ end
28
+
29
+ # Enforce typing:
30
+ # Hashes are for subtypes
31
+ # Arrays are for multiple entries
32
+ def normalize_attribute(name, value)
33
+ if name.nil?
34
+ raise RuntimeError, _('The first argument, name, must not be nil. ' \
35
+ 'Please report this as a bug!')
36
+ end
37
+
38
+ name = normalize_attribute_name(name)
39
+ rubyish_class_name = Inflector.underscore(value.class.name)
40
+ handler = "normalize_attribute_value_of_#{rubyish_class_name}"
41
+ if respond_to?(handler, true)
42
+ [name, send(handler, name, value)]
43
+ else
44
+ [name, [value.to_s]]
45
+ end
46
+ end
47
+
48
+ def unnormalize_attributes(attributes)
49
+ result = {}
50
+ attributes.each do |name, values|
51
+ unnormalize_attribute(name, values, result)
52
+ end
53
+ result
54
+ end
55
+
56
+ def unnormalize_attribute(name, values, result={})
57
+ if values.empty?
58
+ result[name] = []
59
+ else
60
+ values.each do |value|
61
+ if value.is_a?(Hash)
62
+ suffix, real_value = extract_attribute_options(value)
63
+ new_name = name + suffix
64
+ result[new_name] ||= []
65
+ result[new_name].concat(real_value)
66
+ else
67
+ result[name] ||= []
68
+ result[name] << value.dup
69
+ end
70
+ end
71
+ end
72
+ result
73
+ end
74
+
75
+ private
76
+ def normalize_attribute_value_of_array(name, value)
77
+ attribute = schema.attribute(name)
78
+ if value.size > 1 and attribute.single_value?
79
+ format = _("Attribute %s can only have a single value")
80
+ message = format % self.class.human_attribute_name(attribute)
81
+ raise TypeError, message
82
+ end
83
+ if value.empty?
84
+ if schema.attribute(name).binary_required?
85
+ [{'binary' => value}]
86
+ else
87
+ value
88
+ end
89
+ else
90
+ value.collect do |entry|
91
+ normalize_attribute(name, entry)[1][0]
92
+ end
93
+ end
94
+ end
95
+
96
+ def normalize_attribute_value_of_hash(name, value)
97
+ if value.keys.size > 1
98
+ format = _("Hashes must have one key-value pair only: %s")
99
+ raise TypeError, format % value.inspect
100
+ end
101
+ unless value.keys[0].match(/^(lang-[a-z][a-z]*)|(binary)$/)
102
+ logger.warn do
103
+ format = _("unknown option did not match lang-* or binary: %s")
104
+ format % value.keys[0]
105
+ end
106
+ end
107
+ # Contents MUST be a String or an Array
108
+ if !value.has_key?('binary') and schema.attribute(name).binary_required?
109
+ suffix, real_value = extract_attribute_options(value)
110
+ name, values =
111
+ normalize_attribute_options("#{name}#{suffix};binary", real_value)
112
+ values
113
+ else
114
+ [value]
115
+ end
116
+ end
117
+
118
+ def normalize_attribute_value_of_nil_class(name, value)
119
+ if schema.attribute(name).binary_required?
120
+ [{'binary' => []}]
121
+ else
122
+ []
123
+ end
124
+ end
125
+
126
+ def normalize_attribute_value_of_string(name, value)
127
+ if schema.attribute(name).binary_required?
128
+ [{'binary' => [value]}]
129
+ else
130
+ [value]
131
+ end
132
+ end
133
+
134
+ def normalize_attribute_value_of_date(name, value)
135
+ new_value = sprintf('%.04d%.02d%.02d%.02d%.02d%.02d%s',
136
+ value.year, value.month, value.mday, 0, 0, 0,
137
+ '+0000')
138
+ normalize_attribute_value_of_string(name, new_value)
139
+ end
140
+
141
+ def normalize_attribute_value_of_time(name, value)
142
+ new_value = sprintf('%.04d%.02d%.02d%.02d%.02d%.02d%s',
143
+ 0, 0, 0, value.hour, value.min, value.sec,
144
+ value.zone)
145
+ normalize_attribute_value_of_string(name, new_value)
146
+ end
147
+
148
+ def normalize_attribute_value_of_date_time(name, value)
149
+ new_value = sprintf('%.04d%.02d%.02d%.02d%.02d%.02d%s',
150
+ value.year, value.month, value.mday, value.hour,
151
+ value.min, value.sec, value.zone)
152
+ normalize_attribute_value_of_string(name, new_value)
153
+ end
154
+
155
+ # normalize_attribute_options
156
+ #
157
+ # Makes the Hashized value from the full attribute name
158
+ # e.g. userCertificate;binary => "some_bin"
159
+ # becomes userCertificate => {"binary" => "some_bin"}
160
+ def normalize_attribute_options(attr, value)
161
+ return [attr, value] unless attr.match(/;/)
162
+
163
+ ret_attr, *options = attr.split(/;/)
164
+ [ret_attr,
165
+ [options.reverse.inject(value) {|result, option| {option => result}}]]
166
+ end
167
+
168
+ # extract_attribute_options
169
+ #
170
+ # Extracts all of the subtypes from a given set of nested hashes
171
+ # and returns the attribute suffix and the final true value
172
+ def extract_attribute_options(value)
173
+ options = ''
174
+ ret_val = value
175
+ if value.class == Hash
176
+ options = ';' + value.keys[0]
177
+ ret_val = value[value.keys[0]]
178
+ if ret_val.class == Hash
179
+ sub_options, ret_val = extract_attribute_options(ret_val)
180
+ options += sub_options
181
+ end
182
+ end
183
+ ret_val = [ret_val] unless ret_val.class == Array
184
+ [options, ret_val]
185
+ end
186
+ end
187
+
188
+ private
189
+ def remove_attributes_protected_from_mass_assignment(targets)
190
+ needless_attributes = {}
191
+ (attributes_protected_by_default +
192
+ (self.class.protected_attributes || [])).each do |name|
193
+ needless_attributes[to_real_attribute_name(name)] = true
194
+ end
195
+
196
+ targets.collect do |key, value|
197
+ [to_real_attribute_name(key) || key, value]
198
+ end.reject do |key, value|
199
+ needless_attributes[key]
200
+ end
201
+ end
202
+
203
+ def attributes_protected_by_default
204
+ [dn_attribute, 'objectClass']
205
+ end
206
+
207
+ def normalize_attribute_name(name)
208
+ self.class.normalize_attribute_name(name)
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,1256 @@
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-2006 Will Drewry <will@alum.bu.edu>
5
+ # Some portions Copyright 2006 Google Inc
6
+ #
7
+ # == Summary
8
+ # ActiveLdap lets you read and update LDAP entries in a completely object
9
+ # oriented fashion, even handling attributes with multiple names seamlessly.
10
+ # It was inspired by ActiveRecord so extending it to deal with custom
11
+ # LDAP schemas is as effortless as knowing the 'ou' of the objects, and the
12
+ # primary key. (fix this up some)
13
+ #
14
+ # == Example
15
+ # irb> require 'active_ldap'
16
+ # > true
17
+ # irb> user = ActiveLdap::User.new("drewry")
18
+ # > #<ActiveLdap::User:0x402e...
19
+ # irb> user.cn
20
+ # > "foo"
21
+ # irb> user.common_name
22
+ # > "foo"
23
+ # irb> user.cn = "Will Drewry"
24
+ # > "Will Drewry"
25
+ # irb> user.cn
26
+ # > "Will Drewry"
27
+ # irb> user.save
28
+ #
29
+ #
30
+
31
+ require 'English'
32
+ require 'thread'
33
+
34
+ module ActiveLdap
35
+ # OO-interface to LDAP assuming pam/nss_ldap-style organization with
36
+ # Active specifics
37
+ # Each subclass does a ldapsearch for the matching entry.
38
+ # If no exact match, raise an error.
39
+ # If match, change all LDAP attributes in accessor attributes on the object.
40
+ # -- these are ACTUALLY populated from schema - see active_ldap/schema.rb
41
+ # example
42
+ # -- extract objectClasses from match and populate
43
+ # Multiple entries become lists.
44
+ # If this isn't read-only then lists become multiple entries, etc.
45
+
46
+ class Error < StandardError
47
+ include GetTextSupport
48
+ end
49
+
50
+ # ConfigurationError
51
+ #
52
+ # An exception raised when there is a problem with Base.connect arguments
53
+ class ConfigurationError < Error
54
+ end
55
+
56
+ # DeleteError
57
+ #
58
+ # An exception raised when an ActiveLdap delete action fails
59
+ class DeleteError < Error
60
+ end
61
+
62
+ # SaveError
63
+ #
64
+ # An exception raised when an ActiveLdap save action failes
65
+ class SaveError < Error
66
+ end
67
+
68
+ # AuthenticationError
69
+ #
70
+ # An exception raised when user authentication fails
71
+ class AuthenticationError < Error
72
+ end
73
+
74
+ # ConnectionError
75
+ #
76
+ # An exception raised when the LDAP conenction fails
77
+ class ConnectionError < Error
78
+ end
79
+
80
+ # ObjectClassError
81
+ #
82
+ # An exception raised when an objectClass is not defined in the schema
83
+ class ObjectClassError < Error
84
+ end
85
+
86
+ # AttributeAssignmentError
87
+ #
88
+ # An exception raised when there is an issue assigning a value to
89
+ # an attribute
90
+ class AttributeAssignmentError < Error
91
+ end
92
+
93
+ # TimeoutError
94
+ #
95
+ # An exception raised when a connection action fails due to a timeout
96
+ class TimeoutError < Error
97
+ end
98
+
99
+ class EntryNotFound < Error
100
+ end
101
+
102
+ class EntryAlreadyExist < Error
103
+ end
104
+
105
+ class StrongAuthenticationRequired < Error
106
+ end
107
+
108
+ class DistinguishedNameInvalid < Error
109
+ attr_reader :dn, :reason
110
+ def initialize(dn, reason=nil)
111
+ @dn = dn
112
+ @reason = reason
113
+ if @reason
114
+ message = _("%s is invalid distinguished name (DN): %s") % [@dn, @reason]
115
+ else
116
+ message = _("%s is invalid distinguished name (DN)") % @dn
117
+ end
118
+ super(message)
119
+ end
120
+ end
121
+
122
+ class DistinguishedNameNotSetError < Error
123
+ end
124
+
125
+ class EntryNotSaved < Error
126
+ end
127
+
128
+ class RequiredObjectClassMissed < Error
129
+ end
130
+
131
+ class RequiredAttributeMissed < Error
132
+ end
133
+
134
+ class EntryInvalid < Error
135
+ end
136
+
137
+ class OperationNotPermitted < Error
138
+ end
139
+
140
+ class ConnectionNotEstablished < Error
141
+ end
142
+
143
+ class AdapterNotSpecified < Error
144
+ end
145
+
146
+ class AdapterNotFound < Error
147
+ attr_reader :adapter
148
+ def initialize(adapter)
149
+ @adapter = adapter
150
+ super(_("LDAP configuration specifies nonexistent %s adapter") % adapter)
151
+ end
152
+ end
153
+
154
+ class UnknownAttribute < Error
155
+ attr_reader :name
156
+ def initialize(name)
157
+ @name = name
158
+ super(_("%s is unknown attribute") % @name)
159
+ end
160
+ end
161
+
162
+ # Base
163
+ #
164
+ # Base is the primary class which contains all of the core
165
+ # ActiveLdap functionality. It is meant to only ever be subclassed
166
+ # by extension classes.
167
+ class Base
168
+ include GetTextSupport
169
+ public :gettext
170
+
171
+ if Reloadable.const_defined?(:Deprecated)
172
+ include Reloadable::Deprecated
173
+ else
174
+ include Reloadable::Subclasses
175
+ end
176
+
177
+ VALID_LDAP_MAPPING_OPTIONS = [:dn_attribute, :prefix, :scope,
178
+ :classes, :recommended_classes]
179
+
180
+ cattr_accessor :logger
181
+ cattr_accessor :configurations
182
+ @@configurations = {}
183
+
184
+ def self.class_local_attr_accessor(search_ancestors, *syms)
185
+ syms.flatten.each do |sym|
186
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
187
+ def self.#{sym}(search_superclasses=#{search_ancestors})
188
+ @#{sym} ||= nil
189
+ return @#{sym} if @#{sym}
190
+ if search_superclasses
191
+ target = superclass
192
+ value = nil
193
+ loop do
194
+ break nil unless target.respond_to?(:#{sym})
195
+ value = target.#{sym}
196
+ break if value
197
+ target = target.superclass
198
+ end
199
+ value
200
+ else
201
+ nil
202
+ end
203
+ end
204
+ def #{sym}; self.class.#{sym}; end
205
+ def self.#{sym}=(value); @#{sym} = value; end
206
+ def #{sym}=(value); self.class.#{sym} = value; end
207
+ EOS
208
+ end
209
+ end
210
+
211
+ class_local_attr_accessor false, :prefix, :base, :dn_attribute
212
+ class_local_attr_accessor true, :scope
213
+ class_local_attr_accessor true, :required_classes, :recommended_classes
214
+
215
+ class << self
216
+ # Hide new in Base
217
+ private :new
218
+
219
+ # Connect and bind to LDAP creating a class variable for use by
220
+ # all ActiveLdap objects.
221
+ #
222
+ # == +config+
223
+ # +config+ must be a hash that may contain any of the following fields:
224
+ # :password_block, :logger, :host, :port, :base, :bind_dn,
225
+ # :try_sasl, :allow_anonymous
226
+ # :bind_dn specifies the DN to bind with.
227
+ # :password_block specifies a Proc object that will yield a String to
228
+ # be used as the password when called.
229
+ # :logger specifies a preconfigured Log4r::Logger to be used for all
230
+ # logging
231
+ # :host sets the LDAP server hostname
232
+ # :port sets the LDAP server port
233
+ # :base overwrites Base.base - this affects EVERYTHING
234
+ # :try_sasl indicates that a SASL bind should be attempted when binding
235
+ # to the server (default: false)
236
+ # :sasl_mechanisms is an array of SASL mechanism to try
237
+ # (default: ["GSSAPI", "CRAM-MD5", "EXTERNAL"])
238
+ # :allow_anonymous indicates that a true anonymous bind is allowed when
239
+ # trying to bind to the server (default: true)
240
+ # :retries - indicates the number of attempts to reconnect that will be
241
+ # undertaken when a stale connection occurs. -1 means infinite.
242
+ # :sasl_quiet - if true, sets @sasl_quiet on the Ruby/LDAP connection
243
+ # :method - whether to use :ssl, :tls, or :plain (unencrypted)
244
+ # :retry_wait - seconds to wait before retrying a connection
245
+ # :scope - dictates how to find objects. ONELEVEL by default to
246
+ # avoid dn_attr collisions across OUs. Think before changing.
247
+ # :timeout - time in seconds - defaults to disabled. This CAN interrupt
248
+ # search() requests. Be warned.
249
+ # :retry_on_timeout - whether to reconnect when timeouts occur. Defaults
250
+ # to true
251
+ # See lib/configuration.rb for defaults for each option
252
+ def establish_connection(config=nil)
253
+ super
254
+ ensure_logger
255
+ connection.connect
256
+ # Make irb users happy with a 'true'
257
+ true
258
+ end
259
+
260
+ def create(attributes=nil, &block)
261
+ if attributes.is_a?(Array)
262
+ attributes.collect {|attrs| create(attrs, &block)}
263
+ else
264
+ object = new(attributes, &block)
265
+ object.save
266
+ object
267
+ end
268
+ end
269
+
270
+ # This class function is used to setup all mappings between the subclass
271
+ # and ldap for use in activeldap
272
+ #
273
+ # Example:
274
+ # ldap_mapping :dn_attribute => 'uid', :prefix => 'ou=People',
275
+ # :classes => ['top', 'posixAccount'],
276
+ # :scope => :sub
277
+ def ldap_mapping(options={})
278
+ validate_ldap_mapping_options(options)
279
+ dn_attribute = options[:dn_attribute] || default_dn_attribute
280
+ prefix = options[:prefix] || default_prefix
281
+ classes = options[:classes]
282
+ recommended_classes = options[:recommended_classes]
283
+ scope = options[:scope]
284
+
285
+ self.dn_attribute = dn_attribute
286
+ self.prefix = prefix
287
+ self.scope = scope
288
+ self.required_classes = classes
289
+ self.recommended_classes = recommended_classes
290
+
291
+ public_class_method :new
292
+ end
293
+
294
+ alias_method :base_inheritable, :base
295
+ # Base.base
296
+ #
297
+ # This method when included into Base provides
298
+ # an inheritable, overwritable configuration setting
299
+ #
300
+ # This should be a string with the base of the
301
+ # ldap server such as 'dc=example,dc=com', and
302
+ # it should be overwritten by including
303
+ # configuration.rb into this class.
304
+ # When subclassing, the specified prefix will be concatenated.
305
+ def base
306
+ _base = base_inheritable
307
+ _base = configuration[:base] if _base.nil? and configuration
308
+ _base ||= base_inheritable(true)
309
+ [prefix, _base].find_all do |component|
310
+ component and !component.empty?
311
+ end.join(",")
312
+ end
313
+
314
+ alias_method :scope_without_validation=, :scope=
315
+ def scope=(scope)
316
+ validate_scope(scope)
317
+ self.scope_without_validation = scope
318
+ end
319
+
320
+ def validate_scope(scope)
321
+ scope = scope.to_sym if scope.is_a?(String)
322
+ return if scope.nil? or scope.is_a?(Symbol)
323
+ raise ConfigurationError,
324
+ _("scope '%s' must be a Symbol") % scope.inspect
325
+ end
326
+
327
+ def base_class
328
+ if self == Base or superclass == Base
329
+ self
330
+ else
331
+ superclass.base_class
332
+ end
333
+ end
334
+
335
+ def human_attribute_name(attribute_or_name)
336
+ msgid = human_attribute_name_msgid(attribute_or_name)
337
+ msgid ||= human_attribute_name_with_gettext(attribute_or_name)
338
+ s_(msgid)
339
+ end
340
+
341
+ def human_attribute_name_msgid(attribute_or_name)
342
+ if attribute_or_name.is_a?(Schema::Attribute)
343
+ name = attribute_or_name.name
344
+ else
345
+ attribute = schema.attribute(attribute_or_name)
346
+ return nil if attribute.id.nil?
347
+ if attribute.name == attribute_or_name or
348
+ attribute.aliases.include?(attribute_or_name)
349
+ name = attribute_or_name
350
+ else
351
+ return nil
352
+ end
353
+ end
354
+ "LDAP|Attribute|#{name}"
355
+ end
356
+
357
+ def human_attribute_description(attribute_or_name)
358
+ msgid = human_attribute_description_msgid(attribute_or_name)
359
+ return nil if msgid.nil?
360
+ s_(msgid)
361
+ end
362
+
363
+ def human_attribute_description_msgid(attribute_or_name)
364
+ if attribute_or_name.is_a?(Schema::Attribute)
365
+ attribute = attribute_or_name
366
+ else
367
+ attribute = schema.attribute(attribute_or_name)
368
+ return nil if attribute.nil?
369
+ end
370
+ description = attribute.description
371
+ return nil if description.nil?
372
+ "LDAP|Description|Attribute|#{attribute.name}|#{description}"
373
+ end
374
+
375
+ def human_object_class_name(object_class_or_name)
376
+ s_(human_object_class_name_msgid(object_class_or_name))
377
+ end
378
+
379
+ def human_object_class_name_msgid(object_class_or_name)
380
+ if object_class_or_name.is_a?(Schema::ObjectClass)
381
+ name = object_class_or_name.name
382
+ else
383
+ name = object_class_or_name
384
+ end
385
+ "LDAP|ObjectClass|#{name}"
386
+ end
387
+
388
+ def human_object_class_description(object_class_or_name)
389
+ msgid = human_object_class_description_msgid(object_class_or_name)
390
+ return nil if msgid.nil?
391
+ s_(msgid)
392
+ end
393
+
394
+ def human_object_class_description_msgid(object_class_or_name)
395
+ if object_class_or_name.is_a?(Schema::ObjectClass)
396
+ object_class = object_class_or_name
397
+ else
398
+ object_class = schema.object_class(object_class_or_name)
399
+ return nil if object_class.nil?
400
+ end
401
+ description = object_class.description
402
+ return nil if description.nil?
403
+ "LDAP|Description|ObjectClass|#{object_class.name}|#{description}"
404
+ end
405
+
406
+ private
407
+ def validate_ldap_mapping_options(options)
408
+ options.assert_valid_keys(VALID_LDAP_MAPPING_OPTIONS)
409
+ end
410
+
411
+ def ensure_logger
412
+ @@logger ||= configuration[:logger]
413
+ # Setup default logger to console
414
+ if @@logger.nil?
415
+ require 'log4r'
416
+ @@logger = Log4r::Logger.new('activeldap')
417
+ @@logger.level = Log4r::OFF
418
+ Log4r::StderrOutputter.new 'console'
419
+ @@logger.add('console')
420
+ end
421
+ configuration[:logger] ||= @@logger
422
+ end
423
+
424
+ def instantiate(args)
425
+ dn, attributes, options = args
426
+ options ||= {}
427
+ if self.class == Class
428
+ klass = self.ancestors[0].to_s.split(':').last
429
+ real_klass = self.ancestors[0]
430
+ else
431
+ klass = self.class.to_s.split(':').last
432
+ real_klass = self.class
433
+ end
434
+
435
+ obj = real_klass.allocate
436
+ conn = options[:connection] || connection
437
+ obj.connection = conn if conn != connection
438
+ obj.instance_eval do
439
+ initialize_by_ldap_data(dn, attributes)
440
+ end
441
+ obj
442
+ end
443
+
444
+ def default_dn_attribute
445
+ if name.empty?
446
+ dn_attribute = nil
447
+ parent_class = ancestors[1]
448
+ if parent_class.respond_to?(:dn_attribute)
449
+ dn_attribute = parent_class.dn_attribute
450
+ end
451
+ dn_attribute || "cn"
452
+ else
453
+ Inflector.underscore(Inflector.demodulize(name))
454
+ end
455
+ end
456
+
457
+ def default_prefix
458
+ if name.empty?
459
+ nil
460
+ else
461
+ "ou=#{Inflector.pluralize(Inflector.demodulize(name))}"
462
+ end
463
+ end
464
+ end
465
+
466
+ self.scope = :sub
467
+ self.required_classes = ['top']
468
+ self.recommended_classes = []
469
+
470
+ include Enumerable
471
+
472
+ ### All instance methods, etc
473
+
474
+ # new
475
+ #
476
+ # Creates a new instance of Base initializing all class and all
477
+ # initialization. Defines local defaults. See examples If multiple values
478
+ # exist for dn_attribute, the first one put here will be authoritative
479
+ def initialize(attributes=nil)
480
+ init_base
481
+ @new_entry = true
482
+ initial_classes = required_classes | recommended_classes
483
+ if attributes.nil?
484
+ apply_object_class(initial_classes)
485
+ elsif attributes.is_a?(String) or attributes.is_a?(Array)
486
+ apply_object_class(initial_classes)
487
+ self.dn = attributes
488
+ elsif attributes.is_a?(Hash)
489
+ classes, attributes = extract_object_class(attributes)
490
+ apply_object_class(classes | initial_classes)
491
+ normalized_attributes = {}
492
+ attributes.each do |key, value|
493
+ real_key = to_real_attribute_name(key) || key
494
+ normalized_attributes[real_key] = value
495
+ end
496
+ self.dn = normalized_attributes[dn_attribute]
497
+ self.attributes = normalized_attributes
498
+ else
499
+ message = _("'%s' must be either nil, DN value as String or Array " \
500
+ "or attributes as Hash") % attributes.inspect
501
+ raise ArgumentError, message
502
+ end
503
+ yield self if block_given?
504
+ end
505
+
506
+ # Returns true if the +comparison_object+ is the same object, or is of
507
+ # the same type and has the same dn.
508
+ def ==(comparison_object)
509
+ comparison_object.equal?(self) or
510
+ (comparison_object.instance_of?(self.class) and
511
+ comparison_object.dn == dn and
512
+ !comparison_object.new_entry?)
513
+ end
514
+
515
+ # Delegates to ==
516
+ def eql?(comparison_object)
517
+ self == (comparison_object)
518
+ end
519
+
520
+ # Delegates to id in order to allow two records of the same type and id
521
+ # to work with something like:
522
+ # [ User.find("a"), User.find("b"), User.find("c") ] &
523
+ # [ User.find("a"), User.find("d") ] # => [ User.find("a") ]
524
+ def hash
525
+ dn.hash
526
+ end
527
+
528
+ def may
529
+ ensure_apply_object_class
530
+ @may
531
+ end
532
+
533
+ def must
534
+ ensure_apply_object_class
535
+ @must
536
+ end
537
+
538
+ # attributes
539
+ #
540
+ # Return attribute methods so that a program can determine available
541
+ # attributes dynamically without schema awareness
542
+ def attribute_names(normalize=false)
543
+ ensure_apply_object_class
544
+ names = @attr_methods.keys
545
+ if normalize
546
+ names.collect do |name|
547
+ to_real_attribute_name(name)
548
+ end.uniq
549
+ else
550
+ names
551
+ end
552
+ end
553
+
554
+ def attribute_present?(name)
555
+ values = get_attribute(name, true)
556
+ !values.empty? or values.any? {|x| not (x and x.empty?)}
557
+ end
558
+
559
+ # exist?
560
+ #
561
+ # Return whether the entry exists in LDAP or not
562
+ def exist?
563
+ self.class.exists?(dn)
564
+ end
565
+ alias_method(:exists?, :exist?)
566
+
567
+ # new_entry?
568
+ #
569
+ # Return whether the entry is new entry in LDAP or not
570
+ def new_entry?
571
+ @new_entry
572
+ end
573
+
574
+ # dn
575
+ #
576
+ # Return the authoritative dn
577
+ def dn
578
+ dn_value = id
579
+ if dn_value.nil?
580
+ raise DistinguishedNameNotSetError.new,
581
+ _("%s's DN attribute (%s) isn't set") % [self, dn_attribute]
582
+ end
583
+ _base = base
584
+ _base = nil if _base.empty?
585
+ ["#{dn_attribute}=#{dn_value}", _base].compact.join(",")
586
+ end
587
+
588
+ def id
589
+ get_attribute(dn_attribute)
590
+ end
591
+
592
+ def to_param
593
+ id
594
+ end
595
+
596
+ def dn=(value)
597
+ set_attribute(dn_attribute, value)
598
+ end
599
+ alias_method(:id=, :dn=)
600
+
601
+ alias_method(:dn_attribute_of_class, :dn_attribute)
602
+ def dn_attribute
603
+ @dn_attribute || dn_attribute_of_class
604
+ end
605
+
606
+ # destroy
607
+ #
608
+ # Delete this entry from LDAP
609
+ def destroy
610
+ begin
611
+ self.class.delete(dn)
612
+ @new_entry = true
613
+ rescue Error
614
+ raise DeleteError.new(_("Failed to delete LDAP entry: %s") % dn)
615
+ end
616
+ end
617
+
618
+ def delete(options={})
619
+ super(dn, options)
620
+ end
621
+
622
+ # save
623
+ #
624
+ # Save and validate this object into LDAP
625
+ # either adding or replacing attributes
626
+ # TODO: Relative DN support
627
+ def save
628
+ create_or_update
629
+ end
630
+
631
+ def save!
632
+ unless create_or_update
633
+ raise EntryNotSaved, _("entry %s can't be saved") % dn
634
+ end
635
+ end
636
+
637
+ # method_missing
638
+ #
639
+ # If a given method matches an attribute or an attribute alias
640
+ # then call the appropriate method.
641
+ # TODO: Determine if it would be better to define each allowed method
642
+ # using class_eval instead of using method_missing. This would
643
+ # give tab completion in irb.
644
+ def method_missing(name, *args, &block)
645
+ ensure_apply_object_class
646
+
647
+ key = name.to_s
648
+ case key
649
+ when /=$/
650
+ real_key = $PREMATCH
651
+ if have_attribute?(real_key, ['objectClass'])
652
+ if args.size != 1
653
+ raise ArgumentError,
654
+ _("wrong number of arguments (%d for 1)") % args.size
655
+ end
656
+ return set_attribute(real_key, *args, &block)
657
+ end
658
+ when /(?:(_before_type_cast)|(\?))?$/
659
+ real_key = $PREMATCH
660
+ before_type_cast = !$1.nil?
661
+ query = !$2.nil?
662
+ if have_attribute?(real_key, ['objectClass'])
663
+ if args.size > 1
664
+ raise ArgumentError,
665
+ _("wrong number of arguments (%d for 1)") % args.size
666
+ end
667
+ if before_type_cast
668
+ return get_attribute_before_type_cast(real_key, *args)
669
+ elsif query
670
+ return get_attribute_as_query(real_key, *args)
671
+ else
672
+ return get_attribute(real_key, *args)
673
+ end
674
+ end
675
+ end
676
+ super
677
+ end
678
+
679
+ # Add available attributes to the methods
680
+ def methods(inherited_too=true)
681
+ ensure_apply_object_class
682
+ target_names = @attr_methods.keys + @attr_aliases.keys
683
+ target_names -= ['objectClass', Inflector.underscore('objectClass')]
684
+ super + target_names.uniq.collect do |x|
685
+ [x, "#{x}=", "#{x}?", "#{x}_before_type_cast"]
686
+ end.flatten
687
+ end
688
+
689
+ alias_method :respond_to_without_attributes?, :respond_to?
690
+ def respond_to?(name, include_priv=false)
691
+ have_attribute?(name.to_s) or
692
+ (/(?:=|\?|_before_type_cast)$/ =~ name.to_s and
693
+ have_attribute?($PREMATCH)) or
694
+ super
695
+ end
696
+
697
+ # Updates a given attribute and saves immediately
698
+ def update_attribute(name, value)
699
+ send("#{name}=", value)
700
+ save
701
+ end
702
+
703
+ # This performs a bulk update of attributes and immediately
704
+ # calls #save.
705
+ def update_attributes(attrs)
706
+ self.attributes = attrs
707
+ save
708
+ end
709
+
710
+ def update_attributes!(attrs)
711
+ self.attributes = attrs
712
+ save!
713
+ end
714
+
715
+ # This returns the key value pairs in @data with all values
716
+ # cloned
717
+ def attributes
718
+ Marshal.load(Marshal.dump(@data))
719
+ end
720
+
721
+ # This allows a bulk update to the attributes of a record
722
+ # without forcing an immediate save or validation.
723
+ #
724
+ # It is unwise to attempt objectClass updates this way.
725
+ # Also be sure to only pass in key-value pairs of your choosing.
726
+ # Do not let URL/form hackers supply the keys.
727
+ def attributes=(hash_or_assoc)
728
+ _schema = nil
729
+ targets = remove_attributes_protected_from_mass_assignment(hash_or_assoc)
730
+ targets.each do |key, value|
731
+ setter = "#{key}="
732
+ unless respond_to?(setter)
733
+ _schema ||= schema
734
+ attribute = _schema.attribute(key)
735
+ next if attribute.id.nil?
736
+ define_attribute_methods(attribute)
737
+ end
738
+ send(setter, value)
739
+ end
740
+ end
741
+
742
+ def to_ldif
743
+ super(dn, normalize_data(@data))
744
+ end
745
+
746
+ def to_xml(options={})
747
+ root = options[:root] || Inflector.underscore(self.class.name)
748
+ result = "<#{root}>\n"
749
+ result << " <dn>#{dn}</dn>\n"
750
+ normalize_data(@data).sort_by {|key, values| key}.each do |key, values|
751
+ targets = []
752
+ values.each do |value|
753
+ if value.is_a?(Hash)
754
+ value.each do |option, real_value|
755
+ targets << [real_value, " #{option}=\"true\""]
756
+ end
757
+ else
758
+ targets << [value]
759
+ end
760
+ end
761
+ targets.sort_by {|value, attr| value}.each do |value, attr|
762
+ result << " <#{key}#{attr}>#{value}</#{key}>\n"
763
+ end
764
+ end
765
+ result << "</#{root}>\n"
766
+ result
767
+ end
768
+
769
+ def have_attribute?(name, except=[])
770
+ real_name = to_real_attribute_name(name)
771
+ real_name and !except.include?(real_name)
772
+ end
773
+ alias_method :has_attribute?, :have_attribute?
774
+
775
+ def reload
776
+ clear_association_cache
777
+ _, attributes = search(:value => id).find do |_dn, _attributes|
778
+ dn == _dn
779
+ end
780
+ if attributes.nil?
781
+ raise EntryNotFound, _("Can't find DN '%s' to reload") % dn
782
+ end
783
+
784
+ @ldap_data.update(attributes)
785
+ classes, attributes = extract_object_class(attributes)
786
+ apply_object_class(classes)
787
+ self.attributes = attributes
788
+ @new_entry = false
789
+ self
790
+ end
791
+
792
+ def [](name, force_array=false)
793
+ if name == "dn"
794
+ array_of(dn, force_array)
795
+ else
796
+ get_attribute(name, force_array)
797
+ end
798
+ end
799
+
800
+ def []=(name, value)
801
+ set_attribute(name, value)
802
+ end
803
+
804
+ def each
805
+ @data.each do |key, values|
806
+ yield(key.dup, values.dup)
807
+ end
808
+ end
809
+
810
+ def establish_connection(config={})
811
+ if config.is_a?(Hash)
812
+ config = {:bind_dn => dn, :allow_anonymous => false}.merge(config)
813
+ end
814
+ super(config)
815
+ before_connection = @connection
816
+ begin
817
+ @connection = nil
818
+ connection.connect
819
+ @connection = connection
820
+ @schema = nil
821
+ clear_association_cache
822
+ rescue ActiveLdap::Error
823
+ remove_connection
824
+ @connection = before_connection
825
+ raise
826
+ end
827
+ true
828
+ end
829
+
830
+ def schema
831
+ @schema ||= super
832
+ end
833
+
834
+ def inspect
835
+ schema, @schema = @schema, nil
836
+ must, may = @must, @may
837
+ object_classes = @object_classes
838
+ @must, @may = @must.collect(&:name), @may.collect(&:name)
839
+ @object_classes = @object_classes.collect(&:name)
840
+ super
841
+ ensure
842
+ @schema = schema
843
+ @must = must
844
+ @may = may
845
+ @object_classes = object_classes
846
+ end
847
+
848
+ private
849
+ def extract_object_class(attributes)
850
+ classes = []
851
+ attrs = attributes.stringify_keys.reject do |key, value|
852
+ if key == 'objectClass' or
853
+ key.underscore == 'object_class' or
854
+ key.downcase == 'objectclass'
855
+ classes |= [value].flatten
856
+ true
857
+ else
858
+ false
859
+ end
860
+ end
861
+ [classes, attrs]
862
+ end
863
+
864
+ def init_base
865
+ check_configuration
866
+ init_instance_variables
867
+ end
868
+
869
+ def initialize_by_ldap_data(dn, attributes)
870
+ init_base
871
+ @new_entry = false
872
+ @ldap_data = attributes
873
+ classes, attributes = extract_object_class(attributes)
874
+ apply_object_class(classes)
875
+ self.dn = dn
876
+ self.attributes = attributes
877
+ yield self if block_given?
878
+ end
879
+
880
+ def instantiate(args)
881
+ dn, attributes, options = args
882
+ options ||= {}
883
+
884
+ obj = self.class.allocate
885
+ obj.connection = options[:connection] || @connection
886
+ obj.instance_eval do
887
+ initialize_by_ldap_data(dn, attributes)
888
+ end
889
+ obj
890
+ end
891
+
892
+ def to_real_attribute_name(name, allow_normalized_name=false)
893
+ ensure_apply_object_class
894
+ name = name.to_s
895
+ real_name = @attr_methods[name]
896
+ real_name ||= @attr_aliases[Inflector.underscore(name)]
897
+ if real_name
898
+ real_name
899
+ elsif allow_normalized_name
900
+ @attr_methods[@normalized_attr_names[normalize_attribute_name(name)]]
901
+ else
902
+ nil
903
+ end
904
+ end
905
+
906
+ def ensure_apply_object_class
907
+ current_object_class = @data['objectClass']
908
+ return if current_object_class.nil? or current_object_class == @last_oc
909
+ apply_object_class(current_object_class)
910
+ end
911
+
912
+ # enforce_type
913
+ #
914
+ # enforce_type applies your changes without attempting to write to LDAP.
915
+ # This means that if you set userCertificate to somebinary value, it will
916
+ # wrap it up correctly.
917
+ def enforce_type(key, value)
918
+ ensure_apply_object_class
919
+ # Enforce attribute value formatting
920
+ normalize_attribute(key, value)[1]
921
+ end
922
+
923
+ def init_instance_variables
924
+ @mutex = Mutex.new
925
+ @data = {} # where the r/w entry data is stored
926
+ @ldap_data = {} # original ldap entry data
927
+ @attr_methods = {} # list of valid method calls for attributes used for
928
+ # dereferencing
929
+ @normalized_attr_names = {} # list of normalized attribute name
930
+ @attr_aliases = {} # aliases of @attr_methods
931
+ @last_oc = false # for use in other methods for "caching"
932
+ @dn_attribute = nil
933
+ @base = nil
934
+ @scope = nil
935
+ @connection ||= nil
936
+ end
937
+
938
+ # apply_object_class
939
+ #
940
+ # objectClass= special case for updating appropriately
941
+ # This updates the objectClass entry in @data. It also
942
+ # updating all required and allowed attributes while
943
+ # removing defined attributes that are no longer valid
944
+ # given the new objectclasses.
945
+ def apply_object_class(val)
946
+ new_oc = val
947
+ new_oc = [val] if new_oc.class != Array
948
+ new_oc = new_oc.uniq
949
+ return new_oc if @last_oc == new_oc
950
+
951
+ # Store for caching purposes
952
+ @last_oc = new_oc.dup
953
+
954
+ # Set the actual objectClass data
955
+ define_attribute_methods(schema.attribute('objectClass'))
956
+ replace_class(*new_oc)
957
+
958
+ # Build |data| from schema
959
+ # clear attr_method mapping first
960
+ @attr_methods = {}
961
+ @normalized_attr_names = {}
962
+ @attr_aliases = {}
963
+ @must = []
964
+ @may = []
965
+ @object_classes = []
966
+ new_oc.each do |objc|
967
+ # get all attributes for the class
968
+ object_class = schema.object_class(objc)
969
+ @object_classes << object_class
970
+ @must.concat(object_class.must)
971
+ @may.concat(object_class.may)
972
+ end
973
+ @must.uniq!
974
+ @may.uniq!
975
+ (@must + @may).each do |attr|
976
+ # Update attr_method with appropriate
977
+ define_attribute_methods(attr)
978
+ end
979
+ end
980
+
981
+ alias_method :base_of_class, :base
982
+ def base
983
+ [@base, base_of_class].compact.join(",")
984
+ end
985
+
986
+ undef_method :base=
987
+ def base=(object_local_base)
988
+ @base = object_local_base
989
+ end
990
+
991
+ alias_method :scope_of_class, :scope
992
+ def scope
993
+ @scope || scope_of_class
994
+ end
995
+
996
+ undef_method :scope=
997
+ def scope=(scope)
998
+ self.class.validate_scope(scope)
999
+ @scope = scope
1000
+ end
1001
+
1002
+ # get_attribute
1003
+ #
1004
+ # Return the value of the attribute called by method_missing?
1005
+ def get_attribute(name, force_array=false)
1006
+ get_attribute_before_type_cast(name, force_array)
1007
+ end
1008
+
1009
+ def get_attribute_as_query(name, force_array=false)
1010
+ value = get_attribute_before_type_cast(name, force_array)
1011
+ if force_array
1012
+ value.collect {|x| !false_value?(x)}
1013
+ else
1014
+ !false_value?(value)
1015
+ end
1016
+ end
1017
+
1018
+ def false_value?(value)
1019
+ value.nil? or value == false or value == [] or
1020
+ value == "false" or value == "FALSE" or value == ""
1021
+ end
1022
+
1023
+ def get_attribute_before_type_cast(name, force_array=false)
1024
+ attr = to_real_attribute_name(name)
1025
+
1026
+ value = @data[attr] || []
1027
+ # Return a copy of the stored data
1028
+ if force_array
1029
+ value.dup
1030
+ else
1031
+ array_of(value.dup, false)
1032
+ end
1033
+ end
1034
+
1035
+ # set_attribute
1036
+ #
1037
+ # Set the value of the attribute called by method_missing?
1038
+ def set_attribute(name, value)
1039
+ # Get the attr and clean up the input
1040
+ attr = to_real_attribute_name(name)
1041
+ raise UnknownAttribute.new(name) if attr.nil?
1042
+
1043
+ if attr == dn_attribute and value.is_a?(String)
1044
+ new_dn_attribute, value, @base = split_dn_value(value)
1045
+ new_dn_attribute = to_real_attribute_name(new_dn_attribute)
1046
+ if dn_attribute != new_dn_attribute
1047
+ @dn_attribute = attr = new_dn_attribute
1048
+ end
1049
+ end
1050
+
1051
+ # Enforce LDAP-pleasing values
1052
+ real_value = value
1053
+ # Squash empty values
1054
+ if value.class == Array
1055
+ real_value = value.collect {|c| (c.nil? or c.empty?) ? [] : c}.flatten
1056
+ end
1057
+ real_value = [] if real_value.nil?
1058
+ real_value = [] if real_value == ''
1059
+ real_value = [real_value] if real_value.class == String
1060
+ real_value = [real_value.to_s] if real_value.class == Fixnum
1061
+ # NOTE: Hashes are allowed for subtyping.
1062
+
1063
+ # Assign the value
1064
+ @data[attr] = enforce_type(attr, real_value)
1065
+
1066
+ # Return the passed in value
1067
+ @data[attr]
1068
+ end
1069
+
1070
+ def split_dn_value(value)
1071
+ dn_value = relative_dn_value = nil
1072
+ begin
1073
+ dn_value = DN.parse(value)
1074
+ rescue DistinguishedNameInvalid
1075
+ dn_value = DN.parse("#{dn_attribute}=#{value}")
1076
+ end
1077
+
1078
+ begin
1079
+ relative_dn_value = dn_value - DN.parse(base_of_class)
1080
+ relative_dn_value = dn_value if relative_dn_value.rdns.empty?
1081
+ rescue ArgumentError
1082
+ relative_dn_value = dn_value
1083
+ end
1084
+
1085
+ val, *bases = relative_dn_value.rdns
1086
+ dn_attribute_name, dn_attribute_value = val.to_a[0]
1087
+ [dn_attribute_name, dn_attribute_value,
1088
+ bases.empty? ? nil : DN.new(*bases).to_s]
1089
+ end
1090
+
1091
+ # define_attribute_methods
1092
+ #
1093
+ # Make a method entry for _every_ alias of a valid attribute and map it
1094
+ # onto the first attribute passed in.
1095
+ def define_attribute_methods(attr)
1096
+ name = attr.name
1097
+ return if @attr_methods.has_key?(name)
1098
+ ([name] + attr.aliases).each do |ali|
1099
+ @attr_methods[ali] = name
1100
+ @attr_aliases[Inflector.underscore(ali)] = name
1101
+ @normalized_attr_names[normalize_attribute_name(ali)] = name
1102
+ end
1103
+ end
1104
+
1105
+ # array_of
1106
+ #
1107
+ # Returns the array form of a value, or not an array if
1108
+ # false is passed in.
1109
+ def array_of(value, to_a=true)
1110
+ case value
1111
+ when Array
1112
+ if to_a or value.size > 1
1113
+ value.collect {|v| array_of(v, to_a)}
1114
+ else
1115
+ if value.empty?
1116
+ nil
1117
+ else
1118
+ array_of(value.first, to_a)
1119
+ end
1120
+ end
1121
+ when Hash
1122
+ if to_a
1123
+ [value]
1124
+ else
1125
+ result = {}
1126
+ value.each {|k, v| result[k] = array_of(v, to_a)}
1127
+ result
1128
+ end
1129
+ else
1130
+ to_a ? [value.to_s] : value.to_s
1131
+ end
1132
+ end
1133
+
1134
+ def normalize_data(data, except=[])
1135
+ _schema = schema
1136
+ result = {}
1137
+ data.each do |key, values|
1138
+ next if except.include?(key)
1139
+ real_name = to_real_attribute_name(key)
1140
+ next if real_name and except.include?(real_name)
1141
+ real_name ||= key
1142
+ next if _schema.attribute(real_name).id.nil?
1143
+ result[real_name] ||= []
1144
+ result[real_name].concat(values)
1145
+ end
1146
+ result
1147
+ end
1148
+
1149
+ def collect_modified_attributes(ldap_data, data)
1150
+ attributes = []
1151
+ # Now that all the options will be treated as unique attributes
1152
+ # we can see what's changed and add anything that is brand-spankin'
1153
+ # new.
1154
+ ldap_data.each do |k, v|
1155
+ value = data[k] || []
1156
+
1157
+ next if v == value
1158
+
1159
+ # Create mod entries
1160
+ if value.empty?
1161
+ # Since some types do not have equality matching rules,
1162
+ # delete doesn't work
1163
+ # Replacing with nothing is equivalent.
1164
+ if !data.has_key?(k) and schema.attribute(k).binary_required?
1165
+ value = [{'binary' => []}]
1166
+ end
1167
+ else
1168
+ # Ditched delete then replace because attribs with no equality
1169
+ # match rules will fails
1170
+ end
1171
+ attributes.push([:replace, k, value])
1172
+ end
1173
+ data.each do |k, v|
1174
+ value = v || []
1175
+ next if ldap_data.has_key?(k) or value.empty?
1176
+
1177
+ # Detect subtypes and account for them
1178
+ # REPLACE will function like ADD, but doesn't hit EQUALITY problems
1179
+ # TODO: Added equality(attr) to Schema
1180
+ attributes.push([:replace, k, value])
1181
+ end
1182
+
1183
+ attributes
1184
+ end
1185
+
1186
+ def collect_all_attributes(data)
1187
+ dn_attr = to_real_attribute_name(dn_attribute)
1188
+ dn_value = data[dn_attr]
1189
+
1190
+ attributes = []
1191
+ attributes.push([:add, dn_attr, dn_value])
1192
+
1193
+ oc_value = data['objectClass']
1194
+ attributes.push([:add, 'objectClass', oc_value])
1195
+ data.each do |key, value|
1196
+ next if value.empty? or key == 'objectClass' or key == dn_attr
1197
+
1198
+ attributes.push([:add, key, value])
1199
+ end
1200
+
1201
+ attributes
1202
+ end
1203
+
1204
+ def check_configuration
1205
+ unless dn_attribute
1206
+ raise ConfigurationError,
1207
+ _("dn_attribute isn't set for this class: %s") % self.class
1208
+ end
1209
+ end
1210
+
1211
+ def create_or_update
1212
+ new_entry? ? create : update
1213
+ end
1214
+
1215
+ def prepare_data_for_saving
1216
+ # Expand subtypes to real ldap_data attributes
1217
+ # We can't reuse @ldap_data because an exception would leave
1218
+ # an object in an unknown state
1219
+ ldap_data = normalize_data(@ldap_data)
1220
+
1221
+ # Expand subtypes to real data attributes, but leave @data alone
1222
+ bad_attrs = @data.keys - attribute_names
1223
+ data = normalize_data(@data, bad_attrs)
1224
+
1225
+ success = yield(data, ldap_data)
1226
+
1227
+ if success
1228
+ @ldap_data = Marshal.load(Marshal.dump(data))
1229
+ # Delete items disallowed by objectclasses.
1230
+ # They should have been removed from ldap.
1231
+ bad_attrs.each do |remove_me|
1232
+ @ldap_data.delete(remove_me)
1233
+ end
1234
+ end
1235
+
1236
+ success
1237
+ end
1238
+
1239
+ def create
1240
+ prepare_data_for_saving do |data, ldap_data|
1241
+ attributes = collect_all_attributes(data)
1242
+ add_entry(dn, attributes)
1243
+ @new_entry = false
1244
+ true
1245
+ end
1246
+ end
1247
+
1248
+ def update
1249
+ prepare_data_for_saving do |data, ldap_data|
1250
+ attributes = collect_modified_attributes(ldap_data, data)
1251
+ modify_entry(dn, attributes)
1252
+ true
1253
+ end
1254
+ end
1255
+ end # Base
1256
+ end # ActiveLdap