dm-ldap-adapter 0.4.3-java

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,75 @@
1
+ version 0.4.3
2
+ =============
3
+
4
+ * test several environments: ruby-1.8.7/1.9.2, jruby-1.5.6/1.6.2 (1.8 and 1.9 where possible), DM-1.0.x/1.1.x, net-ldap and ruby-ldap gem as backend. run the specs against all possible combinations.
5
+
6
+ * improvements with LdapArray properties
7
+
8
+ version 0.4.2
9
+ =============
10
+
11
+ * Serial fieldnames are case insensitive now, i.e. allow capital as well
12
+
13
+ version 0.4.1
14
+ =============
15
+
16
+ * mutliline values inside Ldap::Array
17
+
18
+ version 0.3.5
19
+ =============
20
+
21
+ * sorting can handle nil values now
22
+
23
+ version 0.3.4
24
+ =============
25
+
26
+ * fixed bug with or condition
27
+
28
+ * sort now case insensitive
29
+
30
+ * added setup parameter to choose the facade the ldap adapter shall use
31
+
32
+ version 0.3.3
33
+ =============
34
+
35
+ * fix bug with empty LdapArray for ruby-ldap-adapter
36
+
37
+ * added order option to search with just using the first order attribute and ignoring the direction and other attributes
38
+
39
+ version 0.3.2
40
+ =============
41
+
42
+ * lazy property are not loaded from ldap anymore
43
+
44
+ * new facade which uses ruby-ldap, since it has better support for ldap protocol and is about 30% faster with search queries
45
+
46
+ version 0.3.1
47
+ =============
48
+
49
+ * fixed LdapArray bug in collections
50
+
51
+ * default of LdapArray is now a new object for each resource instance
52
+
53
+ * allow Serial to be used in dn_prefix
54
+
55
+ version 0.3.0
56
+ =============
57
+
58
+ * fixed bug where Serial and Integer,:serial=>true were handled differently. the Integer values are handle with all types which have an Integer as primitive
59
+
60
+ * added dm-core gem dependency with version below 0.10.0
61
+
62
+ * added LdapArray type for resources which allow the use of the multivalue ldap attriutes
63
+
64
+ * allow conditions in queries, but only of the form "<property_name> <comparator> <value> [or <property_name> <comparator> <value>]*" where comparator is one of "=", "like"
65
+
66
+ version 0.2.0
67
+ =============
68
+
69
+ * switched to Slf4r logger
70
+
71
+ * the whole thing became a gem
72
+
73
+ * cleaned up example
74
+
75
+ * moved the SHA and SSHA calculation into its own helper class (incompatible change to older version)
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Kristian Meier
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,247 @@
1
+ # dm-ldap-adapter
2
+
3
+ *Homepage*: [http://github.com/mkristian/dm-ldap-adapter](http://github.com/mkristian/dm-ldap-adapter)
4
+
5
+ *Git*: [git://github.com/mkristian/dm-ldap-adapter.git](git://github.com/mkristian/dm-ldap-adapter.git)
6
+
7
+ *Author*: Kristian Meier
8
+
9
+ *Copyright*: 2008-2009
10
+
11
+ ## Note on Patches/Pull Requests
12
+
13
+ * Fork the project.
14
+
15
+ * Make your feature addition or bug fix.
16
+
17
+ * Add tests for it. This is important so I don't break it in a future version unintentionally.
18
+
19
+ * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
20
+
21
+ * Send me a pull request.
22
+
23
+ ## DESCRIPTION:
24
+
25
+ ### usecase
26
+
27
+ the usecase for that implementation was using an ldap server for user authentication and authorization. the ldap server is configured to have posixAccounts and posixGroups. on the datamapper side these accounts/groups are modeled with many-to-many relationship. further more the model classes should be in such a way that they can be used with another repository as well, i.e. they carry some ldap related configuration but this is only relevant for the ldap-adapter.
28
+
29
+ ### low level ldap library
30
+
31
+ the ldap library which does the actual ldap protocol stuff is [http://rubyforge.org/projects/net-ldap] which is the default. the other ldap library is [http://rubyforge.org/projects/ruby-ldap]. just add a facade parameter when setting up DataMapper
32
+
33
+ DataMapper.setup(:ldap, {
34
+ :adapter => 'ldap',
35
+ :facade => :ruby_ldap,
36
+ .... })
37
+
38
+ or
39
+
40
+ DataMapper.setup(:ldap, {
41
+ :adapter => 'ldap',
42
+ :facade => :net_ldap,
43
+ .... })
44
+
45
+ ### setup DataMapper
46
+
47
+ DataMapper.setup(:ldap, {
48
+ :adapter => 'ldap',
49
+ :host => 'localhost',
50
+ :port => '389',
51
+ :base => "dc=example,dc=com",
52
+ :facade => :ruby_ldap,
53
+ :bind_name => "cn=admin,dc=example,dc=com",
54
+ :password => "behappy"
55
+ })
56
+
57
+ ### examples
58
+
59
+ see 'example/posix.rb' for user/group setup works with default installation of openldap on ubuntu (just change your password as needed in the code)
60
+
61
+ ## FEATURES/PROBLEMS:
62
+
63
+ * the net-ldap has some issues with not closing the connections when an exception/error got raised, with limit the search result to 126 entries which gets fixed by making consecutives searches and collect the result.
64
+
65
+ * error from the ldap server are only logged and do not raise any exceptions (to be changed in next release) with one exception: when creating a new ldap entry a duplicated entry will raise DataMapper::PersistenceError
66
+
67
+ ## SYNOPSIS:
68
+
69
+ ### distinguished name (DN) of a model
70
+
71
+ there are three parts which makes the DN of a model, the base from the ldap conncetion, the `treebase` of the model and `dn_prefix` of an instance.
72
+
73
+ class User
74
+ include DataMapper::Resource
75
+ property :id, Serial, :field => "uidNumber"
76
+ dn_prefix { |user| "uid=#{user.login}"}
77
+ treebase "ou=people"
78
+ end
79
+
80
+ with a base `dc=example,dc=com` we get a DN like the user 'admin'
81
+
82
+ uid=admin,ou=people,dc=example,dc=com
83
+
84
+ ### ldap entities are bigger than the model
85
+
86
+ for example the ldap posixGroup has more attributes than the model class, it needs the `objectclass` attribute set to `posixGroup`.
87
+
88
+ class Group
89
+ include DataMapper::Resource
90
+ property :id, Serial, :field => "gidNumber"
91
+ property :name, String, :field => "cn"
92
+ dn_prefix { |group| "cn=#{group.name}" }
93
+ treebase "ou=groups"
94
+ ldap_properties {{ :objectclass => "posixGroup"}}
95
+ end
96
+
97
+ so with the help of the `ldap_properties` you can define a block which returns an hash with extra attributes. with such block you can make some calculations if needed, i.e. :homedirectory => "/home/#{login}" for the posixAccount.
98
+
99
+ ### authentication
100
+
101
+ this uses the underlying bind of a ldap connection. so on any model where you have the `dn_prefix` and the `treebase` configured, you can call the method `authenticate(password)`. this will forward the request to the ldap server.
102
+
103
+ ### queries
104
+
105
+ conditions in ldap depend on the attributes definition in the ldap schema. here is the list of what is working with that ldap adapter side and the usual AND between the conditions:
106
+
107
+ * :eql
108
+ * :not
109
+ * :like
110
+ * :in
111
+ * Range
112
+
113
+ not working are `:lt, :lte, :gt, :gte`
114
+
115
+ *note*: sql handles `NULL` different from values, i.e.
116
+
117
+ select * from users where name = 'sd';
118
+
119
+ and
120
+
121
+ select * from users where name != 'sd';
122
+
123
+ gives the same result when *all* names are `NULL` !!!
124
+
125
+ ### OR conditions
126
+
127
+ or-conditions can be done with :conditions option but only of the form "<property_name> <comparator> <value> [or <property_name> <comparator> <value>]*" where the comparator is one of "=", "like". it can be also combined with extra ANDs like this example
128
+
129
+ Contact.all(:name.like => "A%", :conditions => ["phone like '+49%' or mobile like '+49%'"])
130
+
131
+ ### multiple repositories
132
+
133
+ most probably you have to work with ldap as one repository and a database as a second repository. for me it worked best to define the `default_repository` for each model in the model itself:
134
+
135
+ class User
136
+ . . .
137
+ def self.default_repository_name
138
+ :ldap
139
+ end
140
+ end
141
+
142
+ class Config
143
+ . . .
144
+ def self.default_repository_name
145
+ :db
146
+ end
147
+ end
148
+
149
+ if you want to benefit from the advantages of the identidy maps you need to wrap your actions for *merb* see http://www.datamapper.org/doku.php?id=docs:identity_map or for *rails* put an `around_filter` inside application.rb
150
+
151
+ around_filter :repositories
152
+
153
+ def repositories
154
+ DataMapper.repository(:ldap) do
155
+ DataMapper.repository(:db) do
156
+ yield
157
+ end
158
+ end
159
+ end
160
+
161
+ and to let the ldap resources use the ldap respository it is best to bind it to that repository like this
162
+
163
+ class User
164
+ . . .
165
+ def self.repository_name
166
+ :ldap
167
+ end
168
+ end
169
+
170
+ ### transactions
171
+
172
+ the adapter offers a noop transaction, i.e. you can wrap everything into a transaction but the ldap part has no functionality.
173
+
174
+ *note*: the ldap protocol does not know transactions
175
+
176
+ ### many-to-many associations
177
+
178
+ staying with posix example there the groups has a memberuid attribute BUT unlike with relational databases it can have multiple values. to achieve a relationship with these values the underlying adapter needs to know that this specific attribute needs to be handled differently. for this `multivalue_field` comes into play. the ldap adapter clones the model and places the each memberuid in its own clone.
179
+
180
+ class GroupUser
181
+ include DataMapper::Resource
182
+ property :memberUid, String, :key => true
183
+ property :gidNumber, Integer, :key => true
184
+ dn_prefix { |group_user| "cn=#{group_user.group.name}" }
185
+ treebase "ou=groups"
186
+ ldap_properties do |group_user|
187
+ {:cn=>"#{group_user.group.name}", :objectclass => "posixGroup"}
188
+ end
189
+
190
+ multivalue_field :memberuid
191
+
192
+ end
193
+
194
+ ### ldap attributes with many values
195
+
196
+ let's say your LDAP has multiple email values for a users then you can define your resource class like that using the type *LdapArray* for such multivalue fields
197
+
198
+ class User
199
+ include DataMapper::Resource
200
+ property :id, Serial, :field => "uidNumber"
201
+ property :login, String, :field => "uid", :unique_index => true
202
+ property :mail, LdapArray
203
+
204
+ dn_prefix { |user| "uid=#{user.login}"}
205
+ treebase "ou=people"
206
+ ldap_properties do |user|
207
+ properties = { :objectclass => ["inetOrgPerson", "posixAccount", "shadowAccount"], :loginshell => "/bin/bash", :gidNumber => "10000" }
208
+ properties
209
+ end
210
+ end
211
+
212
+ ## REQUIREMENTS:
213
+
214
+ * slf4r the logging facade
215
+ * net-ldap pure ruby ldap library
216
+ * ruby-ldap (optional) ruby with native ldap code
217
+ * logging (optional) if logging via logging is desired
218
+ * log4r (optional) if logging via log4r is desired
219
+
220
+ ## INSTALL:
221
+
222
+ * sudo gem install dm-ldap-adapter
223
+
224
+ ## LICENSE:
225
+
226
+ (The MIT License)
227
+
228
+ Copyright (c) 2009 Kristian Meier
229
+
230
+ Permission is hereby granted, free of charge, to any person obtaining
231
+ a copy of this software and associated documentation files (the
232
+ 'Software'), to deal in the Software without restriction, including
233
+ without limitation the rights to use, copy, modify, merge, publish,
234
+ distribute, sublicense, and/or sell copies of the Software, and to
235
+ permit persons to whom the Software is furnished to do so, subject to
236
+ the following conditions:
237
+
238
+ The above copyright notice and this permission notice shall be
239
+ included in all copies or substantial portions of the Software.
240
+
241
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
242
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
243
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
244
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
245
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
246
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
247
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/ldap-commands.txt ADDED
@@ -0,0 +1,17 @@
1
+ # populate ldap with basic data
2
+ ldapadd -x -w behappy -c -D "cn=admin,dc=example,dc=com" < test.ldif
3
+
4
+ # search the whole domain
5
+ ldapsearch -x -w behappy -c -D "cn=admin,dc=example,dc=com" -b 'dc=example,dc=com'
6
+
7
+ # search people
8
+ ldapsearch -x -w behappy -c -D "cn=admin,dc=example,dc=com" -b 'ou=people,dc=example,dc=com'
9
+
10
+ #search groups
11
+ ldapsearch -x -w behappy -c -D "cn=admin,dc=example,dc=com" -b 'ou=groups,dc=example,dc=com'
12
+
13
+ # printout delete commands for all people
14
+ ldapsearch -x -w behappy -c -D "cn=admin,dc=example,dc=com" -b 'ou=people,dc=example,dc=com' "uid=*" | grep ^uid: | sed -e "s/^.....//" -e 's/$/,ou=people,dc=example,dc=com"/' -e 's/^/-x -w behappy -c -D "cn=admin,dc=example,dc=com" "uid=/' | xargs -L 1 echo ldapdelete
15
+
16
+ # all groups
17
+ ldapsearch -x -w behappy -c -D "cn=admin,dc=example,dc=com" -b 'ou=groups,dc=example,dc=com' "cn=*" | grep ^cn: | sed -e "s/^....//" -e 's/$/,ou=groups,dc=example,dc=com"/' -e 's/^/-x -w behappy -c -D "cn=admin,dc=example,dc=com" "cn=/' | xargs -L 1 echo ldapdelete
@@ -0,0 +1,370 @@
1
+ require "dm-core"
2
+ require 'slf4r/logger'
3
+ # require 'adapters/noop_transaction'
4
+
5
+ module Ldap
6
+
7
+ # the class provides two ways of getting a LdapFacade. either
8
+ # one which is put on the current Thread or a new one
9
+ class LdapConnection
10
+
11
+ include ::Slf4r::Logger
12
+
13
+ def initialize(options)
14
+ if options[:facade].nil?
15
+ require 'ldap/net_ldap_facade'
16
+ @facade = ::Ldap::NetLdapFacade
17
+ else
18
+ case options[:facade].to_sym
19
+ when :ruby_ldap
20
+ require 'ldap/ruby_ldap_facade'
21
+ @facade = ::Ldap::RubyLdapFacade
22
+ when :net_ldap
23
+ require 'ldap/net_ldap_facade'
24
+ @facade = ::Ldap::NetLdapFacade
25
+ else
26
+ "please add a :facade parameter to the adapter setup. possible values are :ruby_ldap or net_ldap"
27
+ end
28
+ end
29
+ logger.info("using #{@facade}")
30
+ @ldaps = { }
31
+ auth = {
32
+ :method => :simple,
33
+ :username => options[:bind_name],
34
+ :password => options[:password]
35
+ }
36
+ @config = {
37
+ :host => options[:host],
38
+ :port => options[:port].to_i,
39
+ :auth => auth,
40
+ :base => options[:base]
41
+ }
42
+ end
43
+
44
+ # puts a LdapFacade into the current thread and executes the
45
+ # given block.
46
+ def open
47
+ begin
48
+ @facade.open(@config) do |ldap|
49
+ @ldaps[Thread.current] = @facade.new(ldap)
50
+ yield
51
+ end
52
+ ensure
53
+ @ldaps[Thread.current] = nil
54
+ end
55
+ end
56
+
57
+ # @return [Ldap::LdapFacade]
58
+ # either the one from the current Thread or a new one
59
+ def current
60
+ ldap = @ldaps[Thread.current]
61
+ if ldap
62
+ ldap
63
+ else
64
+ @facade.new(@config)
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ require "dm-core"
71
+ module DataMapper
72
+ class Query
73
+
74
+ class SortCaseInsensitive < Sort
75
+ def initialize(value, ascending = true)
76
+ if(value && value.is_a?(String))
77
+ super(value.upcase, ascending)
78
+ else
79
+ super
80
+ end
81
+ end
82
+ end
83
+
84
+ def sort_records_case_insensitive(records)
85
+ #Return unsorted records unless we have order defined
86
+ return records unless order
87
+ sort_order = order.map { |direction| [ direction.target, direction.operator == :asc ] }
88
+
89
+ records.sort_by do |record|
90
+ sort_order.map do |(property, ascending)|
91
+ SortCaseInsensitive.new(record_value(record, property), ascending)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ module Adapters
97
+ class LdapAdapter < AbstractAdapter
98
+
99
+ # @return [Ldap::LdapFacade]
100
+ # ready to use LdapFacade
101
+ def ldap
102
+ @ldap_connection.current
103
+ end
104
+
105
+ def open_ldap_connection(&block)
106
+ @ldap_connection.open(&block)
107
+ end
108
+
109
+ def key_properties(resource)
110
+ resource.model.key.first
111
+ end
112
+
113
+ COMPARATORS = { "=" => :eql, ">=" => :gte, "<=" => :lte, "like" => :like }
114
+
115
+ # helper to remove datamapper specific classes from the conditions
116
+ # @param [Array] conditions
117
+ # array of tuples: (action, property, new value)
118
+ # @return [Array]
119
+ # tuples: (action, attribute name, new value)
120
+ def to_ldap_conditions(query)
121
+ conditions = query.conditions
122
+ ldap_conditions = []
123
+ conditions.operands.each do |c|
124
+ if c.is_a? Array
125
+ props = {}
126
+ query.fields.each{ |f| props[f.name] = f.field}
127
+ or_conditions = []
128
+ c[0].split('or').each do |e|
129
+ e.strip!
130
+ match = e.match("=|<=|>=|like")
131
+ or_conditions << [COMPARATORS[match.values_at(0)[0]],
132
+ props[match.pre_match.strip.to_sym],
133
+ match.post_match.strip.gsub(/'/, '')]
134
+ end
135
+ ldap_conditions << [:or_operator, or_conditions, nil]
136
+ else
137
+ comparator = c.slug
138
+ case comparator
139
+ when :raw
140
+ when :not
141
+ # TODO proper recursion !!!
142
+ ldap_conditions << [comparator, c.operands.first.subject.field, c.operands.first.send(:dumped_value)]
143
+ when :in
144
+ ldap_conditions << [:eql, c.subject.field, c.send(:dumped_value)]
145
+ else
146
+ if c.subject.is_a? Ldap::LdapArray
147
+ # assume a single value here !!!
148
+ val = c.send(:dumped_value)
149
+ ldap_conditions << [comparator, c.subject.field, val[1, val.size - 2]]
150
+ else
151
+ ldap_conditions << [comparator, c.subject.field, c.send(:dumped_value)]
152
+ end
153
+ end
154
+ end
155
+ end
156
+ ldap_conditions
157
+ end
158
+
159
+ include ::Slf4r::Logger
160
+
161
+ # @see AbstractAdapter
162
+ # def transaction_primitive
163
+ # NoopTransaction.new
164
+ # end
165
+
166
+ public
167
+
168
+ def initialize(name, options)
169
+ super(name, options)
170
+ @ldap_connection = ::Ldap::LdapConnection.new(@options)
171
+ end
172
+
173
+
174
+ def create(resources)
175
+ resources.select do |resource|
176
+ create_resource(resource)
177
+ end.size # just return the number of create resources
178
+ end
179
+
180
+ def update(attributes, collection)
181
+ collection.each do |resource|
182
+ #puts "update"
183
+ #p resource
184
+ update_resource(resource, attributes)
185
+
186
+ end.size
187
+ end
188
+ # @param [DataMapper::Resource] resource
189
+ # to be created
190
+ # @see SimpleAdapter#create_resource
191
+ # @return [Fixnum]
192
+ # value for the primary key or nil
193
+ def create_resource(resource)
194
+ logger.debug { resource.inspect }
195
+
196
+ props = resource.model.ldap_properties(resource)
197
+ key = nil
198
+ resource.send(:properties).each do |prop|
199
+ value = prop.get!(resource)
200
+ if prop.class == ::Ldap::LdapArray
201
+ props[prop.field.to_sym] = value unless value.nil? or value.size == 0
202
+ else
203
+ props[prop.field.to_sym] = value.to_s unless value.nil?
204
+ end
205
+ key = prop if prop.serial?
206
+ end
207
+ resource_dup = resource.dup
208
+ id = ldap.retrieve_next_id(resource.model.treebase,
209
+ key_properties(resource).field)
210
+ resource_dup.send("#{key_properties(resource).name}=".to_sym, id)
211
+ props[key_properties(resource).field.to_sym] = "#{id}"
212
+ key_value = begin
213
+ ldap.create_object(resource.model.dn_prefix(resource_dup),
214
+ resource.model.treebase,
215
+ key_properties(resource).field,
216
+ props, resource.model.multivalue_field)
217
+ rescue => e
218
+ raise e unless resource.model.multivalue_field
219
+ # TODO something with creating these multivalue objects
220
+ end
221
+ logger.debug { "resource #{resource.inspect} key value: #{key_value.inspect}" + ", multivalue_field: " + resource.model.multivalue_field.to_s }
222
+ if key_value and !key.nil?
223
+ key.set!(resource, key_value.to_i)
224
+ resource
225
+ elsif resource.model.multivalue_field
226
+ multivalue_prop = resource.send(:properties).detect do |prop|
227
+ prop.field.to_sym == resource.model.multivalue_field
228
+ end
229
+ update_resource(resource,
230
+ { multivalue_prop =>
231
+ resource.send(multivalue_prop.name.to_sym)})
232
+ else
233
+ nil
234
+ end
235
+ end
236
+
237
+ # @param [DataMapper::Resource] resource
238
+ # to be updated
239
+ # @param [Hash] attributes
240
+ # new attributes for the resource
241
+ # @see SimpleAdapter#update_resource
242
+ def update_resource(resource, attributes)
243
+ actions = []
244
+ attributes.each do |property, value|
245
+ field = property.field.to_sym #TODO sym needed or string ???
246
+ if property.class == ::Ldap::LdapArray
247
+ value = property.load(value)
248
+ if resource.original_attributes[property].nil?
249
+ value.each do |v|
250
+ actions << [:add, field, v]
251
+ end
252
+ else
253
+ array_actions = []
254
+ resource.original_attributes[property].each do |ov|
255
+ unless value.member? ov
256
+ actions << [:delete, field, ov.to_s]
257
+ end
258
+ end
259
+ value.each do |v|
260
+ unless resource.original_attributes[property].member? v
261
+ actions << [:add, field, v.to_s]
262
+ end
263
+ end
264
+ array_actions
265
+ end
266
+ else
267
+ if resource.model.multivalue_field == property.field.to_sym
268
+ if value.nil?
269
+ actions << [:delete, field, resource.attribute_get(property.name).to_s]
270
+ else
271
+ actions << [:add, field, value.to_s]
272
+ end
273
+ elsif value.nil?
274
+ actions << [:delete, field, []]
275
+ elsif resource.original_attributes[property].nil?
276
+ actions << [:add, field, value.to_s]
277
+ else
278
+ actions << [:replace, field, value.to_s]
279
+ end
280
+ end
281
+ end
282
+ #puts "actions"
283
+ #p actions
284
+ #puts
285
+ ldap.update_object(resource.model.dn_prefix(resource),
286
+ resource.model.treebase,
287
+ actions)
288
+ end
289
+
290
+ # @see AbstractAdapter#delete
291
+ def delete(collection)
292
+ collection.each do |resource|
293
+ if resource.model.multivalue_field
294
+ multivalue_prop = resource.send(:properties).detect do |prop|
295
+ prop.field.to_sym == resource.model.multivalue_field
296
+ end
297
+ update_resource(resource,
298
+ { multivalue_prop => nil })
299
+ else
300
+ ldap.delete_object(resource.model.dn_prefix(resource),
301
+ resource.model.treebase)
302
+ end
303
+ end
304
+ end
305
+
306
+ # @see AbstractAdapter#read
307
+ def read(query)
308
+ result = []
309
+ #get data values out of the query
310
+ resources = read_resources(query)
311
+ resources.each do |resource|
312
+ map = {}
313
+ query.fields.each_with_index do |property, idx|
314
+ map[property.field] = property.typecast(resource[idx])
315
+ end
316
+ result << map
317
+ end
318
+
319
+ #puts "read_many"
320
+ #p result
321
+ result = result.uniq if query.unique?
322
+ result = query.match_records(result) if query.model.multivalue_field
323
+ result = query.sort_records_case_insensitive(result)
324
+ result = query.limit_records(result)
325
+ result
326
+ end
327
+
328
+ def read_resources(query)
329
+ query_order = query.order.first.target.field if query.order
330
+ #Use empty string as default for order in query
331
+ order_by = query_order || ''
332
+
333
+ field_names = query.fields.collect {|f| f.field }
334
+ result = ldap.read_objects(query.model.treebase,
335
+ query.model.key.collect { |k| k.field },
336
+ to_ldap_conditions(query),
337
+ field_names, order_by)
338
+ if query.model.multivalue_field
339
+ props_result = []
340
+ result.each do |props|
341
+ # run over all values of the multivalue field
342
+ (props[query.model.multivalue_field] || []).each do |value|
343
+ values = query.fields.collect do |f|
344
+ if query.model.multivalue_field == f.field.to_sym
345
+ value
346
+ else
347
+ prop = props[f.field.to_sym].first
348
+ f.primitive == Integer ? prop.to_i : prop.join
349
+ end
350
+ end
351
+ props_result << values
352
+ end
353
+ end
354
+ props_result
355
+ else # no multivalue field
356
+ result.collect do |props|
357
+ query.fields.collect do |f|
358
+ prop = props[f.field.to_sym]
359
+ if f.class == Ldap::LdapArray
360
+ prop if prop
361
+ elsif prop
362
+ f.primitive == Integer ? prop.first.to_i : prop.join
363
+ end
364
+ end
365
+ end
366
+ end
367
+ end
368
+ end
369
+ end
370
+ end