rubysync 0.1.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,19 +3,26 @@
3
3
  # Copyright (c) 2007 Ritchie Young. All rights reserved.
4
4
  #
5
5
  # This file is part of RubySync.
6
- #
7
- # RubySync is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License
8
- # as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
9
- #
10
- # RubySync is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
11
- # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
12
- #
13
- # You should have received a copy of the GNU General Public License along with RubySync; if not, write to the
14
- # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
6
+ #
7
+ # RubySync is free software; you can redistribute it and/or modify it
8
+ # under the terms of the GNU General Public License
9
+ # as published by the Free Software Foundation; either version 2 of
10
+ # the License, or (at your option) any later version.
11
+ #
12
+ # RubySync is distributed in the hope that it will be useful, but
13
+ # WITHOUT ANY WARRANTY; without even the implied
14
+ # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
15
+ # the GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with RubySync; if not, write to the
19
+ # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20
+ # Boston, MA 02110-1301, USA
15
21
 
16
22
 
17
23
  lib_path = File.dirname(__FILE__) + '/..'
18
- $:.unshift lib_path unless $:.include?(lib_path) || $:.include?(File.expand_path(lib_path))
24
+ $:.unshift lib_path unless $:.include?(lib_path) ||
25
+ $:.include?(File.expand_path(lib_path))
19
26
 
20
27
  require 'ruby_sync'
21
28
  require 'net/ldif'
@@ -26,27 +33,29 @@ require 'net/ldap'
26
33
 
27
34
  class Net::LDAP::Entry
28
35
  def to_hash
29
- return @myhash.dup
36
+ @myhash.dup
30
37
  end
31
38
  end
32
39
 
33
40
  module RubySync::Connectors
34
41
  class LdapConnector < RubySync::Connectors::BaseConnector
35
-
42
+
36
43
  option :host,
37
- :port,
38
- :bind_method,
39
- :username,
40
- :password,
41
- :search_filter,
42
- :search_base,
43
- :association_attribute # name of the attribute in which to store the association key(s)
44
-
44
+ :port,
45
+ :bind_method,
46
+ :encryption,
47
+ :username,
48
+ :password,
49
+ :search_filter,
50
+ :search_base,
51
+ :association_attribute # name of the attribute in which to store the association key(s)
52
+
45
53
  association_attribute 'RubySyncAssociation'
46
54
  bind_method :simple
47
55
  host 'localhost'
48
56
  port 389
49
57
  search_filter "cn=*"
58
+ encryption nil
50
59
 
51
60
  def initialize options={}
52
61
  super options
@@ -55,17 +64,19 @@ module RubySync::Connectors
55
64
 
56
65
  def started
57
66
  #TODO: If vault, check the schema to make sure that the association_attribute is there
67
+ @connections = []
68
+ @connection_index = 0
58
69
  end
59
-
70
+
60
71
 
61
72
  def each_entry
62
73
  Net::LDAP.open(:host=>host, :port=>port, :auth=>auth) do |ldap|
63
- ldap.search :base => search_base, :filter => search_filter, :return_result => false do |ldap_entry|
64
- yield ldap_entry.dn, to_entry(ldap_entry)
65
- end
74
+ ldap.search :base => search_base, :filter => search_filter, :return_result => false do |ldap_entry|
75
+ yield ldap_entry.dn, to_entry(ldap_entry)
76
+ end
66
77
  end
67
78
  end
68
-
79
+
69
80
  # Runs the query specified by the config, gets the objectclass of the first
70
81
  # returned object and returns a list of its allowed attributes
71
82
  def self.fields
@@ -73,19 +84,30 @@ module RubySync::Connectors
73
84
  log.warn "Returning a likely sample set."
74
85
  %w{ cn givenName sn }
75
86
  end
76
-
87
+
77
88
 
78
89
 
79
90
  def self.sample_config
80
91
  return <<END
