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