Skip to content

Commit

Permalink
feature/mx-1455-document-ldap-extractor (#33)
Browse files Browse the repository at this point in the history
This adds general documentation for the ldap extractor.

While reviewing the tests I noticed that they cover none of the
convenience functions (ldap.extract). Should I create a ticket for that?
  • Loading branch information
rababerladuseladim authored Oct 20, 2023
1 parent 57a7734 commit 7114ab7
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 17 deletions.
31 changes: 31 additions & 0 deletions mex/common/ldap/README.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 4 additions & 4 deletions mex/common/ldap/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions mex/common/ldap/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
10 changes: 3 additions & 7 deletions mex/common/public_api/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion mex/common/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions tests/ldap/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
Expand Down

0 comments on commit 7114ab7

Please sign in to comment.