ldap_lookup 2.0.1 → 2.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 393d3bdcafb4cadf5648c6916122ebfb7b0c6fefe79c5d50b6c6c57f6d6997cc
4
- data.tar.gz: de8be14be88d2d4e3102971cf62d4a316cdc6a0aaa7e6268aee62659723e5ca2
3
+ metadata.gz: 8975eb645723483f8856a1a6d8db9cbc5a161c18a5508c81489999e86d8a2ccc
4
+ data.tar.gz: 0ac5979c0acde4809c80f5e3e38a16c22bdc20c07d631b5068f56c0521f90b63
5
5
  SHA512:
6
- metadata.gz: 7dd298be615e445056d806602b27cb05e01a0783556fe09c32d1c5599ff877a18c286d1d2e845f1f5ec30c4b08745668ec5fd8f2717e4393cc7c45dd7e33254d
7
- data.tar.gz: 6bb46112525a64c3c63fd1cb2d356e151ff48154515684040d5c1519464619681942f36a45ba275d0749abcd5b04afd81bc28ddb9a72d9c403eba5dc093d0970
6
+ metadata.gz: 95be63f67dab3e14af8aaa565caac8377415b9123e7cd575098b449ec485345c9913b7f0f2c5c1bc3a4548307c9aa34116e939149e900dfa8ab93061938a14c7
7
+ data.tar.gz: 6e0f98715d5f88726a5d5d3984d90de5af12846687edd62a84bacc2094c75384fadef71de26060b932f8303b42cc0897ef5f8a62d2ec2c20a2f96b16cf317e55
data/.env.example CHANGED
@@ -2,13 +2,17 @@
2
2
  # Copy this file to .env and fill in your actual credentials
3
3
  # The .env file is gitignored and will not be committed
4
4
 
5
- # Your UM uniqname (username)
6
- LDAP_USERNAME=your_uniqname
5
+ # Service account bind DN (recommended for UM LDAP)
6
+ # Example: cn=LSA-TS-McDirApp004,ou=Applications,o=services
7
+ LDAP_BIND_DN=your_service_account_bind_dn
7
8
 
8
- # Your UM password
9
+ # Service account password (required with LDAP_BIND_DN)
9
10
  LDAP_PASSWORD=your_password
10
11
 
11
- # Leave LDAP_USERNAME and LDAP_PASSWORD unset for anonymous binds
12
+ # If you are using a personal uniqname (non-service account), set:
13
+ # LDAP_USERNAME=your_uniqname
14
+
15
+ # UM LDAP does not allow anonymous binds. Leave these unset only if your LDAP server allows them.
12
16
 
13
17
  # Optional: Override default LDAP settings
14
18
  # LDAP_HOST=ldap.umich.edu
@@ -17,7 +21,15 @@ LDAP_PASSWORD=your_password
17
21
  # LDAP_ENCRYPTION=start_tls # Use 'start_tls' for port 389 or 'simple_tls' for port 636 (LDAPS)
18
22
  # LDAP_DEPT_ATTRIBUTE=umichPostalAddressData
19
23
  # LDAP_GROUP_ATTRIBUTE=umichGroupEmail
20
- # LDAP_TLS_VERIFY=true # Set to false to disable cert verification (dev only)
24
+ # LDAP_DIAGNOSTIC_UID=your_uniqname
25
+ # LDAP_TEST_UID=your_uniqname
26
+ # LDAP_TEST_DISPLAY_NAME=Your Name
27
+ # LDAP_TEST_EMAIL=your_uniqname@umich.edu
28
+ # LDAP_TEST_DEPT=Your Department Name
29
+ # LDAP_TEST_GROUP=example-group-name
30
+ # LDAP_USER_BASE=ou=people,dc=umich,dc=edu
31
+ # LDAP_GROUP_BASE=ou=user groups,ou=groups,dc=umich,dc=edu
32
+ # LDAP_TLS_VERIFY=true # Set to false to disable cert verification (local testing only)
21
33
  # LDAP_CA_CERT=/path/to/ca-bundle.pem
22
34
  #
23
35
  # If STARTTLS (port 389) doesn't work, try LDAPS:
data/.gitignore CHANGED
@@ -5,6 +5,7 @@
5
5
  /doc/
6
6
  /pkg/
7
7
  /spec/reports/
8
+ /.rspec_status
8
9
  /tmp/
9
10
  *.gem
10
11
 
data/.rspec_status CHANGED
@@ -1,46 +1,46 @@
1
1
  example_id | status | run_time |
2
2
  ------------------------------------- | ------ | --------------- |
3
- ./spec/configuration_spec.rb[1:1:1:1] | passed | 0.00021 seconds |
4
- ./spec/configuration_spec.rb[1:1:1:2] | passed | 0.00004 seconds |
3
+ ./spec/configuration_spec.rb[1:1:1:1] | passed | 0.00029 seconds |
4
+ ./spec/configuration_spec.rb[1:1:1:2] | passed | 0.00005 seconds |
5
5
  ./spec/configuration_spec.rb[1:1:1:3] | passed | 0.00004 seconds |
6
- ./spec/configuration_spec.rb[1:1:2:1] | passed | 0.00149 seconds |
7
- ./spec/configuration_spec.rb[1:1:2:2] | passed | 0.00004 seconds |
8
- ./spec/configuration_spec.rb[1:1:3:1] | passed | 0.00004 seconds |
9
- ./spec/configuration_spec.rb[1:1:3:2] | passed | 0.00005 seconds |
10
- ./spec/configuration_spec.rb[1:2:1] | passed | 0.00002 seconds |
6
+ ./spec/configuration_spec.rb[1:1:2:1] | passed | 0.00049 seconds |
7
+ ./spec/configuration_spec.rb[1:1:2:2] | passed | 0.00005 seconds |
8
+ ./spec/configuration_spec.rb[1:1:3:1] | passed | 0.00005 seconds |
9
+ ./spec/configuration_spec.rb[1:1:3:2] | passed | 0.00004 seconds |
10
+ ./spec/configuration_spec.rb[1:2:1] | passed | 0.00003 seconds |
11
11
  ./spec/configuration_spec.rb[1:2:2] | passed | 0.00003 seconds |
12
12
  ./spec/configuration_spec.rb[1:3:1] | passed | 0.00003 seconds |
