Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4c154d3
add: Domain Controller OU in first setup
Naksen Feb 13, 2026
4b255be
add: implement domain controller data creation in setup process
Naksen Feb 13, 2026
0afaeba
fix: update type hints for AsyncTTLCache callable signature
Naksen Feb 13, 2026
8385b8d
fix: adjust parameter order in AsyncTTLCache __call__ method
Naksen Feb 16, 2026
fa6ca54
add: implement creation and deletion of 'Domain Controllers' OU in da…
Naksen Feb 17, 2026
ea46d6e
add: include settings parameter in SetupUseCase and use it for ipHost…
Naksen Feb 17, 2026
9175aea
fix: update sAMAccountType to use SAM_MACHINE_ACCOUNT in domain contr…
Naksen Feb 17, 2026
ef874fa
add: introduce DEFAULT_NAMESERVER setting in configuration
Naksen Feb 17, 2026
931feff
add: implement add_domain_controller script and schedule task
Naksen Feb 17, 2026
54b4a71
add: set DEFAULT_NAMESERVER in test environment configuration
Naksen Feb 17, 2026
d192473
add: set is_system attribute for organizational units and groups in s…
Naksen Feb 17, 2026
e54b726
test: set is_system attribute to False for test data and remove from …
Naksen Feb 17, 2026
8117002
fix: ensure DEFAULT_NAMESERVER is correctly defined in local.env
Naksen Feb 17, 2026
8495b65
add: implement test for add_domain_controller function
Naksen Feb 17, 2026
3a29197
refactor: change critical logs to debug level in add_domain_controlle…
Naksen Feb 17, 2026
f82fc4a
refactor: streamline attribute creation in _add_domain_controller fun…
Naksen Feb 17, 2026
00de376
add: define DEFAULT_NAMESERVER in md-test service environment
Naksen Feb 17, 2026
4338452
refactor: update debug log message for existing domain controllers
Naksen Feb 17, 2026
4abc8af
add: include 'cn' attribute in domain controller creation
Naksen Feb 17, 2026
37fda7b
add: include HOSTNAME variable in configuration and setup scripts
Naksen Feb 17, 2026
9c07220
add: use HOSTNAME variable for domain controller naming in setup
Naksen Feb 17, 2026
2b0c9f3
refactor: rename HOSTNAME to HOST_MACHINE_NAME across configuration a…
Naksen Feb 17, 2026
a66d767
test: set is_system attribute to True for group and container definit…
Naksen Feb 17, 2026
afb0dde
fix: add missing newline at end of setup.sh
Naksen Feb 17, 2026
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
6 changes: 6 additions & 0 deletions .package/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,9 @@ if [ ! -d "certs" ]; then
else
echo "Directory already exists: certs"
fi

# HOST_MACHINE_NAME
if ! get_env_var "HOST_MACHINE_NAME"; then
host_machine_name=$(hostname)
add_env_var "HOST_MACHINE_NAME" "$host_machine_name"
fi
2 changes: 1 addition & 1 deletion app/alembic/versions/8164b4a9e1f1_add_ou_computers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
_OU_COMPUTERS_DATA = {
"name": COMPUTERS,
"object_class": "organizationalUnit",
"is_system": True,
"attributes": {"objectClass": ["top", "container"]},
"children": [],
}
Expand Down Expand Up @@ -61,7 +62,6 @@ async def _create_ou_computers(connection: AsyncConnection) -> None: # noqa: AR

await setup_gateway.create_dir(
_OU_COMPUTERS_DATA,
is_system=True,
domain=domain_dir,
parent=domain_dir,
)
Expand Down
102 changes: 102 additions & 0 deletions app/alembic/versions/ebf19750805e_add_domain_controllers_ou.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""Add OU 'Domain Controllers' if it does not exist.

Revision ID: ebf19750805e
Revises: 2dadf40c026a
Create Date: 2026-02-17 08:52:28.048004

