Skip to content

Commit 265e99a

Browse files
Refactor rootdse gw (#856)
* add: rootDSE gateway * refactor: move nl * refactor: move rootDSE * add: sid * fix: remove sid * fix: add rootdse provider * refactor: protocol for Domain * Update app/ldap_protocol/rootdse/gateway.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * add: DCInfoReader * chore: remove nesessary docstring rules * add: unc * fix: ruff * add: docstrings ignore * add: get_dcinfo * fix: ioc --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent fd4097d commit 265e99a

File tree

15 files changed

+203
-73
lines changed

15 files changed

+203
-73
lines changed

app/api/shadow/router.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
from dishka.integrations.fastapi import DishkaRoute
1212
from fastapi import APIRouter, Body
1313

14+
from ldap_protocol.rootdse.dto import DomainControllerInfo
15+
from ldap_protocol.rootdse.reader import DCInfoReader
16+
1417
from .adapter import ShadowAdapter
1518

1619
shadow_router = APIRouter(route_class=DishkaRoute)
@@ -46,3 +49,10 @@ async def change_password(
4649
:return None: None
4750
"""
4851
return await adapter.change_password(principal, new_password)
52+
53+
54+
@shadow_router.get("/metadata/dcinfo")
55+
async def get_dcinfo(
56+
dcreader: FromDishka[DCInfoReader],
57+
) -> DomainControllerInfo:
58+
return await dcreader.get()

app/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,3 +326,6 @@
326326
"children": [],
327327
},
328328
]
329+
330+
DEFAULT_DC_POSTFIX = "DC1"
331+
UNC_PREFIX = "\\\\"

app/ioc.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@
128128
from ldap_protocol.roles.ace_dao import AccessControlEntryDAO
129129
from ldap_protocol.roles.role_dao import RoleDAO
130130
from ldap_protocol.roles.role_use_case import RoleUseCase
131+
from ldap_protocol.rootdse.gateway import SADomainGateway
132+
from ldap_protocol.rootdse.gw_protocol import DomainReadProtocol
133+
from ldap_protocol.rootdse.reader import DCInfoReader, RootDSEReader
131134
from ldap_protocol.session_storage import RedisSessionStorage, SessionStorage
132135
from ldap_protocol.session_storage.repository import SessionRepository
133136
from password_utils import PasswordUtils
@@ -451,6 +454,13 @@ async def get_dhcp_mngr(
451454
entity_type_use_case = provide(EntityTypeUseCase, scope=Scope.REQUEST)
452455
dns_use_case = provide(DNSUseCase, scope=Scope.REQUEST)
453456
dns_state_gateway = provide(DNSStateGateway, scope=Scope.REQUEST)
457+
rootdse_gw = provide(
458+
SADomainGateway,
459+
provides=DomainReadProtocol,
460+
scope=Scope.REQUEST,
461+
)
462+
rootdse_reader = provide(RootDSEReader, scope=Scope.REQUEST)
463+
dcinfo_reader = provide(DCInfoReader, scope=Scope.REQUEST)
454464

455465

456466
class LDAPContextProvider(Provider):

app/ldap_protocol/ldap_requests/contexts.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from ldap_protocol.policies.password import PasswordPolicyUseCases
1717
from ldap_protocol.roles.access_manager import AccessManager
1818
from ldap_protocol.roles.role_use_case import RoleUseCase
19+
from ldap_protocol.rootdse.reader import RootDSEReader
1920
from ldap_protocol.session_storage import SessionStorage
2021
from password_utils import PasswordUtils
2122

@@ -70,6 +71,7 @@ class LDAPSearchRequestContext:
7071
ldap_session: LDAPSession
7172
settings: Settings
7273
access_manager: AccessManager
74+
rootdse_rd: RootDSEReader
7375

7476

7577
@dataclass

app/ldap_protocol/ldap_requests/search.py

Lines changed: 3 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from sqlalchemy.sql.elements import ColumnElement, UnaryExpression
1919
from sqlalchemy.sql.expression import Select
2020

21-
from config import Settings
2221
from entities import (
2322
Attribute,
2423
AttributeType,
@@ -42,13 +41,12 @@
4241
SearchResultEntry,
4342
SearchResultReference,
4443
)
45-
from ldap_protocol.netlogon import NetLogonAttributeHandler
4644
from ldap_protocol.objects import DerefAliases, ProtocolRequests, Scope
4745
from ldap_protocol.roles.access_manager import AccessManager
46+
from ldap_protocol.rootdse.netlogon import NetLogonAttributeHandler
4847
from ldap_protocol.utils.cte import get_all_parent_group_directories
4948
from ldap_protocol.utils.helpers import (
5049
dt_to_ft,
51-
get_generalized_now,
5250
get_windows_timestamp,
5351
string_to_sid,
5452
)
@@ -217,70 +215,6 @@ async def _get_subschema(self, session: AsyncSession) -> SearchResultEntry:
217215
],
218216
)
219217

220-
async def get_root_dse(
221-
self,
222-
session: AsyncSession,
223-
settings: Settings,
224-
) -> defaultdict[str, list[str]]:
225-
"""Get RootDSE.
226-
227-
:return defaultdict[str, list[str]]: queried attrs
228-
"""
229-
data = defaultdict(list)
230-
domain_query = select(Directory).filter_by(object_class="domain")
231-
domain = (await session.scalars(domain_query)).one()
232-
233-
schema = "CN=Schema"
234-
if self.requested_attrs == ["subschemasubentry"]:
235-
data["subschemaSubentry"].append(schema)
236-
return data
237-
238-
data["dnsHostName"].append(domain.name)
239-
data["serverName"].append(domain.name)
240-
data["serviceName"].append(domain.name)
241-
data["dsServiceName"].append(domain.name)
242-
data["LDAPServiceName"].append(domain.name)
243-
data["dnsForestName"].append(domain.name)
244-
data["dnsDomainName"].append(domain.name)
245-
data["domainGuid"].append(str(domain.object_guid))
246-
data["vendorName"].append(settings.VENDOR_NAME)
247-
data["vendorVersion"].append(settings.VENDOR_VERSION)
248-
data["namingContexts"].append(domain.path_dn)
249-
data["namingContexts"].append(schema)
250-
data["rootDomainNamingContext"].append(domain.path_dn)
251-
data["supportedLDAPVersion"].append("3")
252-
data["defaultNamingContext"].append(domain.path_dn)
253-
data["currentTime"].append(get_generalized_now(settings.TIMEZONE))
254-
data["subschemaSubentry"].append(schema)
255-
data["schemaNamingContext"].append(schema)
256-
data["supportedSASLMechanisms"] = [
257-
"ANONYMOUS",
258-
"PLAIN",
259-
"GSSAPI",
260-
"GSS-SPNEGO",
261-
]
262-
data["highestCommittedUSN"].append("126991")
263-
data["supportedExtension"] = [
264-
"1.3.6.1.4.1.4203.1.11.3", # whoami
265-
"1.3.6.1.4.1.4203.1.11.1", # password modify
266-
]
267-
data["supportedControl"] = [
268-
"2.16.840.1.113730.3.4.4", # password expire policy
269-
]
270-
data["domainFunctionality"].append("0")
271-
data["supportedLDAPPolicies"] = [
272-
"MaxConnIdleTime",
273-
"MaxPageSize",
274-
"MaxValRange",
275-
]
276-
data["supportedCapabilities"] = [
277-
"1.2.840.113556.1.4.800", # ACTIVE_DIRECTORY_OID
278-
"1.2.840.113556.1.4.1670", # ACTIVE_DIRECTORY_V51_OID
279-
"1.2.840.113556.1.4.1791", # ACTIVE_DIRECTORY_LDAP_INTEG_OID
280-
]
281-
282-
return data
283-
284218
def _cast_filter(self) -> UnaryExpression | ColumnElement:
285219
"""Convert asn1 row filter_ to sqlalchemy obj.
286220
@@ -308,7 +242,7 @@ def check_netlogon_filter(self) -> bool:
308242
return "netlogon" in self.requested_attrs
309243

310244
async def _get_netlogon(self, ctx: LDAPSearchRequestContext) -> bytes:
311-
rootdse = await self.get_root_dse(ctx.session, ctx.settings)
245+
rootdse = await ctx.rootdse_rd.get(self.requested_attrs)
312246
nl = NetLogonAttributeHandler.from_filter(rootdse, self.filter)
313247
return nl.get_attr()
314248

@@ -343,7 +277,7 @@ async def get_result(
343277
],
344278
)
345279
elif is_root_dse:
346-
attrs = await self.get_root_dse(ctx.session, ctx.settings)
280+
attrs = await ctx.rootdse_rd.get(self.requested_attrs)
347281
yield SearchResultEntry(
348282
object_name="",
349283
partial_attributes=[

app/ldap_protocol/policies/audit/events/sender.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
class AuditEventSenderManager:
2727
"""Audit event manager."""
2828

29-
def __init__( # noqa: D107
29+
def __init__(
3030
self,
3131
normalized_audit_manager: NormalizedAuditManager,
3232
session: AsyncSession,

app/ldap_protocol/rootdse/__init__.py

Whitespace-only changes.

app/ldap_protocol/rootdse/dto.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Domain controller info Dataclasses.
2+
3+
Copyright (c) 2025 MultiFactor
4+
License: https://github.com/MultiDirectoryLab/MultiDirectory/blob/main/LICENSE
5+
"""
6+
7+
from dataclasses import dataclass
8+
9+
10+
@dataclass(frozen=True)
11+
class DomainControllerInfo:
12+
"""DC info dataclass."""
13+
14+
net_bios_domain: str
15+
net_bios_hostname: str
16+
unc: str
17+
dns: str
18+
dns_forest: str
19+
object_sid: str
20+
object_guid: str
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""LDAP SQLAlchemy gw for handle requests.
2+
3+
Copyright (c) 2025 MultiFactor
4+
License: https://github.com/MultiDirectoryLab/MultiDirectory/blob/main/LICENSE
5+
"""
6+
7+
from sqlalchemy import select
8+
from sqlalchemy.ext.asyncio import AsyncSession
9+
10+
from entities import Directory
11+
12+
13+
class SADomainGateway:
14+
"""RootDSE gw."""
15+
16+
def __init__(self, session: AsyncSession) -> None:
17+
"""Set up gw."""
18+
self._session = session
19+
20+
async def get_domain(self) -> Directory:
21+
domain_query = select(Directory).filter_by(object_class="domain")
22+
return (await self._session.scalars(domain_query)).one()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Domain/Directory gw for handle requests.
2+
3+
Copyright (c) 2025 MultiFactor
4+
License: https://github.com/MultiDirectoryLab/MultiDirectory/blob/main/LICENSE
5+
"""
6+
7+
from typing import Protocol
8+
9+
from entities import Directory
10+
11+
12+
class DomainReadProtocol(Protocol):
13+
"""RootDSE gw."""
14+
15+
async def get_domain(self) -> Directory: ...

0 commit comments

Comments
 (0)