rom-ldap 0.2.2

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.
Files changed (104) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +251 -0
  3. data/CONTRIBUTING.md +18 -0
  4. data/README.md +172 -0
  5. data/TODO.md +33 -0
  6. data/config/responses.yml +328 -0
  7. data/lib/dry/monitor/ldap/colorizers/default.rb +17 -0
  8. data/lib/dry/monitor/ldap/colorizers/rouge.rb +31 -0
  9. data/lib/dry/monitor/ldap/logger.rb +58 -0
  10. data/lib/rom-ldap.rb +1 -0
  11. data/lib/rom/ldap.rb +22 -0
  12. data/lib/rom/ldap/alias.rb +30 -0
  13. data/lib/rom/ldap/associations.rb +6 -0
  14. data/lib/rom/ldap/associations/core.rb +23 -0
  15. data/lib/rom/ldap/associations/many_to_many.rb +18 -0
  16. data/lib/rom/ldap/associations/many_to_one.rb +22 -0
  17. data/lib/rom/ldap/associations/one_to_many.rb +32 -0
  18. data/lib/rom/ldap/associations/one_to_one.rb +19 -0
  19. data/lib/rom/ldap/associations/self_ref.rb +35 -0
  20. data/lib/rom/ldap/attribute.rb +327 -0
  21. data/lib/rom/ldap/client.rb +185 -0
  22. data/lib/rom/ldap/client/authentication.rb +118 -0
  23. data/lib/rom/ldap/client/operations.rb +233 -0
  24. data/lib/rom/ldap/commands.rb +6 -0
  25. data/lib/rom/ldap/commands/create.rb +41 -0
  26. data/lib/rom/ldap/commands/delete.rb +17 -0
  27. data/lib/rom/ldap/commands/update.rb +35 -0
  28. data/lib/rom/ldap/constants.rb +193 -0
  29. data/lib/rom/ldap/dataset.rb +286 -0
  30. data/lib/rom/ldap/dataset/conversion.rb +62 -0
  31. data/lib/rom/ldap/dataset/dsl.rb +299 -0
  32. data/lib/rom/ldap/dataset/persistence.rb +44 -0
  33. data/lib/rom/ldap/directory.rb +126 -0
  34. data/lib/rom/ldap/directory/capabilities.rb +71 -0
  35. data/lib/rom/ldap/directory/entry.rb +200 -0
  36. data/lib/rom/ldap/directory/env.rb +155 -0
  37. data/lib/rom/ldap/directory/operations.rb +282 -0
  38. data/lib/rom/ldap/directory/password.rb +122 -0
  39. data/lib/rom/ldap/directory/root.rb +187 -0
  40. data/lib/rom/ldap/directory/tokenization.rb +66 -0
  41. data/lib/rom/ldap/directory/transactions.rb +31 -0
  42. data/lib/rom/ldap/directory/vendors/active_directory.rb +129 -0
  43. data/lib/rom/ldap/directory/vendors/apache_ds.rb +27 -0
  44. data/lib/rom/ldap/directory/vendors/e_directory.rb +16 -0
  45. data/lib/rom/ldap/directory/vendors/open_directory.rb +12 -0
  46. data/lib/rom/ldap/directory/vendors/open_dj.rb +25 -0
  47. data/lib/rom/ldap/directory/vendors/open_ldap.rb +35 -0
  48. data/lib/rom/ldap/directory/vendors/three_eight_nine.rb +16 -0
  49. data/lib/rom/ldap/directory/vendors/unknown.rb +22 -0
  50. data/lib/rom/ldap/dsl.rb +76 -0
  51. data/lib/rom/ldap/errors.rb +47 -0
  52. data/lib/rom/ldap/expression.rb +77 -0
  53. data/lib/rom/ldap/expression_encoder.rb +174 -0
  54. data/lib/rom/ldap/extensions.rb +50 -0
  55. data/lib/rom/ldap/extensions/active_support_notifications.rb +26 -0
  56. data/lib/rom/ldap/extensions/compatibility.rb +11 -0
  57. data/lib/rom/ldap/extensions/dsml.rb +165 -0
  58. data/lib/rom/ldap/extensions/msgpack.rb +23 -0
  59. data/lib/rom/ldap/extensions/optimised_json.rb +25 -0
  60. data/lib/rom/ldap/extensions/rails_log_subscriber.rb +38 -0
  61. data/lib/rom/ldap/formatter.rb +26 -0
  62. data/lib/rom/ldap/functions.rb +207 -0
  63. data/lib/rom/ldap/gateway.rb +145 -0
  64. data/lib/rom/ldap/ldif.rb +74 -0
  65. data/lib/rom/ldap/ldif/exporter.rb +77 -0
  66. data/lib/rom/ldap/ldif/importer.rb +95 -0
  67. data/lib/rom/ldap/mapper_compiler.rb +19 -0
  68. data/lib/rom/ldap/matchers.rb +69 -0
  69. data/lib/rom/ldap/message_queue.rb +7 -0
  70. data/lib/rom/ldap/oid.rb +101 -0
  71. data/lib/rom/ldap/parsers/abstract_syntax.rb +91 -0
  72. data/lib/rom/ldap/parsers/attribute.rb +290 -0
  73. data/lib/rom/ldap/parsers/filter_syntax.rb +133 -0
  74. data/lib/rom/ldap/pdu.rb +285 -0
  75. data/lib/rom/ldap/plugin/pagination.rb +145 -0
  76. data/lib/rom/ldap/plugins.rb +7 -0
  77. data/lib/rom/ldap/projection_dsl.rb +38 -0
  78. data/lib/rom/ldap/relation.rb +135 -0
  79. data/lib/rom/ldap/relation/exporting.rb +72 -0
  80. data/lib/rom/ldap/relation/reading.rb +461 -0
  81. data/lib/rom/ldap/relation/writing.rb +64 -0
  82. data/lib/rom/ldap/responses.rb +17 -0
  83. data/lib/rom/ldap/restriction_dsl.rb +45 -0
  84. data/lib/rom/ldap/schema.rb +123 -0
  85. data/lib/rom/ldap/schema/attributes_inferrer.rb +59 -0
  86. data/lib/rom/ldap/schema/dsl.rb +13 -0
  87. data/lib/rom/ldap/schema/inferrer.rb +50 -0
  88. data/lib/rom/ldap/schema/type_builder.rb +133 -0
  89. data/lib/rom/ldap/scope.rb +19 -0
  90. data/lib/rom/ldap/search_request.rb +249 -0
  91. data/lib/rom/ldap/socket.rb +210 -0
  92. data/lib/rom/ldap/tasks/ldap.rake +103 -0
  93. data/lib/rom/ldap/tasks/ldif.rake +80 -0
  94. data/lib/rom/ldap/transaction.rb +29 -0
  95. data/lib/rom/ldap/type_map.rb +88 -0
  96. data/lib/rom/ldap/types.rb +158 -0
  97. data/lib/rom/ldap/version.rb +17 -0
  98. data/lib/rom/plugins/relation/ldap/active_directory.rb +182 -0
  99. data/lib/rom/plugins/relation/ldap/auto_restrictions.rb +69 -0
  100. data/lib/rom/plugins/relation/ldap/e_directory.rb +27 -0
  101. data/lib/rom/plugins/relation/ldap/instrumentation.rb +35 -0
  102. data/lib/rouge/lexers/ldap.rb +72 -0
  103. data/lib/rouge/themes/ldap.rb +49 -0
  104. metadata +231 -0
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rom/ldap/parsers/abstract_syntax'
4
+ require 'rom/ldap/parsers/filter_syntax'
5
+
6
+ module ROM
7
+ module LDAP
8
+ class Dataset
9
+
10
+ # Parsing Formats
11
+ #
12
+ # @api public
13
+ module Conversion
14
+ # Extends the class with filter abstraction behavior.
15
+ #
16
+ # @api private
17
+ def self.included(klass)
18
+ klass.class_eval do
19
+ extend Dry::Core::ClassAttributes
20
+
21
+ defines :filter_class
22
+ filter_class Parsers::FilterSyntax
23
+
24
+ defines :ast_class
25
+ ast_class Parsers::AbstractSyntax
26
+ end
27
+ end
28
+
29
+ # Convert the full query to an LDAP filter string
30
+ #
31
+ # @return [String]
32
+ #
33
+ # @api private
34
+ def to_filter
35
+ self.class.ast_class.new(to_ast, directory.attribute_types).call.to_filter
36
+ end
37
+
38
+ # Combine original relation dataset name (LDAP filter string)
39
+ # with search criteria (AST).
40
+ #
41
+ # @return [Array]
42
+ #
43
+ # @api private
44
+ def to_ast
45
+ criteria.empty? ? source_ast : [:con_and, [source_ast, criteria]]
46
+ end
47
+
48
+ private
49
+
50
+ # Convert the relation's source filter string to a query AST.
51
+ #
52
+ # @return [Array]
53
+ #
54
+ # @api private
55
+ def source_ast
56
+ self.class.filter_class.new(name, directory.attribute_types).call
57
+ end
58
+ end
59
+
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,299 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ROM
4
+ module LDAP
5
+ class Dataset
6
+
7
+ # AST Query Interface
8
+ #
9
+ # @api private
10
+ module DSL
11
+ # Invert the whole query
12
+ #
13
+ # @return [Dataset]
14
+ def inverse
15
+ with(criteria: [:con_not, criteria])
16
+ end
17
+
18
+ # Equality filter aliased as 'where'.
19
+ #
20
+ # @example
21
+ # relation.where(uid: 'Pietro')
22
+ # relation.where(uid: %w[Pietro Wanda])
23
+ # relation.where(uid: 'Pietro', sn: 'Maximoff')
24
+ #
25
+ # @param args [Hash]
26
+ #
27
+ # @return [Dataset]
28
+ def equal(args)
29
+ chain(*array_dsl(args))
30
+ end
31
+
32
+ # Antonym of 'equal'
33
+ #
34
+ # @example
35
+ # relation.unequal(uid: 'Pietro')
36
+ # relation.unequal(uid: %w[Pietro Wanda])
37
+ #
38
+ # @param args [Hash]
39
+ #
40
+ # @return [Dataset]
41
+ def unequal(args)
42
+ chain(:con_not, array_dsl(args))
43
+ end
44
+
45
+ # Presence filter aliased as 'has'.
46
+ #
47
+ # @example
48
+ # relation.present(:uid)
49
+ # relation.has(:mail)
50
+ #
51
+ # @param attribute [Symbol]
52
+ #
53
+ # @return [Dataset]
54
+ def present(attribute)
55
+ chain(:op_eql, attribute, :wildcard)
56
+ end
57
+ alias_method :has, :present
58
+
59
+ # Absence filter aliased as 'hasnt'. Inverse of 'present'.
60
+ #
61
+ # @example
62
+ # relation.missing(:uid)
63
+ # relation.hasnt(:mail)
64
+ #
65
+ # @param attribute [Symbol]
66
+ #
67
+ # @return [Dataset]
68
+ def missing(attribute)
69
+ chain(:con_not, [:op_eql, attribute, :wildcard])
70
+ end
71
+ alias_method :hasnt, :missing
72
+
73
+ # Greater than filter
74
+ #
75
+ # @param args [Hash]
76
+ #
77
+ # @return [Dataset]
78
+ def gt(args)
79
+ chain(:con_not, [:op_lte, *args.to_a[0]])
80
+ end
81
+ alias_method :above, :gt
82
+
83
+ # Less than filter
84
+ #
85
+ # @param args [Hash]
86
+ #
87
+ # @return [Dataset]
88
+ def lt(args)
89
+ chain(:con_not, [:op_gte, *args.to_a[0]])
90
+ end
91
+ alias_method :below, :lt
92
+
93
+ # Greater than or equal filter
94
+ #
95
+ # @param args [Hash]
96
+ #
97
+ # @return [Dataset]
98
+ def gte(args)
99
+ chain(:op_gte, *args.to_a[0])
100
+ end
101
+
102
+ # Less than or equal filter
103
+ #
104
+ # @param args [Hash]
105
+ #
106
+ # @return [Dataset]
107
+ def lte(args)
108
+ chain(:op_lte, *args.to_a[0])
109
+ end
110
+
111
+ # Starts with filter
112
+ #
113
+ # @param args [Hash]
114
+ #
115
+ # @return [Dataset]
116
+ def begins(args)
117
+ chain(*match_dsl(args, right: WILDCARD))
118
+ end
119
+
120
+ # Ends with filter
121
+ #
122
+ # @param args [Hash]
123
+ #
124
+ # @return [Dataset]
125
+ def ends(args)
126
+ chain(*match_dsl(args, left: WILDCARD))
127
+ end
128
+
129
+ # @param args [Hash]
130
+ #
131
+ # @return [Dataset]
132
+ def contains(args)
133
+ chain(*match_dsl(args, left: WILDCARD, right: WILDCARD))
134
+ end
135
+ alias_method :matches, :contains
136
+
137
+ # negate #contains
138
+ #
139
+ # @param args [Hash]
140
+ #
141
+ # @return [Dataset]
142
+ def excludes(args)
143
+ chain(:con_not, match_dsl(args, left: WILDCARD, right: WILDCARD))
144
+ end
145
+
146
+ # @param args [Range]
147
+ #
148
+ # @return [Dataset]
149
+ def within(args)
150
+ chain(:con_and, cover_dsl(args))
151
+ end
152
+ alias_method :between, :within
153
+
154
+ # negate #outside
155
+ #
156
+ # @param args [Range]
157
+ #
158
+ # @return [Dataset]
159
+ def outside(args)
160
+ chain(:con_not, [:con_and, cover_dsl(args)])
161
+ end
162
+
163
+ # @param args [Hash]
164
+ #
165
+ # @return [Dataset]
166
+ def binary_equal(args)
167
+ chain(:op_bineq, *args.to_a[0])
168
+ end
169
+
170
+ # @param args [Hash]
171
+ #
172
+ # @return [Dataset]
173
+ def approx(args)
174
+ chain(:op_prx, *args.to_a[0])
175
+ end
176
+
177
+ # @param args [Hash]
178
+ #
179
+ # @return [Dataset]
180
+ def bitwise(args)
181
+ chain(:op_ext, *args.to_a[0])
182
+ end
183
+
184
+ private
185
+
186
+ # Update the criteria.
187
+ # If criteria already exist join with AND.
188
+ #
189
+ # @example
190
+ # chain(:op_eql, :uid, "*foo*")
191
+ #
192
+ # @param exprs [Mixed] AST built by QueryDSL
193
+ #
194
+ # @return [Dataset]
195
+ def chain(*exprs)
196
+ if criteria.empty?
197
+ with(criteria: exprs)
198
+ else
199
+ with(criteria: [:con_and, [criteria, exprs]])
200
+ end
201
+ end
202
+
203
+ # Handle multiple criteria with an OR join.
204
+ # @see #chain for AND join.
205
+ #
206
+ # @param args [Hash]
207
+ #
208
+ # @return [Array] AST
209
+ #
210
+ # @example
211
+ # array_dsl(sn: 'Maximoff', gn: %w[Wanda Pietror])
212
+ # =>
213
+ # [ :con_or,
214
+ # [
215
+ # [ :op_eql, :sn, "Maximoff" ],
216
+ # [ :con_or,
217
+ # [
218
+ # [ :op_eql, :gn, "Wanda" ],
219
+ # [ :op_eql, :gn, "Pietror" ]
220
+ # ]
221
+ # ]
222
+ # ]
223
+ # ]
224
+ #
225
+ def array_dsl(args)
226
+ expressions = args.map do |left, right|
227
+ values = Array(right).map { |v| [:op_eql, left, escape(v)] }
228
+ join_dsl(:con_or, values)
229
+ end
230
+ join_dsl(:con_or, expressions)
231
+
232
+ # join_dsl(:con_or, [ match_dsl(args.map(&:to_a)) ])
233
+ # join_dsl(:con_or, args.map { |arg| match_dsl([arg]) })
234
+ end
235
+
236
+ # Wrap criteria with a join operator.
237
+ #
238
+ # @param operator [Symbol] :con_or, :con_and
239
+ #
240
+ # @param ary [Array] [[op, left, right],[op, left, right]]
241
+ def join_dsl(operator, ary)
242
+ ary.size >= 2 ? [operator, ary] : ary.first
243
+ end
244
+
245
+ # Wrap criteria value with extra characters, used for wildcard
246
+ #
247
+ # @param args [Array]
248
+ #
249
+ # @option :left [String] Prepended to value.
250
+ # @option :right [String] Appended to value.
251
+ #
252
+ # @return [Array]
253
+ #
254
+ # @example
255
+ # match_dsl(bar: 'foo', left: '*', right: '*')
256
+ # => [:op_eql, :bar, '*foo*']
257
+ #
258
+ def match_dsl(args, left: EMPTY_STRING, right: EMPTY_STRING)
259
+ expressions = args.map do |att, val|
260
+ values = Array(val).map do |v|
261
+ value = left.to_s + escape(v) + right.to_s
262
+ [:op_eql, att, value]
263
+ end
264
+
265
+ join_dsl(:con_or, values)
266
+ end
267
+ join_dsl(:con_or, expressions)
268
+ end
269
+
270
+ #
271
+ # @param args [Range,Array]
272
+ #
273
+ # @return [Array]
274
+ def cover_dsl(args)
275
+ attribute, range = args.to_a[0]
276
+ lower, *_, upper = range.to_a
277
+
278
+ lower = [:op_gte, attribute, lower]
279
+ upper = [:op_lte, attribute, upper]
280
+
281
+ [lower, upper]
282
+ end
283
+
284
+ # If any of the following special characters appear in the
285
+ # search filter as literals, they must be escaped with a backslash.
286
+ #
287
+ # "(", ")", "\", ,"/" "*", "null"
288
+ #
289
+ # @param value [String, Integer]
290
+ #
291
+ # @return [String]
292
+ def escape(value)
293
+ value.to_s.gsub(ESCAPE_REGEX) { |char| '\\' + ESCAPES[char] }
294
+ end
295
+ end
296
+
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ROM
4
+ module LDAP
5
+ class Dataset
6
+
7
+ module Persistence
8
+ # Interface to Directory#add
9
+ #
10
+ # @param tuple [Hash]
11
+ #
12
+ # @return [Boolean]
13
+ #
14
+ # @api public
15
+ def add(tuple)
16
+ directory.add(tuple)
17
+ end
18
+
19
+ # Interface to Directory#modify
20
+ #
21
+ # @param tuple [Changeset, Hash] Modification params
22
+ #
23
+ # @return [Array<Directory::Entry, Boolean>]
24
+ #
25
+ # @api public
26
+ def modify(tuple)
27
+ map { |e| directory.modify(e.dn, tuple) }
28
+ end
29
+
30
+ # Interface to Directory#delete
31
+ #
32
+ # @see Directory::Operations#delete
33
+ #
34
+ # @return [Array<Directory::Entry, Boolean>]
35
+ #
36
+ # @api public
37
+ def delete
38
+ map { |e| directory.delete(e.dn) }
39
+ end
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rom/ldap/client'
4
+
5
+ require 'rom/ldap/directory/env'
6
+ require 'rom/ldap/directory/entry'
7
+ require 'rom/ldap/directory/root'
8
+ require 'rom/ldap/directory/capabilities'
9
+ require 'rom/ldap/directory/operations'
10
+ require 'rom/ldap/directory/transactions'
11
+ require 'rom/ldap/directory/tokenization'
12
+
13
+ module ROM
14
+ module LDAP
15
+ # @abstract
16
+ # Builds LDAP directory abstraction over TCP connection instance.
17
+ # Includes vendor specific modules once initialised.
18
+ #
19
+ class Directory
20
+
21
+ attr_accessor :logger
22
+ attr_reader :env
23
+
24
+ # Load vendor specific methods into the instance as singletons
25
+ #
26
+ # @see Gateway#directory
27
+ #
28
+ # @return [ROM::LDAP::Directory]
29
+ #
30
+ def initialize(uri, options)
31
+ @env = ENV.new(uri, options)
32
+ @logger = options.fetch(:logger, Logger.new($stdout))
33
+
34
+ require "rom/ldap/directory/vendors/#{type}"
35
+ extend LDAP.const_get(Inflector.camelize(type))
36
+ end
37
+
38
+ include Root
39
+ include Capabilities
40
+ include Tokenization
41
+ include Operations
42
+ include Transactions
43
+
44
+ # Initial search base.
45
+ #
46
+ # @return [String] Defaults to empty string.
47
+ #
48
+ # @api public
49
+ def base
50
+ env.base
51
+ end
52
+
53
+ # Encapsulates encoding and binding using socket.
54
+ #
55
+ # @return [Client]
56
+ #
57
+ def client
58
+ @client ||= Client.new(env.to_h)
59
+ end
60
+
61
+ # Expected method inside gateway.
62
+ #
63
+ # @return [TrueClass]
64
+ #
65
+ def disconnect
66
+ client.close
67
+ client.closed?
68
+ end
69
+
70
+ # Parsed attributes.
71
+ # Output influenced by LDAP.formatter. Ordered alphabetically.
72
+ #
73
+ # @return [Array<Hash>]
74
+ #
75
+ def attribute_types
76
+ @attribute_types ||= schema_attribute_types.map(&method(:to_attribute)).flatten.freeze
77
+ end
78
+
79
+ # Look up canonical name for a formatted attribute.
80
+ #
81
+ # @see Dataset#order_by and Dataset#select
82
+ #
83
+ # @param attrs [Array<Symbol,String>] formatted attribute names.
84
+ #
85
+ # @return [Array<String>] camelCased canonical LDAP attributes.
86
+ #
87
+ def canonical_attributes(attrs)
88
+ attrs.map do |formatted|
89
+ attribute_by(:name, formatted).fetch(:canonical, formatted.to_s)
90
+ end
91
+ end
92
+
93
+ # Query parsed attribute definition hashes by key.
94
+ #
95
+ # @example
96
+ # directory.attribute_by(:name, :jpeg_photo) => { name: :jpeg_photo, ...}
97
+ #
98
+ # @param key [Symbol]
99
+ # @param value [Mixed]
100
+ #
101
+ # @return [Hash]
102
+ #
103
+ def attribute_by(key, value)
104
+ attribute_types.find { |a| a[key].eql?(value) } || EMPTY_HASH
105
+ end
106
+
107
+ # Hash of attribute names converted => original
108
+ #
109
+ # @example { formatted_name: 'canonicalName' }
110
+ #
111
+ # @return [Hash]
112
+ #
113
+ def key_map
114
+ attribute_types.map { |a| a.values_at(:name, :canonical) }.sort.to_h
115
+ end
116
+
117
+ # @return [String]
118
+ #
119
+ # @api public
120
+ def inspect
121
+ "#<#{self.class} uri='#{env.connection}' vendor='#{vendor_name}' version='#{vendor_version}' />"
122
+ end
123
+
124
+ end
125
+ end
126
+ end