81
-
82
- host 'localhost'
83
- port 389
84
- username 'cn=Manager,dc=my-domain,dc=com'
85
- password 'secret'
86
- search_filter "cn=*"
87
- search_base "ou=users,o=my-organization,dc=my-domain,dc=com"
88
- #:bind_method :simple
92
+
93
+ # Using :memory is ok for testing.
94
+ # For production, you will need to change to a persistent form of tracking
95
+ # such as :dbm or :ldap.
96
+ track_changes_with :memory
97
+ track_associations_with :memory
98
+
99
+ host 'localhost'
100
+ port 389
101
+ username 'cn=Manager,dc=my-domain,dc=com'
102
+ password 'secret'
103
+ search_filter "cn=*"
104
+ search_base "ou=users,o=my-organization,dc=my-domain,dc=com"
105
+ #bind_method :simple
106
+
107
+ #Uncomment the following for LDAPS. If you do, make sure that
108
+ #you're using the LDAPS port (probably 636) and be aware that
109
+ #the server's certificate WON'T be checked for validity.
110
+ #encryption :simple_tls
89
111
  END
90
112
  end
91
113
 
@@ -94,11 +116,12 @@ END
94
116
  def add(path, operations)
95
117
  result = nil
96
118
  with_ldap do |ldap|
97
- attributes = perform_operations(operations)
98
- result = ldap.add :dn=>path, :attributes=>attributes
119
+ attributes = perform_operations(operations)
120
+ attributes['objectclass'] || log.warn("Add without objectclass attribute is unlikely to work.")
121
+ result = ldap.add :dn=>path, :attributes=>attributes
99
122
  end
100
123
  log.debug("ldap.add returned '#{result}'")
101
- return true
124
+ return result
102
125
  rescue Exception
103
126
  log.warn "Exception occurred while adding LDAP record"
104
127
  log.debug $!
@@ -116,23 +139,23 @@ END
116
139
 
117
140
  def [](path)
118
141
  with_ldap do |ldap|
119
- result = ldap.search :base=>path, :scope=>Net::LDAP::SearchScope_BaseObject, :filter=>'objectclass=*'
120
- return nil if !result or result.size == 0
121
- # todo: See if this can be shortened
122
- answer = {}
123
- result[0].attribute_names.each do |name|
124
- answer[name.to_s] = result[0][name]
125
- end
126
- answer
142
+ result = ldap.search :base=>path, :scope=>Net::LDAP::SearchScope_BaseObject, :filter=>'objectclass=*'
143
+ return nil if !result or result.size == 0
144
+ answer = {}
145
+ result[0].attribute_names.each do |name|
146
+ name = name.to_s.downcase
147
+ answer[name] = result[0][name] unless name == 'dn'
148
+ end
149
+ answer
127
150
  end
128
151
  end
129
-
152
+
130
153
  # Called by unit tests to inject data
131
154
  def test_add id, details
132
155
  details << RubySync::Operation.new(:add, "objectclass", ['inetOrgPerson'])
133
156
  add id, details
134
157
  end
135
-
158
+
136
159
  def target_transform event
137
160
  #event.add_default 'objectclass', 'inetOrgUser'
138
161
  #is_vault? and event.add_value 'objectclass', RUBYSYNC_ASSOCIATION_CLASS
@@ -145,7 +168,7 @@ END
145
168
  def to_entry ldap_entry
146
169
  entry = {}
147
170
  ldap_entry.each do |name, values|
148
- entry[name.to_s] = values.map {|v| String.new(v)}
171
+ entry[name.to_s] = values.map {|v| String.new(v)}
149
172
  end
150
173
  entry
151
174
  end
@@ -153,31 +176,35 @@ END
153
176
  def operations_for_entry entry
154
177
  ops = []
155
178
  entry.each do |name, values|
156
- ops << RubySync::Operation.add(name, values)
179
+ ops << RubySync::Operation.add(name, values)
157
180
  end
158
181
  ops
159
182
  end
160
183
 
161
-
162
-
163
-
164
184
 
165
185
  def with_ldap
166
186
  result = nil
167
- Net::LDAP.open(:host=>host, :port=>port, :auth=>auth) do |ldap|
168
- result = yield ldap
187
+ connection_options = {:host=>host, :port=>port, :auth=>auth}
188
+ connection_options[:encryption] = encryption if encryption
189
+ @connections[@connection_index] = Net::LDAP.new(connection_options) unless @connections[@connection_index]
190
+ if @connections[@connection_index]
191
+ ldap = @connections[@connection_index]
192
+ @connection_index += 1
193
+ result = yield ldap
194
+ @connection_index -= 1
169
195
  end