13
- ./spec/configuration_spec.rb[1:3:2] | passed | 0.00003 seconds |
14
- ./spec/configuration_spec.rb[1:3:3] | passed | 0.00003 seconds |
15
- ./spec/ldap_lookup_spec.rb[1:1:1:1] | passed | 0.63596 seconds |
16
- ./spec/ldap_lookup_spec.rb[1:1:2:1] | passed | 0.61562 seconds |
17
- ./spec/ldap_lookup_spec.rb[1:1:3:1] | passed | 0.60601 seconds |
18
- ./spec/ldap_lookup_spec.rb[1:2:1:1] | passed | 0.64499 seconds |
19
- ./spec/ldap_lookup_spec.rb[1:2:2:1] | passed | 0.6525 seconds |
20
- ./spec/ldap_lookup_spec.rb[1:3:1:1] | passed | 0.66369 seconds |
21
- ./spec/ldap_lookup_spec.rb[1:3:2:1] | passed | 1.29 seconds |
22
- ./spec/ldap_lookup_spec.rb[1:4:1:1] | passed | 0.64139 seconds |
23
- ./spec/ldap_lookup_spec.rb[1:4:2:1] | passed | 0.64128 seconds |
24
- ./spec/ldap_lookup_spec.rb[1:5:1:1] | passed | 1.69 seconds |
25
- ./spec/ldap_lookup_spec.rb[1:5:2:1] | passed | 0.75576 seconds |
26
- ./spec/ldap_lookup_spec.rb[1:5:3:1] | passed | 0.70272 seconds |
27
- ./spec/ldap_lookup_spec.rb[1:5:4:1] | passed | 0.74101 seconds |
28
- ./spec/ldap_lookup_spec.rb[1:6:1:1] | passed | 0.78507 seconds |
29
- ./spec/ldap_lookup_spec.rb[1:6:1:2] | passed | 0.77477 seconds |
30
- ./spec/ldap_lookup_spec.rb[1:6:1:3] | passed | 0.74474 seconds |
31
- ./spec/ldap_lookup_spec.rb[1:6:2:1] | passed | 0.70436 seconds |
32
- ./spec/ldap_lookup_spec.rb[1:7:1:1] | passed | 13.94 seconds |
33
- ./spec/ldap_lookup_spec.rb[1:7:1:2] | passed | 21.97 seconds |
34
- ./spec/ldap_lookup_spec.rb[1:7:1:3] | passed | 27.87 seconds |
35
- ./spec/ldap_lookup_spec.rb[1:7:2:1] | passed | 7.88 seconds |
36
- ./spec/ldap_lookup_spec.rb[1:8:1:1] | passed | 0.0026 seconds |
37
- ./spec/ldap_lookup_spec.rb[1:8:1:2] | passed | 0.00012 seconds |
38
- ./spec/ldap_lookup_spec.rb[1:8:1:3] | passed | 0.00009 seconds |
39
- ./spec/ldap_lookup_spec.rb[1:8:2:1] | passed | 0.0001 seconds |
40
- ./spec/ldap_lookup_spec.rb[1:8:3:1] | passed | 0.0001 seconds |
41
- ./spec/ldap_lookup_spec.rb[1:9:1:1] | passed | 0.0001 seconds |
42
- ./spec/ldap_lookup_spec.rb[1:9:2:1] | passed | 0.00009 seconds |
43
- ./spec/ldap_lookup_spec.rb[1:10:1:1] | passed | 0.525 seconds |
44
- ./spec/ldap_lookup_spec.rb[1:10:2:1] | passed | 0.46772 seconds |
45
- ./spec/ldap_lookup_spec.rb[1:11:1:1] | failed | 0.54755 seconds |
46
- ./spec/ldap_lookup_spec.rb[1:11:2:1] | passed | 0.47023 seconds |
13
+ ./spec/configuration_spec.rb[1:3:2] | passed | 0.00005 seconds |
14
+ ./spec/configuration_spec.rb[1:3:3] | passed | 0.00004 seconds |
15
+ ./spec/ldap_lookup_spec.rb[1:1:1:1] | passed | 1.75 seconds |
16
+ ./spec/ldap_lookup_spec.rb[1:1:2:1] | passed | 0.51135 seconds |
17
+ ./spec/ldap_lookup_spec.rb[1:1:3:1] | passed | 0.0003 seconds |
18
+ ./spec/ldap_lookup_spec.rb[1:2:1:1] | passed | 0.47541 seconds |
19
+ ./spec/ldap_lookup_spec.rb[1:2:2:1] | passed | 0.4878 seconds |
20
+ ./spec/ldap_lookup_spec.rb[1:3:1:1] | passed | 0.52654 seconds |
21
+ ./spec/ldap_lookup_spec.rb[1:3:2:1] | passed | 0.96146 seconds |
22
+ ./spec/ldap_lookup_spec.rb[1:4:1:1] | passed | 0.51242 seconds |
23
+ ./spec/ldap_lookup_spec.rb[1:4:2:1] | passed | 0.49175 seconds |
24
+ ./spec/ldap_lookup_spec.rb[1:5:1:1] | passed | 0.47786 seconds |
25
+ ./spec/ldap_lookup_spec.rb[1:5:2:1] | passed | 0.52746 seconds |
26
+ ./spec/ldap_lookup_spec.rb[1:5:3:1] | passed | 0.50551 seconds |
27
+ ./spec/ldap_lookup_spec.rb[1:5:4:1] | passed | 0.51433 seconds |
28
+ ./spec/ldap_lookup_spec.rb[1:6:1:1] | passed | 0.54583 seconds |
29
+ ./spec/ldap_lookup_spec.rb[1:6:1:2] | passed | 0.4901 seconds |
30
+ ./spec/ldap_lookup_spec.rb[1:6:1:3] | passed | 0.46969 seconds |
31
+ ./spec/ldap_lookup_spec.rb[1:6:2:1] | passed | 0.49174 seconds |
32
+ ./spec/ldap_lookup_spec.rb[1:7:1:1] | passed | 9.39 seconds |
33
+ ./spec/ldap_lookup_spec.rb[1:7:1:2] | passed | 8.56 seconds |
34
+ ./spec/ldap_lookup_spec.rb[1:7:1:3] | passed | 9.43 seconds |
35
+ ./spec/ldap_lookup_spec.rb[1:7:2:1] | passed | 4.31 seconds |
36
+ ./spec/ldap_lookup_spec.rb[1:8:1:1] | passed | 0.00299 seconds |
37
+ ./spec/ldap_lookup_spec.rb[1:8:1:2] | passed | 0.00019 seconds |
38
+ ./spec/ldap_lookup_spec.rb[1:8:1:3] | passed | 0.00118 seconds |
39
+ ./spec/ldap_lookup_spec.rb[1:8:2:1] | passed | 0.00015 seconds |
40
+ ./spec/ldap_lookup_spec.rb[1:8:3:1] | passed | 0.00016 seconds |
41
+ ./spec/ldap_lookup_spec.rb[1:9:1:1] | passed | 0.00008 seconds |
42
+ ./spec/ldap_lookup_spec.rb[1:9:2:1] | passed | 0.00007 seconds |
43
+ ./spec/ldap_lookup_spec.rb[1:10:1:1] | passed | 0.4988 seconds |
44
+ ./spec/ldap_lookup_spec.rb[1:10:2:1] | passed | 0.50763 seconds |
45
+ ./spec/ldap_lookup_spec.rb[1:11:1:1] | passed | 0.55333 seconds |
46
+ ./spec/ldap_lookup_spec.rb[1:11:2:1] | passed | 0.55887 seconds |
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ldap_lookup (0.1.8)
4
+ ldap_lookup (2.0.1)
5
5
  net-ldap (~> 0.18.0)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,64 +1,74 @@
1
1
  # LdapLookup for Ruby [![Gem Version](https://badge.fury.io/rb/ldap_lookup.svg)](https://badge.fury.io/rb/ldap_lookup)
2
2
 
3
- ### Description
4
- This module is to be used for authenticated or anonymous lookup of user attributes in the MCommunity service provided at the University of Michigan. It supports authenticated LDAP binds with encryption as per UM IT Security requirements (effective Jan 20, 2026). It can be easily modified to use other LDAP server configurations.
3
+ ## Overview
5
4
 
6
- ---
5
+ LdapLookup provides authenticated or anonymous lookups of user attributes in the University of Michigan MCommunity LDAP service. It supports encrypted binds per UM IT Security (effective Jan 28, 2026) and can be adapted for other LDAP servers.
7
6
 
