From 3ad04320c52c226d61646e0f67730ef61545d8b3 Mon Sep 17 00:00:00 2001 From: "m.shvets" Date: Mon, 19 Jan 2026 13:21:39 +0300 Subject: [PATCH 1/7] Add: new CONTACT entity type and corresponding enum in constants and enums files --- .../c5a9b3f2e8d7_add_contact_object_class.py | 157 ++++++++++++++++++ app/constants.py | 10 ++ app/enums.py | 1 + interface | 2 +- 4 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py 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..82860fedb --- /dev/null +++ b/app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py @@ -0,0 +1,157 @@ +"""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, ObjectClass +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 ldap_protocol.utils.raw_definition_parser import ( + RawDefinitionParser as RDParser, +) +from repo.pg.tables import queryable_attr as qa + +# revision identifiers, used by Alembic. +revision = "c5a9b3f2e8d7" +down_revision = ("8164b4a9e1f1", "f1abf7ef2443") +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_object_classes( + connection: AsyncConnection, # noqa: ARG001 + ) -> None: + """Create Contact and mailRecipient object classes.""" + async with container(scope=Scope.REQUEST) as cnt: + session = await cnt.get(AsyncSession) + + if not await get_base_directories(session): + return + + mail_recipient_raw = ( + "( 1.2.840.113556.1.3.46 NAME 'mailRecipient' " + "SUP top AUXILIARY " + "MAY (mail $ cn $ displayName $ description) )" + ) + + mail_recipient_info = RDParser.get_object_class_info( + raw_definition=mail_recipient_raw, + ) + mail_recipient = await RDParser.create_object_class_by_info( + session=session, + object_class_info=mail_recipient_info, + ) + session.add(mail_recipient) + await session.flush() + + contact_raw = ( + "( 1.2.840.113556.1.5.15 NAME 'contact' " + "SUP organizationalPerson STRUCTURAL " + "MAY (displayName $ description $ telephoneNumber $ " + "mail $ mobile $ title $ department $ company $ " + "facsimileTelephoneNumber $ homePhone $ street $ " + "postalCode $ l $ st $ co $ c) )" + ) + + contact_info = RDParser.get_object_class_info( + raw_definition=contact_raw, + ) + contact = await RDParser.create_object_class_by_info( + session=session, + object_class_info=contact_info, + ) + session.add(contact) + await session.flush() + + await session.commit() + + 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_object_classes) + 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() + + async def _delete_object_classes( + connection: AsyncConnection, # noqa: ARG001 + ) -> None: + """Delete Contact and mailRecipient object classes.""" + 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(ObjectClass).where( + qa(ObjectClass.name) == "contact", + ), + ) + await session.execute( + delete(ObjectClass).where( + qa(ObjectClass.name) == "mailRecipient", + ), + ) + + await session.commit() + + op.run_async(_delete_entity_type) + op.run_async(_delete_object_classes) 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 From 6c5ea4028ffff794f77708481bf758a7122f89c4 Mon Sep 17 00:00:00 2001 From: "m.shvets" Date: Mon, 19 Jan 2026 13:23:38 +0300 Subject: [PATCH 2/7] Remove unnecessary session flush calls in contact object class migration --- app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py b/app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py index 82860fedb..af74d7f61 100644 --- a/app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py +++ b/app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py @@ -55,7 +55,6 @@ async def _create_object_classes( object_class_info=mail_recipient_info, ) session.add(mail_recipient) - await session.flush() contact_raw = ( "( 1.2.840.113556.1.5.15 NAME 'contact' " @@ -74,7 +73,6 @@ async def _create_object_classes( object_class_info=contact_info, ) session.add(contact) - await session.flush() await session.commit() From f019cb1f63729e8078b162f1b9af519569c12eff Mon Sep 17 00:00:00 2001 From: "m.shvets" Date: Mon, 19 Jan 2026 13:35:51 +0300 Subject: [PATCH 3/7] . --- app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py b/app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py index af74d7f61..03c0400bf 100644 --- a/app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py +++ b/app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py @@ -23,7 +23,7 @@ # revision identifiers, used by Alembic. revision = "c5a9b3f2e8d7" -down_revision = ("8164b4a9e1f1", "f1abf7ef2443") +down_revision = "71e642808369" branch_labels: None | str = None depends_on: None | str = None From 0b09be40301af283ccd1f9c0e4a1d17d23bdbd2a Mon Sep 17 00:00:00 2001 From: "m.shvets" Date: Mon, 19 Jan 2026 13:49:21 +0300 Subject: [PATCH 4/7] Update test for extended object class to include CONTACT entity type in assertions --- tests/test_api/test_ldap_schema/test_object_class_router.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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..b717d6473 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 @@ -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")) == { + EntityTypeNames.CONTACT, + EntityTypeNames.USER, + } @pytest.mark.parametrize( From 170a44105f9d98a0d20db95cf7bc861146450701 Mon Sep 17 00:00:00 2001 From: "m.shvets" Date: Mon, 19 Jan 2026 13:51:48 +0300 Subject: [PATCH 5/7] Fix test assertion for entity_type_names to ignore type checking --- tests/test_api/test_ldap_schema/test_object_class_router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b717d6473..e42aa1773 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 @@ -26,7 +26,7 @@ async def test_get_one_extended_object_class( assert response.status_code == status.HTTP_200_OK data = response.json() assert isinstance(data, dict) - assert set(data.get("entity_type_names")) == { + assert set(data.get("entity_type_names")) == { # type: ignore EntityTypeNames.CONTACT, EntityTypeNames.USER, } From 13d06586d3b4da3798cbac4f34140b99dca8feb4 Mon Sep 17 00:00:00 2001 From: "m.shvets" Date: Mon, 19 Jan 2026 16:49:58 +0300 Subject: [PATCH 6/7] Refactor: remove object class creation and deletion logic from contact object class migration --- .../c5a9b3f2e8d7_add_contact_object_class.py | 75 +------------------ 1 file changed, 1 insertion(+), 74 deletions(-) diff --git a/app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py b/app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py index 03c0400bf..33bd3a433 100644 --- a/app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py +++ b/app/alembic/versions/c5a9b3f2e8d7_add_contact_object_class.py @@ -11,14 +11,11 @@ from sqlalchemy import delete from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession -from entities import EntityType, ObjectClass +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 ldap_protocol.utils.raw_definition_parser import ( - RawDefinitionParser as RDParser, -) from repo.pg.tables import queryable_attr as qa # revision identifiers, used by Alembic. @@ -31,51 +28,6 @@ def upgrade(container: AsyncContainer) -> None: """Add Contact objectClass and mailRecipient to LDAP schema.""" - async def _create_object_classes( - connection: AsyncConnection, # noqa: ARG001 - ) -> None: - """Create Contact and mailRecipient object classes.""" - async with container(scope=Scope.REQUEST) as cnt: - session = await cnt.get(AsyncSession) - - if not await get_base_directories(session): - return - - mail_recipient_raw = ( - "( 1.2.840.113556.1.3.46 NAME 'mailRecipient' " - "SUP top AUXILIARY " - "MAY (mail $ cn $ displayName $ description) )" - ) - - mail_recipient_info = RDParser.get_object_class_info( - raw_definition=mail_recipient_raw, - ) - mail_recipient = await RDParser.create_object_class_by_info( - session=session, - object_class_info=mail_recipient_info, - ) - session.add(mail_recipient) - - contact_raw = ( - "( 1.2.840.113556.1.5.15 NAME 'contact' " - "SUP organizationalPerson STRUCTURAL " - "MAY (displayName $ description $ telephoneNumber $ " - "mail $ mobile $ title $ department $ company $ " - "facsimileTelephoneNumber $ homePhone $ street $ " - "postalCode $ l $ st $ co $ c) )" - ) - - contact_info = RDParser.get_object_class_info( - raw_definition=contact_raw, - ) - contact = await RDParser.create_object_class_by_info( - session=session, - object_class_info=contact_info, - ) - session.add(contact) - - await session.commit() - async def _create_entity_type( connection: AsyncConnection, # noqa: ARG001 ) -> None: @@ -103,7 +55,6 @@ async def _create_entity_type( await session.commit() - op.run_async(_create_object_classes) op.run_async(_create_entity_type) @@ -128,28 +79,4 @@ async def _delete_entity_type( await session.commit() - async def _delete_object_classes( - connection: AsyncConnection, # noqa: ARG001 - ) -> None: - """Delete Contact and mailRecipient object classes.""" - 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(ObjectClass).where( - qa(ObjectClass.name) == "contact", - ), - ) - await session.execute( - delete(ObjectClass).where( - qa(ObjectClass.name) == "mailRecipient", - ), - ) - - await session.commit() - op.run_async(_delete_entity_type) - op.run_async(_delete_object_classes) From 740f29e197e0566e01d2f483e67a7977593f8146 Mon Sep 17 00:00:00 2001 From: "m.shvets" Date: Mon, 19 Jan 2026 16:53:35 +0300 Subject: [PATCH 7/7] Rename test function for clarity: updated test_get_one_extended_object_class to test_get_extended_object_classes --- tests/test_api/test_ldap_schema/test_object_class_router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e42aa1773..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."""