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,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ber'
4
+
5
+ require 'rom/initializer'
6
+ require 'rom/ldap/message_queue'
7
+ require 'rom/ldap/pdu'
8
+ require 'rom/ldap/socket'
9
+ require 'rom/ldap/client/operations'
10
+ require 'rom/ldap/client/authentication'
11
+
12
+ module ROM
13
+ module LDAP
14
+ #
15
+ # Uses socket to read and write using BER encoding.
16
+ #
17
+ # @api private
18
+ class Client
19
+
20
+ using ::BER
21
+
22
+ extend Initializer
23
+
24
+ # @!attribute [r] host
25
+ # @return [String]
26
+ option :host, reader: :private, type: Types::Strict::String
27
+
28
+ option :port, reader: :private, type: Types::Strict::Integer
29
+ option :path, reader: :private, type: Types::Strict::String
30
+ option :auth, reader: :private, type: Types::Strict::Hash, optional: true
31
+ option :ssl, reader: :private, type: Types::Strict::Hash, optional: true
32
+ option :queue, default: -> { MessageQueue }
33
+
34
+ include Operations
35
+ include Authentication
36
+
37
+ attr_reader :socket
38
+
39
+ # Create connection (encrypted) and authenticate.
40
+ #
41
+ # @yield [Socket]
42
+ #
43
+ # @raise [BindError, SecureBindError]
44
+ def open
45
+ unless alive?
46
+ @socket = Socket.new(options).call
47
+
48
+ # tls
49
+ if ssl
50
+ start_tls
51
+ sasl_bind # (mechanism:, credentials:, challenge:)
52
+ end
53
+
54
+ bind(auth) unless auth.nil? # simple auth
55
+ end
56
+
57
+ yield(@socket)
58
+ end
59
+
60
+ # @return [Boolean]
61
+ #
62
+ def closed?
63
+ socket.nil? || (socket.is_a?(::Socket) && socket.closed?)
64
+ end
65
+
66
+ # @return [NilClass]
67
+ #
68
+ def close
69
+ return if socket.nil?
70
+
71
+ socket.close
72
+ @socket = nil
73
+ end
74
+
75
+ # @return [Boolean]
76
+ #
77
+ def alive?
78
+ return false if closed?
79
+
80
+ if IO.select([socket], nil, nil, 0)
81
+ begin
82
+ !socket.eof?
83
+ rescue StandardError
84
+ false
85
+ end
86
+ else
87
+ true
88
+ end
89
+ rescue IOError
90
+ false
91
+ end
92
+
93
+ private
94
+
95
+ # @see BER
96
+ #
97
+ # @param symbol [Symbol]
98
+ #
99
+ # @return [Integer]
100
+ def pdu_lookup(symbol)
101
+ ::BER.fetch(:response, symbol)
102
+ end
103
+
104
+ # Read from socket and wrap in PDU class.
105
+ #
106
+ # @return [PDU, NilClass]
107
+ def read
108
+ open do |socket|
109
+ return unless (ber_object = socket.read_ber)
110
+
111
+ PDU.new(*ber_object)
112
+ end
113
+ rescue Errno::ECONNRESET
114
+ close
115
+ retry
116
+ end
117
+
118
+ # Write to socket.
119
+ #
120
+ # @api private
121
+ def write(request, message_id, controls = nil)
122
+ open do |socket|
123
+ packet = [message_id.to_ber, request, controls].compact.to_ber_sequence
124
+ socket.write(packet)
125
+ socket.flush
126
+ end
127
+ rescue Errno::EPIPE, IOError
128
+ close
129
+ retry
130
+ end
131
+
132
+ # @return [PDU]
133
+ #
134
+ # @api private
135
+ def from_queue(message_id)
136
+ if (pdu = queue[message_id].shift)
137
+ return pdu
138
+ end
139
+
140
+ while (pdu = read)
141
+ return pdu if pdu.message_id.eql?(message_id)
142
+
143
+ queue[pdu.message_id].push(pdu)
144
+ next
145
+ end
146
+
147
+ pdu
148
+ end
149
+
150
+ # Increment the message counter.
151
+ #
152
+ # @return [Integer]
153
+ #
154
+ # @api private
155
+ def next_msgid
156
+ @msgid ||= 0
157
+ @msgid += 1
158
+ end
159
+
160
+ # Persist changes to the server and return response object.
161
+ # Enable stdout debugging with DEBUG=y.
162
+ #
163
+ # @return [PDU]
164
+ #
165
+ # @raise [ResponseError]
166
+ #
167
+ # @api private
168
+ def submit(type, request, controls = nil)
169
+ message_id = next_msgid
170
+
171
+ write(request, message_id, controls)
172
+
173
+ pdu = from_queue(message_id)
174
+
175
+ if pdu&.app_tag == pdu_lookup(type)
176
+ puts pdu.advice if ENV['DEBUG'] && pdu.advice && !pdu.advice.empty?
177
+ pdu
178
+ else
179
+ raise(ResponseError, "Invalid #{type}")
180
+ end
181
+ end
182
+
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Lint/SuppressedException
4
+ begin
5
+ # TODO: Windows NTLM authentication
6
+ # @see https://github.com/winrb/rubyntlm
7
+ require 'rubyntlm'
8
+ rescue LoadError
9
+ # ntlm_bind
10
+ end
11
+ # rubocop:enable Lint/SuppressedException
12
+
13
+ module ROM
14
+ module LDAP
15
+ # @api private
16
+ class Client
17
+
18
+ using ::BER
19
+
20
+ # Adds authentication capability to the client.
21
+ #
22
+ # @api private
23
+ module Authentication
24
+ #
25
+ # The Bind request is defined as follows:
26
+ #
27
+ # BindRequest ::= [APPLICATION 0] SEQUENCE {
28
+ # version INTEGER (1 .. 127),
29
+ # name LDAPDN,
30
+ # authentication AuthenticationChoice }
31
+ #
32
+ # AuthenticationChoice ::= CHOICE {
33
+ # simple [0] OCTET STRING,
34
+ # -- 1 and 2 reserved
35
+ # sasl [3] SaslCredentials,
36
+ # ... }
37
+ #
38
+ # SaslCredentials ::= SEQUENCE {
39
+ # mechanism LDAPString,
40
+ # credentials OCTET STRING OPTIONAL }
41
+ #
42
+ # @see https://tools.ietf.org/html/rfc4511#section-4.2
43
+ # @see https://tools.ietf.org/html/rfc4513
44
+ #
45
+ #
46
+ # @option :username [String]
47
+ #
48
+ # @option :password [String]
49
+ #
50
+ # @return [PDU] result object
51
+ #
52
+ # @raise [BindError]
53
+ #
54
+ # @api public
55
+ def bind(username:, password:)
56
+ request_type = pdu_lookup(:bind_request)
57
+
58
+ request = [
59
+ 3.to_ber,
60
+ username.to_ber,
61
+ password.to_ber_contextspecific(0)
62
+ ].to_ber_appsequence(request_type)
63
+
64
+ pdu = submit(:bind_result, request)
65
+ raise(BindError, username) if pdu.failure?
66
+
67
+ pdu
68
+ end
69
+
70
+ #
71
+ #
72
+ # @return [PDU] result object
73
+ #
74
+ # @api private
75
+ def start_tls
76
+ request_type = pdu_lookup(:extended_request)
77
+
78
+ request = [
79
+ OID[:start_tls].to_ber_contextspecific(0)
80
+ ].to_ber_appsequence(request_type)
81
+
82
+ submit(:extended_response, request)
83
+ end
84
+
85
+ #
86
+ # @return
87
+ #
88
+ # @raise [SecureBindError]
89
+ #
90
+ # @api private
91
+ def sasl_bind(mechanism:, credentials:, challenge:)
92
+ request_type = pdu_lookup(:bind_request)
93
+ n = 0
94
+
95
+ loop do
96
+ sasl = [
97
+ mechanism.to_ber,
98
+ credentials.to_ber
99
+ ].to_ber_contextspecific(3)
100
+
101
+ request = [
102
+ 3.to_ber,
103
+ EMPTY_STRING.to_ber,
104
+ sasl
105
+ ].to_ber_appsequence(request_type)
106
+
107
+ raise SecureBindError, 'sasl-challenge overflow' if (n += 1) > 10
108
+
109
+ pdu = submit(:bind_request, request)
110
+
111
+ credentials = challenge.call(pdu.result_server_sasl_creds)
112
+ end
113
+ end
114
+ end
115
+
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rom/ldap/search_request'
4
+
5
+ module ROM
6
+ module LDAP
7
+ # @api private
8
+ class Client
9
+
10
+ using ::BER
11
+
12
+ # Adds entry creation capability to the connection.
13
+ #
14
+ # @api private
15
+ module Operations
16
+ # Connection Search Operation
17
+ #
18
+ # @see https://tools.ietf.org/html/rfc4511#section-4.5.3
19
+ # @see https://tools.ietf.org/html/rfc2696
20
+ #
21
+ # @todo Write spec for yielding search_referrals
22
+ #
23
+ # @yield [Entry]
24
+ # @yield [Hash] :search_referrals
25
+ #
26
+ # @api private
27
+ def search(return_refs: true, **params)
28
+ search_request = SearchRequest.new(params)
29
+ request_type = pdu_lookup(:search_request)
30
+ result_pdu = nil
31
+
32
+ more_pages = false
33
+ paged_results_cookie = [126, EMPTY_STRING]
34
+ # paged_results_cookie = [10, EMPTY_STRING]
35
+
36
+ request = search_request.parts.to_ber_appsequence(request_type)
37
+ controls = search_request.controls
38
+ message_id = next_msgid
39
+
40
+ loop do
41
+ write(request, message_id, controls)
42
+
43
+ result_pdu = nil
44
+ controls = EMPTY_ARRAY
45
+
46
+ while (pdu = from_queue(message_id))
47
+ case pdu.app_tag
48
+ when pdu_lookup(:search_returned_data) # 4
49
+ yield(pdu.search_entry)
50
+
51
+ when pdu_lookup(:search_result) # 5
52
+ result_pdu = pdu
53
+ controls = pdu.result_controls
54
+ yield(search_referrals: pdu.search_referrals) if return_refs && pdu.search_referral?
55
+ break
56
+
57
+ when pdu_lookup(:search_result_referral) # 19
58
+ yield(search_referrals: pdu.search_referrals) if return_refs
59
+
60
+ else
61
+ raise ResponseTypeInvalidError, "invalid response-type in search: #{pdu.app_tag}"
62
+ end
63
+ end
64
+
65
+ if result_pdu&.success?
66
+ controls.each do |c|
67
+ next if c.oid != OID[:paged_results]
68
+
69
+ next if c.value&.empty?
70
+
71
+ # [ 0, "" ]
72
+ _int, cookie = c.value.read_ber
73
+
74
+ # next page of results
75
+ #
76
+ unless cookie&.empty?
77
+
78
+ # cookie => "\u0001\u0000\u0000"
79
+ # cookie.read_ber => true
80
+
81
+ paged_results_cookie[1] = cookie
82
+ more_pages = true
83
+ end
84
+ end
85
+ end
86
+
87
+ break unless more_pages
88
+ end
89
+
90
+ result_pdu
91
+ ensure
92
+ queue.delete(message_id)
93
+ end
94
+
95
+ #
96
+ # @option :dn [String] distinguished name
97
+ # @option :attrs [Hash]
98
+ #
99
+ # @api private
100
+ def add(dn:, attrs:)
101
+ request_type = pdu_lookup(:add_request)
102
+
103
+ ber_attrs = attrs.each_with_object([]) do |(k, v), attributes|
104
+ ber_values = values_to_ber_set(v)
105
+ attributes << [k.to_s.to_ber, ber_values].to_ber_sequence
106
+ end
107
+
108
+ request = [
109
+ dn.to_ber,
110
+ ber_attrs.to_ber_sequence
111
+ ].to_ber_appsequence(request_type)
112
+
113
+ submit(:add_response, request)
114
+ end
115
+
116
+ # @option :dn [String] distinguished name
117
+ #
118
+ # @option :controls [Array<String>] e.g. DELETE_TREE
119
+ #
120
+ # @api private
121
+ def delete(dn:, controls: nil)
122
+ request_type = pdu_lookup(:delete_request)
123
+ request = dn.to_ber_application_string(request_type)
124
+
125
+ if controls
126
+ submit(:delete_response, request, controls.to_ber_control)
127
+ else
128
+ submit(:delete_response, request)
129
+ end
130
+ end
131
+
132
+ # @option :dn [String] distinguished name
133
+ #
134
+ # @option :ops [Array<Mixed>] operation ast
135
+ #
136
+ # @return [PDU] result object
137
+ #
138
+ # @api private
139
+ def update(dn:, ops:)
140
+ request_type = pdu_lookup(:modify_request)
141
+ operations = modify_ops(ops)
142
+
143
+ request = [
144
+ dn.to_ber,
145
+ operations.to_ber_sequence
146
+ ].to_ber_appsequence(request_type)
147
+
148
+ submit(:modify_response, request)
149
+ end
150
+
151
+ # TODO: spec rename and use by relations
152
+ #
153
+ # @option :dn [String] current distinguished name
154
+ #
155
+ # @option :rdn [String] new relative distinguished name
156
+ #
157
+ # @option :replace [TrueClass] replace existing rdn
158
+ #
159
+ # @option :superior [String] new parent dn
160
+ #
161
+ # @return [PDU] result object
162
+ #
163
+ # @api public
164
+ def rename(dn:, rdn:, replace: false, superior: nil)
165
+ request_type = pdu_lookup(:modify_rdn_request)
166
+
167
+ request = [dn, rdn, replace].map { |a| a.to_ber } # &:to_ber
168
+
169
+ request << superior.to_ber_contextspecific(0) if superior
170
+
171
+ request = request.to_ber_appsequence(request_type)
172
+
173
+ submit(:modify_rdn_response, request)
174
+ end
175
+
176
+ # Password should have a minimum of 5 characters.
177
+ #
178
+ # @see http://tools.ietf.org/html/rfc3062
179
+ #
180
+ # @option :dn [String] distinguished name
181
+ #
182
+ # @option :old_pwd [String] current password (optional for admin reset)
183
+ #
184
+ # @option :new_pwd [String] replacement password
185
+ #
186
+ # @return [PDU] result object
187
+ #
188
+ # @api public
189
+ def password_modify(dn:, old_pwd: nil, new_pwd:)
190
+ request_type = pdu_lookup(:extended_request)
191
+ context = OID[:password_modify].to_ber_contextspecific(0)
192
+
193
+ payload = [dn.to_ber(0x80)]
194
+ payload << old_pwd.to_ber(0x81) if old_pwd
195
+ payload << new_pwd.to_ber(0x82)
196
+ payload = payload.to_ber_sequence.to_ber(0x81)
197
+
198
+ request = [context, payload].to_ber_appsequence(request_type)
199
+
200
+ submit(:extended_response, request)
201
+ end
202
+
203
+ private
204
+
205
+ # Encode (replace) operation AST to BER.
206
+ # Operation tokens are add=0, delete=1 and replace=2.
207
+ #
208
+ # @param operations [Array]
209
+ #
210
+ # @return [Array] BER encoded operations
211
+ def modify_ops(operations = EMPTY_ARRAY)
212
+ operations.each_with_object([]) do |(attribute, values), ops|
213
+ payload = [
214
+ attribute.to_s.to_ber,
215
+ values_to_ber_set(values)
216
+ ].to_ber_sequence
217
+
218
+ ops << [2.to_ber_enumerated, payload].to_ber
219
+ end
220
+ end
221
+
222
+ # @param values [String, Array<String>]
223
+ #
224
+ # @return [String] Encoding:ASCII-8BIT
225
+ #
226
+ def values_to_ber_set(values)
227
+ Array(values).map { |v| v&.to_ber }.to_ber_set
228
+ end
229
+ end
230
+
231
+ end
232
+ end
233
+ end