fix(ad): normalize ADUserAdapter to snake_case contract (#4)
- ad_adapter.py: emit snake_case keys from PS queries and surface email via the `mail` attribute in both get_user and search paths - adapters.py: update ADUserAdapter.to_canonical to consume normalized keys (e.g. `username`, `last_logon_utc`, `ou`) instead of raw LDAP names (sAMAccountName, lastLogonTimestamp, dn) - Resolves field-name alignment tech debt noted in SESSION_SNAPSHOT_2026-04-15
This commit is contained in:
parent
f6cfd17e30
commit
6bf5d8dd05
@ -1,3 +1,7 @@
|
|||||||
|
# Rule:
|
||||||
|
# ad_adapter.py outputs normalized snake_case fields
|
||||||
|
# All adapters (ADUserAdapter, EntraUserAdapter, etc.) consume that contract
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@ -118,17 +122,18 @@ class ActiveDirectoryIdentityBackend:
|
|||||||
# Build PowerShell command using JSON output for reliable parsing
|
# Build PowerShell command using JSON output for reliable parsing
|
||||||
escaped_username = self._escape_ps_single_quoted(username)
|
escaped_username = self._escape_ps_single_quoted(username)
|
||||||
command = f"""
|
command = f"""
|
||||||
$user = Get-ADUser -Filter "samAccountName -eq '{escaped_username}'" -Properties GivenName,Surname,DisplayName,Enabled,DistinguishedName,Description,lastLogonTimestamp -ErrorAction Stop
|
$user = Get-ADUser -Filter "samAccountName -eq '{escaped_username}'" -Properties displayName,mail,givenName,sn,Enabled,DistinguishedName,Description,lastLogonTimestamp -ErrorAction Stop
|
||||||
if ($user) {{
|
if ($user) {{
|
||||||
$lastLogon = if ($user.lastLogonTimestamp) {{ [DateTime]::FromFileTime($user.lastLogonTimestamp).ToUniversalTime().ToString('o') }} else {{ $null }}
|
$lastLogon = if ($user.lastLogonTimestamp) {{ [DateTime]::FromFileTime($user.lastLogonTimestamp).ToUniversalTime().ToString('o') }} else {{ $null }}
|
||||||
@{{
|
@{{
|
||||||
username = $user.SamAccountName
|
username = $user.SamAccountName
|
||||||
first_name = if ($user.GivenName) {{ $user.GivenName }} else {{ '' }}
|
display_name = if ($user.displayName) {{$user.displayName}} else {{$user.Name}}
|
||||||
last_name = if ($user.Surname) {{ $user.Surname }} else {{ '' }}
|
first_name = if ($user.givenName) {{$user.givenName}} else {{''}}
|
||||||
display_name = if ($user.DisplayName) {{ $user.DisplayName }} else {{ '' }}
|
last_name = if ($user.sn) {{$user.sn}} else {{''}}
|
||||||
|
email = if ($user.mail) {{$user.mail}} else {{''}}
|
||||||
enabled = $user.Enabled
|
enabled = $user.Enabled
|
||||||
ou = $user.DistinguishedName
|
ou = $user.DistinguishedName
|
||||||
description = if ($user.Description) {{ $user.Description }} else {{ '' }}
|
description = if ($user.Description) {{$user.Description}} else {{''}}
|
||||||
last_logon_utc = $lastLogon
|
last_logon_utc = $lastLogon
|
||||||
}} | ConvertTo-Json -Compress
|
}} | ConvertTo-Json -Compress
|
||||||
}}
|
}}
|
||||||
@ -152,6 +157,7 @@ class ActiveDirectoryIdentityBackend:
|
|||||||
"first_name": user_data.get("first_name", "") or "",
|
"first_name": user_data.get("first_name", "") or "",
|
||||||
"last_name": user_data.get("last_name", "") or "",
|
"last_name": user_data.get("last_name", "") or "",
|
||||||
"display_name": user_data.get("display_name", "") or "",
|
"display_name": user_data.get("display_name", "") or "",
|
||||||
|
"email": user_data.get("email", "") or "",
|
||||||
"enabled": user_data["enabled"],
|
"enabled": user_data["enabled"],
|
||||||
"ou": user_data["ou"],
|
"ou": user_data["ou"],
|
||||||
"description": user_data["description"] or "",
|
"description": user_data["description"] or "",
|
||||||
@ -171,36 +177,34 @@ class ActiveDirectoryIdentityBackend:
|
|||||||
|
|
||||||
max_results = max(1, min(limit, 100))
|
max_results = max(1, min(limit, 100))
|
||||||
escaped_query = self._escape_ps_single_quoted(query)
|
escaped_query = self._escape_ps_single_quoted(query)
|
||||||
|
|
||||||
command = f"""
|
command = f"""
|
||||||
$query = '{escaped_query}'
|
$query = '{escaped_query}'
|
||||||
$tokens = @($query.Split(' ', [System.StringSplitOptions]::RemoveEmptyEntries))
|
$tokens = @($query.Split(' ', [System.StringSplitOptions]::RemoveEmptyEntries))
|
||||||
|
|
||||||
if ($tokens.Count -ge 2) {{
|
if ($tokens.Count -ge 2) {{
|
||||||
$first = $tokens[0]
|
$first = $tokens[0]
|
||||||
$last = $tokens[1]
|
$last = $tokens[1]
|
||||||
$adFilter = "(givenName -like '$($first)*' -and surname -like '$($last)*') -or (displayName -like '$query*')"
|
$adFilter = "(givenName -like '$($first)*' -and sn -like '$($last)*') -or (displayName -like '$query*')"
|
||||||
}} else {{
|
}} else {{
|
||||||
$adFilter = "givenName -like '$query*' -or surname -like '$query*' -or displayName -like '$query*'"
|
$adFilter = "givenName -like '$query*' -or sn -like '$query*' -or displayName -like '$query*'"
|
||||||
}}
|
}}
|
||||||
|
|
||||||
$users = @(Get-ADUser -Filter $adFilter -Properties GivenName,Surname,DisplayName,Enabled,DistinguishedName -ErrorAction Stop |
|
$users = @(Get-ADUser -Filter $adFilter -Properties displayName,mail,givenName,sn,Enabled,DistinguishedName -ErrorAction Stop |
|
||||||
Sort-Object DisplayName |
|
|
||||||
Select-Object -First {max_results} |
|
|
||||||
ForEach-Object {{
|
ForEach-Object {{
|
||||||
@{{
|
@{{
|
||||||
username = $_.SamAccountName
|
username = $_.SamAccountName
|
||||||
first_name = if ($_.GivenName) {{ $_.GivenName }} else {{ '' }}
|
display_name = if ($_.displayName) {{ $_.displayName }} else {{ $_.Name }}
|
||||||
last_name = if ($_.Surname) {{ $_.Surname }} else {{ '' }}
|
first_name = if ($_.givenName) {{ $_.givenName }} else {{ '' }}
|
||||||
display_name = if ($_.DisplayName) {{ $_.DisplayName }} else {{ '' }}
|
last_name = if ($_.sn) {{ $_.sn }} else {{ '' }}
|
||||||
|
email = if ($_.mail) {{ $_.mail }} else {{ '' }}
|
||||||
enabled = $_.Enabled
|
enabled = $_.Enabled
|
||||||
ou = $_.DistinguishedName
|
ou = $_.DistinguishedName
|
||||||
}}
|
}}
|
||||||
}})
|
}} | Select-Object -First {max_results})
|
||||||
|
|
||||||
$users | ConvertTo-Json -Compress
|
$users | ConvertTo-Json -Compress
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
result = await self._run_powershell(
|
result = await self._run_powershell(
|
||||||
command,
|
command,
|
||||||
{"name_query": name_query, "limit": max_results},
|
{"name_query": name_query, "limit": max_results},
|
||||||
@ -234,6 +238,7 @@ class ActiveDirectoryIdentityBackend:
|
|||||||
"first_name": user.get("first_name", "") or "",
|
"first_name": user.get("first_name", "") or "",
|
||||||
"last_name": user.get("last_name", "") or "",
|
"last_name": user.get("last_name", "") or "",
|
||||||
"display_name": user.get("display_name", "") or "",
|
"display_name": user.get("display_name", "") or "",
|
||||||
|
"email": user.get("email", "") or "",
|
||||||
"enabled": bool(user.get("enabled", False)),
|
"enabled": bool(user.get("enabled", False)),
|
||||||
"ou": user.get("ou", "") or "",
|
"ou": user.get("ou", "") or "",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,27 +114,27 @@ class ADUserAdapter:
|
|||||||
# This is a simplification; in reality we'd need to look up the manager's email
|
# This is a simplification; in reality we'd need to look up the manager's email
|
||||||
manager_email = None # Would require a separate AD lookup
|
manager_email = None # Would require a separate AD lookup
|
||||||
|
|
||||||
|
|
||||||
return CanonicalUser(
|
return CanonicalUser(
|
||||||
email=_get(ad_user, "mail", default=""),
|
email=_get(ad_user, "email", default=""),
|
||||||
employee_id=_get(ad_user, "employeeID"),
|
employee_id=_get(ad_user, "employee_id"),
|
||||||
username=_get(ad_user, "sAMAccountName"),
|
username=_get(ad_user, "username"),
|
||||||
display_name=_get(ad_user, "displayName", default="Unknown"),
|
display_name=_get(ad_user, "display_name", default="Unknown"),
|
||||||
first_name=_get(ad_user, "givenName"),
|
first_name=_get(ad_user, "first_name"),
|
||||||
last_name=_get(ad_user, "sn"),
|
last_name=_get(ad_user, "last_name"),
|
||||||
job_title=_get(ad_user, "title"),
|
job_title=_get(ad_user, "job_title"),
|
||||||
department=_get(ad_user, "department"),
|
department=_get(ad_user, "department"),
|
||||||
manager_email=manager_email,
|
manager_email=manager_email,
|
||||||
office_location=_get(ad_user, "physicalDeliveryOfficeName"),
|
office_location=_get(ad_user, "office_location"),
|
||||||
status=status,
|
status=status,
|
||||||
is_enabled=not is_disabled,
|
is_enabled=not is_disabled,
|
||||||
last_login=_parse_iso_date(_get(ad_user, "lastLogonTimestamp")),
|
last_login=_parse_iso_date(_get(ad_user, "last_logon_utc")),
|
||||||
created_date=_parse_iso_date(_get(ad_user, "whenCreated")),
|
created_date=_parse_iso_date(_get(ad_user, "created_date")),
|
||||||
phone=_get(ad_user, "telephoneNumber"),
|
phone=_get(ad_user, "phone"),
|
||||||
source_system="ActiveDirectory",
|
source_system="ActiveDirectory",
|
||||||
source_id=_get(ad_user, "dn"),
|
source_id=_get(ad_user, "ou"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ── Microsoft Entra ID Adapters ───────────────────────────────────────────────
|
# ── Microsoft Entra ID Adapters ───────────────────────────────────────────────
|
||||||
|
|
||||||
class EntraUserAdapter:
|
class EntraUserAdapter:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user