rom-ldap 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +251 -0
- data/CONTRIBUTING.md +18 -0
- data/README.md +172 -0
- data/TODO.md +33 -0
- data/config/responses.yml +328 -0
- data/lib/dry/monitor/ldap/colorizers/default.rb +17 -0
- data/lib/dry/monitor/ldap/colorizers/rouge.rb +31 -0
- data/lib/dry/monitor/ldap/logger.rb +58 -0
- data/lib/rom-ldap.rb +1 -0
- data/lib/rom/ldap.rb +22 -0
- data/lib/rom/ldap/alias.rb +30 -0
- data/lib/rom/ldap/associations.rb +6 -0
- data/lib/rom/ldap/associations/core.rb +23 -0
- data/lib/rom/ldap/associations/many_to_many.rb +18 -0
- data/lib/rom/ldap/associations/many_to_one.rb +22 -0
- data/lib/rom/ldap/associations/one_to_many.rb +32 -0
- data/lib/rom/ldap/associations/one_to_one.rb +19 -0
- data/lib/rom/ldap/associations/self_ref.rb +35 -0
- data/lib/rom/ldap/attribute.rb +327 -0
- data/lib/rom/ldap/client.rb +185 -0
- data/lib/rom/ldap/client/authentication.rb +118 -0
- data/lib/rom/ldap/client/operations.rb +233 -0
- data/lib/rom/ldap/commands.rb +6 -0
- data/lib/rom/ldap/commands/create.rb +41 -0
- data/lib/rom/ldap/commands/delete.rb +17 -0
- data/lib/rom/ldap/commands/update.rb +35 -0
- data/lib/rom/ldap/constants.rb +193 -0
- data/lib/rom/ldap/dataset.rb +286 -0
- data/lib/rom/ldap/dataset/conversion.rb +62 -0
- data/lib/rom/ldap/dataset/dsl.rb +299 -0
- data/lib/rom/ldap/dataset/persistence.rb +44 -0
- data/lib/rom/ldap/directory.rb +126 -0
- data/lib/rom/ldap/directory/capabilities.rb +71 -0
- data/lib/rom/ldap/directory/entry.rb +200 -0
- data/lib/rom/ldap/directory/env.rb +155 -0
- data/lib/rom/ldap/directory/operations.rb +282 -0
- data/lib/rom/ldap/directory/password.rb +122 -0
- data/lib/rom/ldap/directory/root.rb +187 -0
- data/lib/rom/ldap/directory/tokenization.rb +66 -0
- data/lib/rom/ldap/directory/transactions.rb +31 -0
- data/lib/rom/ldap/directory/vendors/active_directory.rb +129 -0
- data/lib/rom/ldap/directory/vendors/apache_ds.rb +27 -0
- data/lib/rom/ldap/directory/vendors/e_directory.rb +16 -0
- data/lib/rom/ldap/directory/vendors/open_directory.rb +12 -0
- data/lib/rom/ldap/directory/vendors/open_dj.rb +25 -0
- data/lib/rom/ldap/directory/vendors/open_ldap.rb +35 -0
- data/lib/rom/ldap/directory/vendors/three_eight_nine.rb +16 -0
- data/lib/rom/ldap/directory/vendors/unknown.rb +22 -0
- data/lib/rom/ldap/dsl.rb +76 -0
- data/lib/rom/ldap/errors.rb +47 -0
- data/lib/rom/ldap/expression.rb +77 -0
- data/lib/rom/ldap/expression_encoder.rb +174 -0
- data/lib/rom/ldap/extensions.rb +50 -0
- data/lib/rom/ldap/extensions/active_support_notifications.rb +26 -0
- data/lib/rom/ldap/extensions/compatibility.rb +11 -0
- data/lib/rom/ldap/extensions/dsml.rb +165 -0
- data/lib/rom/ldap/extensions/msgpack.rb +23 -0
- data/lib/rom/ldap/extensions/optimised_json.rb +25 -0
- data/lib/rom/ldap/extensions/rails_log_subscriber.rb +38 -0
- data/lib/rom/ldap/formatter.rb +26 -0
- data/lib/rom/ldap/functions.rb +207 -0
- data/lib/rom/ldap/gateway.rb +145 -0
- data/lib/rom/ldap/ldif.rb +74 -0
- data/lib/rom/ldap/ldif/exporter.rb +77 -0
- data/lib/rom/ldap/ldif/importer.rb +95 -0
- data/lib/rom/ldap/mapper_compiler.rb +19 -0
- data/lib/rom/ldap/matchers.rb +69 -0
- data/lib/rom/ldap/message_queue.rb +7 -0
- data/lib/rom/ldap/oid.rb +101 -0
- data/lib/rom/ldap/parsers/abstract_syntax.rb +91 -0
- data/lib/rom/ldap/parsers/attribute.rb +290 -0
- data/lib/rom/ldap/parsers/filter_syntax.rb +133 -0
- data/lib/rom/ldap/pdu.rb +285 -0
- data/lib/rom/ldap/plugin/pagination.rb +145 -0
- data/lib/rom/ldap/plugins.rb +7 -0
- data/lib/rom/ldap/projection_dsl.rb +38 -0
- data/lib/rom/ldap/relation.rb +135 -0
- data/lib/rom/ldap/relation/exporting.rb +72 -0
- data/lib/rom/ldap/relation/reading.rb +461 -0
- data/lib/rom/ldap/relation/writing.rb +64 -0
- data/lib/rom/ldap/responses.rb +17 -0
- data/lib/rom/ldap/restriction_dsl.rb +45 -0
- data/lib/rom/ldap/schema.rb +123 -0
- data/lib/rom/ldap/schema/attributes_inferrer.rb +59 -0
- data/lib/rom/ldap/schema/dsl.rb +13 -0
- data/lib/rom/ldap/schema/inferrer.rb +50 -0
- data/lib/rom/ldap/schema/type_builder.rb +133 -0
- data/lib/rom/ldap/scope.rb +19 -0
- data/lib/rom/ldap/search_request.rb +249 -0
- data/lib/rom/ldap/socket.rb +210 -0
- data/lib/rom/ldap/tasks/ldap.rake +103 -0
- data/lib/rom/ldap/tasks/ldif.rake +80 -0
- data/lib/rom/ldap/transaction.rb +29 -0
- data/lib/rom/ldap/type_map.rb +88 -0
- data/lib/rom/ldap/types.rb +158 -0
- data/lib/rom/ldap/version.rb +17 -0
- data/lib/rom/plugins/relation/ldap/active_directory.rb +182 -0
- data/lib/rom/plugins/relation/ldap/auto_restrictions.rb +69 -0
- data/lib/rom/plugins/relation/ldap/e_directory.rb +27 -0
- data/lib/rom/plugins/relation/ldap/instrumentation.rb +35 -0
- data/lib/rouge/lexers/ldap.rb +72 -0
- data/lib/rouge/themes/ldap.rb +49 -0
- 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
|