Repository navigation
Conversation
joewaltman
left a comment
There was a problem hiding this comment.
I tested this against a real OpenLDAP directory, comparing develop (de21b4c) with this branch (09a99a7). The main change works, but I found one regression for setups without a bind user.
Setup
- phpIPAM in the
phpipam/phpipam-wwwDocker image (Apache, PHP 8.3.36), MariaDB loaded fromdb/SCHEMA.sql, OpenLDAP 2.4.57 (osixia/openldap:1.5.0), no TLS. - LDAP method: base DN
ou=people,dc=example,dc=org, UID attributeuid, bind user set to a read-only account. The directory denies anonymous search (that image's default ACL). - Directory entries:
uid=alice,ou=people,dc=example,dc=org(fits the olduid=<user>,<base_dn>pattern)uid=bob,ou=engineering,ou=people,dc=example,dc=org(nested OU)cn=Carol Jones,ou=staff,ou=people,dc=example,dc=orgwithuid: carol(RDN is not the uid)uid=dupunder bothou=engineeringandou=staff
- phpIPAM users with the LDAP method for each of those, plus users literally named
*,b*,bob)and*)(uid=*for the filter tests.
Results with a bind user configured
| Login | develop | this PR |
|---|---|---|
| alice, correct password | OK | OK |
| bob, correct password | rejected | OK |
| carol, correct password | rejected | OK |
| alice and bob, wrong password | rejected | rejected |
*, b*, bob), *)(uid=* with bob's password |
rejected | rejected |
| dup (two matching entries) | rejected | rejected, logged as "Could not resolve a unique LDAP DN" |
Users DN set to ou=engineering,... (base DN dc=example,dc=org): bob / alice |
OK / rejected | OK / rejected |
The slapd log shows the username escaped in the filter: (uid=\2A), (uid=b\2A), (uid=bob\29), (uid=\2A\29\28uid=\2A). With the Users DN set, the search base is the Users DN, as expected. No PHP warnings or errors in the web server log during any of these.
Problems
1. Logins that work today break when no bind user is set and the directory denies anonymous search. The form describes the bind user as optional. With it left empty, alice logs in on develop but is rejected with this PR: the search runs anonymously, returns nothing (slapd: err=32 nentries=0), and the login stops there with "Invalid username or password". If the directory allows anonymous read, the PR works without a bind user (alice, bob and carol all log in).
Suggestion: only reject outright when the search finds more than one entry, and fall back to the old DN when it finds none or cannot run. I tried this on top of the PR. Every login in the table above gave the same result (I did not rerun the Users DN row), and without a bind user alice logs in again (bob is still rejected, since there is no way to find his DN):
// Never authenticate an ambiguous UID.
if ($entries !== false && $entries['count'] > 1) {
// ... same rejection as now ...
}
if ($entries === false || $entries['count'] === 0 || empty($entries[0]['dn'])) {
// Search not possible (e.g. no bind user and anonymous search is denied) or no match:
// fall back to the previous DN pattern and let the bind decide.
$userDn = $uidAttr.'='.ldap_escape($username, '', LDAP_ESCAPE_DN).','.$searchBase;
} else {
$userDn = $entries[0]['dn'];
}2. The bind user is now actually used, which changes behaviour for existing configs. On develop the check looks at admin_username / admin_password, so the bind user is never used (slapd shows no bind as the read-only account on develop). The camelCase fix here is right, but an install with wrong bind credentials saved goes from working to "Error: Bind to Active Directory failed. Check the login credentials and/or server details." on the login page. I confirmed that with alice: OK on develop, that error with the PR. Probably worth a line in the description or the release notes.
3. Formatting. The diff undoes the Pint formatting in the functions it touches (double quotes, # comments, a space before the argument list, one-line if/elseif, the missing blank line before the NetIQ docblock). ./vendor/bin/pint --test functions/classes/class.User.php fails on this branch; running pint on the file changes about 25 lines and would shrink the diff to the real change. CONTRIBUTING.md asks to avoid formatting changes.
4. Small things. The description says the PR adds a require_once, but the current diff does not contain one; develop already has it in directory_connect(). A successful login now opens two LDAP connections and binds as the bind user three times, because directory_authenticate() connects again; passing the first connection through would avoid that. Not a blocker.
I only tested on PHP 8.3. With problem 1 addressed I would be glad to see this merged, and I am happy to retest.
The authentication-method form always stores adminUsername and adminPassword, using empty strings when no service account is configured. isset() therefore returned true and adLDAP attempted an anonymous bind, which fails on servers with 'disallow bind_anon' before the DN fallback can run. Treat blank values as no service account configured.
|
Thanks for the detailed review and for testing against a real directory. I addressed your suggestions in
I also merged the current I reproduced your matrix with PHP 8.3.36, MariaDB 10.11 and OpenLDAP 2.4.57:
Your point 1 is therefore fixed, and the slapd log confirms the escaped filter ( One additional regression turned up that your setup does not cover: the authentication-method form always stores The wrong bind credentials behavior is as you described: saved credentials are now actually used, so an invalid service account changes from "login works" to "Error: Bind ... failed". That is intentional, but I agree it belongs in the release notes. I would be glad if you could retest. |
Problem: LDAP authentication failed because phpIPAM constructed the user's distinguished name (DN) directly from the username, assuming the UID was part of the DN's relative distinguished name (RDN). This does not work for LDAP directories where users have a different DN structure.
Solution: The authentication process now searches the configured LDAP base DN using the configured UID attribute to resolve the user's actual DN. The username is properly escaped for LDAP filter queries, and authentication proceeds only if exactly one matching entry is found. Additionally, the service account configuration check was corrected, and
require_onceprevents duplicate class loading.