lom 0.10.0 → 0.12.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d37a56b9ef6c9dad3ae785439cb49560c641304fdb12f3d3d070541e8bf27b9
4
- data.tar.gz: cd71e20fd089ab2b489bcdb2f857f508b8c283c0d4f2de875b81cfc722fab05b
3
+ metadata.gz: b7080950578c9a8cdcff8a1a6e527d2c53a2981fb05f32099e114ac3a73f9221
4
+ data.tar.gz: 49c5a2a4072773cf445f6ba30ee60acb59fb8745a78f927fda4eb987e60d4efe
5
5
  SHA512:
6
- metadata.gz: 956540737ca9944e790aeead959ef079f61f8d9469b22867969858c993bd97897f742fd9ca43ef3b99110e76e81902474f88fd6a95e0bd216fee0b620ca5377e
7
- data.tar.gz: 7340668aaeef6a7821e88d95bf5da772aedfcb4a3dce9ab89d858c08c2b4be9165e127f0c5871decce112edc06337ba0c7e611601d12b56a6cbfe5b52e490202
6
+ metadata.gz: dc64dfa9fcaa777ae4e9dd4dd65e0b3527f2bfa1482389aa83714c1d6bad346a4806a174886c2fc74c1ab3a235bbbbf548d13a978650bc6a1a580eb65d778f17
7
+ data.tar.gz: e79faf8dd9af3b388ccf6b7ee67a084832ad1eadab13bc955838492f6feb9cccbcc341f4dd526bb92fe145fcaffc08e9c1d85b12abe02370fd9c61b8aff0ab29
data/README.md CHANGED
@@ -156,3 +156,46 @@ User.locked(true).manager('jdoe').list
156
156
  # Return list of users (as User instance) without managers
157
157
  User.manager(false).all
158
158
  ~~~
159
+
160
+
161
+ Threaded servers
162
+ ================
163
+
164
+ A `Net::LDAP` handler is not thread safe: each operation stores its
165
+ outcome in the handler itself, so two threads sharing a handler can
166
+ read each other's result. When LOM is used from several threads
167
+ (Puma, background jobs, ...), give it a callable instead of a
168
+ handler: it is called once per thread (or fiber), and the handler it
169
+ builds is reused by that thread.
170
+
171
+ ~~~ruby
172
+ LH = -> { Net::LDAP.connect('ldap://127.0.0.1')
173
+ .tap { it.auth 'uid=root,ou=Admins,dc=example,dc=com',
174
+ 'foobar' } }
175
+ ~~~
176
+
177
+ Without an opened connection, `Net::LDAP` connects and binds for each
178
+ operation. `LOM.open` keeps a single connection for all the operations
179
+ of its block; nested calls reuse it, and if the connection or bind
180
+ fails the block still runs with one connection per operation. It only
181
+ opens when the handler comes from a callable, as an opened connection
182
+ must not be shared between threads.
183
+
184
+ ~~~ruby
185
+ LOM.open { User.locked(true).all }
186
+ ~~~
187
+
188
+ In a Rack application, a middleware gives one connection per request:
189
+
190
+ ~~~ruby
191
+ class LDAPConnection
192
+ def initialize(app) = @app = app
193
+ def call(env) = LOM.open { @app.call(env) }
194
+ end
195
+
196
+ use LDAPConnection
197
+ ~~~
198
+
199
+ `LOM.open` only opens the handler of `LOM.lh`; a mapper given its own
200
+ handler (`lh=` or its own `LH` constant) still uses one connection per
201
+ operation.
data/lib/lom/handler.rb CHANGED
@@ -4,6 +4,19 @@ class LOM
4
4
  using LDAP::Extensions
5
5
 
6
6
 
7
+ # Set the LDAP handler to use
8
+ #
9
+ # It can be a Net::LDAP handler, or a callable (proc, lambda, ...)
10
+ # building one. A Net::LDAP handler is not thread safe, so a
11
+ # callable is required as soon as LOM is used from several threads:
12
+ # it will be called once per thread (or fiber), and the resulting
13
+ # handler reused by that thread.
14
+ #
15
+ # ~~~
16
+ # LOM.lh = -> { Net::LDAP.connect('ldap://127.0.0.1')
17
+ # .tap { it.auth(dn, password) } }
18
+ # ~~~
19
+ #
7
20
  def self.lh=(lh)
8
21
  @lh = lh
9
22
  end