"""

from alembic import op
from dishka import AsyncContainer, Scope
from sqlalchemy import delete, exists, select
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession

from constants import DOMAIN_CONTROLLERS_OU_NAME
from entities import Directory
from ldap_protocol.auth.setup_gateway import SetupGateway
from ldap_protocol.roles.role_use_case import RoleUseCase
from ldap_protocol.utils.queries import get_base_directories
from repo.pg.tables import queryable_attr as qa

# revision identifiers, used by Alembic.
revision: None | str = "ebf19750805e"
down_revision: None | str = "2dadf40c026a"
branch_labels: None | list[str] = None
depends_on: None | list[str] = None


_OU_DOMAIN_CONTROLLERS_DATA = {
"name": DOMAIN_CONTROLLERS_OU_NAME,
"object_class": "organizationalUnit",
"is_system": True,
"attributes": {"objectClass": ["top", "container"]},
"children": [],
}


def upgrade(container: AsyncContainer) -> None:
"""Upgrade."""

async def _create_domain_controllers_ou(
connection: AsyncConnection, # noqa: ARG001
) -> None:
async with container(scope=Scope.REQUEST) as cnt:
session = await cnt.get(AsyncSession)
setup_gateway = await cnt.get(SetupGateway)
role_use_case = await cnt.get(RoleUseCase)

base_directories = await get_base_directories(session)
if not base_directories:
return
domain_dir = base_directories[0]

exists_dc_ou = await session.scalar(
select(
exists(Directory)
.where(qa(Directory.name) == DOMAIN_CONTROLLERS_OU_NAME),
),
) # fmt: skip
if exists_dc_ou:
return

await setup_gateway.create_dir(
_OU_DOMAIN_CONTROLLERS_DATA,
domain=domain_dir,
parent=domain_dir,
)

dc_ou = await session.scalar(
select(Directory).where(
qa(Directory.name) == DOMAIN_CONTROLLERS_OU_NAME,
),
)
if not dc_ou:
raise Exception("Domain Controllers OU was not created")

await role_use_case.inherit_parent_aces(
parent_directory=domain_dir,
directory=dc_ou,
)

await session.commit()

op.run_async(_create_domain_controllers_ou)


def downgrade(container: AsyncContainer) -> None:
"""Downgrade."""

async def _delete_domain_controllers_ou(
connection: AsyncConnection, # noqa: ARG001
) -> None:
async with container(scope=Scope.REQUEST) as cnt:
session = await cnt.get(AsyncSession)

await session.execute(
delete(Directory).where(
qa(Directory.name) == DOMAIN_CONTROLLERS_OU_NAME,
),
)
await session.commit()

op.run_async(_delete_domain_controllers_ou)
2 changes: 2 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Settings(BaseModel):
"""Settigns with database dsn."""

DOMAIN: str
HOST_MACHINE_NAME: str

DEBUG: bool = False
AUTO_RELOAD: bool = False
Expand All @@ -47,6 +48,7 @@ class Settings(BaseModel):
GLOBAL_LDAP_TLS_PORT: int = 3269
USE_CORE_TLS: bool = False
LDAP_LOAD_SSL_CERT: bool = False
DEFAULT_NAMESERVER: str

TCP_PACKET_SIZE: int = 1024
COROUTINES_NUM_PER_CLIENT: int = 3
Expand Down
7 changes: 7 additions & 0 deletions app/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
GROUPS_CONTAINER_NAME = "Groups"
COMPUTERS_CONTAINER_NAME = "Computers"
USERS_CONTAINER_NAME = "Users"
DOMAIN_CONTROLLERS_OU_NAME = "Domain Controllers"

READ_ONLY_GROUP_NAME = "read-only"

Expand Down Expand Up @@ -295,6 +296,7 @@ class EntityTypeData(TypedDict):
{
"name": GROUPS_CONTAINER_NAME,
"object_class": "container",
"is_system": True,
"attributes": {
"objectClass": ["top"],
"sAMAccountName": ["groups"],
Expand All @@ -303,6 +305,7 @@ class EntityTypeData(TypedDict):
{
"name": DOMAIN_ADMIN_GROUP_NAME,
"object_class": "group",
"is_system": True,
"attributes": {
"objectClass": ["top", "posixGroup"],
"groupType": ["-2147483646"],
Expand All @@ -318,6 +321,7 @@ class EntityTypeData(TypedDict):
{
"name": DOMAIN_USERS_GROUP_NAME,
"object_class": "group",
"is_system": True,
"attributes": {
"objectClass": ["top", "posixGroup"],
"groupType": ["-2147483646"],
Expand All @@ -333,6 +337,7 @@ class EntityTypeData(TypedDict):
{
"name": READ_ONLY_GROUP_NAME,
"object_class": "group",
"is_system": True,
"attributes": {
"objectClass": ["top", "posixGroup"],
"groupType": ["-2147483646"],
Expand All @@ -348,6 +353,7 @@ class EntityTypeData(TypedDict):
{
"name": DOMAIN_COMPUTERS_GROUP_NAME,
"object_class": "group",
"is_system": True,
"attributes": {
"objectClass": ["top", "posixGroup"],
"groupType": ["-2147483646"],
Expand All @@ -365,6 +371,7 @@ class EntityTypeData(TypedDict):
{
"name": COMPUTERS_CONTAINER_NAME,
"object_class": "container",
"is_system": True,
"attributes": {"objectClass": ["top"]},
"children": [],
},
Expand Down
147 changes: 147 additions & 0 deletions app/extra/scripts/add_domain_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""Add domain controller.

Copyright (c) 2026 MultiFactor
License: https://github.com/MultiDirectoryLab/MultiDirectory/blob/main/LICENSE
"""