8
- ### Try it out
7
+ ### UM LDAP Requirements (as of Jan 28, 2026)
8
+
9
+ * **Authenticated binds only** - UM LDAP does not allow anonymous binds.
10
+ * Username and password are required for UM LDAP.
11
+ * Encrypted connections (STARTTLS or LDAPS) are mandatory.
12
+ * The gem uses LDAP "simple bind" authentication (authenticated with username/password).
13
+
14
+ The gem can also perform **anonymous binds** for LDAP servers that allow them. To use anonymous binds, leave `LDAP_USERNAME` and `LDAP_PASSWORD` unset.
15
+
16
+ ## Quick Start (Local Test Runner)
17
+
18
+ ### Requirements
9
19
 
10
- Requirements:
11
20
  * Ruby at least 2.0.0
12
- * Gem 'net-ldap' ~> '0.18.0'
13
- > *The Net::LDAP (aka net-ldap) gem before 0.16.0 for Ruby has a Missing SSL Certificate Validation.*
21
+ * Gem `net-ldap` ~> `0.18.0`
22
+
23
+ > The Net::LDAP (aka net-ldap) gem before 0.16.0 for Ruby has a missing SSL certificate validation.
24
+
25
+ 1. Clone the repo.
26
+ 2. Copy the env template: `cp .env.example .env`.
27
+ 3. Load the env vars into your shell:
14
28
 
15
- To try the module out:
16
- 1. Clone the repo
17
- 2. Copy the env template and set credentials: `cp .env.example .env`
18
- 3. Load the env vars into your shell (example):
19
29
  ```bash
20
30
  set -a
21
31
  source .env
22
32
  set +a
23
33
  ```
24
- 4. Edit the configurations by opening ldaptest.rb and set the *CONFIGURATION BLOCK* to your environment (it reads from the `.env` values you just loaded).
25
- <pre>
26
- LdapLookup.configuration do |config|
27
- config.host = ENV['LDAP_HOST'] || "ldap.umich.edu"
28
- config.port = ENV['LDAP_PORT'] || "389"
29
- config.base = ENV['LDAP_BASE'] || "dc=umich,dc=edu"
30
- # Leave username/password unset for anonymous binds
31
- config.username = ENV['LDAP_USERNAME']
32
- config.password = ENV['LDAP_PASSWORD']
33
- # Read encryption from ENV, default to start_tls
34
- encryption_str = ENV['LDAP_ENCRYPTION'] || 'start_tls'
35
- config.encryption = encryption_str.to_sym
36
- config.dept_attribute = ENV['LDAP_DEPT_ATTRIBUTE'] || "umichPostalAddressData"
37
- config.group_attribute = ENV['LDAP_GROUP_ATTRIBUTE'] || "umichGroupEmail"
38
- # Enable LDAP debug logging in this test runner
39
- debug_str = ENV['LDAP_DEBUG']
40
- config.debug = debug_str ? debug_str.to_s.downcase == 'true' : true
41
- end
42
- </pre>
43
34
 
44
- **Important:** As of January 20, 2026, UM LDAP requires:
45
- - **Authenticated binds only** - Anonymous (unauthenticated) binds are not supported by UM LDAP
46
- - Username and password are required for UM LDAP connections
47
- - Encrypted connections (STARTTLS or LDAPS) are mandatory
48
- - The gem uses LDAP "simple bind" authentication (authenticated with username/password)
49
-
50
- The gem can also perform **anonymous binds** for LDAP servers that allow them. To use anonymous binds, leave `LDAP_USERNAME` and `LDAP_PASSWORD` unset.
35
+ 4. Edit the configuration in `ldaptest.rb` (reads from `.env`):
36
+
37
+ ```ruby
38
+ LdapLookup.configuration do |config|
39
+ config.host = ENV['LDAP_HOST'] || "ldap.umich.edu"
40
+ config.port = ENV['LDAP_PORT'] || "389"
41
+ config.base = ENV['LDAP_BASE'] || "dc=umich,dc=edu"
42
+ # Leave username/password unset for anonymous binds
43
+ config.username = ENV['LDAP_USERNAME']
44
+ config.password = ENV['LDAP_PASSWORD']
45
+ # Service account bind DN (preferred for UM LDAP)
46
+ config.bind_dn = ENV['LDAP_BIND_DN']
47
+ # Read encryption from ENV, default to start_tls
48
+ encryption_str = ENV['LDAP_ENCRYPTION'] || 'start_tls'
49
+ config.encryption = encryption_str.to_sym
50
+ config.dept_attribute = ENV['LDAP_DEPT_ATTRIBUTE'] || "umichPostalAddressData"
51
+ config.group_attribute = ENV['LDAP_GROUP_ATTRIBUTE'] || "umichGroupEmail"
52
+ # Optional diagnostic UID (used by LdapLookup.test_connection)
53
+ config.diagnostic_uid = ENV['LDAP_DIAGNOSTIC_UID'] if ENV['LDAP_DIAGNOSTIC_UID']
54
+ # Optional search bases for UM LDAP
55
+ config.user_base = ENV['LDAP_USER_BASE'] if ENV['LDAP_USER_BASE']
56
+ config.group_base = ENV['LDAP_GROUP_BASE'] if ENV['LDAP_GROUP_BASE']
57
+ # Enable LDAP debug logging in this test runner
58
+ debug_str = ENV['LDAP_DEBUG']
59
+ config.debug = debug_str ? debug_str.to_s.downcase == 'true' : false
60
+ end
61
+ ```
51
62
 
52
- 5. run the ldaptest.rb script
53
- ```ruby
54
- ruby ./ldaptest.rb
55
- ```
63
+ 5. Run the test script:
56
64
 
57
- ---
65
+ ```bash
66
+ ruby ./ldaptest.rb
67
+ ```
58
68
 
59
- ### Installation
69
+ ## Installation
60
70
 
61
- #### Step 1: Add to Gemfile
71
+ ### Step 1: Add to Gemfile
62
72
 
63
73
  Add this line to your application's Gemfile:
64
74
 
@@ -72,15 +82,17 @@ Then run:
72
82
  bundle install
73
83
  ```
74
84
 
75
- #### Step 2: Get LDAP Credentials
85
+ ### Step 2: Get LDAP Credentials
76
86
 
77
87
  **For Production Applications (Recommended):**
78
- Request a **service account** from your IT department. Service accounts are designed for automated applications and don't require password changes.
88
+
89
+ Request a **service account** from your IT department. Service accounts are designed for automated applications and do not require password changes.
79
90
 
80
91
  **For Development/Testing:**
92
+
81
93
  You can use your personal UM uniqname and password temporarily, but switch to a service account for production.
82
94
 
83
- #### Step 3: Configure the Gem
95
+ ### Step 3: Configure the Gem
84
96
 
85
97
  **For Rails Applications:**
86
98
 
@@ -99,7 +111,8 @@ LdapLookup.configuration do |config|
99
111
  config.password = ENV['LDAP_PASSWORD']
100
112
 
101
113
  # If using a service account with custom bind DN, uncomment and set:
102
- # config.bind_dn = 'cn=service-account,ou=Service Accounts,dc=umich,dc=edu'
114
+ # config.bind_dn = ENV['LDAP_BIND_DN']
115
+ # Note: LDAP_BIND_DN replaces LDAP_USERNAME (LDAP_USERNAME can be unset), not LDAP_PASSWORD.
103
116
 
104
117
  # Encryption - REQUIRED (defaults to STARTTLS)
105
118
  config.encryption = ENV.fetch('LDAP_ENCRYPTION', 'start_tls').to_sym
@@ -110,6 +123,13 @@ LdapLookup.configuration do |config|
110
123
  # Optional: Attribute Configuration
111
124
  config.dept_attribute = ENV.fetch('LDAP_DEPT_ATTRIBUTE', 'umichPostalAddressData')
112
125
  config.group_attribute = ENV.fetch('LDAP_GROUP_ATTRIBUTE', 'umichGroupEmail')
126
+
127
+ # Optional: Logger for debug output (responds to debug/info or call)
128
+ # config.logger = Rails.logger
129
+
130
+ # Optional: Separate search bases for users and groups (UM service accounts)
131
+ # config.user_base = ENV.fetch('LDAP_USER_BASE', 'ou=people,dc=umich,dc=edu')
132
+ # config.group_base = ENV.fetch('LDAP_GROUP_BASE', 'ou=user groups,ou=groups,dc=umich,dc=edu')
113
133
  end
114
134
  ```