170
196
  result
171
197
  end
172
-
198
+
199
+
173
200
  def auth
174
201
  {:method=>bind_method, :username=>username, :password=>password}
175
202
  end
176
-
203
+
177
204
  # Produce an array of operation arrays suitable for the LDAP library
178
205
  def to_ldap_operations operations
179
206
  operations.map {|op| [op.type, op.subject, op.values]}
180
207
  end
181
-
208
+
182
209
  end
183
210
  end
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Copyright (c) 2007 Ritchie Young. All rights reserved.
4
+ #
5
+ # This file is part of RubySync.
6
+ #
7
+ # RubySync is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License
8
+ # as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
9
+ #
10
+ # RubySync is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
11
+ # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License along with RubySync; if not, write to the
14
+ # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
15
+
16
+
17
+
18
+ module RubySync::Connectors::MemoryAssociationTracking
19
+
20
+ # Returns an instance based hash association=>path
21
+ def paths_by_association
22
+ @paths_by_association ||= {}
23
+ end
24
+
25
+ # Returns an instance based hash path=>{context=>key}
26
+ def associations_by_path
27
+ @assocications_by_path ||= {}
28
+ end
29
+
30
+ # Store association for the given path
31
+ def associate association, path
32
+ paths_by_association[association.to_s] = path
33
+ associations_for(path)[association.context] = association.key
34
+ end
35
+
36
+ def path_for_association association
37
+ paths_by_association[association.to_s]
38
+ end
39
+
40
+ def associations_for path
41
+ associations_by_path[path] ||= {}
42
+ end
43
+
44
+ def remove_association association
45
+ path = paths_by_association[association]
46
+ if path
47
+ paths_by_association.delete(association)
48
+ associations_for(path).delete(association.context)
49
+ end
50
+ end
51
+
52
+ def association_key_for context, path
53
+ associations_for(path)[context]
54
+ end
55
+
56
+ def remove_associations
57
+ @paths_by_association = nil
58
+ @associations_by_path = nil
59
+ end
60
+ end
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Copyright (c) 2007 Ritchie Young. All rights reserved.
4
+ #
5
+ # This file is part of RubySync.
6
+ #
7
+ # RubySync is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License
8
+ # as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
9
+ #
10
+ # RubySync is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
11
+ # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License along with RubySync; if not, write to the
14
+ # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
15
+
16
+
17
+ require 'digest/md5'
18
+
19
+ # When included by a connector, tracks changes to the connector using
20
+ # a dbm database of the form path:digest. Digest is a MD5 hash of the
21
+ # record so we can tell if the record has changed. We can't, however,
22
+ # tell how the record has changed so change events generated will be
23
+ # for the entire record.
24
+ module RubySync::Connectors::MemoryChangeTracking
25
+
26
+
27
+ def shadow
28
+ @shadow ||= {}
29
+ end
30
+
31
+ # Subclasses MAY override this to interface with the external system
32
+ # and generate an event for every change that affects items within
33
+ # the scope of this connector.
34
+ #
35
+ # The default behaviour is to compare a hash of each entry in the
36
+ # database with a stored hash of its previous value and generate
37
+ # add, modify and delete events appropriately. This is normally a very
38
+ # inefficient way to operate so overriding this method is highly
39
+ # recommended if you can detect changes in a more efficient manner.
40
+ #
41
+ # This method will be called repeatedly until the connector is
42
+ # stopped.
43
+ def each_change
44
+ # scan existing entries to see if any new or modified
45
+ each_entry do |path, entry|
46
+ digest = digest(entry)
47
+ unless stored_digest = shadow[path.to_s] and digest == stored_digest
48
+ operations = create_operations_for(entry)
49
+ yield RubySync::Event.add(self, path, nil, operations)
50
+ shadow[path.to_s] = digest
51
+ end
52
+ end
53
+
54
+ # scan shadow to find deleted
55
+ shadow.each do |key, stored_hash|
56
+ unless self[key]
57
+ yield RubySync::Event.delete(self, key)
58
+ shadow.delete key
59
+ if is_vault? and @pipeline
60
+ association = association_for @pipeline.association_context, key
61
+ remove_association association
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ def digest(o)
68
+ Digest::MD5.hexdigest(o.to_yaml)
69
+ end
70
+
71
+ def remove_mirror
72
+ @shadow = nil
73
+ end
74
+
75
+ def delete_from_mirror path
76
+ shadow.delete(path.to_s)
77
+ end
78
+
79
+ def update_mirror path
80
+ entry = self[path.to_s]
81
+ shadow[path.to_s] = digest(entry)
82
+ end
83
+
84
+ end
@@ -20,6 +20,9 @@ defined?(RubySync::Connectors::BaseConnector) or require 'ruby_sync/connectors/b
20
20
  module RubySync::Connectors
