Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions app/alembic/versions/275222846605_initial_ldap_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: it added in 2dadf40c026a_add_system_flags_to_attribute_types.py
sa.Column("system_flags", sa.Integer(), nullable=False),
# NOTE: it added in f24ed0e49df2_add_filter_anr.py
sa.Column("is_included_anr", sa.Boolean(), nullable=True),
)
op.create_index(
op.f("ix_AttributeTypes_oid"),
Expand Down Expand Up @@ -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,
),
)
Expand Down Expand Up @@ -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: it added in 2dadf40c026a_add_system_flags_to_attribute_types.py
op.drop_column("AttributeTypes", "system_flags")

session.commit()


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
"""Add systemFlags for AttributeTypes.

Revision ID: 2dadf40c026a
Revises: f4e6cd18a01d
Create Date: 2026-02-04 09:33:33.218126

"""

import contextlib

import sqlalchemy as sa
from alembic import op
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_use_case import (
AttributeTypeUseCase,
)
from ldap_protocol.ldap_schema.exceptions import AttributeTypeNotFoundError

# 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:
"""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}))

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()

op.run_async(_set_attr_replication_flag)

op.alter_column("AttributeTypes", "system_flags", nullable=False)

session.commit()


def downgrade(container: AsyncContainer) -> None: # noqa: ARG001
"""Downgrade."""
op.drop_column("AttributeTypes", "system_flags")
1 change: 1 addition & 0 deletions app/api/ldap_schema/adapters/attribute_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down
1 change: 1 addition & 0 deletions app/api/ldap_schema/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions app/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions app/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,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()
Expand Down
7 changes: 7 additions & 0 deletions app/ioc.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@
LDAPUnbindRequestContext,
)
from ldap_protocol.ldap_schema.attribute_type_dao import AttributeTypeDAO
from ldap_protocol.ldap_schema.attribute_type_system_flags_use_case import (
AttributeTypeSystemFlagsUseCase,
)
from ldap_protocol.ldap_schema.attribute_type_use_case import (
AttributeTypeUseCase,
)
Expand Down Expand Up @@ -450,6 +453,10 @@ 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(
Expand Down
22 changes: 14 additions & 8 deletions app/ldap_protocol/ldap_schema/attribute_type_dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""Get Attribute Type by id."""
return _convert_model_to_dto(await self._get_one_raw_by_name(_id))
async def get(self, name: str) -> AttributeTypeDTO:
"""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]:
"""Get all Attribute Types."""
Expand All @@ -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:
Expand All @@ -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

Expand All @@ -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()

Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""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 ldap_protocol.ldap_schema.dto import AttributeTypeDTO


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_attr_replicated(
self,
attribute_type_dto: AttributeTypeDTO,
) -> bool:
"""Check if attribute is replicated based on system_flags."""
return not bool(
attribute_type_dto.system_flags
& AttributeTypeSystemFlags.ATTR_NOT_REPLICATED,
)

def set_attr_replication_flag(
self,
attribute_type_dto: AttributeTypeDTO,
need_to_replicate: bool,
) -> AttributeTypeDTO:
"""Set/clear replication flag in systemFlags."""
if not need_to_replicate:
attribute_type_dto.system_flags = int(
attribute_type_dto.system_flags
| AttributeTypeSystemFlags.ATTR_NOT_REPLICATED,
)
else:
attribute_type_dto.system_flags = int(
attribute_type_dto.system_flags
& ~AttributeTypeSystemFlags.ATTR_NOT_REPLICATED,
)

return attribute_type_dto
Loading