ldap_lookup 0.1.8 → 2.0.1

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: 775477e51275859dcb210f1b996b14b10b9814bc7443df7a3fa9c63736460298
4
- data.tar.gz: f1248f24d49b795c196a16f1b5edcaff1b4ab435127561fce5d24c320bd99630
3
+ metadata.gz: 393d3bdcafb4cadf5648c6916122ebfb7b0c6fefe79c5d50b6c6c57f6d6997cc
4
+ data.tar.gz: de8be14be88d2d4e3102971cf62d4a316cdc6a0aaa7e6268aee62659723e5ca2
5
5
  SHA512:
6
- metadata.gz: ba6cfe2623779bc3e73fde5258bf15bfc83fe0018d04ae8e7f052e95952f216e9a23c30a379bb3e4748453d60160f49c4a95636cda3966f39000714ecf479385
7
- data.tar.gz: 84e07889f3a1b37d45c57fc690f62e878246c18b8da37c49a0ad5d65de5691ad2b160869c937af459fc02590297ec07474013fce30658f1a2870f8c0294ffbbb
6
+ metadata.gz: 7dd298be615e445056d806602b27cb05e01a0783556fe09c32d1c5599ff877a18c286d1d2e845f1f5ec30c4b08745668ec5fd8f2717e4393cc7c45dd7e33254d
7
+ data.tar.gz: 6bb46112525a64c3c63fd1cb2d356e151ff48154515684040d5c1519464619681942f36a45ba275d0749abcd5b04afd81bc28ddb9a72d9c403eba5dc093d0970
data/.env.example ADDED
@@ -0,0 +1,25 @@
1
+ # LDAP Configuration for Testing
2
+ # Copy this file to .env and fill in your actual credentials
3
+ # The .env file is gitignored and will not be committed
4
+
5
+ # Your UM uniqname (username)
6
+ LDAP_USERNAME=your_uniqname
7
+
8
+ # Your UM password
9
+ LDAP_PASSWORD=your_password
10
+
11
+ # Leave LDAP_USERNAME and LDAP_PASSWORD unset for anonymous binds
12
+
13
+ # Optional: Override default LDAP settings
14
+ # LDAP_HOST=ldap.umich.edu
15
+ # LDAP_PORT=389 # Use 389 for STARTTLS or 636 for LDAPS
16
+ # LDAP_BASE=dc=umich,dc=edu
17
+ # LDAP_ENCRYPTION=start_tls # Use 'start_tls' for port 389 or 'simple_tls' for port 636 (LDAPS)
18
+ # LDAP_DEPT_ATTRIBUTE=umichPostalAddressData
19
+ # LDAP_GROUP_ATTRIBUTE=umichGroupEmail
20
+ # LDAP_TLS_VERIFY=true # Set to false to disable cert verification (dev only)
21
+ # LDAP_CA_CERT=/path/to/ca-bundle.pem
22
+ #
23
+ # If STARTTLS (port 389) doesn't work, try LDAPS:
24
+ # LDAP_PORT=636
25
+ # LDAP_ENCRYPTION=simple_tls
data/.gitignore CHANGED
@@ -7,3 +7,8 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
  *.gem