115
135
 
@@ -119,6 +139,7 @@ Configure in your application startup:
119
139
 
120
140
  ```ruby
121
141
  require 'ldap_lookup'
142
+ require 'logger'
122
143
 
123
144
  LdapLookup.configuration do |config|
124
145
  config.host = 'ldap.umich.edu'
@@ -126,17 +147,31 @@ LdapLookup.configuration do |config|
126
147
  config.username = ENV['LDAP_USERNAME']
127
148
  config.password = ENV['LDAP_PASSWORD']
128
149
  config.encryption = :start_tls
150
+ # Optional: Logger for debug output
151
+ # config.logger = Logger.new($stdout)
129
152
  end
130
153
  ```
131
154
 
132
- #### Step 4: Set Environment Variables
155
+ ### Logger and Debug Output
156
+
157
+ * Debug logging is controlled by `config.debug` (default is false).
158
+ * If `config.logger` is set, it is used in this order:
159
+ * `logger.debug(message)` if available.
160
+ * `logger.info(message)` if `debug` is not available.
161
+ * `logger.call(message)` if neither `debug` nor `info` are available.
162
+ * If no logger is configured, debug output goes to STDOUT.
163
+ * Security note: debug output can include identifiers (uids, group names, search filters). Avoid sharing logs publicly.
133
164
 
134
- **Never hardcode credentials in your code!** Use environment variables (Hatchbox, Heroku, etc.).
165
+ ## Environment Variables
166
+
167
+ **Never hardcode credentials in your code.** Use environment variables (Hatchbox, Heroku, etc.).
168
+
169
+ ### Development with `.env.example` (Recommended)
135
170
 
136
- **Development with `.env.example` (recommended):**
137
171
  1. Copy the template: `cp .env.example .env`
138
172
  2. Update the values in `.env` for your environment.
139
173
  3. Load the variables into your shell (example):
174
+
140
175
  ```bash
141
176
  set -a
142
177
  source .env
@@ -144,118 +179,175 @@ end
144
179
  ```
145
180
 
146
181
  **Typical `.env` values:**
182
+
147
183
  ```bash
148
184
  LDAP_USERNAME=your_service_account_uniqname
149
185
  LDAP_PASSWORD=your_service_account_password
186
+ LDAP_BIND_DN=cn=service-account,ou=Applications,o=services
150
187
  ```
151
188
 
152
- **Optional settings (override defaults as needed):**
189
+ ### Production (Hatchbox)
190
+
191
+ Hatchbox uses environment variables per app or per deployment. Configure these in
192
+ **Hatchbox > App > Environment Variables** (or your deployment pipeline), then redeploy or restart.
193
+
194
+ **Minimum required for authenticated UM LDAP:**
195
+
153
196
  ```bash
197
+ LDAP_USERNAME=service_account_uniqname
198
+ LDAP_PASSWORD=service_account_password
199
+ LDAP_ENCRYPTION=start_tls
154
200
  LDAP_HOST=ldap.umich.edu
155
201
  LDAP_PORT=389
156
202
  LDAP_BASE=dc=umich,dc=edu
157
- LDAP_ENCRYPTION=start_tls
203
+ ```
204
+
205
+ **Recommended for service accounts with a custom bind DN:**
206
+
207
+ ```bash
208
+ LDAP_BIND_DN=cn=service-account,ou=Applications,o=services
209
+ ```
210
+
211
+ **Optional tuning:**
212
+
213
+ ```bash
158
214
  LDAP_TLS_VERIFY=true
159
215
  LDAP_CA_CERT=/path/to/ca-bundle.pem
216
+ LDAP_USER_BASE=ou=people,dc=umich,dc=edu
217
+ LDAP_GROUP_BASE=ou=user groups,ou=groups,dc=umich,dc=edu
160
218
  LDAP_DEPT_ATTRIBUTE=umichPostalAddressData
161
219
  LDAP_GROUP_ATTRIBUTE=umichGroupEmail
220
+ LDAP_DIAGNOSTIC_UID=your_uniqname
162
221
  ```
163
222
 
164
- **Alternative: export in your shell**
223
+ ### Alternative: Export in Your Shell
224
+
165
225
  ```bash
166
226
  export LDAP_USERNAME=your_service_account_uniqname
167
227
  export LDAP_PASSWORD=your_service_account_password
168
228
  ```
169
229
 
170
- **For Production:**
171
- - Use your platform's secrets management (Rails credentials, AWS Secrets Manager, etc.)
172
- - Never commit credentials to version control
173
- - Use service accounts, not personal accounts
174
-
175
- #### Service Account Bind DN
230
+ ### Service Account Bind DN
176
231
 
177
232
  If your service account uses a non-standard bind DN format, you can specify it:
178
233
 
179
234
  ```ruby
180
- config.bind_dn = 'cn=my-service-account,ou=Service Accounts,dc=umich,dc=edu'
235
+ config.bind_dn = 'cn=my-service-account,ou=Applications,o=services'
181
236
  ```
182
237
 
183
- If `bind_dn` is not set, it defaults to: `uid=username,ou=People,base`
238
+ If `bind_dn` is not set, it defaults to: `uid=username,ou=People,base`.
184
239
 
185
- ---
240
+ ## Methods Available
186
241
 
187
- ### Methods available
242
+ **uid_exist?** returns true if uid is in LDAP.
188
243
 
189
- __uid_exist?:__ returns true if uid is in LDAP
190
- ```
244
+ ```ruby
191
245
  LdapLookup.uid_exist?(uniqname)
192
246
  response: true or false (boolean)
193
247
  ```
194
- __get_simple_name:__ returns the Display Name
195
- ```
248
+
249
+ **get_simple_name** returns the display name or `"not available"`.
250
+
251
+ ```ruby
196
252
  LdapLookup.get_simple_name(uniqname = nil)
197
- response: name or "No #{attribute} found for #{uniqname}"
198
- ```
199
- __get_dept:__ returns the users Department_name
253
+ response: name or "not available"
200
254
  ```
255
+
256
+ **get_dept** returns the user's department name or `nil`.
257
+
258
+ ```ruby
201
259
  LdapLookup.get_dept(uniqname = nil)
202
- response: dept name or "No #{nested_attribute} found for #{uniqname}"
203
- ```
204
- __get_email:__ returns the users email address
260
+ response: dept name or nil
205
261
  ```
262
+
263
+ **get_email** returns the user's email address or `nil`.
264
+
265
+ ```ruby
206
266
  LdapLookup.get_email(uniqname = nil)
207
- response: email or "No #{attribute} found for #{uniqname}"
208
- ```
209
- __is_member_of_group?:__ returns true/false if uniqname is a member of the specified group
267
+ response: email or nil
210
268
  ```
269
+
270
+ **is_member_of_group?** returns true/false if uniqname is a member of the specified group.
271
+
272
+ ```ruby
211
273
  LdapLookup.is_member_of_group?(uid = nil, group_name = nil)
212
274
  response: true or false (boolean)
