feat(security): Tier 5a — LDAP / Active Directory authentication (opt-in ldap) [26.6.34]#42
Merged
Merged
Conversation
added 4 commits
June 19, 2026 22:33
Begin Tier 5a (LDAP/AD) — Spring's ldapAuthentication(). Feature-gated `ldap`
module (optional ldap3 dep, mirroring the webauthn opt-in), the default build
untouched:
- LdapOperations port (search + bind) + LdapEntry — the seam that makes the
provider logic unit-testable without a live directory.
- escape_filter_value: RFC 4515 §3 filter-value escaping, preventing LDAP
injection via the username/DN.
- LdapAuthenticationProvider implements the Tier 1 AuthenticationProvider
(plugs into ProviderManager) via bind authentication: search the user DN
under a base+filter ({0} = escaped username), bind as that DN with the
password (the directory verifies it), then populate ROLE_<GROUP> authorities
from a group search ({0} = user DN). An empty password is rejected before
binding (a simple bind with an empty password is an anonymous bind = bypass).
Unknown user and wrong password both fail as "Bad credentials"
(enumeration-safe).
5 unit tests against a scriptable mock LdapOperations (bind + group→role,
wrong-password/unknown-user reject, empty-password reject, filter-injection
escaped, escape_filter_value specials). fmt + clippy clean (--features ldap).
Complete the Tier 5a LDAP surface: - ActiveDirectoryLdapAuthenticationProvider (Spring's ActiveDirectoryLdapAuthenticationProvider): binds as the userPrincipalName (username@domain — or a full UPN if supplied), then reads the user's memberOf group DNs and maps each leading CN to a ROLE_<CN> authority. Empty password rejected; bad credential fails uniformly. - cn_from_dn(): extracts the leading CN RDN of a DN (CN=Admins,... → Admins). - Ldap3Operations: the production LdapOperations adapter over ldap3 (LdapConnAsync, search with optional manager bind, simple_bind whose .success() turns a non-zero result code into a clean Err). Each op uses a fresh connection so a failed user bind never disturbs searches. 4 new tests (AD bind+memberOf→roles, AD wrong-password reject, AD empty-password reject, cn_from_dn) + an env-gated live-adapter smoke test (skipped without FIREFLY_TEST_LDAP_URL). 9 ldap tests green; fmt + clippy clean (--features ldap).
Applies the four actionable findings from the multi-agent adversarial review of the LDAP / Active Directory providers (7 confirmed, 3 were positive confirmations, 1 benign wire-cleanliness note): - Reject ambiguous user searches. Both providers took the first entry via `.into_iter().next()`, silently binding against / reading authorities from an arbitrary match when the filter matched more than one entry. A new single_entry() helper fails closed on >1 result, mirroring Spring's IncorrectResultSizeDataAccessException. - Propagate directory errors during authorities population. authorities_for (and the AD memberOf search) used unwrap_or_default(), authenticating the user with an empty role set on a transient directory error (silent privilege loss). Errors now propagate and fail the login, matching Spring's DefaultLdapAuthoritiesPopulator. - Guard SearchEntry::construct against panics. ldap3 0.11 has no fallible variant and panics on a malformed / non-schema-conformant entry; a compromised or MITM'd directory could send one. The construct call is now wrapped in catch_unwind so a bad entry becomes a clean Err rather than aborting the in-flight authentication task. - Correct the over-claimed "enumeration-safe" docs. The unknown-user and wrong-password paths return the same error value but, as in Spring's BindAuthenticator, the unknown-user path skips the bind, leaving a residual timing channel. The comment now states this honestly. Adds three tests: ambiguous_user_search_is_rejected, group_search_error_fails_the_login_not_silent_role_loss, and active_directory_rejects_ambiguous_member_of_search. 12/12 LDAP tests pass; fmt + clippy clean with --features ldap.
Bumps the workspace 26.6.33 -> 26.6.34 and ships the Tier 5a docs: - Parity appendix (EN + ES): LDAP / Active Directory row + dedicated section, with the Spring-faithful safety behaviours. - CHANGELOG v26.6.34 entry. - MODULES.md firefly-security LDAP note. - Rebuilt book PDF + EPUB (EN + ES). The docs reflect the hardening from the pre-release adversarial review: ambiguous-search rejection (IncorrectResultSizeDataAccessException), authorities-error propagation (no silent role loss), the malformed-entry panic guard, and the corrected (honest) enumeration-timing note.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Tier 5a — LDAP / Active Directory authentication
First of the Tier 5 "big subsystems", delivered as an opt-in
ldapfeature. Fully additive: the default build does not compile the new module, so there is no behaviour change to existing code.What's new
LdapAuthenticationProvider— bind authentication as anAuthenticationProvider(plugs into the Tier 1ProviderManager): search the user DN under a base+filter ((uid={0}), username RFC 4515-escaped), bind as that DN with the password (the directory verifies it), then map group membership ((member={0})) toROLE_<GROUP>authorities — Spring'sBindAuthenticator+DefaultLdapAuthoritiesPopulator.ActiveDirectoryLdapAuthenticationProvider— binds as theuserPrincipalName(user@domain) and maps the user'smemberOfgroups to roles.LdapOperationsport (+escape_filter_value,cn_from_dn,LdapEntry) with the productionLdap3Operationsadapter overldap3. The port makes the provider logic unit-testable without a live directory.Spring-faithful safety behaviours
IncorrectResultSizeDataAccessException).Verification
LdapOperations);fmt+clippy -D warningsclean with--features ldap.Ldap3Operationsadapter exercised by an integration test gated onFIREFLY_TEST_LDAP_URL(skipped when unset).cargo checkclean at 26.6.34.constructpanic, over-claimed enumeration docs) are all fixed here; the rest were positive confirmations / a benign wire-cleanliness note.Docs
Next Tier 5 subsystems (separate releases): SAML2, then ACL / domain-object security.