10
+
11
+ # Environment variables
12
+ .env
13
+ .env.local
14
+ .env.*.local
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --format documentation
3
+ --color
data/.rspec_status ADDED
@@ -0,0 +1,46 @@
1
+ example_id | status | run_time |
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 |
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 |
11
+ ./spec/configuration_spec.rb[1:2:2] | passed | 0.00003 seconds |
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 |
data/Gemfile.lock CHANGED
@@ -1,14 +1,15 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ldap_lookup (0.1.7)
5
- net-ldap (~> 0.17.0)
4
+ ldap_lookup (0.1.8)
5
+ net-ldap (~> 0.18.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
10
  diff-lcs (1.3)
11
- net-ldap (0.17.1)
11
+ dotenv (2.8.1)
12
+ net-ldap (0.18.0)
12
13
  rake (13.0.1)
13
14
  rspec (3.7.0)
14
15
  rspec-core (~> 3.7.0)
@@ -29,6 +30,7 @@ PLATFORMS
29
30
 
30
31
  DEPENDENCIES
31
32
  bundler (~> 2.2.26)
33
+ dotenv (~> 2.8)
32
34
  ldap_lookup!
33
35
  rake (~> 13.0)
34
36
  rspec (~> 3.7.0)
data/README.md CHANGED
@@ -1,7 +1,7 @@
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
3
  ### Description
4
- This module is to be used for anonymous lookup of user attributes in the MCommunity service provide at the University of Michigan. It can be easily modifed to use other LDAP server configurations.
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.
5
5
 
6
6
  ---
7
7
 
@@ -9,23 +9,47 @@ This module is to be used for anonymous lookup of user attributes in the MCommun
9
9
 
10
10
  Requirements:
11
11
  * Ruby at least 2.0.0
12
- * Gem 'net-ldap' ~> '0.17.0'
12
+ * Gem 'net-ldap' ~> '0.18.0'
13
13
  > *The Net::LDAP (aka net-ldap) gem before 0.16.0 for Ruby has a Missing SSL Certificate Validation.*
14
14
 
15
15
  To try the module out:
16
16
  1. Clone the repo
17
- 2. Edit the configurations by opening ldaptest.rb and set the *CONFIGURATION BLOCK* to your environment.
17
+ 2. Copy the env template and set credentials: `cp .env.example .env`
18
+ 3. Load the env vars into your shell (example):
19
+ ```bash
20
+ set -a
21
+ source .env
22
+ set +a
23
+ ```
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).
18
25
  <pre>
19
26
  LdapLookup.configuration do |config|
20
- config.host = <em>< your host ></em> # "ldap.umich.edu"
21
- config.port = <em>< your port ></em> # "986" the default is set to "389" so this optional
22
- config.base = <em>< your LDAP base ></em> # "dc=umich,dc=edu"
23
- config.dept_attribute = <em>< your dept attribute ></em> # "umichPostalAddressData"
24
- config.group_attribute = <em>< your group email attribute ></em> # "umichGroupEmail"
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
25
41
  end
26
42
  </pre>
27
43
 
28
- 3. run the ldaptest.rb script
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.
51
+
52
+ 5. run the ldaptest.rb script
29
53
  ```ruby
30
54
  ruby ./ldaptest.rb
31
55
  ```
@@ -34,30 +58,129 @@ ruby ./ldaptest.rb
34
58
 
35
59
  ### Installation
36
60
 
61
+ #### Step 1: Add to Gemfile
62
+
37
63
  Add this line to your application's Gemfile:
38
64
 
39
65
  ```ruby
40
66
  gem 'ldap_lookup'
41
67
  ```
42
68
 
43
- And then execute:
69
+ Then run:
44
70
 
45
- $ bundle
71
+ ```bash
72
+ bundle install
73
+ ```
46
74
 
47
- Or install it yourself as:
75
+ #### Step 2: Get LDAP Credentials
48
76
 