213
275
  ```
214
- __get_email_distribution_list:__ Returns the list of emails that are associated to a group.
215
- ```
276
+
277
+ **get_email_distribution_list** returns a hash with group data or an empty hash.
278
+
279
+ ```ruby
216
280
  LdapLookup.get_email_distribution_list(group_name = nil)
217
281
  response: result_hash
218
282
  ```
219
- __all_groups_for_user:__ Returns the list of groups that a user is a member of.
220
- ```
283
+
284
+ **all_groups_for_user** returns the list of groups that a user is a member of.
285
+
286
+ ```ruby
221
287
  LdapLookup.all_groups_for_user(uniqname = nil)
222
288
  response: result_array
223
289
  ```
224
290
 
225
- ### Running Tests
291
+ ## Troubleshooting
292
+
293
+ ### Auth failures
294
+
295
+ * UM LDAP requires authenticated binds. Ensure `LDAP_USERNAME` and `LDAP_PASSWORD` are set.
296
+ * For service accounts, set `LDAP_BIND_DN` to the DN provided by your IT team.
297
+ * Confirm the account is enabled for LDAP and the password is current.
298
+
299
+ ### TLS/SSL errors
300
+
301
+ * Use `LDAP_ENCRYPTION=start_tls` with port `389`, or `simple_tls` with port `636`.
302
+ * If certificate validation fails, set `LDAP_CA_CERT` to a CA bundle path.
303
+ * Avoid `LDAP_TLS_VERIFY=false` outside local testing.
304
+
305
+ ### Bind DN tips
306
+
307
+ * Default bind DN is `uid=username,ou=People,base`.
308
+ * Service accounts often require a custom DN; set `LDAP_BIND_DN` accordingly.
309
+
310
+ ## Running Tests
226
311
 
227
312
  **Security Note:** Never put passwords in command line arguments. They are visible in process lists and shell history.
228
313
 
229
- **Recommended: Use a .env file (most secure)**
314
+ **Test Vars Note:** `LDAP_TEST_*` and `LDAP_DIAGNOSTIC_UID` are optional and only used by the test suite/diagnostics.
315
+
316
+ ### Recommended: Use a .env file (most secure)
317
+
230
318
  1. Copy the example file: `cp .env.example .env`
231
319
  2. Edit `.env` with your credentials:
232
- ```
320
+
321
+ ```bash
233
322
  LDAP_USERNAME=your_uniqname
234
323
  LDAP_PASSWORD=your_password
235
324
  ```
325
+
236
326
  3. Run tests: `bundle exec rspec`
237
327
 
238
- **Alternative: Export environment variables**
328
+ ### Alternative: Export environment variables
329
+
239
330
  ```bash
240
331
  export LDAP_USERNAME=your_uniqname
241
332
  export LDAP_PASSWORD=your_password
242
333
  bundle exec rspec
243
334
  ```
244
335
 
245
- **Never do this (insecure):**
336
+ ### Never do this (insecure)
337
+
246
338
  ```bash
247
- # DON'T: Password visible in process list
339
+ # DON'T: Password visible in process list
248
340
  LDAP_PASSWORD=xxx bundle exec rspec
249
341
  ```
250
342
 
251
- ### Contributing
343
+ ## Contributing
252
344
 
253
- Bug reports and pull requests are welcome on GitHub at https://github.com/rsmoke/ldap_lookup. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
345
+ Bug reports and pull requests are welcome on GitHub at [rsmoke/ldap_lookup](https://github.com/rsmoke/ldap_lookup). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](https://www.contributor-covenant.org) code of conduct.
254
346
 
255
- ### License
347
+ ## License
256
348
 
257
349
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
258
350
 
259
- ### Code of Conduct
351
+ ## Code of Conduct
260
352
 
261
- Everyone interacting in the LdapLookup project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/ldap_lookup/blob/master/CODE_OF_CONDUCT.md).
353
+ Everyone interacting in the LdapLookup project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/rsmoke/ldap_lookup/blob/master/CODE_OF_CONDUCT.md).
data/SETUP.md CHANGED
@@ -1,14 +1,18 @@
1
1
  # Quick Setup Guide for Gem Users
2
2
 
3
- This guide will help you quickly set up `ldap_lookup` in your Rails application.
3
+ This guide helps you set up `ldap_lookup` in a Rails app.
4
4
 
5
- ## Prerequisites
5
+ ## Overview
6
6
 
7
- 1. **Get LDAP Credentials**
8
- - For production: Request a service account from your IT department
9
- - For development: You can temporarily use your personal uniqname/password
7
+ LdapLookup provides authenticated or anonymous lookups of user attributes in the University of Michigan MCommunity LDAP service. It supports encrypted binds per UM IT Security (effective Jan 28, 2026) and can be adapted for other LDAP servers.
10
8
 
11
- ## Installation Steps
9
+ ### UM LDAP Requirements (as of Jan 28, 2026)
10
+
11
+ - **Authenticated binds only** - UM LDAP does not allow anonymous binds.
12
+ - Username and password are required for UM LDAP.
13
+ - Encrypted connections (STARTTLS or LDAPS) are mandatory.
14
+
15
+ ## Installation
12
16
 
13
17
  ### 1. Add to Gemfile
14
18
 
@@ -16,12 +20,10 @@ This guide will help you quickly set up `ldap_lookup` in your Rails application.
16
20
  gem 'ldap_lookup'
17
21
  ```
18
22
 
19
- Run `bundle install`
23
+ Run `bundle install`.
20
24
 
21
25
  ### 2. Create Initializer
22
26
 
23
- Copy the example initializer:
24
-
25
27
  ```bash
26
28
  # If you have the gem source
27
29
  cp config/initializers/ldap_lookup.rb.example config/initializers/ldap_lookup.rb
@@ -30,45 +32,92 @@ cp config/initializers/ldap_lookup.rb.example config/initializers/ldap_lookup.rb
30
32
  touch config/initializers/ldap_lookup.rb
31
33
  ```
32
34
 
33
- ### 3. Configure Credentials
35
+ ### 3. Configure the Gem
34
36
 
35
37
  Edit `config/initializers/ldap_lookup.rb`:
36
38
 
37
39
  ```ruby
38
40
  LdapLookup.configuration do |config|
41
+ # Server Configuration (defaults work for UM LDAP)
39
42
  config.host = ENV.fetch('LDAP_HOST', 'ldap.umich.edu')
43
+ config.port = ENV.fetch('LDAP_PORT', '389')
40
44
  config.base = ENV.fetch('LDAP_BASE', 'dc=umich,dc=edu')
45
+
46
+ # Authentication (optional for anonymous binds)
41
47
  # Leave unset to use anonymous binds (if your LDAP server allows it)
42
48
  config.username = ENV['LDAP_USERNAME']
43
49
  config.password = ENV['LDAP_PASSWORD']
44
- config.encryption = :start_tls
50
+
51
+ # If using a service account with custom bind DN, uncomment and set:
52
+ # config.bind_dn = ENV['LDAP_BIND_DN']
53
+ # Note: LDAP_BIND_DN replaces LDAP_USERNAME (LDAP_USERNAME can be unset), not LDAP_PASSWORD.
54
+
55
+ # Encryption - REQUIRED (defaults to STARTTLS)
56
+ config.encryption = ENV.fetch('LDAP_ENCRYPTION', 'start_tls').to_sym
57
+ # Use :simple_tls for LDAPS on port 636
58
+ # TLS verification (defaults to true). Set LDAP_TLS_VERIFY=false only for local testing.
59
+ # Optional custom CA bundle: set LDAP_CA_CERT=/path/to/ca-bundle.pem
60
+
61
+ # Optional: Logger for debug output (responds to debug/info or call)
62
+ # config.logger = Rails.logger
45
63
  end
