This commit is contained in:
Marsway 2026-04-30 15:46:01 +08:00
parent e60f568746
commit 7ce2bcb034
3 changed files with 38 additions and 1 deletions

View File

@ -12,6 +12,7 @@
- 只处理当前在职用户。
- 只更新 AD 中已存在的 `sAMAccountName` 用户,不自动创建 AD 账号。
- 更新前会比对 AD 当前值与 EHR 目标值;一致则跳过,不一致才更新差异字段。
- 默认写入后立即回读校验;如果 LDAP 返回成功但字段未落地Job 会报失败并记录未生效字段。
- EHR 目标值为空时不会清空 AD 字段。
- 如果配置了 `target_sam_accounts`,只对列表中的 AD 账号执行同步。
@ -24,6 +25,7 @@
"ldap_user_filter": "(sAMAccountName={sAMAccountName})",
"ldap_verify_tls": false,
"dry_run": true,
"verify_after_write": true,
"target_sam_accounts": ["fchen", "jqian"],
"proxy_alias_domain": "vastaitech.com",
"default_company": "Vastai Technologies",
@ -56,6 +58,7 @@
- `target_sam_accounts`: 可选,字符串数组。传入后只同步这些 AD 账号;不传则同步所有当前用户。
- `dry_run`: 可选,默认 `false`。建议首次配置为 `true`,只记录会变更的字段,不写入 AD。
- `verify_after_write`: 可选,默认 `true`。真实写入后回读 AD 字段,确认修改已落地。
- `ldap_uri`: AD LDAP 地址,如 `ldaps://dc01.vastai.com:636`
- `ldap_base_dn`: AD 用户搜索根 DN。
- `ldap_user_filter`: 用户搜索过滤器,默认 `(sAMAccountName={sAMAccountName})`
@ -75,6 +78,7 @@
"ldap_base_dn": "DC=vastai,DC=com",
"ldap_verify_tls": false,
"dry_run": true,
"verify_after_write": true,
"target_sam_accounts": ["fchen"],
"proxy_alias_domain": "vastaitech.com",
"default_company": "Vastai Technologies"

View File

@ -4,7 +4,7 @@ import logging
import ssl
from typing import Any
from ldap3 import ALL, MODIFY_REPLACE, SUBTREE, Connection, Server, Tls
from ldap3 import ALL, BASE, MODIFY_REPLACE, SUBTREE, Connection, Server, Tls
from ldap3.utils.conv import escape_filter_chars
from extensions.sync_ehr_to_oa.api import SyncEhrToOaApi
@ -123,6 +123,25 @@ class ActiveDirectoryClient:
finally:
conn.unbind()
def read_user_by_dn(self, dn: str, *, attributes: list[str] | None = None) -> dict[str, Any] | None:
clean_dn = str(dn or "").strip()
if not clean_dn:
return None
conn = self._connect()
try:
conn.search(
clean_dn,
"(objectClass=*)",
search_scope=BASE,
attributes=attributes or ["distinguishedName", "sAMAccountName"],
)
if not conn.entries:
return None
entry = conn.entries[0]
return {"dn": entry.entry_dn, "attributes": dict(entry.entry_attributes_as_dict)}
finally:
conn.unbind()
@staticmethod
def _normalize_change_value(value: Any) -> list[Any]:
if isinstance(value, (list, tuple, set)):
@ -155,6 +174,7 @@ class ActiveDirectoryClient:
ok = bool(conn.modify(clean_dn, changes))
if not ok:
raise RuntimeError(f"AD modify failed dn={clean_dn!r} result={conn.result!r}")
logger.info("AD modify success: dn=%s result=%s changed_attrs=%s", clean_dn, conn.result, sorted(changes.keys()))
return True
finally:
conn.unbind()

View File

@ -347,6 +347,7 @@ class SyncEhrToAdUserJob(BaseJob):
ldap_verify_tls = _to_bool_or_none(params.get("ldap_verify_tls"))
dry_run = _to_bool_or_none(params.get("dry_run"))
verbose_trace = _to_bool_or_none(params.get("verbose_trace"))
verify_after_write = _to_bool_or_none(params.get("verify_after_write"))
if ldap_use_starttls is None:
ldap_use_starttls = False
if ldap_verify_tls is None:
@ -355,6 +356,8 @@ class SyncEhrToAdUserJob(BaseJob):
dry_run = False
if verbose_trace is None:
verbose_trace = True
if verify_after_write is None:
verify_after_write = True
stop_time = params.get("stop_time")
capacity = int(params.get("capacity") or 300)
@ -586,6 +589,16 @@ class SyncEhrToAdUserJob(BaseJob):
changed = ad.modify_user(str(ad_user["dn"]), diff_attributes, dry_run=dry_run)
if changed:
if verify_after_write and not dry_run:
verify_attrs = list(diff_attributes.keys())
readback_user = ad.read_user_by_dn(str(ad_user["dn"]), attributes=verify_attrs)
readback_attrs = (readback_user or {}).get("attributes") or {}
remaining_diff = _diff_ad_attributes(readback_attrs, diff_attributes)
if remaining_diff:
raise RuntimeError(
"AD modify returned success but readback still differs: "
f"sam={sam!r} dn={ad_user['dn']!r} attrs={sorted(remaining_diff.keys())}"
)
updated += 1
if verbose_trace:
logger.info(