From 7ce2bcb034a94c6ded7a8361509a52cb16ab61e3 Mon Sep 17 00:00:00 2001 From: Marsway Date: Thu, 30 Apr 2026 15:46:01 +0800 Subject: [PATCH] update --- extensions/sync_ehr_to_ad/README.md | 4 ++++ extensions/sync_ehr_to_ad/api.py | 22 +++++++++++++++++++++- extensions/sync_ehr_to_ad/job.py | 13 +++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/extensions/sync_ehr_to_ad/README.md b/extensions/sync_ehr_to_ad/README.md index 3beb786..b20694b 100644 --- a/extensions/sync_ehr_to_ad/README.md +++ b/extensions/sync_ehr_to_ad/README.md @@ -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" diff --git a/extensions/sync_ehr_to_ad/api.py b/extensions/sync_ehr_to_ad/api.py index 7cf5b14..61140a5 100644 --- a/extensions/sync_ehr_to_ad/api.py +++ b/extensions/sync_ehr_to_ad/api.py @@ -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() diff --git a/extensions/sync_ehr_to_ad/job.py b/extensions/sync_ehr_to_ad/job.py index dec2514..1131a79 100644 --- a/extensions/sync_ehr_to_ad/job.py +++ b/extensions/sync_ehr_to_ad/job.py @@ -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(