diff --git a/mex/common/ldap/README.md b/mex/common/ldap/README.md new file mode 100644 index 00000000..fdc56242 --- /dev/null +++ b/mex/common/ldap/README.md @@ -0,0 +1,31 @@ +Helper extractor to extract data from Lightweight Directory Access Protocol (LDAP). + +Common use cases: +- extract employee accounts of your organization +- extract functional accounts of your organization + +Possible queries are for example the account name, surname, given name, or email. + +# Configuration + +For configuring the ldap connection, set the settings parameter `ldap_url` +(see `mex.common.settings` for further info) to an LDAP url (see +[LDAP URL definition](https://datatracker.ietf.org/doc/html/rfc2255#section-3) for +further information). + +# Extracting data + +Use the `LDAPConnector` from the `ldap.connector` module to extract data. + +# Transforming data + +The module `ldap.transform` contains functions for transforming LDAP data into MEx +models. + +The `mex_person.stableTargetId` attribute can be used in any entity that requires a +`PersonID`. + +# Convenience Functions + +The module `ldap.extract` holds convenience functions, e.g. for build a mapping from +query strings to `stableTargetId`s. diff --git a/mex/common/ldap/connector.py b/mex/common/ldap/connector.py index 4c3665b2..a797be50 100644 --- a/mex/common/ldap/connector.py +++ b/mex/common/ldap/connector.py @@ -43,19 +43,19 @@ def __init__(self, settings: BaseSettings) -> None: auto_bind=AUTO_BIND_NO_TLS, read_only=True, ) - self.connection = connection.__enter__() + self._connection = connection.__enter__() if not self._is_service_available(): raise MExError(f"LDAP service not available at url: {host}:{port}") def _is_service_available(self) -> bool: try: - return self.connection.server.check_availability() is True + return self._connection.server.check_availability() is True except LDAPExceptionError: return False def close(self) -> None: """Close the connector's underlying LDAP connection.""" - self.connection.__exit__(None, None, None) + self._connection.__exit__(None, None, None) def _fetch( self, model_cls: type[ModelT], /, **filters: str @@ -81,7 +81,7 @@ def _fetch( def _paged_ldap_search( self, fields: tuple[str], search_filter: str, search_base: str ) -> list[dict[str, str]]: - entries = self.connection.extend.standard.paged_search( + entries = self._connection.extend.standard.paged_search( search_base=search_base, search_filter=f"(&{search_filter})", attributes=fields, diff --git a/mex/common/ldap/extract.py b/mex/common/ldap/extract.py index 4d424f3f..b0460a93 100644 --- a/mex/common/ldap/extract.py +++ b/mex/common/ldap/extract.py @@ -7,7 +7,7 @@ from mex.common.types import Identifier -def get_merged_ids_by_attribute( +def _get_merged_ids_by_attribute( attribute: str, persons: Iterable[LDAPPerson], primary_source: ExtractedPrimarySource, @@ -54,7 +54,7 @@ def get_merged_ids_by_employee_ids( Returns: Mapping from `LDAPPerson.employeeID` to corresponding `Identity.merged_id` """ - return get_merged_ids_by_attribute("employeeID", persons, primary_source) + return _get_merged_ids_by_attribute("employeeID", persons, primary_source) def get_merged_ids_by_email( @@ -72,7 +72,7 @@ def get_merged_ids_by_email( Returns: Mapping from `LDAPPerson.mail` to corresponding `Identity.merged_id` """ - return get_merged_ids_by_attribute("mail", persons, primary_source) + return _get_merged_ids_by_attribute("mail", persons, primary_source) def get_merged_ids_by_query_string( diff --git a/mex/common/public_api/connector.py b/mex/common/public_api/connector.py index 8f7658f1..8d4a902b 100644 --- a/mex/common/public_api/connector.py +++ b/mex/common/public_api/connector.py @@ -209,14 +209,10 @@ def get_item(self, identifier: Identifier | UUID) -> PublicApiItem | None: try: response = self.request("GET", f"metadata/items/{identifier}") except HTTPError as error: - # if no rows in result set (error code 2) - if ( - # bw-compat to rki-mex-metadata before rev 2486424 - error.response.status_code == 500 - and error.response.json().get("code") == 2 - ) or error.response.status_code == 404: + # no rows in result set, return None + if error.response and error.response.status_code == 404: return None - # Re-raise any unexpected errors + # re-raise any unexpected errors raise error else: return PublicApiItem.parse_obj(response) diff --git a/mex/common/settings.py b/mex/common/settings.py index 6ee3ad2c..dc9cbe84 100644 --- a/mex/common/settings.py +++ b/mex/common/settings.py @@ -171,7 +171,12 @@ def get(cls: type[SettingsType]) -> SettingsType: ) ldap_url: SecretStr = Field( SecretStr("ldap://user:pw@ldap:636"), - description="LDAP server for person queries with authentication credentials.", + description="LDAP server for person queries with authentication credentials. " + "Must follow format `ldap://user:pw@host:port`, where " + "`user` is the username, and " + "`pw` is the password for authenticating against ldap, " + "`host` is the url of the ldap server, and " + "`port` is the port of the ldap server.", env="MEX_LDAP_URL", ) wiki_api_url: AnyUrl = Field( diff --git a/tests/ldap/conftest.py b/tests/ldap/conftest.py index 676a8d68..c9722426 100644 --- a/tests/ldap/conftest.py +++ b/tests/ldap/conftest.py @@ -57,8 +57,8 @@ def ldap_mocker(monkeypatch: MonkeyPatch) -> LDAPMocker: def mocker(results: PagedSearchResults) -> None: def __init__(self: LDAPConnector, settings: BaseSettings) -> None: - self.connection = MagicMock(spec=Connection, extend=Mock()) - self.connection.extend.standard.paged_search = MagicMock( + self._connection = MagicMock(spec=Connection, extend=Mock()) + self._connection.extend.standard.paged_search = MagicMock( side_effect=[ [dict(attributes=e) for e in entries] for entries in results ]