-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathldap.rb
More file actions
341 lines (304 loc) · 11.9 KB
/
ldap.rb
File metadata and controls
341 lines (304 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
require 'net/ldap'
require 'forwardable'
require 'github/ldap/filter'
require 'github/ldap/domain'
require 'github/ldap/group'
require 'github/ldap/posix_group'
require 'github/ldap/virtual_group'
require 'github/ldap/virtual_attributes'
require 'github/ldap/instrumentation'
require 'github/ldap/member_search'
require 'github/ldap/forest_search'
require 'github/ldap/membership_validators'
module GitHub
class Ldap
include Instrumentation
extend Forwardable
# Internal: The capability required to use ActiveDirectory features.
# See: http://msdn.microsoft.com/en-us/library/cc223359.aspx.
ACTIVE_DIRECTORY_V51_OID = "1.2.840.113556.1.4.1670".freeze
# Utility method to get the last operation result with a human friendly message.
#
# Returns an OpenStruct with `code` and `message`.
# If `code` is 0, the operation succeeded and there is no message.
def_delegator :@connection, :get_operation_result, :last_operation_result
# Utility method to bind entries in the ldap server.
#
# It takes the same arguments than Net::LDAP::Connection#bind.
# Returns a Net::LDAP::Entry if the operation succeeded.
def_delegator :@connection, :bind
# Public - Opens a connection to the server and keeps it open for the
# duration of the block.
#
# Returns the return value of the block.
def_delegator :@connection, :open
attr_reader :uid, :search_domains, :virtual_attributes,
:membership_validator,
:member_search_strategy,
:instrumentation_service
# Build a new GitHub::Ldap instance
#
# ## Connection
#
# host: required string ldap server host address
# port: required string or number ldap server port
# encryption: optional string. `ssl` or `tls`. nil by default
# admin_user: optional string ldap administrator user dn for authentication
# admin_password: optional string ldap administrator user password
#
# ## Behavior
#
# uid: optional field name used to authenticate users. Defaults to `sAMAccountName` (what ActiveDirectory uses)
# virtual_attributes: optional. boolean true to use server's virtual attributes. Hash to specify custom mapping. Default false.
# recursive_group_search_fallback: optional boolean whether membership checks should recurse into nested groups when virtual attributes aren't enabled. Default false.
# posix_support: optional boolean `posixGroup` support. Default true.
# search_domains: optional array of string bases to search through
#
# ## Diagnostics
#
# instrumentation_service: optional ActiveSupport::Notifications compatible object
#
def initialize(options = {})
@uid = options[:uid] || "sAMAccountName"
@connection = Net::LDAP.new({
host: options[:host],
port: options[:port],
instrumentation_service: options[:instrumentation_service]
})
if options[:admin_user] && options[:admin_password]
@connection.authenticate(options[:admin_user], options[:admin_password])
end
if encryption = check_encryption(options[:encryption])
@connection.encryption(encryption)
end
configure_virtual_attributes(options[:virtual_attributes])
configure_entry_search_strategy(options[:use_forest_search])
# enable fallback recursive group search unless option is false
@recursive_group_search_fallback = (options[:recursive_group_search_fallback] != false)
# enable posixGroup support unless option is false
@posix_support = (options[:posix_support] != false)
# search_domains is a connection of bases to perform searches
# when a base is not explicitly provided.
@search_domains = Array(options[:search_domains])
# configure both the membership validator and the member search strategies
configure_search_strategy(options[:search_strategy])
# enables instrumenting queries
@instrumentation_service = options[:instrumentation_service]
end
# Public - Whether membership checks should recurse into nested groups when
# virtual attributes aren't enabled. The fallback search has poor
# performance characteristics in some cases, in which case this should be
# disabled by passing :recursive_group_search_fallback => false.
#
# Returns true or false.
def recursive_group_search_fallback?
@recursive_group_search_fallback
end
# Public - Whether membership checks should include posixGroup filter
# conditions on `memberUid`. Configurable since some LDAP servers don't
# handle unsupported attribute queries gracefully.
#
# Enable by passing :posix_support => true.
#
# Returns true, false, or nil (assumed false).
def posix_support_enabled?
@posix_support
end
# Public - Utility method to check if the connection with the server can be stablished.
# It tries to bind with the ldap auth default configuration.
#
# Returns an OpenStruct with `code` and `message`.
# If `code` is 0, the operation succeeded and there is no message.
def test_connection
@connection.bind
last_operation_result
end
# Public - Creates a new domain object to perform operations
#
# base_name: is the dn of the base root.
#
# Returns a new Domain object.
def domain(base_name)
Domain.new(self, base_name, @uid)
end
# Public - Creates a new group object to perform operations
#
# base_name: is the dn of the base root.
#
# Returns a new Group object.
# Returns nil if the dn is not in the server.
def group(base_name)
entry = domain(base_name).bind
return unless entry
load_group(entry)
end
# Public - Create a new group object based on a Net::LDAP::Entry.
#
# group_entry: is a Net::LDAP::Entry.
#
# Returns a Group, PosixGroup or VirtualGroup object.
def load_group(group_entry)
if @virtual_attributes.enabled?
VirtualGroup.new(self, group_entry)
elsif posix_support_enabled? && PosixGroup.valid?(group_entry)
PosixGroup.new(self, group_entry)
else
Group.new(self, group_entry)
end
end
# Public - Search entries in the ldap server.
#
# options: is a hash with the same options that Net::LDAP::Connection#search supports.
# block: is an optional block to pass to the search.
#
# Returns an Array of Net::LDAP::Entry.
def search(options, &block)
instrument "search.github_ldap", options.dup do |payload|
result =
if options[:base]
@entry_search_strategy.search(options, &block)
else
search_domains.each_with_object([]) do |base, result|
rs = @entry_search_strategy.search(options.merge(:base => base), &block)
result.concat Array(rs) unless rs == false
end
end
return [] if result == false
Array(result)
end
end
# Internal: Searches the host LDAP server's Root DSE for capabilities and
# extensions.
#
# Returns a Net::LDAP::Entry object.
def capabilities
@capabilities ||=
instrument "capabilities.github_ldap" do |payload|
begin
rs = @connection.search(
:ignore_server_caps => true,
:base => "",
:scope => Net::LDAP::SearchScope_BaseObject)
(rs and rs.first) || Net::LDAP::Entry.new
rescue Net::LDAP::LdapError => error
payload[:error] = error
# stubbed result
Net::LDAP::Entry.new
end
end
end
# Internal - Determine whether to use encryption or not.
#
# encryption: is the encryption method, either 'ssl', 'tls', 'simple_tls' or 'start_tls'.
#
# Returns the real encryption type.
def check_encryption(encryption)
return unless encryption
case encryption.downcase.to_sym
when :ssl, :simple_tls
:simple_tls
when :tls, :start_tls
:start_tls
end
end
# Internal - Configure virtual attributes for this server.
# If the option is `true`, we'll use the default virual attributes.
# If it's a Hash we'll map the attributes in the hash.
#
# attributes: is the option set when Ldap is initialized.
#
# Returns a VirtualAttributes.
def configure_virtual_attributes(attributes)
@virtual_attributes = if attributes == true
VirtualAttributes.new(true)
elsif attributes.is_a?(Hash)
VirtualAttributes.new(true, attributes)
else
VirtualAttributes.new(false)
end
end
# Internal: Configure the member search and membership validation strategies.
#
# TODO: Inline the logic in these two methods here.
#
# Returns nothing.
def configure_search_strategy(strategy = nil)
# configure which strategy should be used to validate user membership
configure_membership_validation_strategy(strategy)
# configure which strategy should be used for member search
configure_member_search_strategy(strategy)
end
# Internal: Configure the entry search strategy.
#
# If the user has configured GHE to use forest searches AND we have an active
# Directory instance that has the right capabilities, use a ForestSearch
# strategy. Otherwise, the entry search strategy is simple the existing LDAP
# connection object.
#
def configure_entry_search_strategy(use_forest_search)
@entry_search_strategy = if use_forest_search && active_directory_capability? && capabilities[:configurationnamingcontext].any?
@entry_search_strategy = GitHub::Ldap::ForestSearch.new(@connection, capabilities[:configurationnamingcontext].first)
else
@entry_search_strategy = @connection
end
end
# Internal: Configure the membership validation strategy.
#
# If no known strategy is provided, detects ActiveDirectory capabilities or
# falls back to the Recursive strategy by default.
#
# Returns the membership validator strategy Class.
def configure_membership_validation_strategy(strategy = nil)
@membership_validator =
case strategy.to_s
when "classic"
GitHub::Ldap::MembershipValidators::Classic
when "recursive"
GitHub::Ldap::MembershipValidators::Recursive
when "active_directory"
GitHub::Ldap::MembershipValidators::ActiveDirectory
else
# fallback to detection, defaulting to recursive strategy
if active_directory_capability?
GitHub::Ldap::MembershipValidators::ActiveDirectory
else
GitHub::Ldap::MembershipValidators::Recursive
end
end
end
# Internal: Configure the member search strategy.
#
#
# If no known strategy is provided, detects ActiveDirectory capabilities or
# falls back to the Recursive strategy by default.
#
# Returns the selected strategy Class.
def configure_member_search_strategy(strategy = nil)
@member_search_strategy =
case strategy.to_s
when "classic"
GitHub::Ldap::MemberSearch::Classic
when "recursive"
GitHub::Ldap::MemberSearch::Recursive
when "active_directory"
GitHub::Ldap::MemberSearch::ActiveDirectory
else
# fallback to detection, defaulting to recursive strategy
if active_directory_capability?
GitHub::Ldap::MemberSearch::ActiveDirectory
else
GitHub::Ldap::MemberSearch::Recursive
end
end
end
# Internal: Detect whether the LDAP host is an ActiveDirectory server.
#
# See: http://msdn.microsoft.com/en-us/library/cc223359.aspx.
#
# Returns true if the host is an ActiveDirectory server, false otherwise.
def active_directory_capability?
capabilities[:supportedcapabilities].include?(ACTIVE_DIRECTORY_V51_OID)
end
private :active_directory_capability?
end
end