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.
- 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
|