49
- $ gem install ldap_lookup
77
+ **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.
79
+
80
+ **For Development/Testing:**
81
+ You can use your personal UM uniqname and password temporarily, but switch to a service account for production.
82
+
83
+ #### Step 3: Configure the Gem
84
+
85
+ **For Rails Applications:**
86
+
87
+ Create `config/initializers/ldap_lookup.rb`:
88
+
89
+ ```ruby
90
+ LdapLookup.configuration do |config|
91
+ # Server Configuration (defaults work for UM LDAP)
92
+ config.host = ENV.fetch('LDAP_HOST', 'ldap.umich.edu')
93
+ config.port = ENV.fetch('LDAP_PORT', '389')
94
+ config.base = ENV.fetch('LDAP_BASE', 'dc=umich,dc=edu')
95
+
96
+ # Authentication (optional for anonymous binds)
97
+ # Leave unset to use anonymous binds (if your LDAP server allows it)
98
+ config.username = ENV['LDAP_USERNAME']
99
+ config.password = ENV['LDAP_PASSWORD']
100
+
101
+ # 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'
103
+
104
+ # Encryption - REQUIRED (defaults to STARTTLS)
105
+ config.encryption = ENV.fetch('LDAP_ENCRYPTION', 'start_tls').to_sym
106
+ # Use :simple_tls for LDAPS on port 636
107
+ # TLS verification (defaults to true). Set LDAP_TLS_VERIFY=false only for local testing.
108
+ # Optional custom CA bundle: set LDAP_CA_CERT=/path/to/ca-bundle.pem
109
+
110
+ # Optional: Attribute Configuration
111
+ config.dept_attribute = ENV.fetch('LDAP_DEPT_ATTRIBUTE', 'umichPostalAddressData')
112
+ config.group_attribute = ENV.fetch('LDAP_GROUP_ATTRIBUTE', 'umichGroupEmail')
113
+ end
114
+ ```
115
+
116
+ **For Non-Rails Applications:**
117
+
118
+ Configure in your application startup:
119
+
120
+ ```ruby
121
+ require 'ldap_lookup'
50
122
 
51
- In your application create a file config/initializers/ldap_lookup.rb
52
- <pre>
53
123
  LdapLookup.configuration do |config|
54
- config.host = <em>< your host ></em> # "ldap.umich.edu"
55
- config.port = <em>< your port ></em> # "954" port 389 is set by default
56
- config.base = <em>< your LDAP base ></em> # "dc=umich,dc=edu"
57
- config.dept_attribute = <em>< your dept attribute ></em> # "umichPostalAddressData"
58
- config.group_attribute = <em>< your group email attribute ></em> # "umichGroupEmail"
124
+ config.host = 'ldap.umich.edu'
125
+ config.base = 'dc=umich,dc=edu'
126
+ config.username = ENV['LDAP_USERNAME']
127
+ config.password = ENV['LDAP_PASSWORD']
128
+ config.encryption = :start_tls
59
129
  end
60
- </pre>
130
+ ```
131
+
132
+ #### Step 4: Set Environment Variables
133
+
134
+ **Never hardcode credentials in your code!** Use environment variables (Hatchbox, Heroku, etc.).
135
+
136
+ **Development with `.env.example` (recommended):**
137
+ 1. Copy the template: `cp .env.example .env`
138
+ 2. Update the values in `.env` for your environment.
139
+ 3. Load the variables into your shell (example):
140
+ ```bash
141
+ set -a
142
+ source .env
143
+ set +a
144
+ ```
145
+
146
+ **Typical `.env` values:**
147
+ ```bash
148
+ LDAP_USERNAME=your_service_account_uniqname
149
+ LDAP_PASSWORD=your_service_account_password
150
+ ```
151
+
152
+ **Optional settings (override defaults as needed):**
153
+ ```bash
154
+ LDAP_HOST=ldap.umich.edu
155
+ LDAP_PORT=389
156
+ LDAP_BASE=dc=umich,dc=edu
157
+ LDAP_ENCRYPTION=start_tls
158
+ LDAP_TLS_VERIFY=true
159
+ LDAP_CA_CERT=/path/to/ca-bundle.pem
160
+ LDAP_DEPT_ATTRIBUTE=umichPostalAddressData
161
+ LDAP_GROUP_ATTRIBUTE=umichGroupEmail
162
+ ```
163
+
164
+ **Alternative: export in your shell**
165
+ ```bash
166
+ export LDAP_USERNAME=your_service_account_uniqname
167
+ export LDAP_PASSWORD=your_service_account_password
168
+ ```
169
+
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
176
+
177
+ If your service account uses a non-standard bind DN format, you can specify it:
178
+
179
+ ```ruby
180
+ config.bind_dn = 'cn=my-service-account,ou=Service Accounts,dc=umich,dc=edu'
181
+ ```
182
+
183
+ If `bind_dn` is not set, it defaults to: `uid=username,ou=People,base`
61
184
 
62
185
  ---
63
186
 
@@ -66,30 +189,63 @@ end
66
189
  __uid_exist?:__ returns true if uid is in LDAP
67
190
  ```
68
191
  LdapLookup.uid_exist?(uniqname)
192
+ response: true or false (boolean)
69
193
  ```
70
194
  __get_simple_name:__ returns the Display Name
71
195
  ```
72
196
  LdapLookup.get_simple_name(uniqname = nil)
197
+ response: name or "No #{attribute} found for #{uniqname}"
73
198
  ```
74
199
  __get_dept:__ returns the users Department_name
75
200
  ```
76
201
  LdapLookup.get_dept(uniqname = nil)
202
+ response: dept name or "No #{nested_attribute} found for #{uniqname}"
77
203
  ```
78
204
  __get_email:__ returns the users email address
