From 7d04d4cc9cf2f606547f3174942145d7bb5688d4 Mon Sep 17 00:00:00 2001 From: Milov Dmitriy Date: Thu, 5 Feb 2026 14:30:00 +0300 Subject: [PATCH 1/2] 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/2] 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