From 7d04d4cc9cf2f606547f3174942145d7bb5688d4 Mon Sep 17 00:00:00 2001 From: Milov Dmitriy Date: Thu, 5 Feb 2026 14:30:00 +0300 Subject: [PATCH 1/8] add: AttributeType system_flags task_1160 --- .../275222846605_initial_ldap_schema.py | 13 +- ...26a_add_system_flags_to_attribute_types.py | 167 ++++++++++++++++++ .../ldap_schema/adapters/attribute_type.py | 1 + app/api/ldap_schema/schema.py | 1 + app/entities.py | 1 + app/ioc.py | 6 + .../attribute_type_system_flags.py | 59 +++++++ .../ldap_schema/attribute_type_use_case.py | 25 +++ app/ldap_protocol/ldap_schema/dto.py | 1 + .../utils/raw_definition_parser.py | 1 + app/repo/pg/tables.py | 1 + tests/conftest.py | 33 ++-- .../test_attribute_type_router.py | 32 ++++ 13 files changed, 320 insertions(+), 21 deletions(-) create mode 100644 app/alembic/versions/2dadf40c026a_add_system_flags_to_attribute_types.py create mode 100644 app/ldap_protocol/ldap_schema/attribute_type_system_flags.py diff --git a/app/alembic/versions/275222846605_initial_ldap_schema.py b/app/alembic/versions/275222846605_initial_ldap_schema.py index 226c9270b..5bdc54dea 100644 --- a/app/alembic/versions/275222846605_initial_ldap_schema.py +++ b/app/alembic/versions/275222846605_initial_ldap_schema.py @@ -50,12 +50,11 @@ def upgrade(container: AsyncContainer) -> None: sa.Column("single_value", sa.Boolean(), nullable=False), sa.Column("no_user_modification", sa.Boolean(), nullable=False), sa.Column("is_system", sa.Boolean(), nullable=False), - sa.Column( - "is_included_anr", - sa.Boolean(), - nullable=True, - ), # NOTE: added in f24ed0e49df2_add_filter_anr.py sa.PrimaryKeyConstraint("id"), + # NOTE: added in 2dadf40c026a_.py + sa.Column("system_flags", sa.Integer(), nullable=False), + # NOTE: added in f24ed0e49df2_add_filter_anr.py # noqa: ERA001 + sa.Column("is_included_anr", sa.Boolean(), nullable=True), ) op.create_index( op.f("ix_AttributeTypes_oid"), @@ -359,6 +358,7 @@ async def _create_attribute_types(connection: AsyncConnection) -> None: # noqa: single_value=True, no_user_modification=False, is_system=True, + system_flags=0, is_included_anr=False, ), ) @@ -400,6 +400,9 @@ async def _modify_object_classes(connection: AsyncConnection) -> None: # noqa: # NOTE: it added in f24ed0e49df2_add_filter_anr.py op.drop_column("AttributeTypes", "is_included_anr") + # NOTE: added in 2dadf40c026a_.py + op.drop_column("AttributeTypes", "system_flags") + session.commit() diff --git a/app/alembic/versions/2dadf40c026a_add_system_flags_to_attribute_types.py b/app/alembic/versions/2dadf40c026a_add_system_flags_to_attribute_types.py new file mode 100644 index 000000000..90f7f02bb --- /dev/null +++ b/app/alembic/versions/2dadf40c026a_add_system_flags_to_attribute_types.py @@ -0,0 +1,167 @@ +"""Add systemFlags for AttributeTypes. + +Revision ID: 2dadf40c026a +Revises: f4e6cd18a01d +Create Date: 2026-02-04 09:33:33.218126 + +""" + +import sqlalchemy as sa +from alembic import op +from dishka import AsyncContainer +from sqlalchemy.orm import Session + +from entities import AttributeType +from ldap_protocol.ldap_schema.attribute_type_system_flags import ( + AttributeTypeSystemFlags, +) +from repo.pg.tables import queryable_attr as qa + +# revision identifiers, used by Alembic. +revision: None | str = "2dadf40c026a" +down_revision: None | str = "f4e6cd18a01d" +branch_labels: None | list[str] = None +depends_on: None | list[str] = None + + +_NON_REPLICATED_ATTRIBUTES_TYPE_NAMES = ( + "badPasswordTime", + "badPwdCount", + "bridgeheadServerListBL", + "dSCorePropagationData", + "frsComputerReferenceBL", + "fRSMemberReferenceBL", + "isMemberOfDL", + "isPrivilegeHolder", + "lastLogoff", + "lastLogon", + "logonCount", + "managedObjects", + "masteredBy", + "modifiedCount", + "msCOMPartitionSetLink", + "msCOMUserLink", + "msDSAuthenticatedToAccountlist", + "msDSCachedMembership", + "msDSCachedMembershipTimeStamp", + "msDSEnabledFeatureBL", + "msDSExecuteScriptPassword", + "msDSHostServiceAccountBL", + "msDSMasteredBy", + "msDSOIDToGroupLinkBL", + "msDSPSOApplied", + "msDSMembersForAzRoleBL", + "msDSNCType", + "msDSNonMembersBL", + "msDSObjectReferenceBL", + "msDSOperationsForAzRoleBL", + "msDSOperationsForAzTaskBL", + "msDSNCROReplicaLocationsBL", + "msDSReplicationEpoch", + "msDSRetiredReplNCSignatures", + "msDSTasksForAzRoleBL", + "msDSTasksForAzTaskBL", + "msDSRevealedDSAs", + "msDSKrbTgtLinkBL", + "msDSIsFullReplicaFor", + "msDSIsDomainFor", + "msDSIsPartialReplicaFor", + "msDSUSNLastSyncSuccess", + "msDSValueTypeReferenceBL", + "msDSTokenGroupNames", + "msDSTokenGroupNamesGlobalAndUniversal", + "msDSTokenGroupNamesNoGCAcceptable", + "msExchOwnerBL", + "msDFSRMemberReferenceBL", + "msDFSRComputerReferenceBL", + "netbootSCPBL", + "nonSecurityMemberBL", + "objDistName", + "objectGuid", + "partialAttributeDeletionList", + "partialAttributeSet", + "pekList", + "prefixMap", + "queryPolicyBL", + "replPropertyMetaData", + "replUpToDateVector", + "reports", + "repsFrom", + "repsTo", + "rIDNextRID", + "rIDPreviousAllocationPool", + "schemaUpdate", + "serverReferenceBL", + "serverState", + "siteObjectBL", + "subRefs", + "uSNChanged", + "uSNCreated", + "uSNLastObjRem", + "whenChanged", + "msSFU30PosixMemberOf", + "msTSPrimaryDesktopBL", + "msTSSecondaryDesktopBL", + "msDSBridgeHeadServersUsed", + "msDSClaimSharesPossibleValuesWithBL", + "msDSMembersOfResourcePropertyListBL", + "msTPMTpmInformationForComputerBL", + "msAuthzMemberRulesInCentralAccessPolicyBL", + "msDSGenerationId", + "msDSIsPrimaryComputerFor", + "msDSTDOEgressBL", + "msDSTDOIngressBL", + "msDSTransformationRulesCompiled", + "msDSIsMemberOfDLTransitive", + "msDSMemberTransitive", + "msDSParentDistName", + "msDSAssignedAuthNPolicySiloBL", + "msDSAuthNPolicySiloMembersBL", + "msDSUserAuthNPolicyBL", + "msDSComputerAuthNPolicyBL", + "msDSServiceAuthNPolicyBL", + "msDSAssignedAuthNPolicyBL", + "msDSKeyPrincipalBL", + "msDSKeyCredentialLinkBL", +) + + +def upgrade(container: AsyncContainer) -> None: # noqa: ARG001 + """Upgrade.""" + bind = op.get_bind() + session = Session(bind=bind) + + op.add_column( + "AttributeTypes", + sa.Column( + "system_flags", + sa.Integer(), + nullable=True, + server_default=sa.text("0"), + ), + ) + + session.execute(sa.update(AttributeType).values({"system_flags": 0})) + + session.execute( + sa.update(AttributeType) + .where( + qa(AttributeType.name).in_(_NON_REPLICATED_ATTRIBUTES_TYPE_NAMES), + ) + .values( + { + "system_flags": int( + AttributeTypeSystemFlags.ATTR_NOT_REPLICATED, + ), + }, + ), + ) + + op.alter_column("AttributeTypes", "system_flags", nullable=False) + + session.commit() + + +def downgrade(container: AsyncContainer) -> None: # noqa: ARG001 + """Downgrade.""" + op.drop_column("AttributeTypes", "system_flags") diff --git a/app/api/ldap_schema/adapters/attribute_type.py b/app/api/ldap_schema/adapters/attribute_type.py index ad1ea6516..73e5f32bc 100644 --- a/app/api/ldap_schema/adapters/attribute_type.py +++ b/app/api/ldap_schema/adapters/attribute_type.py @@ -44,6 +44,7 @@ def _convert_update_uschema_to_dto( single_value=request.single_value, no_user_modification=request.no_user_modification, is_system=False, + system_flags=0, is_included_anr=request.is_included_anr, ) diff --git a/app/api/ldap_schema/schema.py b/app/api/ldap_schema/schema.py index 9e6453eff..b3dabefb6 100644 --- a/app/api/ldap_schema/schema.py +++ b/app/api/ldap_schema/schema.py @@ -28,6 +28,7 @@ class AttributeTypeSchema(BaseModel, Generic[_IdT]): single_value: bool no_user_modification: bool is_system: bool + system_flags: int = 0 is_included_anr: bool = False object_class_names: list[str] = Field(default_factory=list) diff --git a/app/entities.py b/app/entities.py index 807df8565..8309f510a 100644 --- a/app/entities.py +++ b/app/entities.py @@ -69,6 +69,7 @@ class AttributeType: single_value: bool = False no_user_modification: bool = False is_system: bool = False + system_flags: int = 0 # NOTE: ms-adts/cf133d47-b358-4add-81d3-15ea1cff9cd9 # see section 3.1.1.2.3 `searchFlags` (fANR) for details is_included_anr: bool = False diff --git a/app/ioc.py b/app/ioc.py index d6489f842..9c865dcee 100644 --- a/app/ioc.py +++ b/app/ioc.py @@ -78,6 +78,9 @@ LDAPUnbindRequestContext, ) from ldap_protocol.ldap_schema.attribute_type_dao import AttributeTypeDAO +from ldap_protocol.ldap_schema.attribute_type_system_flags import ( + AttributeTypeSystemFlagsUseCase, +) from ldap_protocol.ldap_schema.attribute_type_use_case import ( AttributeTypeUseCase, ) @@ -435,6 +438,9 @@ def get_dhcp_mngr( scope=Scope.RUNTIME, ) attribute_type_dao = provide(AttributeTypeDAO, scope=Scope.REQUEST) + attribute_type_system_flags_use_case = provide( + AttributeTypeSystemFlagsUseCase, scope=Scope.REQUEST + ) object_class_dao = provide(ObjectClassDAO, scope=Scope.REQUEST) entity_type_dao = provide(EntityTypeDAO, scope=Scope.REQUEST) attribute_type_use_case = provide( diff --git a/app/ldap_protocol/ldap_schema/attribute_type_system_flags.py b/app/ldap_protocol/ldap_schema/attribute_type_system_flags.py new file mode 100644 index 000000000..0618e4914 --- /dev/null +++ b/app/ldap_protocol/ldap_schema/attribute_type_system_flags.py @@ -0,0 +1,59 @@ +"""SystemFlags helpers for LDAP schema objects. + +Copyright (c) 2026 MultiFactor +License: https://github.com/MultiDirectoryLab/MultiDirectory/blob/main/LICENSE +""" + +from __future__ import annotations + +from enum import IntFlag + +from entities import AttributeType + + +class AttributeTypeSystemFlags(IntFlag): + """SystemFlags for attributeSchema objects in AD. + + Bits from 7 to 25 unused. Must be zero and ignored. + ms-adts/1e38247d-8234-4273-9de3-bbf313548631 + """ + + ATTR_NOT_REPLICATED = 0x00000001 + ATTR_REQ_PARTIAL_SET_MEMBER = 0x00000002 + ATTR_IS_CONSTRUCTED = 0x00000004 + ATTR_IS_OPERATIONAL = 0x00000008 + SCHEMA_BASE_OBJECT = 0x00000010 + ATTR_IS_RDN = 0x00000020 + DISALLOW_MOVE_ON_DELETE = 0x02000000 + DOMAIN_DISALLOW_MOVE = 0x04000000 + DOMAIN_DISALLOW_RENAME = 0x08000000 + CONFIG_ALLOW_LIMITED_MOVE = 0x10000000 + CONFIG_ALLOW_MOVE = 0x20000000 + CONFIG_ALLOW_RENAME = 0x40000000 + DISALLOW_DELETE = 0x80000000 + + +class AttributeTypeSystemFlagsUseCase: + def is_replicated(self, attribute_type: AttributeType) -> bool: + """Check if attribute is replicated based on system_flags.""" + return not bool( + attribute_type.system_flags + & AttributeTypeSystemFlags.ATTR_NOT_REPLICATED, + ) + + def set_is_replicated( + self, + attribute_type: AttributeType, + need_to_replicate: bool, + ) -> None: + """Set/clear replication flag in systemFlags.""" + if not need_to_replicate: + attribute_type.system_flags = int( + attribute_type.system_flags + | AttributeTypeSystemFlags.ATTR_NOT_REPLICATED, + ) + else: + attribute_type.system_flags = int( + attribute_type.system_flags + & ~AttributeTypeSystemFlags.ATTR_NOT_REPLICATED, + ) diff --git a/app/ldap_protocol/ldap_schema/attribute_type_use_case.py b/app/ldap_protocol/ldap_schema/attribute_type_use_case.py index ebaf1f986..9b1d64508 100644 --- a/app/ldap_protocol/ldap_schema/attribute_type_use_case.py +++ b/app/ldap_protocol/ldap_schema/attribute_type_use_case.py @@ -7,8 +7,12 @@ from typing import ClassVar from abstract_service import AbstractService +from entities import AttributeType from enums import AuthorizationRules from ldap_protocol.ldap_schema.attribute_type_dao import AttributeTypeDAO +from ldap_protocol.ldap_schema.attribute_type_system_flags import ( + AttributeTypeSystemFlagsUseCase, +) from ldap_protocol.ldap_schema.dto import AttributeTypeDTO from ldap_protocol.ldap_schema.object_class_dao import ObjectClassDAO from ldap_protocol.utils.pagination import PaginationParams, PaginationResult @@ -20,10 +24,14 @@ class AttributeTypeUseCase(AbstractService): def __init__( self, attribute_type_dao: AttributeTypeDAO, + attribute_type_system_flags_use_case: AttributeTypeSystemFlagsUseCase, object_class_dao: ObjectClassDAO, ) -> None: """Init AttributeTypeUseCase.""" self._attribute_type_dao = attribute_type_dao + self._attribute_type_system_flags_use_case = ( + attribute_type_system_flags_use_case + ) self._object_class_dao = object_class_dao async def get(self, _id: str) -> AttributeTypeDTO: @@ -68,6 +76,23 @@ async def delete_all_by_names(self, names: list[str]) -> None: """Delete not system Attribute Types by names.""" return await self._attribute_type_dao.delete_all_by_names(names) + def is_replicated(self, attribute_type: AttributeType) -> bool: + """Check if attribute is replicated based on systemFlags.""" + return self._attribute_type_system_flags_use_case.is_replicated( + attribute_type, + ) + + def set_replication_flag( + self, + attribute_type: AttributeType, + need_to_replicate: bool, + ) -> None: + """Set replication flag in systemFlags.""" + self._attribute_type_system_flags_use_case.set_is_replicated( + attribute_type, + need_to_replicate, + ) + PERMISSIONS: ClassVar[dict[str, AuthorizationRules]] = { get.__name__: AuthorizationRules.ATTRIBUTE_TYPE_GET, create.__name__: AuthorizationRules.ATTRIBUTE_TYPE_CREATE, diff --git a/app/ldap_protocol/ldap_schema/dto.py b/app/ldap_protocol/ldap_schema/dto.py index 118a6e1e8..7699b6966 100644 --- a/app/ldap_protocol/ldap_schema/dto.py +++ b/app/ldap_protocol/ldap_schema/dto.py @@ -22,6 +22,7 @@ class AttributeTypeDTO(Generic[_IdT]): single_value: bool no_user_modification: bool is_system: bool + system_flags: int is_included_anr: bool id: _IdT = None # type: ignore object_class_names: set[str] = field(default_factory=set) diff --git a/app/ldap_protocol/utils/raw_definition_parser.py b/app/ldap_protocol/utils/raw_definition_parser.py index 0d3ddfa27..4fa7361e0 100644 --- a/app/ldap_protocol/utils/raw_definition_parser.py +++ b/app/ldap_protocol/utils/raw_definition_parser.py @@ -59,6 +59,7 @@ def create_attribute_type_by_raw( single_value=attribute_type_info.single_value, no_user_modification=attribute_type_info.no_user_modification, is_system=True, + system_flags=0, is_included_anr=False, ) diff --git a/app/repo/pg/tables.py b/app/repo/pg/tables.py index 5391c95d5..a13db43ae 100644 --- a/app/repo/pg/tables.py +++ b/app/repo/pg/tables.py @@ -343,6 +343,7 @@ def _compile_create_uc( Column("single_value", Boolean, nullable=False), Column("no_user_modification", Boolean, nullable=False), Column("is_system", Boolean, nullable=False), + Column("system_flags", Integer, nullable=False, server_default=text("0")), Column("is_included_anr", Boolean, nullable=False), Index("idx_attribute_types_name_gin_trgm", "name", postgresql_using="gin"), ) diff --git a/tests/conftest.py b/tests/conftest.py index efe46fd21..b19147531 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -99,6 +99,9 @@ LDAPUnbindRequestContext, ) from ldap_protocol.ldap_schema.attribute_type_dao import AttributeTypeDAO +from ldap_protocol.ldap_schema.attribute_type_system_flags import ( + AttributeTypeSystemFlagsUseCase, +) from ldap_protocol.ldap_schema.attribute_type_use_case import ( AttributeTypeUseCase, ) @@ -291,23 +294,12 @@ async def resolve() -> str: yield await dns_state_gateway.get_dns_manager_settings(resolver) weakref.finalize(resolver, resolver.close) - @provide(scope=Scope.REQUEST, provides=AttributeTypeDAO, cache=False) - def get_attribute_type_dao( - self, - session: AsyncSession, - ) -> AttributeTypeDAO: - """Get Attribute Type DAO.""" - return AttributeTypeDAO(session) - - @provide(scope=Scope.REQUEST, provides=ObjectClassDAO, cache=False) - def get_object_class_dao(self, session: AsyncSession) -> ObjectClassDAO: - """Get Object Class DAO.""" - return ObjectClassDAO(session=session) - - get_entity_type_dao = provide( - EntityTypeDAO, + attribute_type_dao = provide(AttributeTypeDAO, scope=Scope.REQUEST) + object_class_dao = provide(ObjectClassDAO, scope=Scope.REQUEST) + entity_type_dao = provide(EntityTypeDAO, scope=Scope.REQUEST) + attribute_type_system_flags_use_case = provide( + AttributeTypeSystemFlagsUseCase, scope=Scope.REQUEST, - cache=False, ) attribute_type_use_case = provide( AttributeTypeUseCase, @@ -1196,6 +1188,15 @@ async def attribute_type_dao( yield AttributeTypeDAO(session) +@pytest_asyncio.fixture(scope="function") +async def attribute_type_system_flags_use_case( + container: AsyncContainer, +) -> AsyncIterator[AttributeTypeSystemFlagsUseCase]: + """Get session and acquire after completion.""" + async with container(scope=Scope.APP) as container: + yield AttributeTypeSystemFlagsUseCase() + + @pytest_asyncio.fixture(scope="function") async def role_dao(container: AsyncContainer) -> AsyncIterator[RoleDAO]: """Get session and acquire after completion.""" diff --git a/tests/test_api/test_ldap_schema/test_attribute_type_router.py b/tests/test_api/test_ldap_schema/test_attribute_type_router.py index bc9018948..0d4e666ad 100644 --- a/tests/test_api/test_ldap_schema/test_attribute_type_router.py +++ b/tests/test_api/test_ldap_schema/test_attribute_type_router.py @@ -5,6 +5,9 @@ from httpx import AsyncClient from api.ldap_schema.schema import AttributeTypeSchema +from ldap_protocol.ldap_schema.attribute_type_system_flags import ( + AttributeTypeSystemFlags, +) from .test_attribute_type_router_datasets import ( test_delete_bulk_attribute_types_dataset, @@ -177,3 +180,32 @@ async def test_delete_bulk_attribute_types( f"/schema/attribute_type/{attribute_type_name}", ) assert response.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.asyncio +async def test_attribute_type_system_flags_is_replicated( + http_client: AsyncClient, +) -> None: + """Test attribute_type system_flags.""" + schema = AttributeTypeSchema[None]( + oid="1.2.3.5", + name="testAttributeNonReplicated", + syntax="1.3.6.1.4.1.1466.115.121.1.15", + single_value=True, + no_user_modification=False, + is_system=False, + system_flags=int(AttributeTypeSystemFlags.ATTR_NOT_REPLICATED), + is_included_anr=False, + ) + response = await http_client.post( + "/schema/attribute_type", + json=schema.model_dump(), + ) + assert response.status_code == status.HTTP_201_CREATED + + response = await http_client.get(f"/schema/attribute_type/{schema.name}") + assert response.status_code == status.HTTP_200_OK + data = response.json() + assert data.get("system_flags") == int( + AttributeTypeSystemFlags.ATTR_NOT_REPLICATED, + ) From b41350168166a61e20a9541690a095503ec17abf Mon Sep 17 00:00:00 2001 From: Milov Dmitriy Date: Thu, 5 Feb 2026 14:43:18 +0300 Subject: [PATCH 2/8] fix: ruff linter task_1160 --- app/ioc.py | 3 ++- interface | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/ioc.py b/app/ioc.py index 9c865dcee..a7ca5a846 100644 --- a/app/ioc.py +++ b/app/ioc.py @@ -439,7 +439,8 @@ def get_dhcp_mngr( ) attribute_type_dao = provide(AttributeTypeDAO, scope=Scope.REQUEST) attribute_type_system_flags_use_case = provide( - AttributeTypeSystemFlagsUseCase, scope=Scope.REQUEST + AttributeTypeSystemFlagsUseCase, + scope=Scope.REQUEST, ) object_class_dao = provide(ObjectClassDAO, scope=Scope.REQUEST) entity_type_dao = provide(EntityTypeDAO, scope=Scope.REQUEST) diff --git a/interface b/interface index e1ca5656a..3c92cf4f0 160000 --- a/interface +++ b/interface @@ -1 +1 @@ -Subproject commit e1ca5656aeabc20a1862aeaf11ded72feaa97403 +Subproject commit 3c92cf4f0fb155978a68e4bcd66241a0b799d1e0 From e3c4a4e3be2187f7a588dc4925b1c42bb1679c8d Mon Sep 17 00:00:00 2001 From: Milov Dmitriy Date: Mon, 9 Feb 2026 16:30:00 +0300 Subject: [PATCH 3/8] refactor: ldap schema task_1160 --- ...26a_add_system_flags_to_attribute_types.py | 2 +- app/enums.py | 1 + app/ioc.py | 2 +- .../ldap_schema/attribute_type_dao.py | 20 +++++---- ...> attribute_type_system_flags_use_case.py} | 30 +++++++++----- .../ldap_schema/attribute_type_use_case.py | 41 +++++++++++-------- .../ldap_schema/entity_type_dao.py | 17 ++++---- .../ldap_schema/entity_type_use_case.py | 12 +++--- .../ldap_schema/object_class_dao.py | 25 ++++------- .../ldap_schema/object_class_use_case.py | 14 +++---- tests/conftest.py | 26 +++++++++++- .../test_attribute_type_router.py | 32 --------------- tests/test_ldap/test_ldap_schema/__init__.py | 5 +++ ...st_attribute_type_system_flags_use_case.py | 31 ++++++++++++++ 14 files changed, 150 insertions(+), 108 deletions(-) rename app/ldap_protocol/ldap_schema/{attribute_type_system_flags.py => attribute_type_system_flags_use_case.py} (66%) create mode 100644 tests/test_ldap/test_ldap_schema/__init__.py create mode 100644 tests/test_ldap/test_ldap_schema/test_attribute_type_system_flags_use_case.py diff --git a/app/alembic/versions/2dadf40c026a_add_system_flags_to_attribute_types.py b/app/alembic/versions/2dadf40c026a_add_system_flags_to_attribute_types.py index 90f7f02bb..78f972b89 100644 --- a/app/alembic/versions/2dadf40c026a_add_system_flags_to_attribute_types.py +++ b/app/alembic/versions/2dadf40c026a_add_system_flags_to_attribute_types.py @@ -12,7 +12,7 @@ from sqlalchemy.orm import Session from entities import AttributeType -from ldap_protocol.ldap_schema.attribute_type_system_flags import ( +from ldap_protocol.ldap_schema.attribute_type_system_flags_use_case import ( AttributeTypeSystemFlags, ) from repo.pg.tables import queryable_attr as qa diff --git a/app/enums.py b/app/enums.py index 1f6e8f798..ec2a21549 100644 --- a/app/enums.py +++ b/app/enums.py @@ -150,6 +150,7 @@ class AuthorizationRules(IntFlag): ATTRIBUTE_TYPE_GET_PAGINATOR = auto() ATTRIBUTE_TYPE_UPDATE = auto() ATTRIBUTE_TYPE_DELETE_ALL_BY_NAMES = auto() + ATTRIBUTE_TYPE_SET_ATTR_REPLICATION_FLAG = auto() ENTITY_TYPE_GET = auto() ENTITY_TYPE_CREATE = auto() diff --git a/app/ioc.py b/app/ioc.py index a7ca5a846..36713eba9 100644 --- a/app/ioc.py +++ b/app/ioc.py @@ -78,7 +78,7 @@ LDAPUnbindRequestContext, ) from ldap_protocol.ldap_schema.attribute_type_dao import AttributeTypeDAO -from ldap_protocol.ldap_schema.attribute_type_system_flags import ( +from ldap_protocol.ldap_schema.attribute_type_system_flags_use_case import ( AttributeTypeSystemFlagsUseCase, ) from ldap_protocol.ldap_schema.attribute_type_use_case import ( diff --git a/app/ldap_protocol/ldap_schema/attribute_type_dao.py b/app/ldap_protocol/ldap_schema/attribute_type_dao.py index 30211f74c..0ace3d31a 100644 --- a/app/ldap_protocol/ldap_schema/attribute_type_dao.py +++ b/app/ldap_protocol/ldap_schema/attribute_type_dao.py @@ -56,9 +56,9 @@ def __init__(self, session: AsyncSession) -> None: """Initialize Attribute Type DAO with session.""" self.__session = session - async def get(self, _id: str) -> AttributeTypeDTO: + async def get(self, name: str) -> AttributeTypeDTO: """Get Attribute Type by id.""" - return _convert_model_to_dto(await self._get_one_raw_by_name(_id)) + return _convert_model_to_dto(await self._get_one_raw_by_name(name)) async def get_all(self) -> list[AttributeTypeDTO]: """Get all Attribute Types.""" @@ -82,7 +82,7 @@ async def create(self, dto: AttributeTypeDTO) -> None: + f" '{dto.name}' already exists.", ) - async def update(self, _id: str, dto: AttributeTypeDTO) -> None: + async def update(self, name: str, dto: AttributeTypeDTO) -> None: """Update Attribute Type. Docs: @@ -95,7 +95,7 @@ async def update(self, _id: str, dto: AttributeTypeDTO) -> None: can only be modified for non-system attributes to preserve LDAP schema integrity. """ - obj = await self._get_one_raw_by_name(_id) + obj = await self._get_one_raw_by_name(name) obj.is_included_anr = dto.is_included_anr @@ -106,9 +106,15 @@ async def update(self, _id: str, dto: AttributeTypeDTO) -> None: await self.__session.flush() - async def delete(self, _id: str) -> None: + async def update_sys_flags(self, name: str, dto: AttributeTypeDTO) -> None: + """Update system flags of Attribute Type.""" + obj = await self._get_one_raw_by_name(name) + obj.system_flags = dto.system_flags + await self.__session.flush() + + async def delete(self, name: str) -> None: """Delete Attribute Type.""" - attribute_type = await self._get_one_raw_by_name(_id) + attribute_type = await self._get_one_raw_by_name(name) await self.__session.delete(attribute_type) await self.__session.flush() @@ -150,7 +156,7 @@ async def _get_one_raw_by_name(self, name: str) -> AttributeType: async def get_all_by_names( self, names: list[str] | set[str], - ) -> list[AttributeTypeDTO]: + ) -> list[AttributeTypeDTO[int]]: """Get list of Attribute Types by names. :param list[str] names: Attribute Type names. diff --git a/app/ldap_protocol/ldap_schema/attribute_type_system_flags.py b/app/ldap_protocol/ldap_schema/attribute_type_system_flags_use_case.py similarity index 66% rename from app/ldap_protocol/ldap_schema/attribute_type_system_flags.py rename to app/ldap_protocol/ldap_schema/attribute_type_system_flags_use_case.py index 0618e4914..f75fed0e3 100644 --- a/app/ldap_protocol/ldap_schema/attribute_type_system_flags.py +++ b/app/ldap_protocol/ldap_schema/attribute_type_system_flags_use_case.py @@ -8,7 +8,7 @@ from enum import IntFlag -from entities import AttributeType +from ldap_protocol.ldap_schema.dto import AttributeTypeDTO class AttributeTypeSystemFlags(IntFlag): @@ -34,26 +34,36 @@ class AttributeTypeSystemFlags(IntFlag): class AttributeTypeSystemFlagsUseCase: - def is_replicated(self, attribute_type: AttributeType) -> bool: + async def is_attr_replicated( + self, + attribute_type_dto: AttributeTypeDTO, + ) -> bool: """Check if attribute is replicated based on system_flags.""" return not bool( - attribute_type.system_flags + attribute_type_dto.system_flags & AttributeTypeSystemFlags.ATTR_NOT_REPLICATED, ) - def set_is_replicated( + async def set_attr_replication_flag( self, - attribute_type: AttributeType, + attribute_type_dto: AttributeTypeDTO, need_to_replicate: bool, - ) -> None: + ) -> AttributeTypeDTO: """Set/clear replication flag in systemFlags.""" + if attribute_type_dto.is_system: + raise ValueError( + "Cannot change replication flag for system attribute types.", + ) + if not need_to_replicate: - attribute_type.system_flags = int( - attribute_type.system_flags + attribute_type_dto.system_flags = int( + attribute_type_dto.system_flags | AttributeTypeSystemFlags.ATTR_NOT_REPLICATED, ) else: - attribute_type.system_flags = int( - attribute_type.system_flags + attribute_type_dto.system_flags = int( + attribute_type_dto.system_flags & ~AttributeTypeSystemFlags.ATTR_NOT_REPLICATED, ) + + return attribute_type_dto diff --git a/app/ldap_protocol/ldap_schema/attribute_type_use_case.py b/app/ldap_protocol/ldap_schema/attribute_type_use_case.py index 9b1d64508..26a49672a 100644 --- a/app/ldap_protocol/ldap_schema/attribute_type_use_case.py +++ b/app/ldap_protocol/ldap_schema/attribute_type_use_case.py @@ -7,10 +7,9 @@ from typing import ClassVar from abstract_service import AbstractService -from entities import AttributeType from enums import AuthorizationRules from ldap_protocol.ldap_schema.attribute_type_dao import AttributeTypeDAO -from ldap_protocol.ldap_schema.attribute_type_system_flags import ( +from ldap_protocol.ldap_schema.attribute_type_system_flags_use_case import ( AttributeTypeSystemFlagsUseCase, ) from ldap_protocol.ldap_schema.dto import AttributeTypeDTO @@ -34,9 +33,9 @@ def __init__( ) self._object_class_dao = object_class_dao - async def get(self, _id: str) -> AttributeTypeDTO: - """Get Attribute Type by id.""" - dto = await self._attribute_type_dao.get(_id) + async def get(self, name: str) -> AttributeTypeDTO: + """Get Attribute Type by name.""" + dto = await self._attribute_type_dao.get(name) dto.object_class_names = await self._object_class_dao.get_object_class_names_include_attribute_type( # noqa: E501 dto.name, ) @@ -50,13 +49,13 @@ async def create(self, dto: AttributeTypeDTO) -> None: """Create Attribute Type.""" await self._attribute_type_dao.create(dto) - async def update(self, _id: str, dto: AttributeTypeDTO) -> None: + async def update(self, name: str, dto: AttributeTypeDTO) -> None: """Update Attribute Type.""" - await self._attribute_type_dao.update(_id, dto) + await self._attribute_type_dao.update(name, dto) - async def delete(self, _id: str) -> None: + async def delete(self, name: str) -> None: """Delete Attribute Type.""" - await self._attribute_type_dao.delete(_id) + await self._attribute_type_dao.delete(name) async def get_paginator( self, @@ -76,22 +75,29 @@ async def delete_all_by_names(self, names: list[str]) -> None: """Delete not system Attribute Types by names.""" return await self._attribute_type_dao.delete_all_by_names(names) - def is_replicated(self, attribute_type: AttributeType) -> bool: + async def is_attr_replicated(self, name: str) -> bool: """Check if attribute is replicated based on systemFlags.""" - return self._attribute_type_system_flags_use_case.is_replicated( - attribute_type, - ) + dtos = await self.get_all_by_names([name]) + if not dtos: + raise ValueError(f"Attribute Type with name '{name}' not found.") + dto = dtos[0] + return await self._attribute_type_system_flags_use_case.is_attr_replicated(dto) # noqa: E501 # fmt: skip - def set_replication_flag( + async def set_attr_replication_flag( self, - attribute_type: AttributeType, + name: str, need_to_replicate: bool, ) -> None: """Set replication flag in systemFlags.""" - self._attribute_type_system_flags_use_case.set_is_replicated( - attribute_type, + dtos = await self.get_all_by_names([name]) + if not dtos: + raise ValueError(f"Attribute Type with name '{name}' not found.") + dto = dtos[0] + dto = await self._attribute_type_system_flags_use_case.set_attr_replication_flag( # noqa: E501 + dto, need_to_replicate, ) + await self._attribute_type_dao.update_sys_flags(dto.name, dto) PERMISSIONS: ClassVar[dict[str, AuthorizationRules]] = { get.__name__: AuthorizationRules.ATTRIBUTE_TYPE_GET, @@ -99,4 +105,5 @@ def set_replication_flag( get_paginator.__name__: AuthorizationRules.ATTRIBUTE_TYPE_GET_PAGINATOR, # noqa: E501 update.__name__: AuthorizationRules.ATTRIBUTE_TYPE_UPDATE, delete_all_by_names.__name__: AuthorizationRules.ATTRIBUTE_TYPE_DELETE_ALL_BY_NAMES, # noqa: E501 + set_attr_replication_flag.__name__: AuthorizationRules.ATTRIBUTE_TYPE_SET_ATTR_REPLICATION_FLAG, # noqa: E501 } diff --git a/app/ldap_protocol/ldap_schema/entity_type_dao.py b/app/ldap_protocol/ldap_schema/entity_type_dao.py index abfdc49d1..1a708d711 100644 --- a/app/ldap_protocol/ldap_schema/entity_type_dao.py +++ b/app/ldap_protocol/ldap_schema/entity_type_dao.py @@ -85,9 +85,9 @@ async def create(self, dto: EntityTypeDTO[None]) -> None: f"Entity Type with name '{dto.name}' already exists.", ) - async def update(self, _id: str, dto: EntityTypeDTO[int]) -> None: + async def update(self, name: str, dto: EntityTypeDTO[int]) -> None: """Update an Entity Type.""" - entity_type = await self._get_one_raw_by_name(_id) + entity_type = await self._get_one_raw_by_name(name) try: await self.__object_class_dao.is_all_object_classes_exists( @@ -153,9 +153,9 @@ async def update(self, _id: str, dto: EntityTypeDTO[int]) -> None: f"names {dto.object_class_names} already exists.", ) - async def delete(self, _id: str) -> None: + async def delete(self, name: str) -> None: """Delete an Entity Type.""" - entity_type = await self._get_one_raw_by_name(_id) + entity_type = await self._get_one_raw_by_name(name) await self.__session.delete(entity_type) await self.__session.flush() @@ -182,10 +182,7 @@ async def get_paginator( session=self.__session, ) - async def _get_one_raw_by_name( - self, - name: str, - ) -> EntityType: + async def _get_one_raw_by_name(self, name: str) -> EntityType: """Get single Entity Type by name. :param str name: Entity Type name. @@ -203,14 +200,14 @@ async def _get_one_raw_by_name( ) return entity_type - async def get(self, _id: str) -> EntityTypeDTO: + async def get(self, name: str) -> EntityTypeDTO: """Get single Entity Type by name. :param str name: Entity Type name. :raise EntityTypeNotFoundError: If Entity Type not found. :return EntityType: Instance of Entity Type. """ - return _convert(await self._get_one_raw_by_name(_id)) + return _convert(await self._get_one_raw_by_name(name)) async def get_entity_type_by_object_class_names( self, diff --git a/app/ldap_protocol/ldap_schema/entity_type_use_case.py b/app/ldap_protocol/ldap_schema/entity_type_use_case.py index 5958e6a99..a9ccdbd83 100644 --- a/app/ldap_protocol/ldap_schema/entity_type_use_case.py +++ b/app/ldap_protocol/ldap_schema/entity_type_use_case.py @@ -42,10 +42,10 @@ async def create(self, dto: EntityTypeDTO) -> None: ) await self._entity_type_dao.create(dto) - async def update(self, _id: str, dto: EntityTypeDTO) -> None: + async def update(self, name: str, dto: EntityTypeDTO) -> None: """Update Entity Type.""" try: - entity_type = await self.get(_id) + entity_type = await self.get(name) except EntityTypeNotFoundError: raise EntityTypeCantModifyError @@ -53,13 +53,13 @@ async def update(self, _id: str, dto: EntityTypeDTO) -> None: raise EntityTypeCantModifyError( f"Entity Type '{dto.name}' is system and cannot be modified.", ) - if _id != dto.name: - await self._validate_name(name=_id) + if name != dto.name: + await self._validate_name(name=name) await self._entity_type_dao.update(entity_type.name, dto) - async def get(self, _id: str) -> EntityTypeDTO: + async def get(self, name: str) -> EntityTypeDTO: """Get Entity Type by name.""" - return await self._entity_type_dao.get(_id) + return await self._entity_type_dao.get(name) async def _validate_name( self, diff --git a/app/ldap_protocol/ldap_schema/object_class_dao.py b/app/ldap_protocol/ldap_schema/object_class_dao.py index 9bc29644e..83bcd7eef 100644 --- a/app/ldap_protocol/ldap_schema/object_class_dao.py +++ b/app/ldap_protocol/ldap_schema/object_class_dao.py @@ -77,9 +77,9 @@ async def get_object_class_names_include_attribute_type( ) # fmt: skip return set(row[0] for row in result.fetchall()) - async def delete(self, _id: str) -> None: + async def delete(self, name: str) -> None: """Delete Object Class.""" - object_class = await self._get_one_raw_by_name(_id) + object_class = await self._get_one_raw_by_name(name) await self.__session.delete(object_class) await self.__session.flush() @@ -245,14 +245,14 @@ async def _get_one_raw_by_name(self, name: str) -> ObjectClass: ) return object_class - async def get(self, _id: str) -> ObjectClassDTO: - """Get single Object Class by id. + async def get(self, name: str) -> ObjectClassDTO: + """Get single Object Class by name. - :param str _id: Object Class name. + :param str name: Object Class name. :raise ObjectClassNotFoundError: If Object Class not found. :return ObjectClass: Instance of Object Class. """ - return _converter(await self._get_one_raw_by_name(_id)) + return _converter(await self._get_one_raw_by_name(name)) async def get_all_by_names( self, @@ -273,16 +273,9 @@ async def get_all_by_names( ) # fmt: skip return list(map(_converter, query.all())) - async def update(self, _id: str, dto: ObjectClassDTO[None, str]) -> None: - """Modify Object Class. - - :param ObjectClassDTO object_class: Object Class. - :param ObjectClassDTO dto: New statement ObjectClass - :raise ObjectClassCantModifyError: If Object Class is system,\ - it cannot be changed. - :return None. - """ - obj = await self._get_one_raw_by_name(_id) + async def update(self, name: str, dto: ObjectClassDTO[None, str]) -> None: + """Update Object Class.""" + obj = await self._get_one_raw_by_name(name) if obj.is_system: raise ObjectClassCantModifyError( "System Object Class cannot be modified.", diff --git a/app/ldap_protocol/ldap_schema/object_class_use_case.py b/app/ldap_protocol/ldap_schema/object_class_use_case.py index c35a845ac..11c171a58 100644 --- a/app/ldap_protocol/ldap_schema/object_class_use_case.py +++ b/app/ldap_protocol/ldap_schema/object_class_use_case.py @@ -30,9 +30,9 @@ async def get_all(self) -> list[ObjectClassDTO[int, AttributeTypeDTO]]: """Get all Object Classes.""" return await self._object_class_dao.get_all() - async def delete(self, _id: str) -> None: + async def delete(self, name: str) -> None: """Delete Object Class.""" - await self._object_class_dao.delete(_id) + await self._object_class_dao.delete(name) async def get_paginator( self, @@ -45,9 +45,9 @@ async def create(self, dto: ObjectClassDTO[None, str]) -> None: """Create a new Object Class.""" await self._object_class_dao.create(dto) - async def get(self, _id: str) -> ObjectClassDTO: - """Get Object Class by id.""" - dto = await self._object_class_dao.get(_id) + async def get(self, name: str) -> ObjectClassDTO: + """Get Object Class by name.""" + dto = await self._object_class_dao.get(name) dto.entity_type_names = ( await self._entity_type_dao.get_entity_type_names_include_oc_name( dto.name, @@ -62,9 +62,9 @@ async def get_all_by_names( """Get list of Object Classes by names.""" return await self._object_class_dao.get_all_by_names(names) - async def update(self, _id: str, dto: ObjectClassDTO[None, str]) -> None: + async def update(self, name: str, dto: ObjectClassDTO[None, str]) -> None: """Modify Object Class.""" - await self._object_class_dao.update(_id, dto) + await self._object_class_dao.update(name, dto) async def delete_all_by_names(self, names: list[str]) -> None: """Delete not system Object Classes by Names.""" diff --git a/tests/conftest.py b/tests/conftest.py index b19147531..292198d3e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -99,7 +99,7 @@ LDAPUnbindRequestContext, ) from ldap_protocol.ldap_schema.attribute_type_dao import AttributeTypeDAO -from ldap_protocol.ldap_schema.attribute_type_system_flags import ( +from ldap_protocol.ldap_schema.attribute_type_system_flags_use_case import ( AttributeTypeSystemFlagsUseCase, ) from ldap_protocol.ldap_schema.attribute_type_use_case import ( @@ -295,12 +295,18 @@ async def resolve() -> str: weakref.finalize(resolver, resolver.close) attribute_type_dao = provide(AttributeTypeDAO, scope=Scope.REQUEST) + attribute_type_dao_app = provide(AttributeTypeDAO, scope=Scope.APP) object_class_dao = provide(ObjectClassDAO, scope=Scope.REQUEST) + object_class_dao_app = provide(ObjectClassDAO, scope=Scope.APP) entity_type_dao = provide(EntityTypeDAO, scope=Scope.REQUEST) attribute_type_system_flags_use_case = provide( AttributeTypeSystemFlagsUseCase, scope=Scope.REQUEST, ) + attribute_type_system_flags_use_case_app = provide( + AttributeTypeSystemFlagsUseCase, + scope=Scope.APP, + ) attribute_type_use_case = provide( AttributeTypeUseCase, scope=Scope.REQUEST, @@ -1197,6 +1203,24 @@ async def attribute_type_system_flags_use_case( yield AttributeTypeSystemFlagsUseCase() +@pytest_asyncio.fixture(scope="function") +async def attribute_type_use_case( + container: AsyncContainer, +) -> AsyncIterator[AttributeTypeUseCase]: + """Get session and acquire after completion.""" + async with container(scope=Scope.APP) as container: + attribute_type_dao = await container.get(AttributeTypeDAO) + object_class_dao = await container.get(ObjectClassDAO) + sys_flags_use_case = await container.get( + AttributeTypeSystemFlagsUseCase, + ) + yield AttributeTypeUseCase( + attribute_type_dao=attribute_type_dao, + object_class_dao=object_class_dao, + attribute_type_system_flags_use_case=sys_flags_use_case, + ) + + @pytest_asyncio.fixture(scope="function") async def role_dao(container: AsyncContainer) -> AsyncIterator[RoleDAO]: """Get session and acquire after completion.""" diff --git a/tests/test_api/test_ldap_schema/test_attribute_type_router.py b/tests/test_api/test_ldap_schema/test_attribute_type_router.py index 0d4e666ad..bc9018948 100644 --- a/tests/test_api/test_ldap_schema/test_attribute_type_router.py +++ b/tests/test_api/test_ldap_schema/test_attribute_type_router.py @@ -5,9 +5,6 @@ from httpx import AsyncClient from api.ldap_schema.schema import AttributeTypeSchema -from ldap_protocol.ldap_schema.attribute_type_system_flags import ( - AttributeTypeSystemFlags, -) from .test_attribute_type_router_datasets import ( test_delete_bulk_attribute_types_dataset, @@ -180,32 +177,3 @@ async def test_delete_bulk_attribute_types( f"/schema/attribute_type/{attribute_type_name}", ) assert response.status_code == status.HTTP_400_BAD_REQUEST - - -@pytest.mark.asyncio -async def test_attribute_type_system_flags_is_replicated( - http_client: AsyncClient, -) -> None: - """Test attribute_type system_flags.""" - schema = AttributeTypeSchema[None]( - oid="1.2.3.5", - name="testAttributeNonReplicated", - syntax="1.3.6.1.4.1.1466.115.121.1.15", - single_value=True, - no_user_modification=False, - is_system=False, - system_flags=int(AttributeTypeSystemFlags.ATTR_NOT_REPLICATED), - is_included_anr=False, - ) - response = await http_client.post( - "/schema/attribute_type", - json=schema.model_dump(), - ) - assert response.status_code == status.HTTP_201_CREATED - - response = await http_client.get(f"/schema/attribute_type/{schema.name}") - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert data.get("system_flags") == int( - AttributeTypeSystemFlags.ATTR_NOT_REPLICATED, - ) diff --git a/tests/test_ldap/test_ldap_schema/__init__.py b/tests/test_ldap/test_ldap_schema/__init__.py new file mode 100644 index 000000000..5134a2e61 --- /dev/null +++ b/tests/test_ldap/test_ldap_schema/__init__.py @@ -0,0 +1,5 @@ +"""Test __init__ module. + +Copyright (c) 2026 MultiFactor +License: https://github.com/MultiDirectoryLab/MultiDirectory/blob/main/LICENSE +""" diff --git a/tests/test_ldap/test_ldap_schema/test_attribute_type_system_flags_use_case.py b/tests/test_ldap/test_ldap_schema/test_attribute_type_system_flags_use_case.py new file mode 100644 index 000000000..ef445aa25 --- /dev/null +++ b/tests/test_ldap/test_ldap_schema/test_attribute_type_system_flags_use_case.py @@ -0,0 +1,31 @@ +"""Test AttributeTypeSystemFlagsUseCase. + +Copyright (c) 2026 MultiFactor +License: https://github.com/MultiDirectoryLab/MultiDirectory/blob/main/LICENSE +""" + +import pytest + +from ldap_protocol.ldap_schema.attribute_type_use_case import ( + AttributeTypeUseCase, +) + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("session") +@pytest.mark.usefixtures("setup_session") +async def test_attribute_type_system_flags_use_case_is_replicated( + attribute_type_use_case: AttributeTypeUseCase, +) -> None: + """Test get all Password Policy endpoint.""" + assert not await attribute_type_use_case.is_attr_replicated("netbootSCPBL") + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("session") +@pytest.mark.usefixtures("setup_session") +async def test_attribute_type_system_flags_use_case_is_not_replicated( + attribute_type_use_case: AttributeTypeUseCase, +) -> None: + """Test get all Password Policy endpoint.""" + assert await attribute_type_use_case.is_attr_replicated("objectClass") From e659bab96bdead1f8be7aa99199915373c91f10c Mon Sep 17 00:00:00 2001 From: Milov Dmitriy Date: Mon, 9 Feb 2026 17:05:57 +0300 Subject: [PATCH 4/8] refactor: migration task_1160 --- ...26a_add_system_flags_to_attribute_types.py | 42 +++++++++++-------- .../attribute_type_system_flags_use_case.py | 5 --- .../ldap_schema/attribute_type_use_case.py | 10 ++--- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/app/alembic/versions/2dadf40c026a_add_system_flags_to_attribute_types.py b/app/alembic/versions/2dadf40c026a_add_system_flags_to_attribute_types.py index 78f972b89..606a6be15 100644 --- a/app/alembic/versions/2dadf40c026a_add_system_flags_to_attribute_types.py +++ b/app/alembic/versions/2dadf40c026a_add_system_flags_to_attribute_types.py @@ -6,16 +6,19 @@ """ +import contextlib + import sqlalchemy as sa from alembic import op -from dishka import AsyncContainer +from dishka import AsyncContainer, Scope +from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession from sqlalchemy.orm import Session from entities import AttributeType -from ldap_protocol.ldap_schema.attribute_type_system_flags_use_case import ( - AttributeTypeSystemFlags, +from ldap_protocol.ldap_schema.attribute_type_use_case import ( + AttributeTypeUseCase, ) -from repo.pg.tables import queryable_attr as qa +from ldap_protocol.ldap_schema.exceptions import AttributeTypeNotFoundError # revision identifiers, used by Alembic. revision: None | str = "2dadf40c026a" @@ -126,7 +129,7 @@ ) -def upgrade(container: AsyncContainer) -> None: # noqa: ARG001 +def upgrade(container: AsyncContainer) -> None: """Upgrade.""" bind = op.get_bind() session = Session(bind=bind) @@ -143,19 +146,22 @@ def upgrade(container: AsyncContainer) -> None: # noqa: ARG001 session.execute(sa.update(AttributeType).values({"system_flags": 0})) - session.execute( - sa.update(AttributeType) - .where( - qa(AttributeType.name).in_(_NON_REPLICATED_ATTRIBUTES_TYPE_NAMES), - ) - .values( - { - "system_flags": int( - AttributeTypeSystemFlags.ATTR_NOT_REPLICATED, - ), - }, - ), - ) + async def _set_attr_replication_flag(connection: AsyncConnection) -> None: # noqa: ARG001 + async with container(scope=Scope.REQUEST) as cnt: + session = await cnt.get(AsyncSession) + at_type_use_case = await cnt.get(AttributeTypeUseCase) + + for name in _NON_REPLICATED_ATTRIBUTES_TYPE_NAMES: + with contextlib.suppress(AttributeTypeNotFoundError): + await at_type_use_case.set_attr_replication_flag( + name, + need_to_replicate=False, + ) + + await session.commit() + await session.close() + + op.run_async(_set_attr_replication_flag) op.alter_column("AttributeTypes", "system_flags", nullable=False) diff --git a/app/ldap_protocol/ldap_schema/attribute_type_system_flags_use_case.py b/app/ldap_protocol/ldap_schema/attribute_type_system_flags_use_case.py index f75fed0e3..91e4f8530 100644 --- a/app/ldap_protocol/ldap_schema/attribute_type_system_flags_use_case.py +++ b/app/ldap_protocol/ldap_schema/attribute_type_system_flags_use_case.py @@ -50,11 +50,6 @@ async def set_attr_replication_flag( need_to_replicate: bool, ) -> AttributeTypeDTO: """Set/clear replication flag in systemFlags.""" - if attribute_type_dto.is_system: - raise ValueError( - "Cannot change replication flag for system attribute types.", - ) - if not need_to_replicate: attribute_type_dto.system_flags = int( attribute_type_dto.system_flags diff --git a/app/ldap_protocol/ldap_schema/attribute_type_use_case.py b/app/ldap_protocol/ldap_schema/attribute_type_use_case.py index 26a49672a..a6ccaa666 100644 --- a/app/ldap_protocol/ldap_schema/attribute_type_use_case.py +++ b/app/ldap_protocol/ldap_schema/attribute_type_use_case.py @@ -77,10 +77,9 @@ async def delete_all_by_names(self, names: list[str]) -> None: async def is_attr_replicated(self, name: str) -> bool: """Check if attribute is replicated based on systemFlags.""" - dtos = await self.get_all_by_names([name]) - if not dtos: + dto = await self.get(name) + if not dto: raise ValueError(f"Attribute Type with name '{name}' not found.") - dto = dtos[0] return await self._attribute_type_system_flags_use_case.is_attr_replicated(dto) # noqa: E501 # fmt: skip async def set_attr_replication_flag( @@ -89,10 +88,9 @@ async def set_attr_replication_flag( need_to_replicate: bool, ) -> None: """Set replication flag in systemFlags.""" - dtos = await self.get_all_by_names([name]) - if not dtos: + dto = await self.get(name) + if not dto: raise ValueError(f"Attribute Type with name '{name}' not found.") - dto = dtos[0] dto = await self._attribute_type_system_flags_use_case.set_attr_replication_flag( # noqa: E501 dto, need_to_replicate, From 3c17b64869325bacdd6bc605f479802fd78f4bc9 Mon Sep 17 00:00:00 2001 From: Milov Dmitriy Date: Mon, 9 Feb 2026 17:55:57 +0300 Subject: [PATCH 5/8] fix: copilot comments task_1160 --- .../ldap_schema/attribute_type_system_flags_use_case.py | 4 ++-- app/ldap_protocol/ldap_schema/attribute_type_use_case.py | 8 ++------ tests/conftest.py | 4 ++-- .../test_attribute_type_system_flags_use_case.py | 8 ++++---- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/app/ldap_protocol/ldap_schema/attribute_type_system_flags_use_case.py b/app/ldap_protocol/ldap_schema/attribute_type_system_flags_use_case.py index 91e4f8530..2cb7901bc 100644 --- a/app/ldap_protocol/ldap_schema/attribute_type_system_flags_use_case.py +++ b/app/ldap_protocol/ldap_schema/attribute_type_system_flags_use_case.py @@ -34,7 +34,7 @@ class AttributeTypeSystemFlags(IntFlag): class AttributeTypeSystemFlagsUseCase: - async def is_attr_replicated( + def is_attr_replicated( self, attribute_type_dto: AttributeTypeDTO, ) -> bool: @@ -44,7 +44,7 @@ async def is_attr_replicated( & AttributeTypeSystemFlags.ATTR_NOT_REPLICATED, ) - async def set_attr_replication_flag( + def set_attr_replication_flag( self, attribute_type_dto: AttributeTypeDTO, need_to_replicate: bool, diff --git a/app/ldap_protocol/ldap_schema/attribute_type_use_case.py b/app/ldap_protocol/ldap_schema/attribute_type_use_case.py index a6ccaa666..95f5425fc 100644 --- a/app/ldap_protocol/ldap_schema/attribute_type_use_case.py +++ b/app/ldap_protocol/ldap_schema/attribute_type_use_case.py @@ -78,9 +78,7 @@ async def delete_all_by_names(self, names: list[str]) -> None: async def is_attr_replicated(self, name: str) -> bool: """Check if attribute is replicated based on systemFlags.""" dto = await self.get(name) - if not dto: - raise ValueError(f"Attribute Type with name '{name}' not found.") - return await self._attribute_type_system_flags_use_case.is_attr_replicated(dto) # noqa: E501 # fmt: skip + return self._attribute_type_system_flags_use_case.is_attr_replicated(dto) # noqa: E501 # fmt: skip async def set_attr_replication_flag( self, @@ -89,9 +87,7 @@ async def set_attr_replication_flag( ) -> None: """Set replication flag in systemFlags.""" dto = await self.get(name) - if not dto: - raise ValueError(f"Attribute Type with name '{name}' not found.") - dto = await self._attribute_type_system_flags_use_case.set_attr_replication_flag( # noqa: E501 + dto = self._attribute_type_system_flags_use_case.set_attr_replication_flag( # noqa: E501 dto, need_to_replicate, ) diff --git a/tests/conftest.py b/tests/conftest.py index 3d899649d..ba02eb195 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1216,9 +1216,9 @@ async def attribute_type_dao( async def attribute_type_system_flags_use_case( container: AsyncContainer, ) -> AsyncIterator[AttributeTypeSystemFlagsUseCase]: - """Get session and acquire after completion.""" + """Get AttributeTypeSystemFlagsUseCase.""" async with container(scope=Scope.APP) as container: - yield AttributeTypeSystemFlagsUseCase() + yield await container.get(AttributeTypeSystemFlagsUseCase) @pytest_asyncio.fixture(scope="function") diff --git a/tests/test_ldap/test_ldap_schema/test_attribute_type_system_flags_use_case.py b/tests/test_ldap/test_ldap_schema/test_attribute_type_system_flags_use_case.py index ef445aa25..b5fb25026 100644 --- a/tests/test_ldap/test_ldap_schema/test_attribute_type_system_flags_use_case.py +++ b/tests/test_ldap/test_ldap_schema/test_attribute_type_system_flags_use_case.py @@ -14,18 +14,18 @@ @pytest.mark.asyncio @pytest.mark.usefixtures("session") @pytest.mark.usefixtures("setup_session") -async def test_attribute_type_system_flags_use_case_is_replicated( +async def test_attribute_type_system_flags_use_case_is_not_replicated( attribute_type_use_case: AttributeTypeUseCase, ) -> None: - """Test get all Password Policy endpoint.""" + """Test AttributeType is not replicated.""" assert not await attribute_type_use_case.is_attr_replicated("netbootSCPBL") @pytest.mark.asyncio @pytest.mark.usefixtures("session") @pytest.mark.usefixtures("setup_session") -async def test_attribute_type_system_flags_use_case_is_not_replicated( +async def test_attribute_type_system_flags_use_case_is_replicated( attribute_type_use_case: AttributeTypeUseCase, ) -> None: - """Test get all Password Policy endpoint.""" + """Test AttributeType is replicated.""" assert await attribute_type_use_case.is_attr_replicated("objectClass") From 94ee0135aff3687f16328a22b9ea75d6d4d166dc Mon Sep 17 00:00:00 2001 From: Milov Dmitriy Date: Mon, 9 Feb 2026 17:58:37 +0300 Subject: [PATCH 6/8] fix: copilot task_1160 --- .../2dadf40c026a_add_system_flags_to_attribute_types.py | 1 - app/ldap_protocol/ldap_schema/attribute_type_dao.py | 2 +- app/ldap_protocol/ldap_schema/entity_type_use_case.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/alembic/versions/2dadf40c026a_add_system_flags_to_attribute_types.py b/app/alembic/versions/2dadf40c026a_add_system_flags_to_attribute_types.py index 606a6be15..b819c1c86 100644 --- a/app/alembic/versions/2dadf40c026a_add_system_flags_to_attribute_types.py +++ b/app/alembic/versions/2dadf40c026a_add_system_flags_to_attribute_types.py @@ -159,7 +159,6 @@ async def _set_attr_replication_flag(connection: AsyncConnection) -> None: # no ) await session.commit() - await session.close() op.run_async(_set_attr_replication_flag) diff --git a/app/ldap_protocol/ldap_schema/attribute_type_dao.py b/app/ldap_protocol/ldap_schema/attribute_type_dao.py index 0ace3d31a..63b795e0a 100644 --- a/app/ldap_protocol/ldap_schema/attribute_type_dao.py +++ b/app/ldap_protocol/ldap_schema/attribute_type_dao.py @@ -57,7 +57,7 @@ def __init__(self, session: AsyncSession) -> None: self.__session = session async def get(self, name: str) -> AttributeTypeDTO: - """Get Attribute Type by id.""" + """Get Attribute Type by name.""" return _convert_model_to_dto(await self._get_one_raw_by_name(name)) async def get_all(self) -> list[AttributeTypeDTO]: diff --git a/app/ldap_protocol/ldap_schema/entity_type_use_case.py b/app/ldap_protocol/ldap_schema/entity_type_use_case.py index a9ccdbd83..e7589c3f4 100644 --- a/app/ldap_protocol/ldap_schema/entity_type_use_case.py +++ b/app/ldap_protocol/ldap_schema/entity_type_use_case.py @@ -54,7 +54,7 @@ async def update(self, name: str, dto: EntityTypeDTO) -> None: f"Entity Type '{dto.name}' is system and cannot be modified.", ) if name != dto.name: - await self._validate_name(name=name) + await self._validate_name(name=dto.name) await self._entity_type_dao.update(entity_type.name, dto) async def get(self, name: str) -> EntityTypeDTO: From 076c2afb28b016c06b6cd6b478a650e52fa17faa Mon Sep 17 00:00:00 2001 From: Milov Dmitriy Date: Mon, 9 Feb 2026 18:29:04 +0300 Subject: [PATCH 7/8] tests: fix naming and tests task_1160 --- ...m_flags_use_case.py => test_attribute_type_use_case.py} | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) rename tests/test_ldap/test_ldap_schema/{test_attribute_type_system_flags_use_case.py => test_attribute_type_use_case.py} (81%) diff --git a/tests/test_ldap/test_ldap_schema/test_attribute_type_system_flags_use_case.py b/tests/test_ldap/test_ldap_schema/test_attribute_type_use_case.py similarity index 81% rename from tests/test_ldap/test_ldap_schema/test_attribute_type_system_flags_use_case.py rename to tests/test_ldap/test_ldap_schema/test_attribute_type_use_case.py index b5fb25026..0c359351a 100644 --- a/tests/test_ldap/test_ldap_schema/test_attribute_type_system_flags_use_case.py +++ b/tests/test_ldap/test_ldap_schema/test_attribute_type_use_case.py @@ -1,4 +1,4 @@ -"""Test AttributeTypeSystemFlagsUseCase. +"""Test AttributeTypeUseCase. Copyright (c) 2026 MultiFactor License: https://github.com/MultiDirectoryLab/MultiDirectory/blob/main/LICENSE @@ -29,3 +29,8 @@ async def test_attribute_type_system_flags_use_case_is_replicated( ) -> None: """Test AttributeType is replicated.""" assert await attribute_type_use_case.is_attr_replicated("objectClass") + await attribute_type_use_case.set_attr_replication_flag( + "objectClass", + False, + ) + assert not await attribute_type_use_case.is_attr_replicated("objectClass") From 7d7e2e32053a2acd12014cd452d3f7c457442a6c Mon Sep 17 00:00:00 2001 From: Milov Dmitriy Date: Mon, 9 Feb 2026 18:32:06 +0300 Subject: [PATCH 8/8] fix: notes task_1160 --- app/alembic/versions/275222846605_initial_ldap_schema.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/alembic/versions/275222846605_initial_ldap_schema.py b/app/alembic/versions/275222846605_initial_ldap_schema.py index 5bdc54dea..6994b0c77 100644 --- a/app/alembic/versions/275222846605_initial_ldap_schema.py +++ b/app/alembic/versions/275222846605_initial_ldap_schema.py @@ -51,9 +51,9 @@ def upgrade(container: AsyncContainer) -> None: sa.Column("no_user_modification", sa.Boolean(), nullable=False), sa.Column("is_system", sa.Boolean(), nullable=False), sa.PrimaryKeyConstraint("id"), - # NOTE: added in 2dadf40c026a_.py + # NOTE: it added in 2dadf40c026a_add_system_flags_to_attribute_types.py sa.Column("system_flags", sa.Integer(), nullable=False), - # NOTE: added in f24ed0e49df2_add_filter_anr.py # noqa: ERA001 + # NOTE: it added in f24ed0e49df2_add_filter_anr.py sa.Column("is_included_anr", sa.Boolean(), nullable=True), ) op.create_index( @@ -400,7 +400,7 @@ async def _modify_object_classes(connection: AsyncConnection) -> None: # noqa: # NOTE: it added in f24ed0e49df2_add_filter_anr.py op.drop_column("AttributeTypes", "is_included_anr") - # NOTE: added in 2dadf40c026a_.py + # NOTE: it added in 2dadf40c026a_add_system_flags_to_attribute_types.py op.drop_column("AttributeTypes", "system_flags") session.commit()