79
205
  ```
80
206
  LdapLookup.get_email(uniqname = nil)
207
+ response: email or "No #{attribute} found for #{uniqname}"
81
208
  ```
82
209
  __is_member_of_group?:__ returns true/false if uniqname is a member of the specified group
83
210
  ```
84
211
  LdapLookup.is_member_of_group?(uid = nil, group_name = nil)
212
+ response: true or false (boolean)
85
213
  ```
86
214
  __get_email_distribution_list:__ Returns the list of emails that are associated to a group.
87
215
  ```
88
216
  LdapLookup.get_email_distribution_list(group_name = nil)
217
+ response: result_hash
89
218
  ```
90
219
  __all_groups_for_user:__ Returns the list of groups that a user is a member of.
91
220
  ```
92
221
  LdapLookup.all_groups_for_user(uniqname = nil)
222
+ response: result_array
223
+ ```
224
+
225
+ ### Running Tests
226
+
227
+ **Security Note:** Never put passwords in command line arguments. They are visible in process lists and shell history.
228
+
229
+ **Recommended: Use a .env file (most secure)**
230
+ 1. Copy the example file: `cp .env.example .env`
231
+ 2. Edit `.env` with your credentials:
232
+ ```
233
+ LDAP_USERNAME=your_uniqname
234
+ LDAP_PASSWORD=your_password
235
+ ```
236
+ 3. Run tests: `bundle exec rspec`
237
+
238
+ **Alternative: Export environment variables**
239
+ ```bash
240
+ export LDAP_USERNAME=your_uniqname
241
+ export LDAP_PASSWORD=your_password
242
+ bundle exec rspec
243
+ ```
244
+
245
+ **Never do this (insecure):**
246
+ ```bash
247
+ # ❌ DON'T: Password visible in process list
248
+ LDAP_PASSWORD=xxx bundle exec rspec
93
249
  ```
94
250
 
95
251
  ### Contributing
data/SETUP.md ADDED
@@ -0,0 +1,117 @@
1
+ # Quick Setup Guide for Gem Users
2
+
3
+ This guide will help you quickly set up `ldap_lookup` in your Rails application.
4
+
5
+ ## Prerequisites
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
10
+
11
+ ## Installation Steps
12
+
13
+ ### 1. Add to Gemfile
14
+
15
+ ```ruby
16
+ gem 'ldap_lookup'
17
+ ```
18
+
19
+ Run `bundle install`
20
+
21
+ ### 2. Create Initializer
22
+
23
+ Copy the example initializer:
24
+
25
+ ```bash
26
+ # If you have the gem source
27
+ cp config/initializers/ldap_lookup.rb.example config/initializers/ldap_lookup.rb
28
+
29
+ # Or create it manually
30
+ touch config/initializers/ldap_lookup.rb
31
+ ```
32
+
33
+ ### 3. Configure Credentials
34
+
35
+ Edit `config/initializers/ldap_lookup.rb`:
36
+
37
+ ```ruby
38
+ LdapLookup.configuration do |config|
39
+ config.host = ENV.fetch('LDAP_HOST', 'ldap.umich.edu')
40
+ config.base = ENV.fetch('LDAP_BASE', 'dc=umich,dc=edu')
41
+ # Leave unset to use anonymous binds (if your LDAP server allows it)
42
+ config.username = ENV['LDAP_USERNAME']
43
+ config.password = ENV['LDAP_PASSWORD']
44
+ config.encryption = :start_tls
45
+ end
46
+ ```
47
+
48
+ ### 4. Set Environment Variables
49
+
50
+ **Development (.env file):**
51
+ ```bash
52
+ LDAP_USERNAME=your_service_account
53
+ LDAP_PASSWORD=your_password
54
+ ```
55
+
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.
62
+
63
+ ### 5. Service Account Bind DN (if needed)
64
+
65
+ If your service account uses a custom bind DN format, add:
66
+
67
+ ```ruby
68
+ config.bind_dn = 'cn=service-account,ou=Service Accounts,dc=umich,dc=edu'
69
+ ```
70
+
71
+ Your IT department will provide this if it's different from the default format.
72
+
73
+ ## Usage
74
+
75
+ ```ruby
76
+ # Check if a user exists
77
+ LdapLookup.uid_exist?('uniqname')
78
+
79
+ # Get user's name
80
+ LdapLookup.get_simple_name('uniqname')
81
+
82
+ # Get user's email
83
+ LdapLookup.get_email('uniqname')
84
+
85
+ # Get user's department
86
+ LdapLookup.get_dept('uniqname')
87
+
88
+ # Check group membership
89
+ LdapLookup.is_member_of_group?('uniqname', 'group-name')
90
+
91
+ # Get all groups for a user
92
+ LdapLookup.all_groups_for_user('uniqname')
93
+ ```
94
+
95
+ ## Troubleshooting
96
+
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
101
+
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
106
+
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
110
+
111
+ ## Security Reminders
112
+
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
@@ -0,0 +1,32 @@
1
+ # LDAP Lookup Configuration
2
+ # Copy this file to config/initializers/ldap_lookup.rb and configure with your credentials
3
+ #
4
+ # IMPORTANT: Never commit credentials to version control!
5
+ # Use environment variables or a secrets management system.
6
+
7
+ LdapLookup.configuration do |config|
8
+ # LDAP Server Configuration
9
+ config.host = ENV.fetch('LDAP_HOST', 'ldap.umich.edu')
10
+ config.port = ENV.fetch('LDAP_PORT', '389')
11
+ config.base = ENV.fetch('LDAP_BASE', 'dc=umich,dc=edu')
12
+
13
+ # Authentication (optional for anonymous binds)
14
+ # Option 1: Use a service account (recommended for production)
15
+ # Request a service account from your IT department
16
+ # Leave unset to use anonymous binds (if your LDAP server allows it)
17
+ config.username = ENV['LDAP_USERNAME']
18
+ 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'
22
+ # (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
+ encryption_method = ENV.fetch('LDAP_ENCRYPTION', 'start_tls').to_sym
27
+ config.encryption = encryption_method
28
+
29
+ # Optional: Attribute Configuration
30
+ config.dept_attribute = ENV.fetch('LDAP_DEPT_ATTRIBUTE', 'umichPostalAddressData')
31
+ config.group_attribute = ENV.fetch('LDAP_GROUP_ATTRIBUTE', 'umichGroupEmail')
32
+ end
data/ldap_lookup.gemspec CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Rick Smoke"]
10
10
  spec.email = ["rsmoke@umich.edu"]