21
21
  class MemoryConnector < RubySync::Connectors::BaseConnector
22
22
 
23
+ include MemoryAssociationTracking
24
+ include MemoryChangeTracking
25
+
23
26
 
24
27
  def each_entry
25
28
  @data.each do |key, entry|
@@ -146,9 +146,13 @@ filename "/tmp/rubysync.xml"
146
146
  unless @with_xml_invoked
147
147
  begin
148
148
  @with_xml_invoked = true
149
- File.exist?(filename) or File.open(filename,'w') {|file| file.write('<entries/>')}
149
+ File.exist?(filename) or File.open(filename,'w') {|file| file.write('<entries/>')}
150
150
  File.open(filename, "r") do |file|
151
- file.flock(File::LOCK_EX)
151
+ # flock behaves differently on
152
+ # Windows. Seems to prevent us from reopening the file for
153
+ # writing even within the same process. Temporarily removed.
154
+ # TODO: add this again but only if not executing on Windows
155
+ #file.flock(File::LOCK_EX)
152
156
  @xml = Document.new(file)
153
157
  begin
154
158
  yield @xml
@@ -17,11 +17,11 @@
17
17
 
18
18
  module RubySync
19
19
 
20
- class Association
20
+ class Association
21
21
  attr_accessor :context, # many associations will share the same context
22
- # it is a function of pipeline and the client connector
23
- # to which the association applies
24
- :key # the key is unique within the context and vault
22
+ # it is a function of pipeline and the client connector
23
+ # to which the association applies
24
+ :key # the key is unique within the context and vault
25
25
 
26
26
 
27
27
  def self.delimiter; '$'; end
@@ -47,12 +47,12 @@ module RubySync
47
47
  include RubySync::Utilities
48
48
 
49
49
  attr_accessor :type, # :delete, :add, :modify, :disassociate
50
- :source,
51
- :target,
52
- :payload,
53
- :source_path,
54
- :target_path,
55
- :association
50
+ :source,
51
+ :target,
52
+ :payload,
53
+ :source_path,
54
+ :target_path,
55
+ :association
56
56
 
57
57
  def self.force_resync source
58
58
  self.new(:force_resync, source)
@@ -105,9 +105,7 @@ module RubySync
105
105
  # Reduces the operations in this event to those that will
106
106
  # alter the target record
107
107
  def merge other
108
- # other.type == :add or raise "Can only merge with add events"
109
- # record = perform_operations(other.payload)
110
- payload = effective_operations(@payload, other)
108
+ @payload = effective_operations(@payload, other)
111
109
  end
112
110
 
113
111
  # Retrieves all known values for the record affected by this event and
@@ -123,17 +121,31 @@ module RubySync
123
121
  @type = :add
124
122
  end
125
123
 
126
- def convert_to_modify
124
+
125
+ def convert_to_modify(other, filter)
127
126
  log.info "Converting '#{type}' event to modify"
127
+
128
+ # The add event contained an operation for each attribute of the source record.
129
+ # Therefore, we should delete any attributes in the target record that don't appear
130
+ # in the event.
131
+ affected = affected_subjects
132
+ other.each do |key, value|
133
+ if filter.include?(key) and !affected.include?(key)
134
+ log.info "Adding delete operation for #{key}"
135
+ @payload << Operation.delete(key)
136
+ end
137
+ end
138
+
128
139
  @type = :modify