@@ -16,11 +29,131 @@ class LOM
16
29
  # * the LH constant in this scope or parent scope
17
30
  # * the one defined in $lh
18
31
  #
32
+ # If the handler is a callable, the handler built by it for the
33
+ # current thread is returned.
34
+ #
19
35
  def self.lh
20
- @lh || const_get(:LH) || $lh
36
+ materialize_lh(lh_source)
21
37
  end
22
38
 
39
+ # Get the LDAP handler source (handler or callable), see lh
40
+ #
41
+ # @api private
42
+ #
43
+ def self.lh_source
44
+ @lh || (const_defined?(:LH) && const_get(:LH)) || $lh
45
+ end
46
+
47
+ # Resolve a handler source to a usable LDAP handler.
48
+ #
49
+ # A callable is called once per thread (or fiber) and its result
50
+ # cached for that thread, anything else is returned unchanged.
51
+ #
52
+ # @api private
53
+ #
54
+ def self.materialize_lh(src)
55
+ return src unless src.respond_to?(:call)
56
+ (Thread.current[:__lom_lh] ||= {}.compare_by_identity)[src] ||=
57
+ src.call
58
+ end
59
+
60
+ # Run the block with a single LDAP connection opened and bound,
61
+ # which is reused by all the LDAP operations of the block,
62
+ # instead of one connection per operation.
63
+ #
64
+ # This is only done if the handler is a callable (see lh=), as an
65
+ # opened connection must not be shared between threads, otherwise
66
+ # the block is simply run.
67
+ #
68
+ # If the connection or bind fails, the block is still run, falling
69
+ # back to one connection per operation.
70
+ #
71
+ # Nested calls reuse the already opened connection.
72
+ #
73
+ # @return the block value
74
+ #
75
+ def self.open
76
+ src = lh_source
77
+ return yield unless src.respond_to?(:call)
78
+
79
+ lh = materialize_lh(src)
80
+ opened = (Thread.current[:__lom_opened] ||= {}.compare_by_identity)
81
+ return yield if opened[lh]
82
+
83
+ entered = false
84
+ result = nil
85
+ begin
86
+ lh.open do
87
+ code = lh.get_operation_result.code
88
+ next unless code == Net::LDAP::ResultCodeSuccess
89
+ opened[lh] = { depth: 0, dirty: false }
90
+ entered = true
91
+ begin
92
+ result = yield
93
+ ensure
94
+ opened.delete(lh)
95
+ end
96
+ end
97
+ rescue Net::LDAP::Error, SystemCallError, IOError
98
+ raise if entered
99
+ end
100
+ entered ? result : yield
101
+ end
102
+
103
+ # Run the block performing a search on the handler.
104
+ #
105
+ # If the handler connection was opened by LOM.open and the search
106
+ # is left before completion (break, exception, ...), the unread
107
+ # search results would be kept until the end of LOM.open, so the
108
+ # connection is replaced by a new one, once no other search is
109
+ # using it.
110
+ #
111
+ # @api private
112
+ #
113
+ def self.searching(lh)
114
+ state = Thread.current[:__lom_opened]&.[](lh)
115
+ return yield if state.nil?
116
+
117
+ state[:depth] += 1
118
+ done = false
119
+ begin
120
+ yield.tap { done = true }
121
+ ensure
122
+ state[:depth] -= 1
123
+ state[:dirty] ||= !done
124
+ reopen(lh, state) if state[:dirty] && state[:depth].zero?
125
+ end
126
+ end
127
+
128
+ # Replace the connection opened by LOM.open with a new one.
129
+ # If it can't be opened and bound, operations fall back to
130
+ # one connection per operation.
131
+ #
132
+ # @note relies on Net::LDAP internals (@open_connection, @auth,
133
+ # #new_connection) as used by Net::LDAP#open
134
+ #
135
+ # @api private
136
+ #
137
+ def self.reopen(lh, state)
138
+ state[:dirty] = false
139
+ begin
140
+ lh.instance_variable_get(:@open_connection)&.close
141
+ rescue StandardError
142
+ end
143
+ lh.instance_variable_set(:@open_connection, nil)
23
144
 