11
11
 
12
- spec.summary = %q{For anonymous lookup of user LDAP attributes.}
13
- spec.description = %q{This module is to be used for anonymous lookup of attributes in the MCommunity service provide at the University of Michigan. It can be easily modifed to use other LDAP server configurations.}
12
+ spec.summary = %q{Authenticated LDAP lookup for MCommunity user attributes at University of Michigan.}
13
+ spec.description = %q{This gem provides authenticated LDAP lookups for user attributes in the MCommunity service at the University of Michigan. It supports encrypted connections (STARTTLS/LDAPS) and service accounts as required by UM IT Security (effective Jan 20, 2026). Can be easily modified for other LDAP server configurations.}
14
14
  spec.homepage = "https://github.com/rsmoke/ldap_lookup.git"
15
15
  spec.license = "MIT"
16
16
 
@@ -24,5 +24,6 @@ Gem::Specification.new do |spec|
24
24
  spec.add_development_dependency "bundler", "~> 2.2.26"
25
25
  spec.add_development_dependency "rake", "~> 13.0"
26
26
  spec.add_development_dependency "rspec", "~> 3.7.0"
27
+ spec.add_development_dependency "dotenv", "~> 2.8"
27
28
  spec.add_dependency 'net-ldap', '~> 0.18.0'
28
29
  end
data/ldaptest.rb CHANGED
@@ -1,5 +1,12 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ # Load .env file if it exists (for local development)
4
+ begin
5
+ require 'dotenv/load'
6
+ rescue LoadError
7
+ # dotenv not available, will use environment variables or fallbacks
8
+ end
9
+
3
10
  require_relative "lib/ldap_lookup"
4
11
 
5
12
  class Ldaptest
@@ -7,10 +14,20 @@ class Ldaptest
7
14
 
8
15
  ############## CONFIGURATION BLOCK ###################
9
16
  LdapLookup.configuration do |config|