46
64
  ```
47
65
 
48
- ### 4. Set Environment Variables
66
+ ### Logger and Debug Output
67
+
68
+ - Debug logging is controlled by `config.debug` (default is false).
69
+ - If `config.logger` is set, it is used in this order:
70
+ - `logger.debug(message)` if available.
71
+ - `logger.info(message)` if `debug` is not available.
72
+ - `logger.call(message)` if neither `debug` nor `info` are available.
73
+ - If no logger is configured, debug output goes to STDOUT.
74
+ - Security note: debug output can include identifiers (uids, group names, search filters). Avoid sharing logs publicly.
75
+
76
+ ## Environment Variables
77
+
78
+ ### Development (.env File)
49
79
 
50
- **Development (.env file):**
51
80
  ```bash
52
81
  LDAP_USERNAME=your_service_account
53
82
  LDAP_PASSWORD=your_password
83
+ LDAP_BIND_DN=cn=service-account,ou=Applications,o=services
84
+ LDAP_DIAGNOSTIC_UID=your_uniqname
85
+ ```
86
+
87
+ ### Production (Hatchbox)
88
+
89
+ Configure these in **Hatchbox > App > Environment Variables**, then redeploy or restart.
90
+
91
+ **Minimum required for authenticated UM LDAP:**
92
+
93
+ ```bash
94
+ LDAP_USERNAME=service_account_uniqname
95
+ LDAP_PASSWORD=service_account_password
96
+ LDAP_ENCRYPTION=start_tls
97
+ LDAP_HOST=ldap.umich.edu
98
+ LDAP_PORT=389
99
+ LDAP_BASE=dc=umich,dc=edu
54
100
  ```
55
101
 
56
- **Production:**
57
- Use your platform's secrets management:
58
- - Rails: `config/credentials.yml.enc`
59
- - Heroku: `heroku config:set LDAP_USERNAME=xxx`
60
- - AWS: Secrets Manager
61
- - etc.
102
+ **Recommended for service accounts with a custom bind DN:**
103
+
104
+ ```bash
105
+ LDAP_BIND_DN=cn=service-account,ou=Applications,o=services
106
+ ```
62
107
 
63
- ### 5. Service Account Bind DN (if needed)
108
+ ### Service Account Bind DN (if needed)
64
109
 
65
110
  If your service account uses a custom bind DN format, add:
66
111
 
67
112
  ```ruby
