rom-ldap 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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