10
- config.host = "ldap.umich.edu"
11
- config.base = "dc=umich,dc=edu"
12
- config.dept_attribute = "umichPostalAddressData"
13
- config.group_attribute = "umichGroupEmail"
17
+ config.host = ENV['LDAP_HOST'] || "ldap.umich.edu"
18
+ config.port = ENV['LDAP_PORT'] || "389"
19
+ config.base = ENV['LDAP_BASE'] || "dc=umich,dc=edu"
20
+ # Leave username/password unset for anonymous binds
21
+ config.username = ENV['LDAP_USERNAME']
22
+ config.password = ENV['LDAP_PASSWORD']
23
+ # Read encryption from ENV, default to start_tls
24
+ encryption_str = ENV['LDAP_ENCRYPTION'] || 'start_tls'
25
+ config.encryption = encryption_str.to_sym
26
+ config.dept_attribute = ENV['LDAP_DEPT_ATTRIBUTE'] || "umichPostalAddressData"
27
+ config.group_attribute = ENV['LDAP_GROUP_ATTRIBUTE'] || "umichGroupEmail"
28
+ # Enable LDAP debug logging in this test runner
29
+ debug_str = ENV['LDAP_DEBUG']
30
+ config.debug = debug_str ? debug_str.to_s.downcase == 'true' : true
14
31
  end
15
32
  #######################################################
16
33
 
@@ -32,7 +49,7 @@ class Ldaptest
32
49
  end
33
50
 
34
51
  def result_box(answer)
35
- print "\e[2J\e[f"
52
+ # print "\e[2J\e[f"
36
53
  2.times { puts " " }
37
54
  puts "Your Results"
38
55
  puts "======================================================"
@@ -65,6 +82,7 @@ class Ldaptest
65
82
  puts "7: check if uid is member of a group"
66
83
  puts "+++++++++++++++++++++++++"
67
84
  puts "8: what time is it?"
85
+ puts "99: test LDAP connection (diagnostic)"
68
86
  puts "0: exit"
69
87
  puts ""
70
88
  print "Enter a number: "
@@ -80,6 +98,7 @@ class Ldaptest
80
98
  when 6 then result_box(LdapLookup.get_email_distribution_list(@group_uid))
81
99
  when 7 then result_box(LdapLookup.is_member_of_group?(@uid, @group_uid))
82
100
  when 8 then result_box(timestamp)
101
+ when 99 then result_box(LdapLookup.test_connection.inspect)
83
102
  when 0 then puts "you chose exit!"
84
103
  throw(:done)
85
104
  else
@@ -1,3 +1,3 @@
1
1
  module LdapLookup
2
- VERSION = "0.1.8"
2
+ VERSION = "2.0.1"
3
3
  end
data/lib/ldap_lookup.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require_relative 'helpers/configuration'
2
2
  require 'net/ldap'
3
+ require 'openssl'
3
4
 
4
5
  module LdapLookup
5
6
  extend Configuration
@@ -9,114 +10,442 @@ module LdapLookup
9
10
  define_setting :base
10
11
  define_setting :dept_attribute
11
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
18
+
19
+ def self.debug_log(message)
20
+ return unless debug
21
+
22
+ puts "[LDAP DEBUG] #{message}"
23
+ end
24
+
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
47
+ end
48
+
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
12
69
 
13
70
  def self.get_ldap_response(ldap)
14
71
  response = ldap.get_operation_result
15
- raise "Response Code: #{response.code}, Message: #{response.message}" unless response.code.zero?
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
16
93
  end
17
94
 
18
- def self.ldap_connection
19
- Net::LDAP.new(
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,
20
111
  host: host,
21
112
  port: port,
113
+ encryption: encryption,
22
114
  base: base,
23
- auth: { method: :anonymous }
24
- )
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
+ )
173
+ else
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
202
+ end
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
221
+ end
222
+
223
+ def self.ldap_connection
224
+ connection_params = {
225
+ host: host,
226
+ port: port.to_i,
227
+ base: base
228
+ }
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
25
280
  end
26
281
 
27
- def self.get_user_attribute(uniqname, attribute)
282
+ def self.get_user_attribute(uniqname, attribute, default_value = nil)
28
283
  ldap = ldap_connection
29
284
  search_param = uniqname
30
285
  result_attrs = [attribute]
286
+ found_value = nil
31
287
 
32
288
  search_filter = Net::LDAP::Filter.eq('uid', search_param)
