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.
data/lib/ldap_lookup.rb CHANGED
@@ -1,158 +1,503 @@
1
- require_relative "helpers/configuration"
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, "389"
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
- # this was developed using guidence from this gist:
15
- # https://gist.githubusercontent.com/jeffjohnson9046/7012167/raw/86587b9637ddc2ece7a42df774980fa9c0aac9b3/ruby-ldap-sample.rb
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
- # SET UP LDAP CONNECTION
33
- # Setting up a connection to the LDAP server using .new() does not actually send any network traffic to the LDAP
34
- # server. When you call an operation on ldap (e.g. add or search), .bind is called implicitly. *That's* when the
35
- # connection is made to the LDAP server. This means that each operation called on the ldap object will create its own
36
- # network connection to the LDAP server.
37
- #######################################################################################################################
38
- def self.ldap_connection
39
- ldap = Net::LDAP.new host: host, # your LDAP host name or IP goes here,
40
- port: port, # your LDAP host port goes here,
41
- base: base, # the base of your AD tree goes here,
42
- auth: {
43
- :method => :anonymous,
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
- # GET THE DISPLAY NAME FOR A SINGLE USER
48
- def self.get_simple_name(uniqname = nil)
49
- ldap = ldap_connection
50
- search_param = uniqname # the AD account goes here
51
- result_attrs = ["displayName"] # Whatever you want to bring back in your result set goes here
52
- # Build filter
53
- search_filter = Net::LDAP::Filter.eq("uid", search_param)
54
- # Execute search
55
- result = ldap.search(filter: search_filter, attributes: result_attrs)
56
- if result.length != 0
57
- return result.first.displayname.first
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
- return "No such user"
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
- get_ldap_response(ldap)
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
- # GET THE PRIMARY DEPARTMENT FOR A SINGLE USER
65
- def self.get_dept(uniqname = nil)
66
- ldap = ldap_connection
67
- search_param = uniqname # the AD account goes here
68
- result_attrs = [dept_attribute] # Whatever you want to bring back in your result set goes here
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
- get_ldap_response(ldap)
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
- # GET THE E-MAIL ADDRESS FOR A SINGLE USER
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 # the AD account goes here
82
- result_attrs = ["mail"] # Whatever you want to bring back in your result set goes here
83
- # Build filter
84
- search_filter = Net::LDAP::Filter.eq("uid", search_param)
85
- # Execute search
86
- ldap.search(filter: search_filter, attributes: result_attrs) { |item|
87
- return item.mail.first
88
- }
89
- get_ldap_response(ldap)
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
- # GET THE MEMBERS OF AN E-MAIL DISTRIBUTION LIST
99
- search_param = group_name # the name of the distribution list you're looking for goes here
100
- result_attrs = ["member"]
101
- # Build filter
102
- search_filter = Net::LDAP::Filter.eq("cn", search_param)
103
- group_filter = Net::LDAP::Filter.eq("objectClass", "group")
104
- composite_filter = Net::LDAP::Filter.join(search_filter, group_filter)
105
- # Execute search, extracting the AD account name from each member of the distribution list
106
- ldap.search(filter: composite_filter, attributes: result_attrs) do |item|
107
- if item.attribute_names.include?(:member)
108
- item.member.each do |entry|
109
- if entry.split(",").first.split("=")[1] == uid
110
- return true
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
- return false
116
- get_ldap_response(ldap)
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
- # Get the Name email and members of an LDAP group as a hash
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
- result_hash = {}
124
- member_hash = {}
125
- # GET THE MEMBERS OF AN E-MAIL DISTRIBUTION LIST
126
- search_param = group_name # the name of the distribution list you're looking for goes here
127
- result_attrs = ["cn", group_attribute, "member"]
128
- # Build filter
129
- search_filter = Net::LDAP::Filter.eq("cn", search_param)
130
- group_filter = Net::LDAP::Filter.eq("objectClass", "group")
131
- composite_filter = Net::LDAP::Filter.join(search_filter, group_filter)
132
- # Execute search, extracting the AD account name from each member of the distribution list
133
- ldap.search(filter: composite_filter, attributes: result_attrs) do |item|
134
- result_hash["group_name"] = item.cn.first
135
- result_hash["group_email"] = item.umichGroupEmail.first
136
- individual_array = []
137
- item.member.each do |individual|
138
- individual_array.push(individual.split(",").first.split("=")[1])
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
- return result_hash
143
- get_ldap_response(ldap)
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
- # Get the groups a user is a member of
148
- def self.all_groups_for_user(uid = nil)
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
- result_attrs = ["dn"]
152
- ldap.search(filter: "member=uid=#{uid},ou=People,dc=umich,dc=edu", attributes: result_attrs) do |item|
153
- item.each { |key, value| result_array << value.first.split("=")[1].split(",")[0] }
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
- return result_array.sort
156
- get_ldap_response(ldap)
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