Skip to content

[Port to dtq-dev] Issue dspace-customers#903: harden LDAP auth (email fallback, groupmap guard, logging) - #1408

Merged
milanmajchrak merged 3 commits into
dtq-devfrom
903-be/ldap-hardening
Aug 14, 2026
Merged

[Port to dtq-dev] Issue dspace-customers#903: harden LDAP auth (email fallback, groupmap guard, logging)#1408
milanmajchrak merged 3 commits into
dtq-devfrom
903-be/ldap-hardening

Conversation

@jr-rk

@jr-rk jr-rk commented Aug 11, 2026

Copy link
Copy Markdown

Problem

Three defects in LDAP authentication (LDAPAuthentication.java):

  1. A first-time LDAP login whose LDAP record has no mail attribute creates/updates an EPerson with a null e-mail, even when the user typed a valid e-mail to log in.
  2. A groupmap config entry missing its : separator throws ArrayIndexOutOfBoundsException and aborts group assignment.
  3. The distinguished name is dumped to stdout via System.out.println("dn:" + dn) instead of the logger.

Port of dataquest-dev/dspace-customers#903 (item 2). Only the LDAPAuthentication.java hunks are ported; the customer's authentication-ldap.cfg / Dockerfile changes stay on customer/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:

  • e-mail fallbackcustomer/vsb-tuo 40e30e6fdd.
  • groupmap guard + stdout→logcustomer/vsb-tuo a097b030b1 ("Added logs to see more info about ldap error") — not listed in the issue; the two cited siblings b64b7859b4 (authentication.cfg) and 1047f4d787 (Dockerfile) are config-only and out of scope.

Root cause

  1. setEpersonAttributes() set the e-mail only from ldap.ldapEmail; when that was empty there was no fallback to the address the user authenticated with.
  2. assignGroups() did String t[] = groupMap.split(":"); … t[1] with no length check.
  3. Debug leftover writing to stdout.

Change set

LDAPAuthentication.java only:

  • Add an overload setEpersonAttributes(context, eperson, ldap, netid, email); when ldap.ldapEmail is empty, fall back to the supplied email. The effective fix is in the self-register/create branch (a freshly created EPerson otherwise 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 with null, so other callers are unchanged.
  • In assignGroups(), guard the groupmap entry parse: split on the first : (split(":", 2)) and skip the entry (log an error naming the index, continue to the next groupmap.<n>) when it has fewer than 2 fields or either part is blank. This stops the original ArrayIndexOutOfBoundsException and also rejects an empty search fragment such as :group, which would otherwise match every DN and assign the mapped group to all LDAP users.
  • Replace System.out.println("dn:" + dn) with log.debug(LogHelper.getHeader(context, "assignGroups", "dn=" + dn)) — DEBUG (not INFO) to match the existing got DN line and keep a semi-sensitive DN off the default log level.

Not ported: the source's verbose per-iteration log.info array-dump lines (debug scaffolding) — omitted per reference/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)

  • DN log lowered INFODEBUG (Copilot: DN at INFO is semi-sensitive / verbose).
  • Reworded the setEpersonAttributes e-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.
  • Both Copilot review threads resolved. No behavior change.

Test evidence

$ mvn -pl dspace-api checkstyle:check   -> 0 violations, BUILD SUCCESS
$ mvn -pl dspace-api compile            -> BUILD SUCCESS

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:

  1. e-mail fallback fills a previously-null EPerson.mail in the create path;
  2. a groupmap entry with no : now logs an error and is skipped instead of throwing ArrayIndexOutOfBoundsException;
  3. the dn is logged via LogHelper at 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)

  • No automated test lands with this port. The login/e-mail paths need a directory server, but the groupmap guard does not — a unit test for the malformed-groupmap case is the one gap worth closing (tracked as follow-up rather than expanding this PR's scope).
  • Tracking issue is cross-repo (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.

… 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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 no mail attribute.
  • Guard groupmap parsing in assignGroups() to skip (and log) malformed entries instead of throwing ArrayIndexOutOfBoundsException.
  • Replace System.out.println() DN output with structured logging via LogHelper.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java Outdated
Comment thread dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java Outdated
- 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>
@jr-rk

jr-rk commented Aug 12, 2026

Copy link
Copy Markdown
Author

Addressed the Copilot review in 640f46b (non-blocking polish, no behavior change):

  • assignGroups: DN logged at DEBUG instead of INFO, matching the existing got DN line.
  • Reworded the setEpersonAttributes e-mail comment to match what the method actually does.

Both threads resolved. checkstyle:check + compile on dspace-api pass.

@jr-rk jr-rk self-assigned this Aug 13, 2026
@jr-rk
jr-rk requested a lite review from Copilot August 13, 2026 07:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Comment thread dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java Outdated
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 groupmap parsing 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 malformed authentication-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;

@jr-rk
jr-rk requested a review from milanmajchrak August 13, 2026 08:43
@milanmajchrak
milanmajchrak merged commit 6615b01 into dtq-dev Aug 14, 2026
14 checks passed
milanmajchrak added a commit that referenced this pull request Sep 9, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants