diff --git a/app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py b/app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py new file mode 100644 index 000000000..33bd3a433 --- /dev/null +++ b/app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py @@ -0,0 +1,82 @@ +"""Add Contact objectClass and mailRecipient to LDAP schema. + +Revision ID: c5a9b3f2e8d7 +Revises: 8164b4a9e1f1, f1abf7ef2443 +Create Date: 2026-01-19 12:00:00.000000 + +""" + +from alembic import op +from dishka import AsyncContainer, Scope +from sqlalchemy import delete +from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession + +from entities import EntityType +from enums import EntityTypeNames +from ldap_protocol.ldap_schema.dto import EntityTypeDTO +from ldap_protocol.ldap_schema.entity_type_use_case import EntityTypeUseCase +from ldap_protocol.utils.queries import get_base_directories +from repo.pg.tables import queryable_attr as qa + +# revision identifiers, used by Alembic. +revision = "c5a9b3f2e8d7" +down_revision = "71e642808369" +branch_labels: None | str = None +depends_on: None | str = None + + +def upgrade(container: AsyncContainer) -> None: + """Add Contact objectClass and mailRecipient to LDAP schema.""" + + async def _create_entity_type( + connection: AsyncConnection, # noqa: ARG001 + ) -> None: + """Create Contact Entity Type.""" + async with container(scope=Scope.REQUEST) as cnt: + session = await cnt.get(AsyncSession) + entity_type_use_case = await cnt.get(EntityTypeUseCase) + + if not await get_base_directories(session): + return + + await entity_type_use_case.create( + EntityTypeDTO( + name=EntityTypeNames.CONTACT, + object_class_names=[ + "top", + "person", + "organizationalPerson", + "contact", + "mailRecipient", + ], + is_system=True, + ), + ) + + await session.commit() + + op.run_async(_create_entity_type) + + +def downgrade(container: AsyncContainer) -> None: + """Remove Contact objectClass and mailRecipient from LDAP schema.""" + + async def _delete_entity_type( + connection: AsyncConnection, # noqa: ARG001 + ) -> None: + """Delete Contact Entity Type.""" + async with container(scope=Scope.REQUEST) as cnt: + session = await cnt.get(AsyncSession) + + if not await get_base_directories(session): + return + + await session.execute( + delete(EntityType).where( + qa(EntityType.name) == EntityTypeNames.CONTACT, + ), + ) + + await session.commit() + + op.run_async(_delete_entity_type) diff --git a/app/constants.py b/app/constants.py index fe4cdf6da..f54d78a35 100644 --- a/app/constants.py +++ b/app/constants.py @@ -262,6 +262,16 @@ class EntityTypeData(TypedDict): "inetOrgPerson", ], ), + EntityTypeData( + name=EntityTypeNames.CONTACT, + object_class_names=[ + "top", + "person", + "organizationalPerson", + "contact", + "mailRecipient", + ], + ), EntityTypeData( name=EntityTypeNames.KRB_CONTAINER, object_class_names=["krbContainer"], diff --git a/app/enums.py b/app/enums.py index f0bec8c21..f482b928e 100644 --- a/app/enums.py +++ b/app/enums.py @@ -58,6 +58,7 @@ class EntityTypeNames(StrEnum): ORGANIZATIONAL_UNIT = "Organizational Unit" GROUP = "Group" USER = "User" + CONTACT = "Contact" KRB_CONTAINER = "KRB Container" KRB_PRINCIPAL = "KRB Principal" KRB_REALM_CONTAINER = "KRB Realm Container" diff --git a/interface b/interface index 97bbc08dd..f31962020 160000 --- a/interface +++ b/interface @@ -1 +1 @@ -Subproject commit 97bbc08dda7584f579f756d8b09abe60db67b47b +Subproject commit f31962020a6689e6a4c61fb3349db5b5c7895f92 diff --git a/tests/test_api/test_ldap_schema/test_object_class_router.py b/tests/test_api/test_ldap_schema/test_object_class_router.py index ce3eacb2c..6e04cecdc 100644 --- a/tests/test_api/test_ldap_schema/test_object_class_router.py +++ b/tests/test_api/test_ldap_schema/test_object_class_router.py @@ -16,7 +16,7 @@ @pytest.mark.asyncio -async def test_get_one_extended_object_class( +async def test_get_extended_object_classes( http_client: AsyncClient, ) -> None: """Test getting a single extended object class.""" @@ -26,7 +26,10 @@ async def test_get_one_extended_object_class( assert response.status_code == status.HTTP_200_OK data = response.json() assert isinstance(data, dict) - assert data.get("entity_type_names") == [EntityTypeNames.USER] + assert set(data.get("entity_type_names")) == { # type: ignore + EntityTypeNames.CONTACT, + EntityTypeNames.USER, + } @pytest.mark.parametrize(