ruby-activeldap 0.8.3 → 0.8.3.1

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 (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