68
- config.bind_dn = 'cn=service-account,ou=Service Accounts,dc=umich,dc=edu'
113
+ config.bind_dn = 'cn=service-account,ou=Applications,o=services'
69
114
  ```
70
115
 
71
- Your IT department will provide this if it's different from the default format.
116
+ Your IT department can provide the correct DN.
117
+
118
+ ## Non-Rails Setup
119
+
120
+ If you are not using Rails, configure `LdapLookup.configuration` during app startup and provide the same environment variables. See the Non-Rails example in `README.md` for a full snippet.
72
121
 
73
122
  ## Usage
74
123
 
@@ -94,24 +143,27 @@ LdapLookup.all_groups_for_user('uniqname')
94
143
 
95
144
  ## Troubleshooting
96
145
 
97
- **Anonymous bind fails**
98
- - Your LDAP server may require authenticated binds
99
- - Set `LDAP_USERNAME` and `LDAP_PASSWORD` (service account recommended)
100
- - Verify credentials are correct
146
+ ### Auth Failures
147
+
148
+ - UM LDAP requires authenticated binds. Ensure `LDAP_USERNAME` and `LDAP_PASSWORD` are set.
149
+ - For service accounts, set `LDAP_BIND_DN` to the DN provided by your IT team.
150
+ - Confirm the account is enabled for LDAP and the password is current.
151
+
152
+ ### TLS/SSL Errors
153
+
154
+ - Use `LDAP_ENCRYPTION=start_tls` with port `389`, or `simple_tls` with port `636`.
155
+ - If certificate validation fails, set `LDAP_CA_CERT` to a CA bundle path.
156
+ - Avoid `LDAP_TLS_VERIFY=false` outside local testing.
101
157
 
102
- **Error: Connection timeout or SSL errors**
103
- - Verify `config.host` is correct
104
- - Try `config.encryption = :simple_tls` with `config.port = '636'` for LDAPS
105
- - Check firewall rules allow outbound LDAP connections
158
+ ### Bind DN Tips
106
159
 
107
- **Service account not working**
108
- - Verify the bind DN format with your IT department
109
- - Set `config.bind_dn` if your service account uses a non-standard format
160
+ - Default bind DN is `uid=username,ou=People,base`.
161
+ - Service accounts often require a custom DN; set `LDAP_BIND_DN` accordingly.
110
162
 
111
163
  ## Security Reminders
112
164
 
113
- - Use environment variables for credentials
114
- - Use service accounts in production
115
- - Never commit credentials to version control
116
- - Don't hardcode passwords in code
117
- - Don't use personal accounts in production
165
+ - Use environment variables for credentials.
166
+ - Use service accounts in production.
167
+ - Never commit credentials to version control.
168
+ - Don't hardcode passwords in code.
169
+ - Don't use personal accounts in production.
@@ -9,24 +9,38 @@ LdapLookup.configuration do |config|
9
9
  config.host = ENV.fetch('LDAP_HOST', 'ldap.umich.edu')
10
10
  config.port = ENV.fetch('LDAP_PORT', '389')
11
11
  config.base = ENV.fetch('LDAP_BASE', 'dc=umich,dc=edu')
12
-
13
- # Authentication (optional for anonymous binds)
12
+
13
+ # UM LDAP requires authenticated binds. Ensure LDAP_USERNAME and LDAP_PASSWORD are set.
14
+ # Confirm the account is enabled for LDAP and the password is current.
14
15
  # Option 1: Use a service account (recommended for production)
15
16
  # Request a service account from your IT department
16
- # Leave unset to use anonymous binds (if your LDAP server allows it)
17
+ # Leave unset only if your LDAP server allows anonymous binds
17
18
  config.username = ENV['LDAP_USERNAME']
18
19
  config.password = ENV['LDAP_PASSWORD']
19
-
20
- # Option 2: If your service account uses a custom bind DN format, specify it:
21
- # config.bind_dn = 'cn=service-account,ou=Service Accounts,dc=umich,dc=edu'
20
+
21
+ # For service accounts, set LDAP_BIND_DN to the DN provided by your IT team.
22
+ # Note: LDAP_BIND_DN replaces LDAP_USERNAME (LDAP_USERNAME can be unset), not LDAP_PASSWORD.
23
+ # config.bind_dn = ENV['LDAP_BIND_DN']
24
+ # Example: cn=service-account,ou=Applications,o=services
22
25
  # (If bind_dn is not set, it defaults to: uid=username,ou=People,base)
23
-
24
- # Encryption - REQUIRED (as of Jan 20, 2026)
25
- # Use :start_tls for port 389 or :simple_tls for LDAPS on port 636
26
+
27
+ # Use LDAP_ENCRYPTION=start_tls with port 389, or simple_tls with port 636.
26
28
  encryption_method = ENV.fetch('LDAP_ENCRYPTION', 'start_tls').to_sym
27
29
  config.encryption = encryption_method
28
-
30
+
29
31
  # Optional: Attribute Configuration
30
32
  config.dept_attribute = ENV.fetch('LDAP_DEPT_ATTRIBUTE', 'umichPostalAddressData')
31
33
  config.group_attribute = ENV.fetch('LDAP_GROUP_ATTRIBUTE', 'umichGroupEmail')
34
+
35
+ # Optional: Diagnostic UID (used by LdapLookup.test_connection)
36
+ # config.diagnostic_uid = ENV['LDAP_DIAGNOSTIC_UID']
37
+
38
+ # Optional: Logger for debug output (responds to debug/info or call)
39
+ # Debug output is only emitted when config.debug is true.
40
+ # config.logger = Rails.logger
41
+
42
+ # Optional: Separate search bases for users and groups
43
+ # Example for UM service accounts:
44
+ # config.user_base = ENV.fetch('LDAP_USER_BASE', 'ou=people,dc=umich,dc=edu')
45
+ # config.group_base = ENV.fetch('LDAP_GROUP_BASE', 'ou=user groups,ou=groups,dc=umich,dc=edu')
32
46
  end
data/ldaptest.rb CHANGED
@@ -17,17 +17,25 @@ class Ldaptest
17
17
  config.host = ENV['LDAP_HOST'] || "ldap.umich.edu"
18
18
  config.port = ENV['LDAP_PORT'] || "389"
19
19
  config.base = ENV['LDAP_BASE'] || "dc=umich,dc=edu"
20
- # Leave username/password unset for anonymous binds
20
+ # UM LDAP requires authenticated binds. Ensure LDAP_USERNAME and LDAP_PASSWORD are set.
21
+ # Confirm the account is enabled for LDAP and the password is current.
21
22
  config.username = ENV['LDAP_USERNAME']
22
23
  config.password = ENV['LDAP_PASSWORD']
23
- # Read encryption from ENV, default to start_tls
24
+ # For service accounts, set LDAP_BIND_DN to the DN provided by your IT team.
25
+ config.bind_dn = ENV['LDAP_BIND_DN']
26
+ # Optional diagnostic UID to avoid size-limit warnings
27
+ config.diagnostic_uid = ENV['LDAP_DIAGNOSTIC_UID'] if ENV['LDAP_DIAGNOSTIC_UID']
28
+ # Use LDAP_ENCRYPTION=start_tls with port 389, or simple_tls with port 636.
24
29
  encryption_str = ENV['LDAP_ENCRYPTION'] || 'start_tls'
25
30
  config.encryption = encryption_str.to_sym
26
31
  config.dept_attribute = ENV['LDAP_DEPT_ATTRIBUTE'] || "umichPostalAddressData"
27
32
  config.group_attribute = ENV['LDAP_GROUP_ATTRIBUTE'] || "umichGroupEmail"
33
+ # Optional search bases for UM LDAP
34
+ config.user_base = ENV['LDAP_USER_BASE'] if ENV['LDAP_USER_BASE']
35
+ config.group_base = ENV['LDAP_GROUP_BASE'] if ENV['LDAP_GROUP_BASE']
28
36
  # Enable LDAP debug logging in this test runner
29
37
  debug_str = ENV['LDAP_DEBUG']
30
- config.debug = debug_str ? debug_str.to_s.downcase == 'true' : true
38
+ config.debug = debug_str ? debug_str.to_s.downcase == 'true' : false
31
39
  end
32
40
  #######################################################
33
41
 
@@ -1,3 +1,3 @@
1
1
  module LdapLookup
2
- VERSION = "2.0.1"
2
+ VERSION = "2.1.0"
3
3
  end
data/lib/ldap_lookup.rb CHANGED
@@ -13,13 +13,81 @@ module LdapLookup
13
13
  define_setting :username
14
14
  define_setting :password
15
15
  define_setting :bind_dn # Optional: custom bind DN (for service accounts). If not set, uses uid=username,ou=People,base
16
+ define_setting :user_base # Optional: base DN for people lookups (e.g., ou=people,dc=umich,dc=edu)
17
+ define_setting :group_base # Optional: base DN for group lookups (e.g., ou=user groups,ou=groups,dc=umich,dc=edu)
18
+ define_setting :diagnostic_uid # Optional: test UID for diagnostic searches
16
19
  define_setting :encryption, :start_tls # :start_tls or :simple_tls (LDAPS)
17
20
  define_setting :debug, false
21
+ define_setting :logger # Optional logger (responds to debug/info or call)
18
22
 
19
23
  def self.debug_log(message)
20
24
  return unless debug
21
25
 
22
- puts "[LDAP DEBUG] #{message}"
26
+ formatted = "[LDAP DEBUG] #{message}"
27
+ log_target = logger
28
+ if log_target
29
+ if log_target.respond_to?(:debug)
30
+ log_target.debug(formatted)
31
+ elsif log_target.respond_to?(:info)
32
+ log_target.info(formatted)
33
+ elsif log_target.respond_to?(:call)
34
+ log_target.call(formatted)
35
+ else
36
+ puts formatted
37
+ end
38
+ else
39
+ puts formatted
40
+ end
41
+ end
42
+
43
+ def self.blank?(value)
44
+ value.nil? || value.to_s.strip.empty?
45
+ end
46
+
47
+ def self.extract_dn_attribute(dn_value, attribute)
48
+ return nil if blank?(dn_value) || blank?(attribute)
49
+
50
+ attr_key = attribute.to_s.downcase
51
+ begin
52
+ parsed = Net::LDAP::DN.new(dn_value)
53
+ parsed.to_a.each do |rdn|
54
+ rdn.each do |pair|
55
+ attr, val, _type = pair
56
+ return val if attr.to_s.downcase == attr_key
57
+ end
58
+ end
59
+ rescue => e
60
+ debug_log("dn parse failed: #{e.class} #{e.message}")
61
+ end
62
+
63
+ # Fallback: basic parsing (may fail with escaped commas)
64
+ dn_value.split(',').each do |segment|
65
+ key, value = segment.split('=', 2)
66
+ return value if key && key.strip.downcase == attr_key
67
+ end
68
+
69
+ nil
70
+ end
71
+
72
+ def self.user_search_base
73
+ base_value = user_base.to_s.strip
74
+ return base_value unless base_value.empty?
75
+
76
+ base
77
+ end
78
+
79
+ def self.group_search_base
80
+ base_value = group_base.to_s.strip
81
+ return base_value unless base_value.empty?
82
+
83
+ base
84
+ end
85
+
86
+ def self.user_dn_base
87
+ base_value = user_base.to_s.strip
88
+ return base_value unless base_value.empty?
89
+
90
+ "ou=People,#{base}"
23
91
  end
24
92
 
25
93
  def self.perform_search(ldap, base: nil, filter:, attributes: nil, label: nil, options: {})
@@ -96,23 +164,33 @@ module LdapLookup
96
164
  def self.test_connection
97
165
  username_present = username && !username.to_s.strip.empty?
98
166
  password_present = password && !password.to_s.strip.empty?
99
- auth_dn = if bind_dn
167
+ diagnostic_uid_value = diagnostic_uid.to_s.strip
168
+ diagnostic_uid_present = !diagnostic_uid_value.empty?
169
+ bind_dn_present = bind_dn && !bind_dn.to_s.strip.empty?
170
+ auth_dn = if bind_dn_present
100
171
  bind_dn
101
172
  elsif username_present
102
- "uid=#{username},ou=People,#{base}"
173
+ "uid=#{username},#{user_dn_base}"
103
174
  end
104
175
 
105
- search_base = username_present ? "ou=People,#{base}" : base
106
- search_filter = username_present ? "(uid=#{username})" : "(objectClass=*)"
176
+ search_base = (diagnostic_uid_present || username_present || (user_base && !user_base.to_s.strip.empty?)) ? user_search_base : base
177
+ search_filter = if diagnostic_uid_present
178
+ "(uid=#{diagnostic_uid_value})"
179
+ elsif username_present
180
+ "(uid=#{username})"
181
+ else
182
+ "(uid=*)"
183
+ end
107
184
 
108
185
  result = {
109
186
  bind_dn: auth_dn,
110
187
  username: username,
188
+ diagnostic_uid: diagnostic_uid_present ? diagnostic_uid_value : nil,
111
189
  host: host,
112
190
  port: port,
113
191
  encryption: encryption,
114
192
  base: base,
115
- auth_mode: (username_present && password_present) ? 'authenticated' : 'anonymous'
193
+ auth_mode: ((username_present || bind_dn_present) && password_present) ? 'authenticated' : 'anonymous'
116
194
  }
117
195
 
118
196
  begin
@@ -133,7 +211,7 @@ module LdapLookup
133
211
  # Net::LDAP binds automatically when performing operations (search, etc.)
134
212
  # Explicit bind may fail with Code 19 on STARTTLS, but actual operations work fine
135
213
  # Test by performing an actual search operation instead of explicit bind
136
-
214
+
137
215
  # Try a simple search - this will trigger automatic bind
138
216
  search_result = perform_search(
139
217
  ldap,
@@ -253,9 +331,10 @@ module LdapLookup
253
331
  # Note: "simple" bind method = authenticated bind with username/password (not anonymous)
254
332
  auth_username = username.to_s.strip
255
333
  auth_password = password.to_s
256
- if !auth_username.empty? && !auth_password.empty?
334
+ auth_bind_dn = bind_dn.to_s.strip
335
+ if !auth_password.empty? && (!auth_bind_dn.empty? || !auth_username.empty?)
257
336
  # 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}"
337
+ auth_bind_dn = auth_bind_dn.empty? ? "uid=#{auth_username},#{user_dn_base}" : auth_bind_dn
259
338
  connection_params[:auth] = {
260
339
  method: :simple, # Simple bind = authenticated bind with username/password
261
340
  username: auth_bind_dn,
@@ -280,6 +359,8 @@ module LdapLookup
280
359
  end
281
360
 
282
361
  def self.get_user_attribute(uniqname, attribute, default_value = nil)
362
+ return default_value if blank?(uniqname) || blank?(attribute)
363
+
283
364
  ldap = ldap_connection
284
365
  search_param = uniqname
285
366
  result_attrs = [attribute]
@@ -289,6 +370,7 @@ module LdapLookup
289
370
 
290
371
  perform_search(
291
372
  ldap,
373
+ base: user_search_base,
292
374
  filter: search_filter,
293
375
  attributes: result_attrs,
294
376
  label: "get_user_attribute",
@@ -316,6 +398,8 @@ module LdapLookup
316
398
  end
317
399
 
318
400
  def self.get_nested_attribute(uniqname, nested_attribute)
401
+ return nil if blank?(uniqname) || blank?(nested_attribute)
402
+
319
403
  ldap = ldap_connection
320
404
  search_param = uniqname
321
405
  # Specify the full nested attribute path using dot notation
@@ -329,6 +413,7 @@ module LdapLookup
329
413
 
330
414
  perform_search(
331
415
  ldap,
416
+ base: user_search_base,
332
417
  filter: search_filter,
333
418
  attributes: result_attrs,
334
419
  label: "get_nested_attribute",
@@ -366,13 +451,22 @@ module LdapLookup
366
451
 
367
452
  # method to check if a uid exist in LDAP
368
453
  def self.uid_exist?(uniqname)
454
+ return false if blank?(uniqname)
455
+
369
456
  ldap = ldap_connection
370
457
  search_param = uniqname
371
458
  found = false
372
459
 
373
460
  search_filter = Net::LDAP::Filter.eq('uid', search_param)
374
461
 
375
- perform_search(ldap, filter: search_filter, label: "uid_exist", options: { size: 1 }).each do |item|
462
+ perform_search(
463
+ ldap,
464
+ base: user_search_base,
465
+ filter: search_filter,
466
+ attributes: ['uid'],
467
+ label: "uid_exist",
468
+ options: { size: 1 }
469
+ ).each do |item|
376
470
  if item['uid'].first == search_param
377
471
  found = true
378
472
  break
@@ -410,6 +504,8 @@ module LdapLookup
410
504
  end
411
505
 
412
506
  def self.is_member_of_group?(uid, group_name)
507
+ return false if blank?(uid) || blank?(group_name)
508
+
413
509
  ldap = ldap_connection
414
510
  search_param = group_name
415
511
  result_attrs = ['member']
@@ -420,9 +516,9 @@ module LdapLookup
420
516
  Net::LDAP::Filter.eq('objectClass', 'group')
421
517
  )
422
518
 
423
- perform_search(ldap, filter: search_filter, attributes: result_attrs, label: "is_member_of_group").each do |item|
519
+ perform_search(ldap, base: group_search_base, filter: search_filter, attributes: result_attrs, label: "is_member_of_group").each do |item|
424
520
  members = item['member']
425
- if members && members.any? { |entry| entry.split(',').first.split('=')[1] == uid }
521
+ if members && members.any? { |entry| extract_dn_attribute(entry, 'uid') == uid }
426
522
  found = true
427
523
  break
428
524
  end
@@ -443,6 +539,8 @@ module LdapLookup
443
539
  end
444
540
 
445
541
  def self.get_email_distribution_list(group_name)
542
+ return {} if blank?(group_name)
543
+
446
544
  ldap = ldap_connection
447
545
  result_hash = {}
448
546
  found_data = false
@@ -455,11 +553,11 @@ module LdapLookup
455
553
  Net::LDAP::Filter.eq('objectClass', 'group')
456
554
  )
457
555
 
458
- perform_search(ldap, filter: search_filter, attributes: result_attrs, label: "get_email_distribution_list").each do |item|
556
+ perform_search(ldap, base: group_search_base, filter: search_filter, attributes: result_attrs, label: "get_email_distribution_list").each do |item|
459
557
  found_data = true
460
558
  result_hash['group_name'] = item['cn']&.first
461
559
  result_hash['group_email'] = item['umichGroupEmail']&.first
462
- members = item['member']&.map { |individual| individual.split(',').first.split('=')[1] }
560
+ members = item['member']&.map { |individual| extract_dn_attribute(individual, 'uid') }&.compact
463
561
  result_hash['members'] = members&.sort || []
464
562
  end
465
563
 
@@ -477,15 +575,20 @@ module LdapLookup
477
575
  end
478
576
 
479
577
  def self.all_groups_for_user(uid)
578
+ return [] if blank?(uid)
579
+
480
580
  ldap = ldap_connection
481
581
  result_array = []
482
582
 
483
583
  result_attrs = ['dn']
484
584
 
485
585
  # 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] }
586
+ member_dn = "uid=#{uid},#{user_dn_base}"
587
+ search_filter = Net::LDAP::Filter.eq('member', member_dn)
588
+ perform_search(ldap, base: group_search_base, filter: search_filter, attributes: result_attrs, label: "all_groups_for_user").each do |item|
589
+ dn_value = item.respond_to?(:dn) ? item.dn : item['dn']&.first
590
+ group_name = extract_dn_attribute(dn_value, 'cn')
591
+ result_array << group_name if group_name
489
592
  end
490
593
 
491
594
  # Check response - may raise Constraint Violation for regular users
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ldap_lookup
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rick Smoke