[Port to dtq-dev] Issue dspace-customers#903: harden LDAP auth (email fallback, groupmap guard, logging) - #1408
Conversation
… logging) Three isolated defects in LDAP login: - setEpersonAttributes now falls back to the login e-mail when LDAP provides none, so an EPerson is never created/updated with a null mail. - assignGroups guards a groupmap entry that has no ':' separator (previously an ArrayIndexOutOfBoundsException) -- it logs and skips the malformed entry. - The distinguished name is logged via LogHelper instead of System.out.println. Only the LDAPAuthentication.java hunks are ported; the customer's LDAP config (authentication-ldap.cfg, Dockerfile) stays on customer/vsb-tuo. The source's verbose per-iteration debug logging is intentionally omitted per reference/coding-standards.md. Port of dataquest-dev/dspace-customers#903 (item 2). Source: customer/vsb-tuo 40e30e6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR ports targeted hardening improvements to LDAPAuthentication to avoid bad/unsafe behavior during LDAP login (null EPerson email on first login, malformed groupmap entries crashing group assignment, and stdout debug output).
Changes:
- Pass the computed login email into
setEpersonAttributes()and add an overload to fall back to that email when LDAP provides nomailattribute. - Guard
groupmapparsing inassignGroups()to skip (and log) malformed entries instead of throwingArrayIndexOutOfBoundsException. - Replace
System.out.println()DN output with structured logging viaLogHelper.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Log the distinguished name at DEBUG (not INFO) in assignGroups, matching the existing "got DN" DEBUG line and keeping a semi-sensitive DN off the default log level. - Reword the setEpersonAttributes e-mail comment: the method leaves the e-mail unchanged when neither LDAP nor the login address supplies one; the non-null guarantee for the create paths lives in the caller, not here. Addresses Copilot review on #1408. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed the Copilot review in 640f46b (non-blocking polish, no behavior change):
Both threads resolved. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java:764
- This new config-parsing behavior (skip malformed groupmap entries and log an error) is unit-testable without an LDAP server. Since this package already has JUnit tests, adding a focused test for a missing/invalid ':' (and blank-side cases, if handled) would help prevent regressions in auth hardening.
if (t.length < 2) {
log.error(LogHelper.getHeader(context, "assignGroups",
"malformed groupmap entry at index " + groupmapIndex + ": " + groupMap +
" - missing ':' separator"));
groupMap = configurationService.getProperty(
The malformed-entry guard in assignGroups only rejected a missing ':'. An entry
with an empty left part (e.g. ":admins") still passed, leaving an empty
ldapSearchString; the subsequent containsIgnoreCase(dn, "" + ",") then matched
essentially every DN and assigned the mapped group to all LDAP users.
Tighten the guard to also reject blank left/right parts (StringUtils.isBlank),
and parse with split(":", 2) so a colon inside a DSpace group name is preserved
instead of truncated. Well-formed entries parse identically; only genuinely
malformed lines (":group", "group:", ":") are now skipped.
Addresses Copilot review on #1408.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (1)
dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java:766
- The new
groupmapparsing guard is a functional behavior change (skipping malformed entries instead of throwing) but there’s no automated test coverage for it. There are existing authentication unit tests in this module (e.g.ShibHeadersTest,IPMatcherTest), so adding a focused unit test for malformedauthentication-ldap.login.groupmap.<n>values (missing ':', leading ':', trailing ':', and whitespace-only parts) would help prevent regressions without requiring an LDAP server.
while (groupMap != null) {
String t[] = groupMap.split(":", 2);
if (t.length < 2 || StringUtils.isBlank(t[0]) || StringUtils.isBlank(t[1])) {
log.error(LogHelper.getHeader(context, "assignGroups",
"malformed groupmap entry at index " + groupmapIndex + ": " + groupMap +
" - expected '<ldapSearchFragment>:<dspaceGroupName>' with both parts non-empty"));
groupMap = configurationService.getProperty(
"authentication-ldap.login.groupmap." + ++groupmapIndex);
continue;
…v2 double-delete guard, LDAP hardening) to the v9 base (#1430) * Port #1409 to dtq-dev-9-base: fix(mediafilter): log unparsable-PDF filter-media errors as WARN, not ERROR (#1409) Source: b0c4850 (dtq-dev PR #1409) A corrupt or malformed PDF makes PDFBoxThumbnail and TikaTextExtractionFilter throw a parse IOException that MediaFilterServiceImpl re-logs at ERROR, flooding the nightly filter-media job and tripping log-based alerting even though the job already skips the file and continues. Both filters now catch the IOException, log at WARN and return null, so the bitstream is skipped cleanly. The encrypted -PDF branch (InvalidPasswordException) stays at ERROR. Applied verbatim -- both files are byte-identical with vanilla 9.3 on this branch, and the v9 `Loader.loadPDF(new RandomAccessReadBuffer(source))` rewrite did not disturb the catch chain the new block attaches to. No test on either branch covers this path (the source PR has none either). Co-authored-by: MatusBeke <matus.beke7@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Port #1407 to dtq-dev-9-base: [Port to dtq-dev] fix: SWORDv2 item double-delete with WorkflowManagerDefault (#1407) Source: f241c47 (dtq-dev PR #1407) ContainerManagerDSpace.doContainerDelete() now checks, before the final itemService.delete(), whether a DELETE event for the same item UUID is already queued on the context -- i.e. the item was deleted earlier in this transaction -- and skips the second delete. Historically that second delete threw when the item came in through WorkflowManagerDefault. Both delete paths keep deleteWrapper(); the guard is the explicit safety net the source PR settled on, in its NPE-hardened form (itemUUID.equals(event.getSubjectID()), constrained to Event.DELETE on Constants.ITEM subjects). As the source PR states, the current base does not double-delete on any path, so this is behaviour-neutral today. Applied verbatim: the file is byte-identical with vanilla 9.3 on this branch and Context.getEvents() / Event.getEventType / getSubjectType / getSubjectID are unchanged in v9. The new java.util.UUID import lands after java.util.TreeMap and org.dspace.event.Event after org.dspace.core.LogHelper, so checkstyle import order (com < jakarta < org) is preserved. Known gap carried over from the source PR: the negative branch of the guard (delete skipped because a DELETE event is already queued) has no test on any branch. The two existing Swordv2IT delete tests only cover the positive path. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Port #1408 to dtq-dev-9-base: [Port to dtq-dev] fix: harden LDAP auth (groupmap guard, DN logging) (#1408) Source: 6615b01 (dtq-dev PR #1408) Two of the source commit's three hunks are ported; the third is already here. Ported (both in assignGroups()): - A groupmap entry without a ':' separator, or with a blank left or right part, no longer reaches `t[1]` (ArrayIndexOutOfBoundsException) and no longer leaves an empty ldapSearchString -- which made containsIgnoreCase(dn, "" + ",") match essentially every DN and assign the mapped group to all LDAP users. The entry is now logged at ERROR with its index and skipped, and scanning continues at the next index. Parsing uses split(":", 2) so a colon inside a DSpace group name is preserved instead of truncated. - System.out.println("dn:" + dn) becomes log.debug(LogHelper.getHeader(context, "assignGroups", "dn=" + dn)) -- a semi-sensitive DN off the default log level and out of stdout. NOT ported -- VANILLA-COVERED: the e-mail fallback hunk (the 5-arg setEpersonAttributes overload plus its two call sites). That fix went upstream as 23b999e (PRs DSpace#11293/DSpace#11331, authored by DataQuest) and is in dspace-9.3: git merge-base --is-ancestor 23b999e dspace-9.3 -> 0 git diff dspace-9.3 origin/dtq-dev-9-base -- .../LDAPAuthentication.java -> empty On this branch setEpersonAttributes already has both overloads, both call sites already pass `email`, and `StringUtils.isNotEmpty(email)` occurs exactly twice -- the same count as on dtq-dev. Re-applying the hunk would have duplicated it. Added beyond the source commit: LDAPAuthenticationTest, 5 unit tests over the groupmap parsing (no LDAP server; ConfigurationService and GroupService mocked, assignGroups reached by reflection because it is private). The source PR lists the missing test under its own "Open points"; card BE-06's AC-3 needs exactly this behaviour proven, and with no LDAP server anywhere in the estate a unit test is the only way to prove it. Worth backporting to dtq-dev. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: jurinecko <juraj.roka@dataquest.sk> Co-authored-by: MatusBeke <matus.beke7@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Problem
Three defects in LDAP authentication (
LDAPAuthentication.java):mailattribute creates/updates anEPersonwith a null e-mail, even when the user typed a valid e-mail to log in.groupmapconfig entry missing its:separator throwsArrayIndexOutOfBoundsExceptionand aborts group assignment.System.out.println("dn:" + dn)instead of the logger.Port of dataquest-dev/dspace-customers#903 (item 2). Only the
LDAPAuthentication.javahunks are ported; the customer'sauthentication-ldap.cfg/ Dockerfile changes stay oncustomer/vsb-tuo.Source provenance (verified by diff, not commit message)
The issue's item 2 cites
b64b7859b4,40e30e6fdd,1047f4d787, but only one of those touches this file:customer/vsb-tuo40e30e6fdd.customer/vsb-tuoa097b030b1("Added logs to see more info about ldap error") — not listed in the issue; the two cited siblingsb64b7859b4(authentication.cfg) and1047f4d787(Dockerfile) are config-only and out of scope.Root cause
setEpersonAttributes()set the e-mail only fromldap.ldapEmail; when that was empty there was no fallback to the address the user authenticated with.assignGroups()didString t[] = groupMap.split(":"); … t[1]with no length check.Change set
LDAPAuthentication.javaonly:setEpersonAttributes(context, eperson, ldap, netid, email); whenldap.ldapEmailis empty, fall back to the suppliedemail. The effective fix is in the self-register/create branch (a freshly createdEPersonotherwise persists with a null mail); the already-registered-by-mail branch also passes it (harmless — that record was just looked up by that e-mail). The original 4-arg signature delegates withnull, so other callers are unchanged.assignGroups(), guard thegroupmapentry parse: split on the first:(split(":", 2)) and skip the entry (log an error naming the index,continueto the nextgroupmap.<n>) when it has fewer than 2 fields or either part is blank. This stops the originalArrayIndexOutOfBoundsExceptionand also rejects an empty search fragment such as:group, which would otherwise match every DN and assign the mapped group to all LDAP users.System.out.println("dn:" + dn)withlog.debug(LogHelper.getHeader(context, "assignGroups", "dn=" + dn))— DEBUG (not INFO) to match the existinggot DNline and keep a semi-sensitive DN off the default log level.Not ported: the source's verbose per-iteration
log.infoarray-dump lines (debug scaffolding) — omitted perreference/coding-standards.md(no narration logging). Only the functional guard + a single dn log line are kept. This accounts for the smaller diff vs the customer commits.Review addressed (commit
640f46bd88)INFO→DEBUG(Copilot: DN at INFO is semi-sensitive / verbose).setEpersonAttributese-mail comment to match the code — the method leaves the e-mail unchanged when neither LDAP nor the login address supplies one; the non-null guarantee for the create paths lives in the caller (lines 302–318), not this method.Test evidence
LDAP first-login (e-mail fallback path) is not reproducible in the local stack (no LDAP directory server), so per
docker-setup§7 that part is stated, not faked. The groupmap guard is pure config-string parsing and is unit-testable without LDAP — a focused test for the malformed-entry case is a reasonable follow-up (see open point below).Behaviour delta:
EPerson.mailin the create path;groupmapentry with no:now logs an error and is skipped instead of throwingArrayIndexOutOfBoundsException;LogHelperat DEBUG, not printed to stdout.Risk & rollback
Confined to LDAP login. The e-mail fallback only fills a value that was previously null; the groupmap guard only adds a skip on malformed config; the log line replaces stdout. Revert = drop the two commits on this branch.
Open points (non-blocking)
groupmapcase is the one gap worth closing (tracked as follow-up rather than expanding this PR's scope).dspace-customers#903), so GitHub's development section cannot auto-link it; the reference above is the trace. A status/provenance note is posted on use dspace.url to dspace.server.url #903.Notes / assumptions
LDAP directory config is customer-specific and intentionally left on
customer/vsb-tuo.