diff --git a/app/alembic/versions/f4e6cd18a01d_add_samaccounttype.py b/app/alembic/versions/f4e6cd18a01d_add_samaccounttype.py new file mode 100644 index 000000000..efb2d0af5 --- /dev/null +++ b/app/alembic/versions/f4e6cd18a01d_add_samaccounttype.py @@ -0,0 +1,89 @@ +"""Add sAMAccountType to existing user/group/computer entries. + +Revision ID: f4e6cd18a01d +Revises: 379fce54fb08 +Create Date: 2026-01-30 13:08:26.299158 + +""" + +from alembic import op +from dishka import AsyncContainer, Scope +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession +from sqlalchemy.orm import joinedload + +from entities import Attribute, Directory, EntityType +from enums import EntityTypeNames, SamAccountTypeCodes +from repo.pg.tables import queryable_attr as qa + +revision: None | str = "f4e6cd18a01d" +down_revision: None | str = "379fce54fb08" +branch_labels: None | list[str] = None +depends_on: None | list[str] = None + +_SAM_ACCOUNT_TYPE_ATTR = "sAMAccountType" +_SECURITY_PRINCIPAL_TYPES = ( + EntityTypeNames.USER, + EntityTypeNames.GROUP, + EntityTypeNames.COMPUTER, +) +_ENTITY_TO_SAM: dict[str, SamAccountTypeCodes] = { + EntityTypeNames.USER: SamAccountTypeCodes.SAM_USER_OBJECT, + EntityTypeNames.GROUP: SamAccountTypeCodes.SAM_GROUP_OBJECT, + EntityTypeNames.COMPUTER: SamAccountTypeCodes.SAM_MACHINE_ACCOUNT, +} + + +def upgrade(container: AsyncContainer) -> None: + """Add sAMAccountType attributes for user/group/computer.""" + + async def _add_samaccounttype(connection: AsyncConnection) -> None: # noqa: ARG001 + async with container(scope=Scope.REQUEST) as cnt: + session = await cnt.get(AsyncSession) + + entity_types = await session.scalars( + select(EntityType) + .where(qa(EntityType.name).in_(_SECURITY_PRINCIPAL_TYPES)), + ) # fmt: skip + entity_type_ids = [et.id for et in entity_types] + if not entity_type_ids: + return + + has_sam = select( + qa(Attribute.directory_id), + ).where( + qa(Attribute.name).ilike(_SAM_ACCOUNT_TYPE_ATTR.lower()), + ) + dirs_without_sam = await session.scalars( + select(Directory) + .where( + qa(Directory.entity_type_id).in_(entity_type_ids), + ~qa(Directory.id).in_(has_sam), + ) + .options(joinedload(qa(Directory.entity_type))), + ) + + for directory in dirs_without_sam: + sam_value = ( + _ENTITY_TO_SAM.get(directory.entity_type.name) + if directory.entity_type + else None + ) + if sam_value is None: + continue + + session.add( + Attribute( + name=_SAM_ACCOUNT_TYPE_ATTR, + value=str(sam_value), + directory_id=directory.id, + ), + ) + + await session.commit() + + op.run_async(_add_samaccounttype) + + +def downgrade(container: AsyncContainer) -> None: + """Downgrade.""" diff --git a/app/constants.py b/app/constants.py index 902a79f59..8a743ed5b 100644 --- a/app/constants.py +++ b/app/constants.py @@ -6,7 +6,7 @@ from typing import TypedDict -from enums import EntityTypeNames +from enums import EntityTypeNames, SamAccountTypeCodes GROUPS_CONTAINER_NAME = "Groups" COMPUTERS_CONTAINER_NAME = "Computers" @@ -24,7 +24,7 @@ "groupType": ["-2147483646"], "instanceType": ["4"], "sAMAccountName": ["groups"], - "sAMAccountType": ["268435456"], + "sAMAccountType": [str(SamAccountTypeCodes.SAM_GROUP_OBJECT.value)], } @@ -308,7 +308,9 @@ class EntityTypeData(TypedDict): "groupType": ["-2147483646"], "instanceType": ["4"], "sAMAccountName": [DOMAIN_ADMIN_GROUP_NAME], - "sAMAccountType": ["268435456"], + "sAMAccountType": [ + str(SamAccountTypeCodes.SAM_GROUP_OBJECT.value), + ], "gidNumber": ["512"], }, "objectSid": 512, @@ -321,7 +323,9 @@ class EntityTypeData(TypedDict): "groupType": ["-2147483646"], "instanceType": ["4"], "sAMAccountName": [DOMAIN_USERS_GROUP_NAME], - "sAMAccountType": ["268435456"], + "sAMAccountType": [ + str(SamAccountTypeCodes.SAM_GROUP_OBJECT.value), + ], "gidNumber": ["513"], }, "objectSid": 513, @@ -334,7 +338,9 @@ class EntityTypeData(TypedDict): "groupType": ["-2147483646"], "instanceType": ["4"], "sAMAccountName": [READ_ONLY_GROUP_NAME], - "sAMAccountType": ["268435456"], + "sAMAccountType": [ + str(SamAccountTypeCodes.SAM_GROUP_OBJECT.value), + ], "gidNumber": ["521"], }, "objectSid": 521, @@ -347,7 +353,9 @@ class EntityTypeData(TypedDict): "groupType": ["-2147483646"], "instanceType": ["4"], "sAMAccountName": [DOMAIN_COMPUTERS_GROUP_NAME], - "sAMAccountType": ["268435456"], + "sAMAccountType": [ + str(SamAccountTypeCodes.SAM_GROUP_OBJECT.value), + ], "gidNumber": ["515"], }, "objectSid": 515, diff --git a/app/enums.py b/app/enums.py index 264b6c16a..1f6e8f798 100644 --- a/app/enums.py +++ b/app/enums.py @@ -254,3 +254,23 @@ class DomainCodes(IntEnum): DHCP = 12 LDAP_SCHEMA = 13 SHADOW = 14 + + +class SamAccountTypeCodes(IntEnum): + """SAM Account Type values.""" + + SAM_DOMAIN_OBJECT = 0 + SAM_GROUP_OBJECT = 268435456 + SAM_NON_SECURITY_GROUP_OBJECT = 268435457 + SAM_ALIAS_OBJECT = 536870912 + SAM_NON_SECURITY_ALIAS_OBJECT = 536870913 + SAM_USER_OBJECT = 805306368 + SAM_MACHINE_ACCOUNT = 805306369 + SAM_TRUST_ACCOUNT = 805306370 + SAM_APP_BASIC_GROUP = 1073741824 + SAM_APP_QUERY_GROUP = 1073741825 + + @staticmethod + def to_hex(value: int) -> str: + """Convert decimal value to hex string.""" + return hex(value) diff --git a/app/ldap_protocol/auth/use_cases.py b/app/ldap_protocol/auth/use_cases.py index b9a53414e..136f2cf23 100644 --- a/app/ldap_protocol/auth/use_cases.py +++ b/app/ldap_protocol/auth/use_cases.py @@ -14,6 +14,7 @@ FIRST_SETUP_DATA, USERS_CONTAINER_NAME, ) +from enums import SamAccountTypeCodes from ldap_protocol.auth.dto import SetupDTO from ldap_protocol.auth.setup_gateway import SetupGateway from ldap_protocol.identity.exceptions import ( @@ -114,6 +115,9 @@ def _create_user_data(self, dto: SetupDTO) -> dict: "userAccountControl": ["512"], "primaryGroupID": ["512"], "givenName": [dto.username], + "sAMAccountType": [ + str(SamAccountTypeCodes.SAM_USER_OBJECT), + ], }, "objectSid": 500, }, diff --git a/app/ldap_protocol/ldap_requests/add.py b/app/ldap_protocol/ldap_requests/add.py index a16c6b182..75a498516 100644 --- a/app/ldap_protocol/ldap_requests/add.py +++ b/app/ldap_protocol/ldap_requests/add.py @@ -13,7 +13,7 @@ from constants import DOMAIN_COMPUTERS_GROUP_NAME, DOMAIN_USERS_GROUP_NAME from entities import Attribute, Directory, Group, User -from enums import AceType, EntityTypeNames +from enums import AceType, EntityTypeNames, SamAccountTypeCodes from ldap_protocol.asn1parser import ASN1Row from ldap_protocol.kerberos.exceptions import ( KRBAPIAddPrincipalError, @@ -426,6 +426,32 @@ async def handle( # noqa: C901 ), ) + if "samaccounttype" not in self.l_attrs_dict: + if is_user: + attributes.append( + Attribute( + name="sAMAccountType", + value=str(SamAccountTypeCodes.SAM_USER_OBJECT), + directory_id=new_dir.id, + ), + ) + elif is_group: + attributes.append( + Attribute( + name="sAMAccountType", + value=str(SamAccountTypeCodes.SAM_GROUP_OBJECT), + directory_id=new_dir.id, + ), + ) + elif is_computer: + attributes.append( + Attribute( + name="sAMAccountType", + value=str(SamAccountTypeCodes.SAM_MACHINE_ACCOUNT), + directory_id=new_dir.id, + ), + ) + if not ctx.attribute_value_validator.is_directory_attributes_valid( entity_type.name if entity_type else "", attributes, diff --git a/app/ldap_protocol/utils/queries.py b/app/ldap_protocol/utils/queries.py index 368af23ec..39d93b016 100644 --- a/app/ldap_protocol/utils/queries.py +++ b/app/ldap_protocol/utils/queries.py @@ -16,6 +16,7 @@ from sqlalchemy.sql.expression import ColumnElement from entities import Attribute, Directory, Group, User +from enums import SamAccountTypeCodes from ldap_protocol.ldap_schema.attribute_value_validator import ( AttributeValueValidator, AttributeValueValidatorError, @@ -394,7 +395,7 @@ async def create_group( "instanceType": ["4"], "sAMAccountName": [dir_.name], dir_.rdname: [dir_.name], - "sAMAccountType": ["268435456"], + "sAMAccountType": [str(SamAccountTypeCodes.SAM_GROUP_OBJECT.value)], "gidNumber": [str(create_integer_hash(dir_.name))], } diff --git a/interface b/interface index e1ca5656a..f31962020 160000 --- a/interface +++ b/interface @@ -1 +1 @@ -Subproject commit e1ca5656aeabc20a1862aeaf11ded72feaa97403 +Subproject commit f31962020a6689e6a4c61fb3349db5b5c7895f92 diff --git a/tests/constants.py b/tests/constants.py index 5542e0742..2163898ed 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -11,6 +11,7 @@ GROUPS_CONTAINER_NAME, USERS_CONTAINER_NAME, ) +from enums import SamAccountTypeCodes from ldap_protocol.objects import UserAccountControlFlag TEST_DATA = [ @@ -30,7 +31,9 @@ "groupType": ["-2147483646"], "instanceType": ["4"], "sAMAccountName": [DOMAIN_ADMIN_GROUP_NAME], - "sAMAccountType": ["268435456"], + "sAMAccountType": [ + str(SamAccountTypeCodes.SAM_GROUP_OBJECT.value), + ], }, }, { @@ -42,7 +45,9 @@ "groupType": ["-2147483646"], "instanceType": ["4"], "sAMAccountName": ["developers"], - "sAMAccountType": ["268435456"], + "sAMAccountType": [ + str(SamAccountTypeCodes.SAM_GROUP_OBJECT.value), + ], }, }, { @@ -53,7 +58,9 @@ "groupType": ["-2147483646"], "instanceType": ["4"], "sAMAccountName": ["admin login only"], - "sAMAccountType": ["268435456"], + "sAMAccountType": [ + str(SamAccountTypeCodes.SAM_GROUP_OBJECT.value), + ], }, }, { @@ -64,7 +71,9 @@ "groupType": ["-2147483646"], "instanceType": ["4"], "sAMAccountName": [DOMAIN_USERS_GROUP_NAME], - "sAMAccountType": ["268435456"], + "sAMAccountType": [ + str(SamAccountTypeCodes.SAM_GROUP_OBJECT.value), + ], }, }, { @@ -75,7 +84,9 @@ "groupType": ["-2147483646"], "instanceType": ["4"], "sAMAccountName": [DOMAIN_COMPUTERS_GROUP_NAME], - "sAMAccountType": ["268435456"], + "sAMAccountType": [ + str(SamAccountTypeCodes.SAM_GROUP_OBJECT.value), + ], }, }, ], @@ -368,7 +379,11 @@ "groupType": ["-2147483646"], "instanceType": ["4"], "sAMAccountName": ["testGroup1"], - "sAMAccountType": ["268435456"], + "sAMAccountType": [ + str( + SamAccountTypeCodes.SAM_GROUP_OBJECT.value, + ), + ], }, }, ], @@ -381,7 +396,9 @@ "groupType": ["-2147483646"], "instanceType": ["4"], "sAMAccountName": ["testGroup2"], - "sAMAccountType": ["268435456"], + "sAMAccountType": [ + str(SamAccountTypeCodes.SAM_GROUP_OBJECT.value), + ], }, }, ], @@ -402,7 +419,9 @@ "groupType": ["-2147483646"], "instanceType": ["4"], "sAMAccountName": ["testGroup3"], - "sAMAccountType": ["268435456"], + "sAMAccountType": [ + str(SamAccountTypeCodes.SAM_GROUP_OBJECT.value), + ], }, }, ], diff --git a/tests/test_api/test_main/test_router/test_add.py b/tests/test_api/test_main/test_router/test_add.py index 3282305f1..f20d516d2 100644 --- a/tests/test_api/test_main/test_router/test_add.py +++ b/tests/test_api/test_main/test_router/test_add.py @@ -8,6 +8,7 @@ from fastapi import status from httpx import AsyncClient +from enums import SamAccountTypeCodes from ldap_protocol.ldap_codes import LDAPCodes from ldap_protocol.objects import UserAccountControlFlag from tests.api_datasets import test_api_forbidden_chars_in_attr_value @@ -179,6 +180,50 @@ async def test_api_add_computer(http_client: AsyncClient) -> None: raise Exception("Computer without sAMAccountName") +@pytest.mark.asyncio +@pytest.mark.usefixtures("session") +async def test_add_user_samaccounttype( + http_client: AsyncClient, +) -> None: + """Add user without sAMAccountType: server sets SAM_USER_OBJECT.""" + entry = "cn=samuser,dc=md,dc=test" + await http_client.post( + "/entry/add", + json={ + "entry": entry, + "password": "P@ssw0rd", + "attributes": [ + {"type": "name", "vals": ["samuser"]}, + {"type": "cn", "vals": ["samuser"]}, + {"type": "objectClass", "vals": ["user", "top"]}, + {"type": "sAMAccountName", "vals": ["samuser"]}, + {"type": "userPrincipalName", "vals": ["samuser@md.test"]}, + ], + }, + ) + response = await http_client.post( + "entry/search", + json={ + "base_object": entry, + "scope": 0, + "deref_aliases": 0, + "size_limit": 1, + "time_limit": 10, + "types_only": True, + "filter": "(objectClass=*)", + "attributes": ["sAMAccountType"], + "page_number": 1, + }, + ) + data = response.json() + attrs = { + a["type"]: a for a in data["search_result"][0]["partial_attributes"] + } + assert attrs["sAMAccountType"]["vals"][0] == str( + SamAccountTypeCodes.SAM_USER_OBJECT, + ) + + @pytest.mark.asyncio @pytest.mark.usefixtures("session") async def test_api_correct_add_double_member_of(