rubysync 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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