Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [unreleased]

### Added
- auth: Add optional `page_size` argument to `LDAPAuthentifier` (default 1000) for
LDAP paged search page size (#41).

### Fixed
- auth: Support LDAP paged results on subtree searches to avoid
`ldap.SIZELIMIT_EXCEEDED` (#41).

## [1.8.0] - 2026-05-22

### Added
Expand Down
77 changes: 72 additions & 5 deletions src/authentication/rfl/authentication/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
try:
import ldap
import ldap.filter
from ldap.controls import SimplePagedResultsControl
except ImportError as err:
raise ImportError("python-ldap is required for RFL LDAP Authentication") from err

Expand All @@ -20,6 +21,8 @@

logger = logging.getLogger(__name__)

DEFAULT_LDAP_PAGE_SIZE = 1000


class LDAPAuthentifier:
def __init__(
Expand All @@ -40,7 +43,13 @@ def __init__(
restricted_groups: Optional[List[str]] = None,
lookup_user_dn: bool = False,
lookup_as_user: Optional[bool] = True,
page_size: int = DEFAULT_LDAP_PAGE_SIZE,
):
if page_size <= 0:
raise LDAPAuthenticationError(
f"LDAP page_size must be a positive integer, got {page_size}"
)
self.page_size = page_size
self.uri = uri
self.cacert = cacert
self.user_base = user_base
Expand Down Expand Up @@ -130,6 +139,49 @@ def connection(self):
) from err
return connection

def _search_paged(
self,
connection: ldap.ldapobject.LDAPObject,
base: str,
scope: int,
search_filter: str,
attrlist: Optional[List[str]] = None,
) -> List:
"""Run an LDAP search with RFC 2696 paged results and return all pages."""
known_ldap_resp_ctrls = {
SimplePagedResultsControl.controlType: SimplePagedResultsControl,
}
page_control = SimplePagedResultsControl(True, size=self.page_size, cookie="")
results = []
while True:
msgid = connection.search_ext(
base,
scope,
search_filter,
attrlist=attrlist,
serverctrls=[page_control],
)
_, rdata, _, serverctrls = connection.result3(
msgid, resp_ctrl_classes=known_ldap_resp_ctrls
)
results.extend(rdata)
pctrls = [
control
for control in (serverctrls or [])
if control.controlType == SimplePagedResultsControl.controlType
]
if not pctrls:
logger.debug(
"LDAP server did not return paged results control for search "
"base %s",
base,
)
break
if not pctrls[0].cookie:
break
page_control.cookie = pctrls[0].cookie
return results

def _get_user_info(
self, connection: ldap.ldapobject.LDAPObject, user_dn: str
) -> Tuple[str, int]:
Expand Down Expand Up @@ -204,7 +256,8 @@ def _get_groups(
f"(member={ldap.filter.escape_filter_chars(user_dn)}){gid_filter}))"
)
try:
results = connection.search_s(
results = self._search_paged(
connection,
self.group_base,
ldap.SCOPE_SUBTREE,
search_filter,
Expand All @@ -214,6 +267,10 @@ def _get_groups(
raise LDAPAuthenticationError(
f"Unable to find group base {self.group_base}"
) from err
except ldap.SIZELIMIT_EXCEEDED as err:
raise LDAPAuthenticationError(
f"LDAP size limit exceeded on group search: {err}"
) from err
logger.debug(
"LDAP search base: %s, scope: subtree, filter: %s, results: %s",
self.group_base,
Expand Down Expand Up @@ -292,7 +349,8 @@ def _lookup_user_dn(self, user):
f"({self.user_name_attribute}={ldap.filter.escape_filter_chars(user)}))"
)
try:
results = connection.search_s(
results = self._search_paged(
connection,
self.user_base,
ldap.SCOPE_SUBTREE,
search_filter,
Expand All @@ -310,11 +368,15 @@ def _lookup_user_dn(self, user):
raise LDAPAuthenticationError(
f"Operations error on user DN lookup: {err}"
) from err
except ldap.SIZELIMIT_EXCEEDED as err:
raise LDAPAuthenticationError(
f"LDAP size limit exceeded on user DN lookup: {err}"
) from err
finally:
connection.unbind_s()
logger.debug(
"LDAP search base: %s, scope: subtree, filter: %s, results: %s",
self.group_base,
self.user_base,
search_filter,
str(results),
)
Expand Down Expand Up @@ -396,7 +458,8 @@ def _list_user_dn(self, connection):
"""Return list of all users name/pairs pairs in LDAP directory."""
search_filter = f"(objectClass={self.user_class})"
try:
results = connection.search_s(
results = self._search_paged(
connection,
self.user_base,
ldap.SCOPE_SUBTREE,
search_filter,
Expand All @@ -410,9 +473,13 @@ def _list_user_dn(self, connection):
raise LDAPAuthenticationError(
f"Operations error on users search: {err}"
) from err
except ldap.SIZELIMIT_EXCEEDED as err:
raise LDAPAuthenticationError(
f"LDAP size limit exceeded on users search: {err}"
) from err
logger.debug(
"LDAP search base: %s, scope: subtree, filter: %s, results: %s",
self.group_base,
self.user_base,
search_filter,
str(results),
)
Expand Down
Loading
Loading