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,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ROM
4
+ module LDAP
5
+ class Directory
6
+
7
+ #
8
+ # Convenience predicates
9
+ #
10
+ module Capabilities
11
+ # Named capabilities
12
+ #
13
+ # @see rom/ldap/constants.rb
14
+ #
15
+ # @return [Array<Symbol>]
16
+ #
17
+ # @api public
18
+ def capabilities
19
+ @capabilities ||= OID.invert.values_at(*supported_controls).compact.freeze
20
+ end
21
+
22
+ # Is the server able to order the entries.
23
+ #
24
+ # @return [Boolean]
25
+ #
26
+ # @api public
27
+ def sortable?
28
+ capabilities.include?(:sort_response)
29
+ end
30
+
31
+ # @return [Boolean]
32
+ #
33
+ # @api public
34
+ def pageable?
35
+ capabilities.include?(:paged_results)
36
+ end
37
+
38
+ # @return [Boolean]
39
+ #
40
+ # @api public
41
+ def chainable?
42
+ capabilities.include?(:matching_rule_in_chain)
43
+ end
44
+
45
+ # @return [Boolean]
46
+ #
47
+ # @api public
48
+ def pruneable?
49
+ capabilities.include?(:delete_tree)
50
+ end
51
+
52
+ # @return [Boolean]
53
+ #
54
+ # @api public
55
+ def bitwise?
56
+ capabilities.include?(:matching_rule_bit_and) &&
57
+ capabilities.include?(:matching_rule_bit_or)
58
+ end
59
+
60
+ # @return [Boolean]
61
+ #
62
+ # @api public
63
+ def i18n?
64
+ capabilities.include?(:language_tag_options) &&
65
+ capabilities.include?(:language_range_options)
66
+ end
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/core/cache'
4
+ require 'dry/equalizer'
5
+ require 'rom/initializer'
6
+ require 'rom/support/memoizable'
7
+ require 'rom/ldap/functions'
8
+
9
+ module ROM
10
+ module LDAP
11
+ class Directory
12
+
13
+ # A Hash-like object wrapping the DN and attributes returned by the server.
14
+ # Contains the canonical attributes hash and a formatted version.
15
+ # BER format converted to primitive String ensures clean output in #to_yaml.
16
+ # Accessed when iterating over dataset during #modify and #delete.
17
+ # Exposes methods #fetch, #first, #each_value and #include?.
18
+ # All other method calls are forwarded to the formatted tuple.
19
+ #
20
+ # @see Directory#query
21
+ #
22
+ # @api public
23
+ class Entry
24
+
25
+ extend Initializer
26
+ extend Dry::Core::Cache
27
+
28
+ # Uses Dry::Equalizer
29
+ # @!parse
30
+ # include Dry::Equalizer
31
+ include Dry::Equalizer(:dn, :attributes, :canonical, :formatted)
32
+
33
+ include Memoizable
34
+
35
+ # @see Dataset::Persistence
36
+ #
37
+ # @!attribute [r] dn
38
+ # @return [String] Distinguished Name
39
+ #
40
+ # @api public
41
+ param :dn, proc(&:to_s), type: Types::Strict::String
42
+
43
+ # @!attribute [r] attributes
44
+ # @return [Array<Array>]
45
+ #
46
+ # @api private
47
+ param :attributes, type: Types::Strict::Array, reader: :private
48
+
49
+ # Retrieve values for a given attribute.
50
+ #
51
+ # @see Directory::Root
52
+ #
53
+ # @return [Array<String>]
54
+ #
55
+ # @param key [String, Symbol]
56
+ #
57
+ def fetch(key)
58
+ formatted.fetch(rename(key), canonical[key])
59
+ end
60
+ alias_method :[], :fetch
61
+
62
+ # Find the first (only) value for an attribute.
63
+ #
64
+ # @see Directory::Root
65
+ #
66
+ # @param [Symbol, String] key Attribute name.
67
+ #
68
+ # @return [String]
69
+ #
70
+ def first(key)
71
+ fetch(key)&.first
72
+ end
73
+
74
+ # Iterate over the values of a given attribute.
75
+ #
76
+ # @param key [Symbol] canonical attribute name
77
+ #
78
+ # @example
79
+ #
80
+ # entry.each_value(:object_class, &:to_sym)
81
+ # entry.each_value(:object_class) { |o| o.to_sym }
82
+ #
83
+ def each_value(key, &block)
84
+ fetch(key).map(&block)
85
+ end
86
+
87
+ # Mostly used by the test suite.
88
+ #
89
+ # @example
90
+ #
91
+ # expect(relation.first).to include(attr: %w[val1 val2])
92
+ #
93
+ # @param tuple [Hash] keys and array of values
94
+ #
95
+ # @return [Boolean]
96
+ #
97
+ def include?(tuple)
98
+ tuple.flat_map { |attr, vals| vals.map { |v| fetch(attr).include?(v) } }.all?
99
+ rescue NoMethodError
100
+ false
101
+ end
102
+
103
+ # Compatibility method with Ruby < 2.5
104
+ #
105
+ # @see Relation::Reading#pluck
106
+ #
107
+ # @return [Hash]
108
+ #
109
+ def slice(*keys)
110
+ formatted.select { |k, _v| keys.include?(k) }
111
+ end
112
+
113
+ # Defer to enumerable hash methods before entry values.
114
+ #
115
+ def method_missing(meth, *args, &block)
116
+ formatted.send(meth, *args, &block) if formatted.respond_to?(meth) || super
117
+ end
118
+
119
+ # @return [String]
120
+ #
121
+ def inspect
122
+ %(#<#{self.class} #{dn.empty? ? 'rootDSE' : dn} />)
123
+ end
124
+
125
+ private
126
+
127
+ # @param meth [Symbol]
128
+ #
129
+ # @return [Boolean]
130
+ #
131
+ # @api private
132
+ def respond_to_missing?(meth, include_private = false)
133
+ formatted.respond_to?(meth) || super
134
+ end
135
+
136
+ # Cache renamed key to improve performance two fold in benchmarks.
137
+ #
138
+ # @param key [String, Symbol]
139
+ #
140
+ # @api private
141
+ def rename(key)
142
+ fetch_or_store(key) { LDAP.formatter[key] }
143
+ end
144
+
145
+ # Convert keys of the canonical tuple using the chosen formatting proc.
146
+ #
147
+ # @api private
148
+ def formatted
149
+ Functions[:map_keys, LDAP.formatter][canonical]
150
+ end
151
+
152
+ # DN combined with attributes array
153
+ #
154
+ # @return [Array]
155
+ #
156
+ # @api private
157
+ def with_dn
158
+ attributes.dup.sort.unshift(['dn', dn])
159
+ end
160
+
161
+ # Create canonical tuple
162
+ #
163
+ # @example
164
+ #
165
+ # # => { 'dn' => [''], 'objectClass' => ['', ''] }
166
+ #
167
+ # @return [Hash] canonical camelCase keys ordered alphabetically
168
+ #
169
+ # @see Dataset#export
170
+ #
171
+ # @api private
172
+ def canonical
173
+ stringify_keys[stringify_values[with_dn]]
174
+ end
175
+
176
+ # Covert hash keys to strings.
177
+ #
178
+ # @return [Proc]
179
+ #
180
+ # @api private
181
+ def stringify_keys
182
+ Functions[:map_keys, Functions[:to_string]]
183
+ end
184
+
185
+ # Convert hash whose values are arrays of strings.
186
+ #
187
+ # @return [Proc]
188
+ #
189
+ # @api private
190
+ def stringify_values
191
+ Functions[:map_values, Functions[:map_array, Functions[:to_string]]]
192
+ end
193
+
194
+ memoize :canonical, :formatted
195
+
196
+ end
197
+
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rom/initializer'
4
+ require 'uri'
5
+
6
+ module ROM
7
+ module LDAP
8
+ class Directory
9
+
10
+ # Parse uri and options received by the gateway configuration.
11
+ #
12
+ # @example
13
+ # scheme:// binddn : passwd @ host : port / base
14
+ # ldap://uid=admin,ou=system:secret@localhost:1389/ou=users,dc=rom,dc=ldap
15
+ #
16
+ # @see https://ldapwiki.com/wiki/LDAP%20URL
17
+ # @see https://docs.oracle.com/cd/E19957-01/816-6402-10/url.htm
18
+ #
19
+ # rubocop:disable Lint/UriEscapeUnescape
20
+ #
21
+ # @api private
22
+ class ENV
23
+
24
+ extend Initializer
25
+
26
+ param :connection,
27
+ reader: true,
28
+ type: Types::URI,
29
+ default: -> { ::ENV.fetch('LDAPURI', default_connection) }
30
+
31
+ param :config,
32
+ reader: :private,
33
+ type: Types::Strict::Hash,
34
+ default: -> { EMPTY_OPTS }
35
+
36
+ # Build LDAP URL with encoded spaces.
37
+ #
38
+ # @return [URI::LDAP, URI::LDAPS]
39
+ #
40
+ # @raise URI::InvalidURIError
41
+ def uri
42
+ URI(connection.gsub(SPACE, PERCENT_SPACE))
43
+ end
44
+
45
+ # Global search base. The value is derived in this order:
46
+ # 1. Gateway Options
47
+ # 2. ENVs
48
+ # 3. URI (unless this is a socket) defaults ""
49
+ # 4. an empty string
50
+ #
51
+ # @example
52
+ # 'ldap://localhost/ou=users,dc=rom,dc=ldap' => ou=users,dc=rom,dc=ldap
53
+ #
54
+ # @return [String]
55
+ #
56
+ def base
57
+ config.fetch(:base) do
58
+ ::ENV['LDAPBASE'] || (path ? EMPTY_STRING : uri.dn.to_s)
59
+ end
60
+ end
61
+
62
+ # Username and password.
63
+ #
64
+ # @return [Hash, NilClass]
65
+ #
66
+ def auth
67
+ { username: bind_dn, password: bind_pw } if bind_dn
68
+ end
69
+
70
+ # @return [Hash, NilClass]
71
+ #
72
+ def ssl
73
+ config[:ssl] if uri.scheme.eql?('ldaps')
74
+ end
75
+
76
+ # @return [Hash]
77
+ #
78
+ def to_h
79
+ { host: host, port: port, path: path, ssl: ssl, auth: auth }
80
+ end
81
+
82
+ # @return [String]
83
+ #
84
+ def inspect
85
+ "<#{self.class.name} #{connection} />"
86
+ end
87
+
88
+ private
89
+
90
+ # @return [String, NilClass]
91
+ #
92
+ def path
93
+ uri.path unless uri.host
94
+ end
95
+
96
+ # @return [String, NilClass]
97
+ #
98
+ def host
99
+ uri.host unless path
100
+ end
101
+
102
+ # @return [Integer, NilClass]
103
+ #
104
+ def port
105
+ uri.port unless path
106
+ end
107
+
108
+ # Override LDAPURI user with options or LDAPBINDDN.
109
+ # Percent decode the URI's user value.
110
+ #
111
+ # @return [String, NilClass]
112
+ #
113
+ def bind_dn
114
+ dn = config.fetch(:username, ::ENV['LDAPBINDDN']) || uri.user
115
+ dn.gsub(PERCENT_SPACE, SPACE) if dn
116
+ end
117
+ # rubocop:enable Lint/UriEscapeUnescape
118
+
119
+ # Override LDAPURI password with options or LDAPBINDPW
120
+ #
121
+ # @return [String, NilClass]
122
+ #
123
+ def bind_pw
124
+ config.fetch(:password, ::ENV['LDAPBINDPW']) || uri.password
125
+ end
126
+
127
+ # LDAPHOST or localhost
128
+ #
129
+ # @return [String]
130
+ #
131
+ def default_host
132
+ ::ENV.fetch('LDAPHOST', 'localhost')
133
+ end
134
+
135
+ # LDAPPORT or 389
136
+ #
137
+ # @return [Integer]
138
+ #
139
+ def default_port
140
+ ::ENV.fetch('LDAPPORT', 389)
141
+ end
142
+
143
+ # Fallback connection scheme is "ldap://"
144
+ #
145
+ # @return [String]
146
+ #
147
+ def default_connection
148
+ "ldap://#{default_host}:#{default_port}"
149
+ end
150
+
151
+ end
152
+
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/core/cache'
4
+
5
+ module ROM
6
+ module LDAP
7
+ class Directory
8
+
9
+ module Operations
10
+ def self.included(klass)
11
+ klass.class_eval do
12
+ extend Dry::Core::Cache
13
+ end
14
+ end
15
+
16
+ # Use connection to communicate with server.
17
+ # If :base is passed, it overwrites the default base keyword.
18
+ #
19
+ # @option :filter [String, Array] AST or LDAP string.
20
+ # Defaults to class attribute.
21
+ #
22
+ # @param options [Hash] @see Connection::SearchRequest
23
+ #
24
+ # @return [Array<Entry>] Formatted hash like objects.
25
+ #
26
+ # @api public
27
+ #
28
+ def query(filter: DEFAULT_FILTER, **options)
29
+ set, counter = [], 0
30
+
31
+ # TODO: pageable and search referrals
32
+ params = {
33
+ base: base,
34
+ expression: to_expression(filter),
35
+ **options
36
+ # paged: pageable?
37
+
38
+ # return_refs: true
39
+ # https://tools.ietf.org/html/rfc4511#section-4.5.3
40
+ }
41
+
42
+ # pdu = client.search(params) do |search_referrals: |
43
+ pdu = client.search(params) do |dn, attributes|
44
+ counter += 1
45
+ logger.debug("#{counter}: #{dn}") if ::ENV['DEBUG']
46
+
47
+ set << entity = Entry.new(dn, attributes)
48
+ yield(entity) if block_given?
49
+ end
50
+
51
+ debug(pdu)
52
+
53
+ set
54
+ end
55
+
56
+ def debug(pdu)
57
+ return unless ::ENV['DEBUG']
58
+
59
+ logger.debug(pdu.advice) if pdu&.advice
60
+ logger.debug(pdu.message) if pdu&.message
61
+ logger.debug(pdu.info) if pdu&.failure?
62
+ end
63
+
64
+ # Return all attributes for a distinguished name.
65
+ #
66
+ # @param dn [String]
67
+ #
68
+ # @return [Array<Entry>]
69
+ #
70
+ # @api public
71
+ def by_dn(dn)
72
+ raise(DistinguishedNameError, 'DN is required') unless dn
73
+
74
+ query(base: dn, max: 1, attributes: ALL_ATTRS)
75
+ end
76
+
77
+ #
78
+ #
79
+ # @option :filter [String]
80
+ # @option :password [String]
81
+ #
82
+ # @return [Boolean]
83
+ #
84
+ # @api public
85
+ def bind_as(filter:, password:)
86
+ if (entity = query(filter: filter, max: 1).first)
87
+ password = password.call if password.respond_to?(:call)
88
+
89
+ pdu = client.bind(username: entity.dn, password: password)
90
+ pdu.success?
91
+ else
92
+ false
93
+ end
94
+ rescue BindError
95
+ false
96
+ end
97
+
98
+ # Used by gateway[filter] to infer schema at boot.
99
+ # Limited to 1000 and cached.
100
+ #
101
+ # @param filter [String] dataset schema filter
102
+ #
103
+ # @return [Array<Entry>]
104
+ #
105
+ # @api public
106
+ def query_attributes(filter)
107
+ fetch_or_store(base, filter) do
108
+ query(
109
+ filter: filter,
110
+ base: base,
111
+ max: 1_000, # attribute sample size
112
+ attributes_only: true
113
+ # paged: false
114
+ )
115
+ end
116
+ end
117
+
118
+ # Count all entries under the search base.
119
+ #
120
+ # @return [Integer]
121
+ #
122
+ # @api public
123
+ def base_total
124
+ query(base: base, attributes: %w[objectClass], attributes_only: true).count
125
+ end
126
+
127
+ #
128
+ # @param tuple [Hash] tuple using formatted attribute names.
129
+ #
130
+ # @return [Entry, FalseClass] created LDAP entry or false.
131
+ #
132
+ # @api public
133
+ def add(tuple)
134
+ dn = tuple.delete(:dn)
135
+ attrs = canonicalise(tuple)
136
+ raise(DistinguishedNameError, 'DN is required') unless dn
137
+
138
+ log(__callee__, dn)
139
+
140
+ pdu = client.add(dn: dn, attrs: attrs)
141
+
142
+ pdu.success? ? find(dn) : pdu.success?
143
+ end
144
+
145
+ # client#rename > client#password_modify > client#update
146
+ #
147
+ # @param dn [String] distinguished name.
148
+ #
149
+ # @param tuple [Hash] tuple using formatted attribute names.
150
+ #
151
+ # @return [Entry, FalseClass] updated LDAP entry or false.
152
+ #
153
+ # @api public
154
+ def modify(dn, tuple)
155
+ log(__callee__, dn)
156
+
157
+ # entry = find(dn)
158
+
159
+ new_dn = tuple.delete(:dn)
160
+ attrs = canonicalise(tuple)
161
+
162
+ rdn_attr, rdn_val = get_rdn(dn).split('=')
163
+
164
+ # 1. Move rename
165
+ if new_dn
166
+ new_rdn = get_rdn(new_dn)
167
+ parent = get_parent_dn(new_dn)
168
+
169
+ new_rdn_attr, new_rdn_val = new_rdn.split('=')
170
+
171
+ replace = rdn_attr.eql?(new_rdn_attr)
172
+
173
+ pdu = client.rename(dn: dn, rdn: new_rdn, replace: replace, superior: parent)
174
+
175
+ if pdu.success?
176
+ dn, rdn_attr, rdn_val = new_dn, new_rdn_attr, new_rdn_val
177
+ end
178
+ end
179
+
180
+ # 2. Change password
181
+ if attrs.key?('userPassword')
182
+ new_pwd = attrs.delete('userPassword')
183
+ entry = find(dn)
184
+ old_pwd = entry['userPassword']
185
+
186
+ pdu = client.password_modify(dn, old_pwd: old_pwd, new_pwd: new_pwd)
187
+ end
188
+
189
+ # 3. Edit attributes
190
+ unless attrs.empty?
191
+
192
+ # Adding to RDN values?
193
+ if attrs.key?(rdn_attr) && !attrs.key?(rdn_val)
194
+ attrs[rdn_attr] = Array(attrs[rdn_attr]).unshift(rdn_val)
195
+ end
196
+
197
+ pdu = client.update(dn: dn, ops: attrs.to_a)
198
+ end
199
+
200
+ pdu.success? ? find(dn) : pdu.success?
201
+ end
202
+
203
+ # Tuple(s) by dn
204
+ #
205
+ # @param dn [String] distinguished name
206
+ #
207
+ # @return [Array<Hash>,Hash]
208
+ #
209
+ # @raise [DistinguishedNameError] DN not found
210
+ #
211
+ def find(dn)
212
+ entry = by_dn(dn)
213
+ raise(DistinguishedNameError, 'DN not found') unless entry
214
+
215
+ entry.one? ? entry.first : entry
216
+ end
217
+
218
+ #
219
+ # @param dn [String] distinguished name.
220
+ #
221
+ # @return [Entry, FalseClass] deleted LDAP entry or false.
222
+ #
223
+ # @api public
224
+ def delete(dn)
225
+ log(__callee__, dn)
226
+ entry = find(dn)
227
+
228
+ pdu = if pruneable?
229
+ controls = [OID[:delete_tree]]
230
+ client.delete(dn: dn, controls: controls)
231
+ else
232
+ client.delete(dn: dn)
233
+ end
234
+
235
+ pdu.success? ? entry : pdu.success?
236
+ end
237
+
238
+ private
239
+
240
+ # RDN - relative distinguished name
241
+ #
242
+ # @return [String]
243
+ #
244
+ # @api private
245
+ def get_rdn(dn)
246
+ dn.split(',')[0]
247
+ end
248
+
249
+ # Parent DN
250
+ #
251
+ # @return [String]
252
+ #
253
+ # @api private
254
+ def get_parent_dn(dn)
255
+ dn.split(',')[1..-1].join(',')
256
+ end
257
+
258
+ # Log operation attempt
259
+ #
260
+ def log(method, dn)
261
+ logger.debug("#{self.class}##{method} '#{dn}'")
262
+ end
263
+
264
+ # Rename the formatted keys of the incoming tuple to their original
265
+ # server-side format.
266
+ #
267
+ # @note Used by Directory#add and Directory#modify
268
+ #
269
+ # @param tuple [Hash]
270
+ #
271
+ # @example
272
+ # # => canonicalise(population_count: 0) => { 'populationCount' => 0 }
273
+ #
274
+ # @api private
275
+ def canonicalise(tuple)
276
+ Functions[:tuplify].call(tuple, key_map)
277
+ end
278
+ end
279
+
280
+ end
281
+ end
282
+ end