33
289
 
34
- ldap.search(filter: search_filter, attributes: result_attrs) do |item|
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|
35
297
  value = item[attribute]&.first
36
- return value unless value.nil?
298
+ if value
299
+ found_value = value
300
+ break
301
+ end
37
302
  end
38
303
 
39
- "No #{attribute} found for #{uniqname}"
40
- ensure
41
- get_ldap_response(ldap)
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
42
316
  end
43
317
 
44
318
  def self.get_nested_attribute(uniqname, nested_attribute)
45
319
  ldap = ldap_connection
46
320
  search_param = uniqname
47
321
  # Specify the full nested attribute path using dot notation
48
- result_attrs = [nested_attribute.split('.').first]
49
-
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
+
50
328
  search_filter = Net::LDAP::Filter.eq('uid', search_param)
51
-
52
- ldap.search(filter: search_filter, attributes: result_attrs) do |item|
53
- # Split the string into key-value pairs
54
- if string1 = item[nested_attribute.split('.').first]&.first
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
55
340
  key_value_pairs = string1.split('}:{')
56
- # Find the key-value pair for addr1
57
- target_pair = key_value_pairs.find { |pair| pair.include?("#{nested_attribute.split('.').last}=") }
341
+ # Find the key-value pair for the nested attribute
342
+ target_pair = key_value_pairs.find { |pair| pair.include?("#{nested_attribute.split('.').last}=") }
58
343
  # Extract the target value
59
- target_pair_value = target_pair.split('=').last
60
- return target_pair_value unless target_pair_value.nil?
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
349
+ end
350
+ end
61
351
  end
62
352
  end
63
- "No #{nested_attribute} found for #{uniqname}"
64
353
 
65
- ensure
66
- get_ldap_response(ldap)
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
67
365
  end
68
366
 
69
367
  # method to check if a uid exist in LDAP
70
368
  def self.uid_exist?(uniqname)
71
369
  ldap = ldap_connection
72
370
  search_param = uniqname
371
+ found = false
73
372
 
74
373
  search_filter = Net::LDAP::Filter.eq('uid', search_param)
75
374
 
76
- ldap.search(filter: search_filter) do |item|
77
- return true if item['uid'].first == search_param
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}"
78
390
  end
79
391
 
80
- false
81
- ensure
82
- get_ldap_response(ldap)
392
+ found
83
393
  end
84
394
 
85
395
  def self.get_simple_name(uniqname)
86
- get_user_attribute(uniqname, 'displayname')
396
+ get_user_attribute(uniqname, 'displayname', 'not available')
87
397
  end
88
398
 
89
399
  def self.get_email(uniqname)
90
- get_user_attribute(uniqname, 'mail')
400
+ get_user_attribute(uniqname, 'mail', nil)
91
401
  end
92
402
 
93
403
  def self.get_dept(uniqname)
94
- get_nested_attribute(uniqname, 'umichpostaladdressdata.addr1')
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)
95
410
  end
96
411
 
97
412
  def self.is_member_of_group?(uid, group_name)
98
413
  ldap = ldap_connection
99
414
  search_param = group_name
100
415
  result_attrs = ['member']
416
+ found = false
101
417
 
102
418
  search_filter = Net::LDAP::Filter.join(
103
419
  Net::LDAP::Filter.eq('cn', search_param),
104
420
  Net::LDAP::Filter.eq('objectClass', 'group')
105
421
  )
106
422
 
107
- ldap.search(filter: search_filter, attributes: result_attrs) do |item|
423
+ perform_search(ldap, filter: search_filter, attributes: result_attrs, label: "is_member_of_group").each do |item|
108
424
  members = item['member']
109
- return true if members&.any? { |entry| entry.split(',').first.split('=')[1] == uid }
425
+ if members && members.any? { |entry| entry.split(',').first.split('=')[1] == uid }
426
+ found = true
427
+ break
428
+ end
110
429
  end
111
430
 
112
- false
113
- ensure
114
- get_ldap_response(ldap)
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
115
443
  end
116
444
 
117
445
  def self.get_email_distribution_list(group_name)
118
446
  ldap = ldap_connection
119
447
  result_hash = {}
448
+ found_data = false
120
449
 
121
450
  search_param = group_name