145
+ conn = lh.send(:new_connection)
146
+ res = conn.bind(lh.instance_variable_get(:@auth))
147
+ if res.result_code == Net::LDAP::ResultCodeSuccess
148
+ lh.instance_variable_set(:@open_connection, conn)
149
+ else
150
+ conn.close
151
+ end
152
+ rescue Net::LDAP::Error, SystemCallError, IOError
153
+ conn&.close rescue nil
154
+ end
155
+
156
+
24
157
  # extend Forwardable
25
158
  #
26
159
  # def self.connect(*args)
@@ -38,6 +171,3 @@ class LOM
38
171
  # def_delegator :@lh, :delete
39
172
 
40
173
  end
41
-
42
-
43
-
@@ -10,25 +10,25 @@ module Converters
10
10
  #
11
11
  # Integer
12
12
  #
13
-
13
+
14
14
  refine Integer do
15
15
  def to_ldap
16
16
  self.to_s
17
17
  end
18
18
  end
19
-
19
+
20
20
  refine Integer.singleton_class do
21
21
  def from_ldap(v)
22
22
  Integer(v)
23
23
  end
24
24
  end
25
25
 
26
-
26
+
27
27
 
28
28
  #
29
29
  # String
30
30
  #
31
-
31
+
32
32
  refine String do
33
33
  def to_ldap
34
34
  self
@@ -41,15 +41,15 @@ module Converters
41
41
  end
42
42
  end
43
43
 
44
-
44
+
45
45
 
46
46
  #
47
47
  # Date / Time
48
48
  #
49
-
49
+
50
50
  refine Date do
51
51
  def to_ldap
52
- self.to_time.to_ldap
52
+ self.strftime("%Y%m%d%H%M%SZ")
53
53
  end
54
54
  end
55
55
 
@@ -74,12 +74,12 @@ module Converters
74
74
  end
75
75
  end
76
76
 
77
-
77
+
78
78
 
79
79
  #
80
80
  # Boolean
81
81
  #
82
-
82
+
83
83
  refine TrueClass do
84
84
  def to_ldap
85
85
  'TRUE'
@@ -96,20 +96,20 @@ module Converters
96
96
  def to_ldap
97
97
  'FALSE'
98
98
  end
99
- end
99
+ end
100
+
100
101
 
101
102
 
102
-
103
103
  #
104
104
  # Array / Set
105
105
  #
106
-
106
+
107
107
  refine Set do
108
108
  def to_ldap
109
109
  self.to_a.to_ldap
110
110
  end
111
111
  end
112
-
112
+
113
113
  refine Array do
114
114
  def to_ldap
115
115
  self.map { |val|
@@ -1,5 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
+ require 'uri'
3
4
  require 'net/ldap'
4
5
  require 'net/ldap/dn'
5
6
 
data/lib/lom/mapper.rb CHANGED
@@ -56,6 +56,27 @@ module InstanceMethods
56
56
  lh.add(dn: dn, attributes: attrs)
57
57
  }
58
58
  end
59
+
60
+
61
+ # Delete object from ldap
62
+ #
63
+ # @return [true, false]
64
+ #
65
+ def delete!
66
+ model = self.class
67
+ attrs = instance_exec(self, &model.ldap_to)
68
+ .transform_values {|v|
69
+ # Don't use Array(), not what you think on
70
+ # some classes such as Time
71
+ v = [ ] if v.nil?
72
+ v = [ v ] unless v.is_a?(Array)
73
+ v.to_ldap
74
+ }
75
+ id, _ = Array(attrs[model.ldap_prefix])
76
+ raise MappingError, 'prefix for dn has multiple values' if _
77
+
78
+ model.delete!(id)
79
+ end
59
80
  end
60
81
  end
61
82
 
@@ -93,8 +114,12 @@ module Mapper
93
114
  # * the LH constant in this scope or parent scope
94
115
  # * the one provided by LOM.lh
95
116
  #
117
+ # If the handler is a callable, the handler built by it for the
118
+ # current thread is returned (see LOM.lh=).
119
+ #
96
120
  def lh
97
- @__ldap_lh || const_get(:LH) || LOM.lh
121
+ LOM.materialize_lh(@__ldap_lh ||
122
+ (const_defined?(:LH) && const_get(:LH)) || LOM.lh)
98
123
  end
99
124
 
100
125
  # Set the LDAP handler to use
@@ -254,23 +279,30 @@ module Mapper
254
279
  end
255
280
 
256
281
  # Perform search
