update
This commit is contained in:
parent
d12cc71bd4
commit
e60f568746
|
|
@ -0,0 +1,96 @@
|
|||
# sync_ehr_to_ad 使用说明
|
||||
|
||||
该 extension 从北森 EHR 拉取当前用户信息,并同步到本地 AD 域中已存在的用户对象。
|
||||
|
||||
## Job
|
||||
|
||||
- `handler_path`: `extensions.sync_ehr_to_ad.job:SyncEhrToAdUserJob`
|
||||
- `job_id`: `sync_ehr_to_ad.sync_users`
|
||||
|
||||
同步行为:
|
||||
|
||||
- 只处理当前在职用户。
|
||||
- 只更新 AD 中已存在的 `sAMAccountName` 用户,不自动创建 AD 账号。
|
||||
- 更新前会比对 AD 当前值与 EHR 目标值;一致则跳过,不一致才更新差异字段。
|
||||
- EHR 目标值为空时不会清空 AD 字段。
|
||||
- 如果配置了 `target_sam_accounts`,只对列表中的 AD 账号执行同步。
|
||||
|
||||
## public_cfg 示例
|
||||
|
||||
```json
|
||||
{
|
||||
"ldap_uri": "ldaps://dc01.vastai.com:636",
|
||||
"ldap_base_dn": "DC=vastai,DC=com",
|
||||
"ldap_user_filter": "(sAMAccountName={sAMAccountName})",
|
||||
"ldap_verify_tls": false,
|
||||
"dry_run": true,
|
||||
"target_sam_accounts": ["fchen", "jqian"],
|
||||
"proxy_alias_domain": "vastaitech.com",
|
||||
"default_company": "Vastai Technologies",
|
||||
"department_code_ad_attribute": "departmentNumber",
|
||||
"postal_code": "201210",
|
||||
"location_mappings": {
|
||||
"上海": {
|
||||
"co": "China",
|
||||
"c": "CN",
|
||||
"countryCode": 156,
|
||||
"st": "Shanghai",
|
||||
"l": "Shanghai"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## secret_cfg 示例
|
||||
|
||||
```json
|
||||
{
|
||||
"app_key": "EHR_APP_KEY",
|
||||
"app_secret": "EHR_APP_SECRET",
|
||||
"ldap_bind_dn": "CN=svc-ehr-ad,OU=Service Accounts,DC=vastai,DC=com",
|
||||
"ldap_bind_password": "password"
|
||||
}
|
||||
```
|
||||
|
||||
## 常用 public_cfg
|
||||
|
||||
- `target_sam_accounts`: 可选,字符串数组。传入后只同步这些 AD 账号;不传则同步所有当前用户。
|
||||
- `dry_run`: 可选,默认 `false`。建议首次配置为 `true`,只记录会变更的字段,不写入 AD。
|
||||
- `ldap_uri`: AD LDAP 地址,如 `ldaps://dc01.vastai.com:636`。
|
||||
- `ldap_base_dn`: AD 用户搜索根 DN。
|
||||
- `ldap_user_filter`: 用户搜索过滤器,默认 `(sAMAccountName={sAMAccountName})`。
|
||||
- `ldap_verify_tls`: 是否校验证书,默认 `true`。
|
||||
- `proxy_alias_domain`: 生成 `smtp:<sAMAccountName>@domain` 别名时使用的域名。
|
||||
- `department_code_ad_attribute`: 部门编码写入的 AD 属性,默认 `departmentNumber`。
|
||||
- `default_company`: 固定公司名;不传时尝试取 EHR 根组织名称。
|
||||
- `location_mappings`: 工作地点到 AD 国家、省、市字段的映射。
|
||||
|
||||
## 指定用户同步示例
|
||||
|
||||
只验证并同步 `fchen` 一个用户:
|
||||
|
||||
```json
|
||||
{
|
||||
"ldap_uri": "ldaps://dc01.vastai.com:636",
|
||||
"ldap_base_dn": "DC=vastai,DC=com",
|
||||
"ldap_verify_tls": false,
|
||||
"dry_run": true,
|
||||
"target_sam_accounts": ["fchen"],
|
||||
"proxy_alias_domain": "vastaitech.com",
|
||||
"default_company": "Vastai Technologies"
|
||||
}
|
||||
```
|
||||
|
||||
确认日志中的 `changed_attrs` 符合预期后,将 `dry_run` 改为 `false` 即可真实写入。
|
||||
|
||||
## 返回统计
|
||||
|
||||
Job 返回结果包含:
|
||||
|
||||
- `filtered_by_target_sam`: 是否启用了指定用户过滤。
|
||||
- `target_sam_accounts`: 指定用户数量。
|
||||
- `processed`: 实际处理的用户数。
|
||||
- `updated`: 有差异并执行更新的用户数。
|
||||
- `skipped_unchanged`: AD 与 EHR 一致而跳过的用户数。
|
||||
- `skipped_not_found_ad`: EHR 有 AD 账号但 AD 中找不到的用户数。
|
||||
- `failed`: 同步失败的用户数。
|
||||
|
|
@ -310,6 +310,15 @@ def _job_post_name(job_post: dict[str, Any]) -> str:
|
|||
return ""
|
||||
|
||||
|
||||
def _parse_target_sam_accounts(params: dict[str, Any]) -> set[str]:
|
||||
raw = params.get("target_sam_accounts")
|
||||
if raw is None:
|
||||
return set()
|
||||
if not isinstance(raw, list):
|
||||
raise ValueError("public_cfg.target_sam_accounts must be a JSON array, e.g. [\"fchen\", \"jqian\"]")
|
||||
return {str(x).strip().lower() for x in raw if str(x).strip()}
|
||||
|
||||
|
||||
class SyncEhrToAdUserJob(BaseJob):
|
||||
"""
|
||||
EHR 当前人员 -> 本地 AD 用户属性同步。
|
||||
|
|
@ -360,6 +369,7 @@ class SyncEhrToAdUserJob(BaseJob):
|
|||
department_code_attr = str(params.get("department_code_ad_attribute") or "departmentNumber").strip()
|
||||
postal_code = str(params.get("postal_code") or "").strip()
|
||||
default_company = str(params.get("default_company") or "").strip()
|
||||
target_sam_accounts = _parse_target_sam_accounts(params)
|
||||
|
||||
current_status_values_param = params.get("current_status_values")
|
||||
if isinstance(current_status_values_param, list):
|
||||
|
|
@ -435,20 +445,24 @@ class SyncEhrToAdUserJob(BaseJob):
|
|||
sam = _field_value(item, domain_account_key)
|
||||
if not sam:
|
||||
continue
|
||||
existing = users_by_sam.get(sam.lower())
|
||||
users_by_sam[sam.lower()] = item if existing is None else _choose_better_record(existing, item)
|
||||
sam_key = sam.lower()
|
||||
user_id = _to_int_safe((item.get("employeeInfo") or {}).get("userID"))
|
||||
if user_id > 0:
|
||||
user_id_to_sam[user_id] = sam
|
||||
if target_sam_accounts and sam_key not in target_sam_accounts:
|
||||
continue
|
||||
existing = users_by_sam.get(sam_key)
|
||||
users_by_sam[sam_key] = item if existing is None else _choose_better_record(existing, item)
|
||||
if max_users > 0 and len(users_by_sam) >= max_users:
|
||||
break
|
||||
|
||||
logger.info(
|
||||
"EHR 当前用户准备完成:employee_rows=%s current_with_ad_account=%s org_rows=%s job_post_rows=%s",
|
||||
"EHR 当前用户准备完成:employee_rows=%s current_with_ad_account=%s org_rows=%s job_post_rows=%s target_sam_accounts=%s",
|
||||
len(emp_rows),
|
||||
len(users_by_sam),
|
||||
len(org_rows),
|
||||
len(job_post_rows),
|
||||
len(target_sam_accounts),
|
||||
)
|
||||
|
||||
processed = 0
|
||||
|
|
@ -587,6 +601,8 @@ class SyncEhrToAdUserJob(BaseJob):
|
|||
result = {
|
||||
"ok": failed == 0,
|
||||
"dry_run": dry_run,
|
||||
"filtered_by_target_sam": bool(target_sam_accounts),
|
||||
"target_sam_accounts": len(target_sam_accounts),
|
||||
"ehr_employee_rows": len(emp_rows),
|
||||
"ehr_current_users_with_ad_account": len(users_by_sam),
|
||||
"processed": processed,
|
||||
|
|
|
|||
Loading…
Reference in New Issue