122
451
  result_attrs = %w[cn umichGroupEmail member]
@@ -126,16 +455,25 @@ module LdapLookup
126
455
  Net::LDAP::Filter.eq('objectClass', 'group')
127
456
  )
128
457
 
129
- ldap.search(filter: search_filter, attributes: result_attrs) do |item|
458
+ perform_search(ldap, filter: search_filter, attributes: result_attrs, label: "get_email_distribution_list").each do |item|
459
+ found_data = true
130
460
  result_hash['group_name'] = item['cn']&.first
131
461
  result_hash['group_email'] = item['umichGroupEmail']&.first
132
462
  members = item['member']&.map { |individual| individual.split(',').first.split('=')[1] }
133
463
  result_hash['members'] = members&.sort || []
134
464
  end
135
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
+
136
476
  result_hash
137
- ensure
138
- get_ldap_response(ldap)
139
477
  end
140
478
 
141
479
  def self.all_groups_for_user(uid)
@@ -144,12 +482,22 @@ module LdapLookup
144
482
 
145
483
  result_attrs = ['dn']
146
484
 
147
- ldap.search(filter: "member=uid=#{uid},ou=People,dc=umich,dc=edu", attributes: result_attrs) do |item|
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|
148
488
  item.each { |key, value| result_array << value.first.split('=')[1].split(',')[0] }
149
489
  end
150
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
+
151
501
  result_array.sort
152
- ensure
153
- get_ldap_response(ldap)
154
502
  end
155
- end
503
+ end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ldap_lookup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rick Smoke
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2023-09-12 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bundler
@@ -52,6 +51,20 @@ dependencies:
52
51
  - - "~>"
53
52
  - !ruby/object:Gem::Version
54
53
  version: 3.7.0
54
+ - !ruby/object:Gem::Dependency
55
+ name: dotenv
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.8'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.8'
55
68
  - !ruby/object:Gem::Dependency
56
69
  name: net-ldap
57
70
  requirement: !ruby/object:Gem::Requirement
@@ -66,25 +79,31 @@ dependencies:
66
79
  - - "~>"
67
80
  - !ruby/object:Gem::Version
68
81
  version: 0.18.0
69
- description: This module is to be used for anonymous lookup of attributes in the MCommunity
70
- service provide at the University of Michigan. It can be easily modifed to use other
71
- LDAP server configurations.
82
+ description: This gem provides authenticated LDAP lookups for user attributes in the
83
+ MCommunity service at the University of Michigan. It supports encrypted connections
84
+ (STARTTLS/LDAPS) and service accounts as required by UM IT Security (effective Jan
85
+ 20, 2026). Can be easily modified for other LDAP server configurations.
72
86
  email:
73
87
  - rsmoke@umich.edu
74
88
  executables: []
75
89
  extensions: []
76
90
  extra_rdoc_files: []
77
91
  files:
92
+ - ".env.example"
78
93
  - ".github/dependabot.yml"
79
94
  - ".gitignore"
95
+ - ".rspec"
96
+ - ".rspec_status"
80
97
  - CODE_OF_CONDUCT.md
81
98
  - Gemfile
82
99
  - Gemfile.lock
83
100
  - LICENSE.txt
84
101
  - README.md
85
102
  - Rakefile
103
+ - SETUP.md
86
104
  - bin/console
87
105
  - bin/setup
106
+ - config/initializers/ldap_lookup.rb.example
88
107
  - ldap_lookup.gemspec
89
108
  - ldaptest.rb
90
109
  - lib/helpers/configuration.rb
@@ -94,7 +113,6 @@ homepage: https://github.com/rsmoke/ldap_lookup.git
94
113
  licenses:
95
114
  - MIT
96
115
  metadata: {}
97
- post_install_message:
98
116
  rdoc_options: []
99
117
  require_paths:
100
118
  - lib
@@ -109,8 +127,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
109
127
  - !ruby/object:Gem::Version
110
128
  version: '0'
111
129
  requirements: []
112
- rubygems_version: 3.4.16
113
- signing_key:
130
+ rubygems_version: 3.6.9
114
131
  specification_version: 4
115
- summary: For anonymous lookup of user LDAP attributes.
132
+ summary: Authenticated LDAP lookup for MCommunity user attributes at University of
133
+ Michigan.
116
134
  test_files: []