Mirror your WeCom (企业微信)
directory into LDAP.
把企业微信通讯录
自动同步进 LDAP。
wecom-ldap-sync is a self-hosted Docker service that mirrors your WeCom (企业微信) directory into OpenLDAP — departments, users, groups, and passwords — for SSO, unified identity, and centralized account management. Open-source, Apache-2.0, five-minute setup.
企业微信通讯录同步到 LDAP / OpenLDAP 的开源工具,支持 SSO 单点登录、统一身份认证、组织架构同步、自建应用、Docker 自托管部署。
Open-source tool to sync the WeCom (Enterprise WeChat) directory into LDAP / OpenLDAP, supporting SSO, unified identity, organization tree mirroring, self-built app credentials, and self-hosted Docker deployment.
One service. Three moving parts.
The sync container reads your WeCom directory through a self-built app, writes a faithful copy into your LDAP server, and runs again every 30 minutes.
WeCom
Your org's directory
wecom-ldap-sync
Python · APScheduler · ldap3
LDAP server
Mirrored OU tree + groups
Small surface, sensible defaults.
Everything you'd expect from a directory sync — and nothing you wouldn't.
Mirrored department tree
WeCom departments become OpenLDAP organizationalUnit entries — full hierarchy, parents and children, with renames handled. Department 1 (corp root) maps to your configured top-level OU.
inetOrgPerson users with full attrs
Each WeCom user becomes an inetOrgPerson with cn, sn, givenName, displayName, uid, employeeNumber, mail, mobile, title, departmentNumber. CJK names are split: first character → sn, rest → givenName.
groupOfNames per department
Every department also becomes a groupOfNames under ou=groups, with members linked by full DN. Apps that expect group-based authz get it for free.
SSHA-hashed default password
Set LDAP_DEFAULT_PASSWORD: new users get it on creation. Existing users without a password get it on the next pass. Existing passwords are never overwritten.
Department moves are tracked
When someone changes department in WeCom, the LDAP entry is removed from the old OU and re-created in the new one — DN stays consistent with the org chart.
Auto-generated emails (workaround)
WeCom only returns email for verified orgs (已验证企业). Set EMAIL_SUFFIX (e.g. @example.com) and the sync generates uid + suffix as mail. Real WeCom emails always take priority.
Cron schedule, single-instance
Runs once at startup, then APScheduler kicks it off every SYNC_INTERVAL_MINUTES (default 30). max_instances=1 + coalesce — never duplicates jobs, never piles up missed runs.
Dry-run before you commit
DRY_RUN=true logs every change without writing to LDAP. See exactly what'll happen before it does.
Optional orphan cleanup
Set SYNC_DELETE_ORPHANS=true to remove LDAP users (and empty groups) that no longer exist in WeCom. Off by default in code; .env.example sets it on — review before you copy.
WeCom field → LDAP attribute, exactly.
Every user becomes an inetOrgPerson with these attributes. Optional fields are only set when WeCom provides a value.
| WeCom field | LDAP attribute | Notes |
|---|---|---|
userid |
uid, employeeNumber |
Used as the RDN — DN is uid=<userid>,ou=<dept>,… |
name |
cn, displayName |
Full name as shown in WeCom. |
name (split) |
sn, givenName |
First character → sn, rest → givenName. Best-effort for CJK names. |
email |
mail |
If present in WeCom; otherwise generated as uid + EMAIL_SUFFIX (when configured). |
mobile |
mobile |
Only available for verified WeCom orgs (已验证企业). |
position |
title |
Job title set in WeCom. |
gender |
initials |
WeCom 1=M, 2=F → initials. Skipped for 0/undefined. |
department[] |
departmentNumber |
All department IDs the user belongs to. |
| (generated) | userPassword |
SSHA hash of LDAP_DEFAULT_PASSWORD. Only set on creation, or for users currently lacking a password. |
From zero to synced in five minutes.
You'll need: Docker, a WeCom corp admin account, and a server with a stable outbound IP.
Create a WeCom self-built app
In the WeCom Admin Console, go to 应用管理 → 自建 and create an app. Grant it 通讯录 → 读取 permission. Copy the Corp ID (我的企业 → 企业信息) and the app's Secret. Add your server's outbound IP to the app's 企业可信IP list.
Clone & configure
Clone the repo, copy .env.example to .env, and fill in your LDAP base DN, admin password, default password, and the WeCom Corp ID + Secret from step 1.
Lift off
Run docker compose up -d. The compose file boots an OpenLDAP container and the wecom-ldap-sync service alongside it — the sync runs immediately on startup, then every 30 minutes.
Verify
Tail the logs (docker compose logs -f wecom-ldap-sync) for the sync summary. Then connect any LDAP client — Apache Directory Studio, ldapsearch, or your app — to localhost:389 with your admin DN.
# 1. Clone git clone https://github.com/caoool/sync-ldap.git cd sync-ldap # 2. Configure cp .env.example .env $EDITOR .env # 3. Launch (uses pre-built image # caoool/wecom-ldap-sync:latest) docker compose up -d # 4. Watch the first sync docker compose logs -f wecom-ldap-sync # Optional: search the LDAP tree ldapsearch -x -H ldap://localhost:389 \ -D "cn=admin,dc=ldap,dc=example,dc=com" \ -W -b "dc=ldap,dc=example,dc=com"
# ─── LDAP ─────────────────────────────── LDAP_DOMAIN=ldap.example.com LDAP_BASE_DN=dc=ldap,dc=example,dc=com LDAP_ADMIN_DN=cn=admin,dc=ldap,dc=example,dc=com LDAP_ADMIN_PASSWORD=change-me LDAP_USER_OU=people LDAP_DEFAULT_PASSWORD=welcome-2026 # ─── Email (workaround) ───────────────── EMAIL_SUFFIX=@example.com # ─── WeCom (企业微信) ─────────────────── WECOM_CORPID=ww1234567890abcdef WECOM_CORPSECRET=your-app-secret-here # ─── Sync ─────────────────────────────── SYNC_INTERVAL_MINUTES=30 SYNC_DELETE_ORPHANS=true DRY_RUN=false
Right tool, right job.
wecom-ldap-sync is intentionally narrow — one source, one destination, one cron. Here's when it fits, when it doesn't, and how it stacks up against the alternatives.
✓ Use it when…
- WeCom (企业微信) is your source of truth for org structure and you want LDAP downstream.
- You need a self-hosted solution — no third-party cloud, no SaaS, no data leaves your network.
- You want a small, single-purpose, auditable tool you can read end-to-end in an afternoon.
- A working stack (OpenLDAP + sync) in under five minutes is more important than a fancy admin UI.
— Skip it if…
- You need bi-directional sync — LDAP → WeCom is not supported and not on the roadmap.
- You target Active Directory specifically — this writes to OpenLDAP / RFC 4519-compliant LDAP; AD is untested.
- You're already on a commercial identity platform (Okta, Authing, JumpCloud) with WeCom SCIM and don't want to self-host.
- You need fine-grained per-user RBAC inside the sync itself — apply RBAC in your LDAP-consuming apps instead.
| vs. | How wecom-ldap-sync compares |
|---|---|
| A custom script | Gives you scheduler, dry-run, orphan handling, SSHA password hashing, CJK name splitting, department-move tracking, and full attribute mapping out of the box. You'd rebuild all of this. |
| Keycloak / FreeIPA | Much smaller surface; no JVM, no app server. Use this if you only need WeCom → LDAP. Use Keycloak/FreeIPA if you also need full IdP/SSO features (OIDC, SAML, MFA). |
| Commercial SSO platforms | Self-hosted, free, Apache-2.0 — but no admin UI. Best when LDAP is what your downstream apps already speak and you don't want to pay per seat. |
| DingTalk / Feishu sync tools | Same architecture is planned for DingTalk (钉钉) and Feishu (飞书) — not yet shipped. Use a platform-specific tool today; switch when those land. |
Every knob, in one table.
All variables live in your .env file. Sensible defaults — only the WeCom credentials are strictly required.
LDAP
| Variable | Default | Description |
|---|---|---|
LDAP_ORGANISATION | mycompany | Organization name shown in LDAP. |
LDAP_DOMAIN | ldap.example.com | Domain used by openldap to derive the base DN. |
LDAP_BASE_DN | dc=ldap,dc=example,dc=com | Must match LDAP_DOMAIN (ldap.example.com → dc=ldap,dc=example,dc=com). |
LDAP_ADMIN_DN | cn=admin,<base_dn> | Admin bind DN used by the sync. Must match LDAP_BASE_DN. |
LDAP_ADMIN_PASSWORD | changeme | LDAP admin password — change this! |
LDAP_HOST | openldap | In Docker Compose, set to 'openldap'; for local dev, 127.0.0.1. |
LDAP_PORT | 389 | LDAP port. The compose file also exposes 636 for LDAPS. |
LDAP_USER_OU | people | Top-level OU under base DN where users are placed. WeCom dept 1 (corp root) maps here. |
LDAP_DEFAULT_PASSWORD | changeme | Password for new users (SSHA-hashed). Also written to existing users without a password. Never overwrites an existing password. |
LDAP_TLS | false | Enable LDAPS / StartTLS in the openldap container. |
WeCom (企业微信)
| Variable | Default | Description |
|---|---|---|
WECOM_CORPIDrequired | — | Corp ID from 我的企业 → 企业信息. |
WECOM_CORPSECRETrequired | — | Self-built app secret with 通讯录读取 permission. |
WECOM_API_BASE | https://qyapi.weixin.qq.com/cgi-bin | WeCom API base URL. Override for proxies or private deployments. |
Sync & Email
| Variable | Default | Description |
|---|---|---|
SYNC_INTERVAL_MINUTES | 30 | How often to re-sync, in minutes. |
SYNC_DELETE_ORPHANS | false · .env.example: true | Delete LDAP users (and empty groups) no longer in WeCom. Code default is false; .env.example sets true. Use with caution. |
DRY_RUN | false | Preview changes without writing to LDAP. |
EMAIL_SUFFIX | @example.com | Generate uid + suffix as mail when WeCom doesn't return an email. Leave blank to skip — real WeCom emails always take priority. |
Questions people actually ask.
Quick answers to the recurring questions about WeCom sync, email fallbacks, password handling, and what happens when employees leave.
How often does the sync run? Can I change the interval?
Every 30 minutes by default. Set SYNC_INTERVAL_MINUTES in your .env file to change it. The sync also runs once immediately when the container starts, so you don't have to wait for the first tick.
Why isn't WeCom returning email or phone numbers for my users?
WeCom treats email and mobile as sensitive fields and only returns them for verified organizations (已验证企业). For unverified orgs the API returns empty strings. As a workaround, set EMAIL_SUFFIX=@yourdomain.com and the sync will generate uid@yourdomain.com as the mail attribute. Real WeCom emails always take priority when present.
What happens when an employee leaves the company?
With SYNC_DELETE_ORPHANS=true (the default), users that no longer exist in WeCom are removed from LDAP on the next sync. Set it to false if you'd rather keep historical entries — useful when downstream systems still reference them.
How are passwords set on synced LDAP users?
New users receive LDAP_DEFAULT_PASSWORD (SSHA-hashed). Existing users without a password also receive it on the next sync. Users who already have a password are never overwritten, so they can change their own credentials safely without worrying about being reset.
Can I preview changes before they hit LDAP?
Yes. Set DRY_RUN=true in .env and the sync logs every operation it would perform without writing anything. Once the diff looks right, set it back to false.
Does this support DingTalk (钉钉) or Feishu (飞书)?
Not yet — both are on the roadmap. The architecture is platform-agnostic, so adding a new source is mostly a matter of writing a new fetcher. Issues and PRs are welcome.
Do I need to expose LDAP publicly on the internet?
No. The sync container talks to OpenLDAP over the internal Docker network. Only expose ports 389/636 to the LAN clients that need them. LDAPS is supported via LDAP_TLS=true if you want encrypted transport on the wire.
Where this is going.
WeCom is the start. The plan is to support all major Chinese workplace platforms — and merge them into a single LDAP source of truth.
WeCom sync
Departments, users (with full attribute mapping), groupOfNames per department, default passwords, orphan cleanup, dry-run mode.
DingTalk (钉钉) sync
Same shape — pull the directory from DingTalk Open Platform, push it into LDAP.
Feishu (飞书) & multi-source merge
Add Feishu, plus a unified mode that merges multiple platforms into one LDAP tree.
Built something? Found a rough edge?
Issues, PRs, and stars are all welcome. The repo is small and easy to read — start there.