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: 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"),
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: added in 2dadf40c026a_.py
op.drop_column("AttributeTypes", "system_flags")

session.commit()


Expand Down
Original file line number Diff line number Diff line change
@@ -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")
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
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 import (
AttributeTypeSystemFlagsUseCase,
)
from ldap_protocol.ldap_schema.attribute_type_use_case import (
AttributeTypeUseCase,
)
Expand Down Expand Up @@ -435,6 +438,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
59 changes: 59 additions & 0 deletions app/ldap_protocol/ldap_schema/attribute_type_system_flags.py
Original file line number Diff line number Diff line change
@@ -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,
)
25 changes: 25 additions & 0 deletions app/ldap_protocol/ldap_schema/attribute_type_use_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions app/ldap_protocol/ldap_schema/dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions app/ldap_protocol/utils/raw_definition_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down
1 change: 1 addition & 0 deletions app/repo/pg/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
)
Expand Down
2 changes: 1 addition & 1 deletion interface
Loading