129
- @payload.each do |op|
130
- op.type = :replace
131
- end
132
140
  end
133
141
 
142
+ # Return a list of subjects that this event affects
143
+ def affected_subjects
144
+ @payload.map {|op| op.subject}.uniq
145
+ end
134
146
 
135
147
  def hint
136
- "(#{source.name} => #{target.name}) #{source_path}"
148
+ "(#{source.name} => #{target.name}) #{source_path}"
137
149
  end
138
150
 
139
151
 
@@ -163,35 +175,39 @@ module RubySync
163
175
  @uncommitted_operations = @uncommitted_operations.delete_if {|op| subjects.include? op.subject }
164
176
  end
165
177
 
178
+ def downcase_subjects
179
+ @uncommitted_operations = uncommitted_operations.map {|op| Operation.new(op.type, op.subject.downcase, op.values)}
180
+ end
181
+
166
182
  def drop_all_but_changes_to *subjects
167
183
  subjects = subjects.flatten.collect {|s| s.to_s}
168
184
  @uncommitted_operations = uncommitted_operations.delete_if {|op| !subjects.include?(op.subject.to_s)}
169
185
  end
170
186
 
171
- def delete_when_blank
172
- @uncommitted_operations = uncommitted_operations.map do |op|
173
- if op.sets_blank?
174
- @type == :modify ? op.same_but_as(:delete) : nil
175
- else
176
- op
177
- end
178
- end.compact
179
- end
187
+ def delete_when_blank
188
+ @uncommitted_operations = uncommitted_operations.map do |op|
189
+ if op.sets_blank?
190
+ @type == :modify ? op.same_but_as(:delete) : nil
191
+ else
192
+ op
193
+ end
194
+ end.compact
195
+ end
180
196
 
181
197
 
182
- # Add a value to a given subject unless it already sets a value
183
- def add_default field_name, value
184
- add_value(field_name.to_s, value) unless sets_value? field_name.to_s
185
- end
198
+ # Add a value to a given subject unless it already sets a value
199
+ def add_default field_name, value
200
+ add_value(field_name.to_s, value) unless sets_value? field_name.to_s
201
+ end
186
202
 
187
203
 
188
- def add_value field_name, value
189
- uncommitted_operations << Operation.new(:add, field_name.to_s, as_array(value))
190
- end
204
+ def add_value field_name, value
205
+ uncommitted_operations << Operation.new(:add, field_name.to_s, as_array(value))
206
+ end
191
207
 
192
- def set_value field_name, value
193
- uncommitted_operations << Operation.new(:replace, field_name.to_s, as_array(value))
194
- end
208
+ def set_value field_name, value
209
+ uncommitted_operations << Operation.new(:replace, field_name.to_s, as_array(value))
210
+ end
195
211
 
196
212
  def values_for field_name, default=[]
197
213
  values = perform_operations @payload, {}, :subjects=>[field_name.to_s]
@@ -252,21 +268,28 @@ module RubySync
252
268
  def place(&blk)
253
269
  self.target_path = blk.call
254
270
  end
271
+
272
+ # true unless this event in its current state would have no impact
273
+ def effective_operation?
274
+ !(
275
+ @type == :modify && @payload.empty?
276
+ )
277
+ end
255
278
 
256
- protected
279
+ protected
257
280
 
258
- # Try to make a sensible association from the passed in object
259
- def make_association o
260
- if o.kind_of? Array
261
- return Association.new(o[0],o[1])
262
- elsif o.kind_of? RubySync::Association
263
- return o
264
- elsif o
265
- return Association.new(nil, o)
266
- else
267
- nil
268
- end
269
- end
281
+ # Try to make a sensible association from the passed in object
282
+ def make_association o
283
+ if o.kind_of?(Array) and o.size == 2
284
+ return Association.new(o[0],o[1])
285
+ elsif o.kind_of? RubySync::Association
286
+ return o
287
+ elsif o
288
+ return Association.new(nil, o)
289
+ else
290
+ nil
291
+ end
292
+ end
270
293
 
271
294
 
272
295