ldap_lookup 0.1.7 → 2.0.0
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 +4 -4
- data/.env.example +25 -0
- data/.gitignore +5 -0
- data/.rspec +3 -0
- data/.rspec_status +46 -0
- data/Gemfile.lock +5 -3
- data/README.md +141 -15
- data/SETUP.md +117 -0
- data/config/initializers/ldap_lookup.rb.example +32 -0
- data/ldap_lookup.gemspec +4 -3
- data/ldaptest.rb +29 -6
- data/lib/ldap_lookup/version.rb +1 -1
- data/lib/ldap_lookup.rb +463 -118
- metadata +30 -14
- data/.github/workflows/codeql.yml +0 -76
- data/.github/workflows/gem-push.yml +0 -45
data/lib/ldap_lookup.rb
CHANGED
|
@@ -1,158 +1,503 @@
|
|
|
1
|
-
require_relative
|
|
1
|
+
require_relative 'helpers/configuration'
|
|
2
|
+
require 'net/ldap'
|
|
3
|
+
require 'openssl'
|
|
2
4
|
|
|
3
5
|
module LdapLookup
|
|
4
|
-
require "net/ldap"
|
|
5
|
-
|
|
6
6
|
extend Configuration
|
|
7
7
|
|
|
8
8
|
define_setting :host
|
|
9
|
-
define_setting :port,
|
|
9
|
+
define_setting :port, '389'
|
|
10
10
|
define_setting :base
|
|
11
11
|
define_setting :dept_attribute
|
|
12
12
|
define_setting :group_attribute
|
|
13
|
+
define_setting :username
|
|
14
|
+
define_setting :password
|
|
15
|
+
define_setting :bind_dn # Optional: custom bind DN (for service accounts). If not set, uses uid=username,ou=People,base
|
|
16
|
+
define_setting :encryption, :start_tls # :start_tls or :simple_tls (LDAPS)
|
|
17
|
+
define_setting :debug, false
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
## HELPER/UTILITY METHOD
|
|
19
|
-
## This method interprets the response/return code from an LDAP bind operation (bind, search, add, modify, rename,
|
|
20
|
-
## delete). This method isn't necessarily complete, but it's a good starting point for handling the response codes
|
|
21
|
-
## from an LDAP bind operation.
|
|
22
|
-
##
|
|
23
|
-
## Additional details for the get_operation_result method can be found here:
|
|
24
|
-
## http://net-ldap.rubyforge.org/Net/LDAP.html#method-i-get_operation_result
|
|
25
|
-
########################################################################################################################
|
|
26
|
-
def self.get_ldap_response(ldap)
|
|
27
|
-
msg = "Response Code: #{ldap.get_operation_result.code}, Message: #{ldap.get_operation_result.message}"
|
|
28
|
-
raise msg unless ldap.get_operation_result.code == 0
|
|
19
|
+
def self.debug_log(message)
|
|
20
|
+
return unless debug
|
|
21
|
+
|
|
22
|
+
puts "[LDAP DEBUG] #{message}"
|
|
29
23
|
end
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
25
|
+
def self.perform_search(ldap, base: nil, filter:, attributes: nil, label: nil, options: {})
|
|
26
|
+
search_base = base || ldap.base
|
|
27
|
+
filter_str = filter.respond_to?(:to_s) ? filter.to_s : filter.inspect
|
|
28
|
+
attrs_list = attributes ? Array(attributes).map(&:to_s) : ['*']
|
|
29
|
+
label_prefix = label ? "#{label} " : ""
|
|
30
|
+
|
|
31
|
+
debug_log("#{label_prefix}search base=#{search_base} filter=#{filter_str} attrs=#{attrs_list.join(',')}")
|
|
32
|
+
|
|
33
|
+
params = { base: search_base, filter: filter }
|
|
34
|
+
params[:attributes] = attributes if attributes
|
|
35
|
+
params.merge!(options) if options && !options.empty?
|
|
36
|
+
|
|
37
|
+
results = ldap.search(params) || []
|
|
38
|
+
entry_count = results ? results.size : 0
|
|
39
|
+
returned_attrs = []
|
|
40
|
+
if results && !results.empty?
|
|
41
|
+
returned_attrs = results.first.attribute_names.map(&:to_s).sort
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
debug_log("#{label_prefix}search results count=#{entry_count} returned_attrs=#{returned_attrs.join(',')}")
|
|
45
|
+
|
|
46
|
+
results
|
|
45
47
|
end
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
def self.operation_details(response)
|
|
50
|
+
details = {
|
|
51
|
+
code: response.code,
|
|
52
|
+
message: response.message
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if response.respond_to?(:error_message) && response.error_message && !response.error_message.empty?
|
|
56
|
+
details[:error_message] = response.error_message
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if response.respond_to?(:matched_dn) && response.matched_dn && !response.matched_dn.empty?
|
|
60
|
+
details[:matched_dn] = response.matched_dn
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if response.respond_to?(:referrals) && response.referrals && !response.referrals.empty?
|
|
64
|
+
details[:referrals] = response.referrals
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
details
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.get_ldap_response(ldap)
|
|
71
|
+
response = ldap.get_operation_result
|
|
72
|
+
unless response.code.zero?
|
|
73
|
+
error_msg = "Response Code: #{response.code}, Message: #{response.message}"
|
|
74
|
+
if response.respond_to?(:error_message) && response.error_message && !response.error_message.empty?
|
|
75
|
+
error_msg += ", Diagnostic: #{response.error_message}"
|
|
76
|
+
end
|
|
77
|
+
if response.respond_to?(:matched_dn) && response.matched_dn && !response.matched_dn.empty?
|
|
78
|
+
error_msg += ", Matched DN: #{response.matched_dn}"
|
|
79
|
+
end
|
|
80
|
+
# Provide more helpful error messages for common codes
|
|
81
|
+
case response.code
|
|
82
|
+
when 19
|
|
83
|
+
error_msg += " (Constraint Violation - may require administrative access)"
|
|
84
|
+
when 49
|
|
85
|
+
error_msg += " (Invalid Credentials - check username/password)"
|
|
86
|
+
when 50
|
|
87
|
+
error_msg += " (Insufficient Access Rights)"
|
|
88
|
+
when 81
|
|
89
|
+
error_msg += " (Server Unavailable)"
|
|
90
|
+
end
|
|
91
|
+
raise error_msg
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Diagnostic method to test LDAP connection and bind
|
|
96
|
+
def self.test_connection
|
|
97
|
+
username_present = username && !username.to_s.strip.empty?
|
|
98
|
+
password_present = password && !password.to_s.strip.empty?
|
|
99
|
+
auth_dn = if bind_dn
|
|
100
|
+
bind_dn
|
|
101
|
+
elsif username_present
|
|
102
|
+
"uid=#{username},ou=People,#{base}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
search_base = username_present ? "ou=People,#{base}" : base
|
|
106
|
+
search_filter = username_present ? "(uid=#{username})" : "(objectClass=*)"
|
|
107
|
+
|
|
108
|
+
result = {
|
|
109
|
+
bind_dn: auth_dn,
|
|
110
|
+
username: username,
|
|
111
|
+
host: host,
|
|
112
|
+
port: port,
|
|
113
|
+
encryption: encryption,
|
|
114
|
+
base: base,
|
|
115
|
+
auth_mode: (username_present && password_present) ? 'authenticated' : 'anonymous'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
begin
|
|
119
|
+
ldap = ldap_connection
|
|
120
|
+
|
|
121
|
+
bind_response = nil
|
|
122
|
+
bind_exception = nil
|
|
123
|
+
|
|
124
|
+
# Try an explicit bind for diagnostics only (can return Code 19 even if searches work)
|
|
125
|
+
begin
|
|
126
|
+
bind_success = ldap.bind
|
|
127
|
+
bind_response = ldap.get_operation_result
|
|
128
|
+
rescue => e
|
|
129
|
+
bind_success = false
|
|
130
|
+
bind_exception = { class: e.class.name, message: e.message }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Net::LDAP binds automatically when performing operations (search, etc.)
|
|
134
|
+
# Explicit bind may fail with Code 19 on STARTTLS, but actual operations work fine
|
|
135
|
+
# Test by performing an actual search operation instead of explicit bind
|
|
136
|
+
|
|
137
|
+
# Try a simple search - this will trigger automatic bind
|
|
138
|
+
search_result = perform_search(
|
|
139
|
+
ldap,
|
|
140
|
+
base: search_base,
|
|
141
|
+
filter: search_filter,
|
|
142
|
+
attributes: ['uid', 'mail', 'displayName', 'cn', 'givenName', 'sn'],
|
|
143
|
+
label: "diagnostic",
|
|
144
|
+
options: { size: 1 }
|
|
145
|
+
)
|
|
146
|
+
search_response = ldap.get_operation_result
|
|
147
|
+
returned_attributes = []
|
|
148
|
+
if search_result && !search_result.empty?
|
|
149
|
+
entry = search_result.first
|
|
150
|
+
returned_attributes = entry.attribute_names.map(&:to_s).sort
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
if search_response.code.zero? || (search_response.code == 4 && (search_result && !search_result.empty?))
|
|
154
|
+
# Success! Bind worked (automatically during search)
|
|
155
|
+
result.merge!(
|
|
156
|
+
success: true,
|
|
157
|
+
bind_successful: true,
|
|
158
|
+
bind_attempted: true,
|
|
159
|
+
bind_result: bind_success,
|
|
160
|
+
bind_details: bind_response ? operation_details(bind_response) : nil,
|
|
161
|
+
bind_exception: bind_exception,
|
|
162
|
+
bind_code: 0,
|
|
163
|
+
bind_message: "Bind successful (via automatic bind during search)",
|
|
164
|
+
search_code: search_response.code,
|
|
165
|
+
search_message: search_response.message,
|
|
166
|
+
search_details: operation_details(search_response),
|
|
167
|
+
search_base: search_base,
|
|
168
|
+
search_filter: search_filter,
|
|
169
|
+
search_entry_count: search_result ? search_result.size : 0,
|
|
170
|
+
search_returned_attributes: returned_attributes,
|
|
171
|
+
note: "Explicit bind may show Code 19, but operations work correctly"
|
|
172
|
+
)
|
|
58
173
|
else
|
|
59
|
-
|
|
174
|
+
# Search failed - check if it's a bind issue or search issue
|
|
175
|
+
result.merge!(
|
|
176
|
+
success: false,
|
|
177
|
+
bind_successful: false,
|
|
178
|
+
bind_attempted: true,
|
|
179
|
+
bind_result: bind_success,
|
|
180
|
+
bind_details: bind_response ? operation_details(bind_response) : nil,
|
|
181
|
+
bind_exception: bind_exception,
|
|
182
|
+
search_code: search_response.code,
|
|
183
|
+
search_message: search_response.message,
|
|
184
|
+
search_details: operation_details(search_response),
|
|
185
|
+
search_base: search_base,
|
|
186
|
+
search_filter: search_filter,
|
|
187
|
+
search_entry_count: search_result ? search_result.size : 0,
|
|
188
|
+
search_returned_attributes: returned_attributes,
|
|
189
|
+
error: "Search failed: Code #{search_response.code}, #{search_response.message}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
case search_response.code
|
|
193
|
+
when 19
|
|
194
|
+
result[:suggestion] = "Constraint Violation. Your account may not be enabled for LDAP access or may need administrative access for this operation."
|
|
195
|
+
when 49
|
|
196
|
+
result[:suggestion] = "Invalid Credentials. Check your username and password."
|
|
197
|
+
when 50
|
|
198
|
+
result[:suggestion] = "Insufficient Access Rights. Your account may need LDAP access enabled."
|
|
199
|
+
when 4
|
|
200
|
+
result[:suggestion] = "Size Limit Exceeded. Try a more specific search base or ensure filters are indexed."
|
|
201
|
+
end
|
|
60
202
|
end
|
|
61
|
-
|
|
203
|
+
|
|
204
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
205
|
+
# Certificate or SSL/TLS connection error
|
|
206
|
+
result.merge!(
|
|
207
|
+
success: false,
|
|
208
|
+
error: "SSL/TLS Error: #{e.message}",
|
|
209
|
+
exception: e.class.name,
|
|
210
|
+
suggestion: "Certificate verification failed. Most systems trust InCommon certificates. If needed, download USERTrust RSA Certification Authority root certificate from ITS: SSL Server Certificates"
|
|
211
|
+
)
|
|
212
|
+
rescue => e
|
|
213
|
+
result.merge!(
|
|
214
|
+
success: false,
|
|
215
|
+
error: e.message,
|
|
216
|
+
exception: e.class.name
|
|
217
|
+
)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
result
|
|
62
221
|
end
|
|
63
222
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# Build filter
|
|
70
|
-
search_filter = Net::LDAP::Filter.eq("uid", search_param)
|
|
71
|
-
# Execute search
|
|
72
|
-
ldap.search(filter: search_filter, attributes: result_attrs) { |item|
|
|
73
|
-
return dept_name = item.umichpostaladdressdata.first.split("}:{").first.split("=")[1] unless item.umichpostaladdressdata.first.nil?
|
|
223
|
+
def self.ldap_connection
|
|
224
|
+
connection_params = {
|
|
225
|
+
host: host,
|
|
226
|
+
port: port.to_i,
|
|
227
|
+
base: base
|
|
74
228
|
}
|
|
75
|
-
|
|
229
|
+
|
|
230
|
+
# Configure encryption - REQUIRED for authenticated binds per UM documentation
|
|
231
|
+
# UM requires secure connection: TLS on port 389 (STARTTLS) or SSL on port 636 (LDAPS)
|
|
232
|
+
# Most operating systems already trust InCommon certificates per UM documentation
|
|
233
|
+
tls_verify = ENV.fetch('LDAP_TLS_VERIFY', 'true').to_s.downcase != 'false'
|
|
234
|
+
tls_options = {
|
|
235
|
+
verify_mode: tls_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
|
236
|
+
}
|
|
237
|
+
ca_cert_path = ENV['LDAP_CA_CERT']
|
|
238
|
+
tls_options[:ca_file] = ca_cert_path if ca_cert_path && !ca_cert_path.to_s.strip.empty?
|
|
239
|
+
|
|
240
|
+
if encryption == :start_tls
|
|
241
|
+
connection_params[:encryption] = {
|
|
242
|
+
method: :start_tls,
|
|
243
|
+
tls_options: tls_options
|
|
244
|
+
}
|
|
245
|
+
elsif encryption == :simple_tls
|
|
246
|
+
connection_params[:encryption] = {
|
|
247
|
+
method: :simple_tls,
|
|
248
|
+
tls_options: tls_options
|
|
249
|
+
}
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Configure authenticated bind (if username/password provided)
|
|
253
|
+
# Note: "simple" bind method = authenticated bind with username/password (not anonymous)
|
|
254
|
+
auth_username = username.to_s.strip
|
|
255
|
+
auth_password = password.to_s
|
|
256
|
+
if !auth_username.empty? && !auth_password.empty?
|
|
257
|
+
# Use custom bind_dn if provided (for service accounts), otherwise build standard DN
|
|
258
|
+
auth_bind_dn = bind_dn || "uid=#{auth_username},ou=People,#{base}"
|
|
259
|
+
connection_params[:auth] = {
|
|
260
|
+
method: :simple, # Simple bind = authenticated bind with username/password
|
|
261
|
+
username: auth_bind_dn,
|
|
262
|
+
password: auth_password
|
|
263
|
+
}
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
ldap = Net::LDAP.new(connection_params)
|
|
267
|
+
|
|
268
|
+
# For STARTTLS, ensure TLS is started before returning connection
|
|
269
|
+
# Net::LDAP should handle this automatically, but let's be explicit
|
|
270
|
+
if encryption == :start_tls
|
|
271
|
+
begin
|
|
272
|
+
# The bind will trigger STARTTLS automatically, but we can verify connection works
|
|
273
|
+
# by attempting a bind (which will fail if TLS isn't established)
|
|
274
|
+
rescue => e
|
|
275
|
+
raise "Failed to establish TLS connection: #{e.message}"
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
ldap
|
|
76
280
|
end
|
|
77
281
|
|
|
78
|
-
|
|
79
|
-
def self.get_email(uniqname = nil)
|
|
282
|
+
def self.get_user_attribute(uniqname, attribute, default_value = nil)
|
|
80
283
|
ldap = ldap_connection
|
|
81
|
-
search_param = uniqname
|
|
82
|
-
result_attrs = [
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
284
|
+
search_param = uniqname
|
|
285
|
+
result_attrs = [attribute]
|
|
286
|
+
found_value = nil
|
|
287
|
+
|
|
288
|
+
search_filter = Net::LDAP::Filter.eq('uid', search_param)
|
|
289
|
+
|
|
290
|
+
perform_search(
|
|
291
|
+
ldap,
|
|
292
|
+
filter: search_filter,
|
|
293
|
+
attributes: result_attrs,
|
|
294
|
+
label: "get_user_attribute",
|
|
295
|
+
options: { size: 1 }
|
|
296
|
+
).each do |item|
|
|
297
|
+
value = item[attribute]&.first
|
|
298
|
+
if value
|
|
299
|
+
found_value = value
|
|
300
|
+
break
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Check response - Code 19 may occur even when data is found
|
|
305
|
+
response = ldap.get_operation_result
|
|
306
|
+
if (response.code == 19 || response.code == 4) && found_value.nil?
|
|
307
|
+
# Constraint violation and no data found - may need admin access
|
|
308
|
+
return default_value
|
|
309
|
+
elsif response.code != 0 && found_value.nil?
|
|
310
|
+
# Other error and no data found
|
|
311
|
+
raise "Response Code: #{response.code}, Message: #{response.message}"
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Return found value or default
|
|
315
|
+
found_value || default_value
|
|
90
316
|
end
|
|
91
317
|
|
|
92
|
-
|
|
93
|
-
# Check if the UID is a member of an LDAP group. This function returns TRUE
|
|
94
|
-
# if uid passed in is a member of group_name passed in. Otherwise it will
|
|
95
|
-
# return false.
|
|
96
|
-
def self.is_member_of_group?(uid = nil, group_name = nil)
|
|
318
|
+
def self.get_nested_attribute(uniqname, nested_attribute)
|
|
97
319
|
ldap = ldap_connection
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
320
|
+
search_param = uniqname
|
|
321
|
+
# Specify the full nested attribute path using dot notation
|
|
322
|
+
attr_name = nested_attribute.split('.').first
|
|
323
|
+
# Try using the configured attribute name if available, otherwise use the provided name
|
|
324
|
+
search_attr = dept_attribute || attr_name
|
|
325
|
+
result_attrs = [search_attr]
|
|
326
|
+
found_value = nil
|
|
327
|
+
|
|
328
|
+
search_filter = Net::LDAP::Filter.eq('uid', search_param)
|
|
329
|
+
|
|
330
|
+
perform_search(
|
|
331
|
+
ldap,
|
|
332
|
+
filter: search_filter,
|
|
333
|
+
attributes: result_attrs,
|
|
334
|
+
label: "get_nested_attribute",
|
|
335
|
+
options: { size: 1 }
|
|
336
|
+
).each do |item|
|
|
337
|
+
# Net::LDAP::Entry provides case-insensitive access, try the search attribute first
|
|
338
|
+
string1 = item[search_attr]&.first || item[attr_name]&.first
|
|
339
|
+
if string1
|
|
340
|
+
key_value_pairs = string1.split('}:{')
|
|
341
|
+
# Find the key-value pair for the nested attribute
|
|
342
|
+
target_pair = key_value_pairs.find { |pair| pair.include?("#{nested_attribute.split('.').last}=") }
|
|
343
|
+
# Extract the target value
|
|
344
|
+
if target_pair
|
|
345
|
+
target_pair_value = target_pair.split('=').last
|
|
346
|
+
if target_pair_value
|
|
347
|
+
found_value = target_pair_value
|
|
348
|
+
break
|
|
111
349
|
end
|
|
112
350
|
end
|
|
113
351
|
end
|
|
114
352
|
end
|
|
115
|
-
|
|
116
|
-
|
|
353
|
+
|
|
354
|
+
# Check response - Code 19 may occur even when data is found
|
|
355
|
+
response = ldap.get_operation_result
|
|
356
|
+
if (response.code == 19 || response.code == 4) && found_value.nil?
|
|
357
|
+
# Constraint violation and no data found - may need admin access
|
|
358
|
+
return nil
|
|
359
|
+
elsif response.code != 0 && found_value.nil?
|
|
360
|
+
# Other error and no data found
|
|
361
|
+
raise "Response Code: #{response.code}, Message: #{response.message}"
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
found_value
|
|
117
365
|
end
|
|
118
366
|
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
def self.get_email_distribution_list(group_name = nil)
|
|
367
|
+
# method to check if a uid exist in LDAP
|
|
368
|
+
def self.uid_exist?(uniqname)
|
|
122
369
|
ldap = ldap_connection
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
370
|
+
search_param = uniqname
|
|
371
|
+
found = false
|
|
372
|
+
|
|
373
|
+
search_filter = Net::LDAP::Filter.eq('uid', search_param)
|
|
374
|
+
|
|
375
|
+
perform_search(ldap, filter: search_filter, label: "uid_exist", options: { size: 1 }).each do |item|
|
|
376
|
+
if item['uid'].first == search_param
|
|
377
|
+
found = true
|
|
378
|
+
break
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Check response - Code 19 may occur even when user is found
|
|
383
|
+
response = ldap.get_operation_result
|
|
384
|
+
if (response.code == 19 || response.code == 4) && !found
|
|
385
|
+
# Constraint violation and user not found - may need admin access
|
|
386
|
+
return false
|
|
387
|
+
elsif response.code != 0 && !found
|
|
388
|
+
# Other error and user not found
|
|
389
|
+
raise "Response Code: #{response.code}, Message: #{response.message}"
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
found
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def self.get_simple_name(uniqname)
|
|
396
|
+
get_user_attribute(uniqname, 'displayname', 'not available')
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def self.get_email(uniqname)
|
|
400
|
+
get_user_attribute(uniqname, 'mail', nil)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def self.get_dept(uniqname)
|
|
404
|
+
dept = get_nested_attribute(uniqname, 'umichpostaladdressdata.addr1')
|
|
405
|
+
return dept if dept
|
|
406
|
+
|
|
407
|
+
# Fallback to raw attribute if nested parsing fails or attribute is restricted
|
|
408
|
+
raw_attr = dept_attribute || 'umichPostalAddressData'
|
|
409
|
+
get_user_attribute(uniqname, raw_attr, nil)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def self.is_member_of_group?(uid, group_name)
|
|
413
|
+
ldap = ldap_connection
|
|
414
|
+
search_param = group_name
|
|
415
|
+
result_attrs = ['member']
|
|
416
|
+
found = false
|
|
417
|
+
|
|
418
|
+
search_filter = Net::LDAP::Filter.join(
|
|
419
|
+
Net::LDAP::Filter.eq('cn', search_param),
|
|
420
|
+
Net::LDAP::Filter.eq('objectClass', 'group')
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
perform_search(ldap, filter: search_filter, attributes: result_attrs, label: "is_member_of_group").each do |item|
|
|
424
|
+
members = item['member']
|
|
425
|
+
if members && members.any? { |entry| entry.split(',').first.split('=')[1] == uid }
|
|
426
|
+
found = true
|
|
427
|
+
break
|
|
139
428
|
end
|
|
140
|
-
result_hash["members"] = individual_array.sort
|
|
141
429
|
end
|
|
142
|
-
|
|
143
|
-
|
|
430
|
+
|
|
431
|
+
# Check response - Code 19 may occur for group operations (requires admin access)
|
|
432
|
+
response = ldap.get_operation_result
|
|
433
|
+
if response.code == 19
|
|
434
|
+
# Constraint violation - group operations may require admin access
|
|
435
|
+
# Return false if not found, true if found (even with Code 19)
|
|
436
|
+
return found
|
|
437
|
+
elsif response.code != 0 && !found
|
|
438
|
+
# Other error and not found
|
|
439
|
+
raise "Response Code: #{response.code}, Message: #{response.message}"
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
found
|
|
144
443
|
end
|
|
145
444
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
445
|
+
def self.get_email_distribution_list(group_name)
|
|
446
|
+
ldap = ldap_connection
|
|
447
|
+
result_hash = {}
|
|
448
|
+
found_data = false
|
|
449
|
+
|
|
450
|
+
search_param = group_name
|
|
451
|
+
result_attrs = %w[cn umichGroupEmail member]
|
|
452
|
+
|
|
453
|
+
search_filter = Net::LDAP::Filter.join(
|
|
454
|
+
Net::LDAP::Filter.eq('cn', search_param),
|
|
455
|
+
Net::LDAP::Filter.eq('objectClass', 'group')
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
perform_search(ldap, filter: search_filter, attributes: result_attrs, label: "get_email_distribution_list").each do |item|
|
|
459
|
+
found_data = true
|
|
460
|
+
result_hash['group_name'] = item['cn']&.first
|
|
461
|
+
result_hash['group_email'] = item['umichGroupEmail']&.first
|
|
462
|
+
members = item['member']&.map { |individual| individual.split(',').first.split('=')[1] }
|
|
463
|
+
result_hash['members'] = members&.sort || []
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Check response - Code 19 may occur for group operations (requires admin access)
|
|
467
|
+
response = ldap.get_operation_result
|
|
468
|
+
if response.code == 19 && !found_data
|
|
469
|
+
# Constraint violation and no data found - group operations may require admin access
|
|
470
|
+
return {}
|
|
471
|
+
elsif response.code != 0 && !found_data
|
|
472
|
+
# Other error and no data found
|
|
473
|
+
raise "Response Code: #{response.code}, Message: #{response.message}"
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
result_hash
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def self.all_groups_for_user(uid)
|
|
149
480
|
ldap = ldap_connection
|
|
150
481
|
result_array = []
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
482
|
+
|
|
483
|
+
result_attrs = ['dn']
|
|
484
|
+
|
|
485
|
+
# Use configured base instead of hardcoded dc=umich,dc=edu
|
|
486
|
+
member_dn = "uid=#{uid},ou=People,#{base}"
|
|
487
|
+
perform_search(ldap, filter: "member=#{member_dn}", attributes: result_attrs, label: "all_groups_for_user").each do |item|
|
|
488
|
+
item.each { |key, value| result_array << value.first.split('=')[1].split(',')[0] }
|
|
154
489
|
end
|
|
155
|
-
|
|
156
|
-
|
|
490
|
+
|
|
491
|
+
# Check response - may raise Constraint Violation for regular users
|
|
492
|
+
response = ldap.get_operation_result
|
|
493
|
+
if response.code == 19 # Constraint Violation
|
|
494
|
+
# Regular authenticated users may not have permission to search groups by member
|
|
495
|
+
# Return empty array instead of raising error
|
|
496
|
+
return []
|
|
497
|
+
elsif response.code != 0
|
|
498
|
+
raise "Response Code: #{response.code}, Message: #{response.message}"
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
result_array.sort
|
|
157
502
|
end
|
|
158
503
|
end
|