257
- lh.search(:base => _ldap_branch,
258
- :filter => filter,
259
- :attributes => attributes,
260
- :scope => _ldap_scope) {|entry|
261
-
262
- if rawfilter && !rawfilter.call(entry)
263
- next
264
- elsif paged.nil?
265
- yield(converter.(entry))
266
- elsif skip > 0
267
- skip -= 1
268
- elsif count <= 0
269
- break
270
- else
271
- count -= 1
272
- yield(converter.(entry))
273
- end
282
+ # Note: the search must not be left early (with break), as it
283
+ # would leave unread results on a connection opened
284
+ # by LOM.open; leaving it from the caller block is
285
+ # handled by LOM.searching
286
+ h = lh
287
+ LOM.searching(h) {
288
+ h.search(:base => _ldap_branch,
289
+ :filter => filter,
290
+ :attributes => attributes,
291
+ :scope => _ldap_scope) {|entry|
292
+
293
+ if rawfilter && !rawfilter.call(entry)
294
+ next
295
+ elsif paged.nil?
296
+ yield(converter.(entry))
297
+ elsif skip > 0
298
+ skip -= 1
299
+ elsif count <= 0
300
+ next
301
+ else
302
+ count -= 1
303
+ yield(converter.(entry))
304
+ end
305
+ }
274
306
  }
275
307
 
276
308
  # Return self
@@ -296,8 +328,16 @@ module Mapper
296
328
  def list(&rawfilter)
297
329
  each(:id, rawfilter: rawfilter).to_a
298
330
  end
299
-
300
- def get(name)
331
+
332
+ # Fetch the requested entry.
333
+ #
334
+ # @param param [String] entry name
335
+ #
336
+ # @raise [LOM::EntryNotFound] if entry not found
337
+ #
338
+ # @return [Object]
339
+ #
340
+ def fetch(name)
301
341
  dn = ldap_dn_from_id(name)
302
342
  attrs = _ldap_attrs
303
343
  entry = lh.get(:dn => dn, :attributes => attrs)
@@ -310,8 +350,33 @@ module Mapper
310
350
  lh.delete(:dn => dn)
311
351
  end
312
352
 
353
+ # Get the requested entry.
354
+ # Same as #fetch but return nil if not found
355
+ #
356
+ # @param param [String] entry name
357
+ #
358
+ # @return [nil] entry not found
359
+ # @return [Object]
360
+ #
361
+ def get(name)
362
+ fetch(name)
363
+ rescue LOM::EntryNotFound
364
+ nil
365
+ end
366
+
313
367
  alias [] get
314
368
 
369
+ # Test existence of entry
370
+ #
371
+ # @param param [String] entry name
372
+ #
373
+ # @return [Boolean]
374
+ #
375
+ def exists?(name)
376
+ dn = ldap_dn_from_id(name)
377
+ lh.get(:dn => dn, :return_result => false)
378
+ end
379
+
315
380
  private
316
381
 
317
382
  def _ldap_branch
data/lib/lom/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class LOM
2
- VERSION = '0.10.0'
2
+ VERSION = '0.12.0'
3
3
  end
data/lom.gemspec CHANGED
@@ -26,4 +26,5 @@ Gem::Specification.new do |s|
26
26
  s.add_dependency 'net-ldap'
27
27
  s.add_development_dependency 'yard', '~>0'
28
28
  s.add_development_dependency 'rake', '~>13'
29
+ s.add_development_dependency 'minitest', '~>5'
29
30
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lom
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stéphane D'Alu
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2021-06-11 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: net-ldap
@@ -52,6 +51,20 @@ dependencies:
52
51
  - - "~>"
53
52
  - !ruby/object:Gem::Version
54
53
  version: '13'
54
+ - !ruby/object:Gem::Dependency
55
+ name: minitest
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '5'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '5'
55
68
  description: |2+
56
69
 
57
70
  Creation of mapping between ldap entry and ruby object.
@@ -79,7 +92,6 @@ homepage: https://github.com/sdalu/lom
79
92
  licenses:
80
93
  - MIT
81
94
  metadata: {}
82
- post_install_message:
83
95
  rdoc_options: []
84
96
  require_paths:
85
97
  - lib
@@ -94,8 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
106
  - !ruby/object:Gem::Version
95
107
  version: '0'
96
108
  requirements: []
97
- rubygems_version: 3.0.8
98
- signing_key:
109
+ rubygems_version: 4.0.21
99
110
  specification_version: 4
100
111
  summary: LDAP Object Mapper
101
112
  test_files: []