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 +4 -4
- data/.env.example +17 -5
- data/.gitignore +1 -0
- data/.rspec_status +41 -41
- data/Gemfile.lock +1 -1
- data/README.md +184 -92
- data/SETUP.md +90 -38
- data/config/initializers/ldap_lookup.rb.example +24 -10
- data/ldaptest.rb +11 -3
- data/lib/ldap_lookup/version.rb +1 -1
- data/lib/ldap_lookup.rb +120 -17
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8975eb645723483f8856a1a6d8db9cbc5a161c18a5508c81489999e86d8a2ccc
|
|
4
|
+
data.tar.gz: 0ac5979c0acde4809c80f5e3e38a16c22bdc20c07d631b5068f56c0521f90b63
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
#
|
|
6
|
-
|
|
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
|
-
#
|
|
9
|
+
# Service account password (required with LDAP_BIND_DN)
|
|
9
10
|
LDAP_PASSWORD=your_password
|
|
10
11
|
|
|
11
|
-
#
|
|
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
|
-
#
|
|
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
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.
|
|
4
|
-
./spec/configuration_spec.rb[1:1:1:2] | passed | 0.
|
|
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.
|
|
7
|
-
./spec/configuration_spec.rb[1:1:2:2] | passed | 0.
|
|
8
|
-
./spec/configuration_spec.rb[1:1:3:1] | passed | 0.
|
|
9
|
-
./spec/configuration_spec.rb[1:1:3:2] | passed | 0.
|
|
10
|
-
./spec/configuration_spec.rb[1:2:1] | passed | 0.
|
|
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.
|
|
14
|
-
./spec/configuration_spec.rb[1:3:3] | passed | 0.
|
|
15
|
-
./spec/ldap_lookup_spec.rb[1:1:1:1] | passed |
|
|
16
|
-
./spec/ldap_lookup_spec.rb[1:1:2:1] | passed | 0.
|
|
17
|
-
./spec/ldap_lookup_spec.rb[1:1:3:1] | passed | 0.
|
|
18
|
-
./spec/ldap_lookup_spec.rb[1:2:1:1] | passed | 0.
|
|
19
|
-
./spec/ldap_lookup_spec.rb[1:2:2:1] | passed | 0.
|
|
20
|
-
./spec/ldap_lookup_spec.rb[1:3:1:1] | passed | 0.
|
|
21
|
-
./spec/ldap_lookup_spec.rb[1:3:2:1] | passed |
|
|
22
|
-
./spec/ldap_lookup_spec.rb[1:4:1:1] | passed | 0.
|
|
23
|
-
./spec/ldap_lookup_spec.rb[1:4:2:1] | passed | 0.
|
|
24
|
-
./spec/ldap_lookup_spec.rb[1:5:1:1] | passed |
|
|
25
|
-
./spec/ldap_lookup_spec.rb[1:5:2:1] | passed | 0.
|
|
26
|
-
./spec/ldap_lookup_spec.rb[1:5:3:1] | passed | 0.
|
|
27
|
-
./spec/ldap_lookup_spec.rb[1:5:4:1] | passed | 0.
|
|
28
|
-
./spec/ldap_lookup_spec.rb[1:6:1:1] | passed | 0.
|
|
29
|
-
./spec/ldap_lookup_spec.rb[1:6:1:2] | passed | 0.
|
|
30
|
-
./spec/ldap_lookup_spec.rb[1:6:1:3] | passed | 0.
|
|
31
|
-
./spec/ldap_lookup_spec.rb[1:6:2:1] | passed | 0.
|
|
32
|
-
./spec/ldap_lookup_spec.rb[1:7:1:1] | passed |
|
|
33
|
-
./spec/ldap_lookup_spec.rb[1:7:1:2] | passed |
|
|
34
|
-
./spec/ldap_lookup_spec.rb[1:7:1:3] | passed |
|
|
35
|
-
./spec/ldap_lookup_spec.rb[1:7:2:1] | passed |
|
|
36
|
-
./spec/ldap_lookup_spec.rb[1:8:1:1] | passed | 0.
|
|
37
|
-
./spec/ldap_lookup_spec.rb[1:8:1:2] | passed | 0.
|
|
38
|
-
./spec/ldap_lookup_spec.rb[1:8:1:3] | passed | 0.
|
|
39
|
-
./spec/ldap_lookup_spec.rb[1:8:2:1] | passed | 0.
|
|
40
|
-
./spec/ldap_lookup_spec.rb[1:8:3:1] | passed | 0.
|
|
41
|
-
./spec/ldap_lookup_spec.rb[1:9:1:1] | passed | 0.
|
|
42
|
-
./spec/ldap_lookup_spec.rb[1:9:2:1] | passed | 0.
|
|
43
|
-
./spec/ldap_lookup_spec.rb[1:10:1:1] | passed | 0.
|
|
44
|
-
./spec/ldap_lookup_spec.rb[1:10:2:1] | passed | 0.
|
|
45
|
-
./spec/ldap_lookup_spec.rb[1:11:1:1] |
|
|
46
|
-
./spec/ldap_lookup_spec.rb[1:11:2:1] | passed | 0.
|
|
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
data/README.md
CHANGED
|
@@ -1,64 +1,74 @@
|
|
|
1
1
|
# LdapLookup for Ruby [](https://badge.fury.io/rb/ldap_lookup)
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
###
|
|
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
|
|
13
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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.
|
|
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
|
-
|
|
69
|
+
## Installation
|
|
60
70
|
|
|
61
|
-
|
|
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
|
-
|
|
85
|
+
### Step 2: Get LDAP Credentials
|
|
76
86
|
|
|
77
87
|
**For Production Applications (Recommended):**
|
|
78
|
-
|
|
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
|
-
|
|
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 = '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
242
|
+
**uid_exist?** returns true if uid is in LDAP.
|
|
188
243
|
|
|
189
|
-
|
|
190
|
-
```
|
|
244
|
+
```ruby
|
|
191
245
|
LdapLookup.uid_exist?(uniqname)
|
|
192
246
|
response: true or false (boolean)
|
|
193
247
|
```
|
|
194
|
-
|
|
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 "
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
336
|
+
### Never do this (insecure)
|
|
337
|
+
|
|
246
338
|
```bash
|
|
247
|
-
#
|
|
339
|
+
# DON'T: Password visible in process list
|
|
248
340
|
LDAP_PASSWORD=xxx bundle exec rspec
|
|
249
341
|
```
|
|
250
342
|
|
|
251
|
-
|
|
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](
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
3
|
+
This guide helps you set up `ldap_lookup` in a Rails app.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Overview
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
-
|
|
60
|
-
|
|
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
|
-
###
|
|
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=
|
|
113
|
+
config.bind_dn = 'cn=service-account,ou=Applications,o=services'
|
|
69
114
|
```
|
|
70
115
|
|
|
71
|
-
Your IT department
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
-
|
|
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
|
-
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
-
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
21
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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' :
|
|
38
|
+
config.debug = debug_str ? debug_str.to_s.downcase == 'true' : false
|
|
31
39
|
end
|
|
32
40
|
#######################################################
|
|
33
41
|
|
data/lib/ldap_lookup/version.rb
CHANGED
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
|
-
|
|
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
|
-
|
|
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}
|
|
173
|
+
"uid=#{username},#{user_dn_base}"
|
|
103
174
|
end
|
|
104
175
|
|
|
105
|
-
search_base = username_present ?
|
|
106
|
-
search_filter =
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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}
|
|
487
|
-
|
|
488
|
-
|
|
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
|