from loguru import logger
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from config import Settings
from constants import DOMAIN_CONTROLLERS_OU_NAME
from entities import Attribute, Directory
from enums import SamAccountTypeCodes
from ldap_protocol.ldap_schema.entity_type_dao import EntityTypeDAO
from ldap_protocol.objects import UserAccountControlFlag
from ldap_protocol.roles.role_use_case import RoleUseCase
from ldap_protocol.utils.helpers import create_object_sid
from ldap_protocol.utils.queries import get_base_directories
from repo.pg.tables import queryable_attr as qa


async def _add_domain_controller(
session: AsyncSession,
role_use_case: RoleUseCase,
entity_type_dao: EntityTypeDAO,
settings: Settings,
domain: Directory,
dc_ou_dir: Directory,
) -> None:
dc_directory = Directory(
object_class="",
name=settings.HOST_MACHINE_NAME,
is_system=False,
)
dc_directory.create_path(dc_ou_dir)
session.add(dc_directory)
await session.flush()

dc_directory.parent_id = dc_ou_dir.id
dc_directory.object_sid = create_object_sid(domain, dc_directory.id)
await session.flush()

attributes = [
Attribute(
name="objectClass",
value="top",
directory_id=dc_directory.id,
),
Attribute(
name="objectClass",
value="computer",
directory_id=dc_directory.id,
),
Attribute(
name="sAMAccountName",
value=settings.HOST_MACHINE_NAME,
directory_id=dc_directory.id,
),
Attribute(
name="userAccountControl",
value=str(
UserAccountControlFlag.SERVER_TRUST_ACCOUNT,
),
directory_id=dc_directory.id,
),
Attribute(
name="sAMAccountType",
value=str(SamAccountTypeCodes.SAM_MACHINE_ACCOUNT),
directory_id=dc_directory.id,
),
Attribute(
name="ipHostNumber",
value=settings.DEFAULT_NAMESERVER,
directory_id=dc_directory.id,
),
Attribute(
name="cn",
value=settings.HOST_MACHINE_NAME,
directory_id=dc_directory.id,
),
]

session.add_all(attributes)
await session.flush()

await role_use_case.inherit_parent_aces(
parent_directory=dc_ou_dir,
directory=dc_directory,
)
await entity_type_dao.attach_entity_type_to_directory(
directory=dc_directory,
is_system_entity_type=False,
object_class_names={"top", "computer"},
)
await session.flush()


async def add_domain_controller(
session: AsyncSession,
settings: Settings,
role_use_case: RoleUseCase,
entity_type_dao: EntityTypeDAO,
) -> None:
logger.info("Adding domain controller.")

domains = await get_base_directories(session)
if not domains:
logger.debug("Cannot get base directory")
return

domain_controllers_ou = await session.scalar(
select(Directory).where(
qa(Directory.name) == DOMAIN_CONTROLLERS_OU_NAME,
),
)

if not domain_controllers_ou:
logger.debug("Domain controllers OU does not exist.")
return

domain_controller = await session.scalar(
select(qa(Directory.id).distinct())
.join(qa(Directory.attributes))
.where(
qa(Directory.parent_id) == domain_controllers_ou.id,
qa(Attribute.name) == "ipHostNumber",
qa(Attribute.value) == settings.DEFAULT_NAMESERVER,
),
)

if domain_controller:
logger.debug("Domain controllers already exists")
return

await _add_domain_controller(
session=session,
role_use_case=role_use_case,
entity_type_dao=entity_type_dao,
settings=settings,
domain=domains[0],
dc_ou_dir=domain_controllers_ou,
)

logger.debug("Domain controller added.")

await session.commit()
6 changes: 1 addition & 5 deletions app/ldap_protocol/auth/setup_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ async def setup_enviroment(
self,
*,
data: list,
is_system: bool,
dn: str = "multifactor.dev",
) -> None:
"""Create directories and users for enviroment."""
Expand Down Expand Up @@ -110,7 +109,6 @@ async def setup_enviroment(
for unit in data:
await self.create_dir(
unit,
is_system=is_system,
domain=domain,
parent=domain,
)
Expand All @@ -125,13 +123,12 @@ async def setup_enviroment(
async def create_dir(
self,
data: dict,
is_system: bool,
domain: Directory,
parent: Directory | None = None,
) -> None:
"""Create data recursively."""
dir_ = Directory(
is_system=is_system,
is_system=data["is_system"],
object_class=data["object_class"],
name=data["name"],
)
Expand Down Expand Up @@ -228,7 +225,6 @@ async def create_dir(
for n_data in data["children"]:
await self.create_dir(
n_data,
is_system=is_system,
domain=domain,
parent=dir_,
)
Expand Down
Loading