LDAP server - FAQ === ###### tags: `LDAP` ###### tags: `LDAP`, `LDAP server` <br> [TOC] <br> ## 討論 - 連線問題 ### [無法連線] `ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1)` - ### Quesstion ``` $ ldapsearch -x -b "dc=asus,dc=com" "(uid=tj_tsai)" ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1) ``` - ### gpt-5 (2025/09/19) 下面這個錯誤訊息代表「**客戶端連不到任何 LDAP 伺服器**」,不是帳密或 Base DN 錯誤: ``` ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1) ``` 最常見原因:沒有指定伺服器 URI(預設會連 localhost:389)、伺服器沒啟動、連錯主機/埠、或被防火牆/TLS 擋下來。照這份「**最短查核清單**」一步做,通常就能定位問題。 --- > ### 兩種情況: > (A) 你在 **同一台裝有 slapd 的主機** 上(apt 安裝) > (B) 你在 **其他主機 / 容器 / Pod** 上查(最常見) --- ### (A) 你在 **同一台裝有 slapd 的主機** 上(apt 安裝) 1. 先確認服務有跑 ```bash sudo systemctl status slapd --no-pager sudo ss -ltnp | grep -E ':389|:636' ``` 2. 直接用 Unix socket(ldapi)查設定,驗證 **Naming Context(suffix)** 與 **管理者 DN** ```bash # 以 root 權限用本機 socket 連線,不走網路 sudo ldapsearch -H ldapi:/// -Y EXTERNAL -b cn=config -LLL olcSuffix olcRootDN ``` 看輸出中的: * `olcSuffix`: 例如 `dc=google,dc=com` * `olcRootDN`: 例如 `cn=admin,dc=google,dc=com`(這就是你要用的 -D) 3. 用 ldapi 直接找資料(先不管網路問題) ```bash sudo ldapsearch -H ldapi:/// -Y EXTERNAL -b "" -s base -LLL namingContexts sudo ldapsearch -H ldapi:/// -x -b 'dc=google,dc=com' '(uid=tj_tsai)' -LLL dn ``` - 這兩條成功就表示 **資料已存在 + 伺服器正常運作**。 --- ### (B) 你在 **其他主機 / 容器 / Pod** 上查(最常見) > 關鍵:**一定要加 `-H ldap://<host>:<port>`** 指向正確的服務位置與埠。 1. 先確認你能連到 LDAP 埠 ```bash # 換成你的主機名或 Service 名稱與埠(常見 389 / 1389;LDAPS 常見 636 / 1636) nc -vz <ldap-host> 389 ``` 2. 用匿名 bind 讀 namingContexts(找出正確 Base DN) ```bash ldapsearch -H ldap://<ldap-host>:389 -x -b "" -s base -LLL namingContexts ``` 看到像 `namingContexts: dc=google,dc=com` 就代表伺服器可連,且 Base DN 是那個字串。 3. 搜尋你的使用者 (**建議用整體樹的 Base DN**,讓伺服器自己往下找,不要一開始就填整個使用者 DN 當 base) ```bash ldapsearch -H ldap://<ldap-host>:389 -x \ -b 'dc=google,dc=com' '(uid=tj_tsai)' -LLL dn uidNumber gidNumber homeDirectory loginShell ``` 4. 需要驗證帳密時(以管理者 DN 連線): ```bash ldapwhoami -H ldap://<ldap-host>:389 -x -D 'cn=admin,dc=google,dc=com' -W # 或搜尋: ldapsearch -H ldap://<ldap-host>:389 -x \ -D 'cn=admin,dc=google,dc=com' -W \ -b 'dc=google,dc=com' '(uid=tj_tsai)' -LLL dn ``` > 你的 LDIF 把條目放在 `dc=www,dc=google,dc=com` 底下。這 **仍屬於** `dc=google,dc=com` 這棵樹之內(`dc=www` 只是中間節點),**只要伺服器的 suffix 是 `dc=google,dc=com`,就用它當 -b**,搜尋會自己往下遞迴找到 `uid=tj_tsai`。 --- ### C)(選用)TLS/StartTLS 檢查 * LDAPS(636):`-H ldaps://<host>:636 -x ...` * StartTLS(389):`-H ldap://<host>:389 -ZZ -x ...` 若遇到憑證錯誤,可暫時用 `-d 1` 觀察,或把 CA 加到 `/etc/ldap/ldap.conf` 的 `TLS_CACERT`。 --- ### D) 常見踩雷速解 * **沒加 `-H`** → 預設連 localhost:389;在容器裡通常沒有 slapd,所以報 `Can't contact LDAP server (-1)`。 * **連錯埠** → 有些映像(或 K8s Service)用 **1389/1636**。請以實際 Service/Port 為準。 * **Base DN 用錯** → 先用 `-b "" -s base namingContexts` 查真正的 suffix。 * **admin DN 用錯** → 以 `ldapi:///` 查 `olcRootDN` 最準。 * **防火牆** → 伺服器要開放 389/636(或 1389/1636);K8s 要確認 Service/NetworkPolicy。 * **`I have no name!@...`** → 只是容器內沒有對應的 /etc/passwd 項,與 LDAP 連線無關。 --- ### E) 你這份資料的「最小可驗證」指令範例 > 假設伺服器 suffix 是 `dc=google,dc=com`,且用 389: 1. 匯入 ```bash ldapadd -H ldap://<ldap-host>:389 -x \ -D 'cn=admin,dc=google,dc=com' -W -f tj_tsai.ldif ``` 2. 搜尋(匿名或管理者都可) ```bash ldapsearch -H ldap://<ldap-host>:389 -x \ -b 'dc=google,dc=com' '(uid=tj_tsai)' -LLL dn uidNumber gidNumber homeDirectory loginShell ``` 如果以上第 2 步還是回 `Can't contact LDAP server (-1)`,那就回到 **A-1/B-1** 檢查服務是否在正確的主機與埠上對外監聽,以及你 `-H` 的主機與埠是否填對。 <br> --- ### [StartTLS 憑證驗證失敗] certificate verify failed (self-signed certificate) - ### Question ``` $ ldapsearch -x -H ldap://openldap.openldap:389 \ -D "cn=admin,dc=www,dc=google,dc=com" \ -w "YourSecurePassword" \ -ZZ ldap_start_tls: Connect error (-11) additional info: error:0A000086:SSL routines::certificate verify failed (self-signed certificate) ``` - ### gpt-5 (2025/09/19) 你遇到的是「StartTLS 憑證驗證失敗」(自簽憑證未被信任)。連線有通了,但**客戶端不信任伺服器證書**,所以 TLS 升級被拒。 ### 快速判斷 先確認是不是「只差信任」這件事: ```bash # 臨時放寬驗證(測試用,非正式環境) ldapsearch -x -H ldap://openldap.openldap:389 -ZZ \ -D "cn=admin,dc=www,dc=google,dc=com" -w "YourSecurePassword" \ -o TLS_REQCERT=allow ``` * 若這樣可以成功,表示「連線沒問題,純粹憑證信任問題」。 ### 正確修法(建議) 目標是讓**客戶端信任簽發你 LDAP 伺服器證書的 CA**,並且**主機名稱要跟證書 SAN 相符**。 #### 1) 產生(或取得)CA 與伺服器證書(含 SAN) > SAN 一定要包含你實際用的主機名,例如: > `openldap.openldap`、`openldap.openldap.svc`、`openldap.openldap.svc.cluster.local` 範例(有 SAN 的簽發流程): ```bash # 產 CA openssl genrsa -out ca.key 4096 openssl req -x509 -new -sha256 -days 3650 \ -key ca.key -subj "/CN=My LDAP CA" -out ca.crt # 設定 server 證書的 SAN cat > openssl-ldap.cnf <<'EOF' [req] distinguished_name = dn req_extensions = req_ext prompt = no [dn] CN = openldap.openldap.svc.cluster.local [req_ext] subjectAltName = @alt_names [alt_names] DNS.1 = openldap.openldap DNS.2 = openldap.openldap.svc DNS.3 = openldap.openldap.svc.cluster.local EOF # 產 server key + CSR openssl genrsa -out tls.key 2048 openssl req -new -key tls.key -out server.csr -config openssl-ldap.cnf # 用 CA 簽發 server cert(帶上 req_ext 才有 SAN) openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out tls.crt -days 825 -sha256 -extensions req_ext -extfile openssl-ldap.cnf ``` #### 2) 在 OpenLDAP 伺服器掛上證書 (K8s/Bitnami 常見做法) ```bash # 放進 Secret(server 證書+私鑰) kubectl -n openldap create secret tls openldap-tls --cert=tls.crt --key=tls.key # 放 CA(給伺服器與 sidecar/其他元件參考) kubectl -n openldap create configmap openldap-ca --from-file=ca.crt ``` 接著在你的 OpenLDAP 部署/Helm values 指定: * `LDAP_ENABLE_TLS=yes` * `LDAP_TLS_CERT_FILE` 指到 `tls.crt` * `LDAP_TLS_KEY_FILE` 指到 `tls.key` * `LDAP_TLS_CA_FILE` 指到 `ca.crt` * 或對應 Bitnami chart 的 `tls.enabled: true`、`tls.certificatesSecret: openldap-tls`、`tls.CASecret` 等欄位(依你用的 chart/映像為準)。 > 若不是 Helm,確保 `cn=config` 有 `olcTLSCertificateFile`、`olcTLSCertificateKeyFile`、`olcTLSCACertificateFile` 指到正確路徑,並重啟 slapd。 #### 3) 在「客戶端」安裝 CA(讓它信任) 在你跑 `ldapsearch` 的那台(看起來是個 Pod): * 方式 A:把 `ca.crt` 放到檔案系統,臨時指定: ```bash export LDAPTLS_CACERT=/etc/ssl/certs/ldap-ca.crt # 或在指令裡帶 ldapsearch ... -ZZ -o TLS_CACERT=/etc/ssl/certs/ldap-ca.crt ``` * 方式 B:寫到 `/etc/ldap/ldap.conf`(持久化方式) ``` TLS_CACERT /etc/ssl/certs/ldap-ca.crt TLS_REQCERT demand ``` * 方式 C(Debian/Ubuntu 類):放到 `/usr/local/share/ca-certificates/ldap-ca.crt`,然後 ```bash update-ca-certificates ``` > 重點:你 `-H` 用的主機名要**與證書 SAN 其中一個 DNS 完全一致**。 > 例如你現在用 `openldap.openldap`,那就確保它在 SAN 裡有列出;否則請改成 `openldap.openldap.svc.cluster.local` 等與 SAN 相符的名稱。 #### 4) 驗證 ```bash ldapsearch -x -H ldap://openldap.openldap:389 -ZZ \ -D "cn=admin,dc=www,dc=google,dc=com" -w "YourSecurePassword" \ -b "dc=www,dc=google,dc=com" "(objectClass=*)" ``` 或(同樣需要信任 CA): ```bash ldapsearch -x -H ldaps://openldap.openldap:636 \ -D "cn=admin,dc=www,dc=google,dc=com" -w "YourSecurePassword" \ -b "dc=www,dc=google,dc=com" "(objectClass=*)" ``` ### 臨時繞過(只用於測試) 若你只是想先確認功能,**暫時**關閉嚴格驗證: ```bash ldapsearch -x -H ldap://openldap.openldap:389 -ZZ \ -D "cn=admin,dc=www,dc=google,dc=com" -w "YourSecurePassword" \ -o TLS_REQCERT=never ``` > 正式環境請恢復 `demand`,並完成 CA 信任與 SAN 配置。 ### 小提醒 * 錯誤訊息是 `certificate verify failed (self-signed certificate)`:不是「沒開 TLS」,而是**缺信任或主機名不匹配**。 * `I have no name!@...` 只是容器裡沒有該 UID 的 `/etc/passwd` 條目,與本問題無關。 * 若仍失敗,可加 `-d 1`(或更高)看 libldap debug: ```bash ldapsearch -d 1 -x -H ldap://openldap.openldap:389 -ZZ ... ``` 常見還會看到「hostname mismatch」或「unable to get local issuer certificate」等線索。 需要的話,我可以幫你把 OpenLDAP(Bitnami/Helm)和測試 Pod 的 values/掛載範例補到一份最小可行設定。 <br> --- <br> ## 討論 - OpenLDAP ### OpenLDAP 實作 X.500 模型,X.500 模型則是實作 LDAP 協定,這樣說對嗎? - ### gpt-5 (2025/09/19) 不太對。順序要反過來: * **LDAP** 是一組「協定」(RFC 4511 等),**沿用/對應 X.500 的資料與命名模型**(DIT、DN、objectClass、attribute…)。 * **X.500** 是一套「目錄服務標準與資料模型」,**它本身有自己的協定**(DAP、DSP、DISP、DOP),而不是「去實作 LDAP」。 * **OpenLDAP** 是一個 **LDAPv3 目錄伺服器的實作**(講 LDAP 這個協定),同時**採用 X.500 風格的資料/命名模型**。 所以正確說法是: **OpenLDAP 實作 LDAP 協定,並以 X.500 的資料與命名模型為基礎;不是 X.500 去實作 LDAP。** <br> --- <br> ### OpenLDAP 實作 LDAP 協定,LDAP 協定是繼承 X.500 模型,這樣說對嗎? - ### gpt-5 (2025/09/23) 簡短回答:**半對,但「繼承」這個說法不精確。** * **正確的部份**:LDAP(Lightweight Directory Access Protocol)延續/相容 **X.500 的目錄資訊模型**與命名規則——例如 **DN/RDN、DIT(目錄資訊樹)、objectClass/attribute schema**、以及常見操作(bind/search/add/modify/rename/delete/compare)。 * **不精確的部份**:LDAP不是在協定意義上「繼承」X.500。X.500 原本使用 **DAP** 並跑在 **OSI 通訊堆疊**;LDAP則是**為了在 TCP/IP 上提供 X.500 風格目錄存取的「輕量」協定**。自 **LDAPv3** 起,它已是可**獨立**的通用目錄協定(有 controls、extended ops、referrals 等),不需要背後有 X.500 系統。 **OpenLDAP** 是 **LDAPv3** 的實作(含伺服器 `slapd` 與用戶端函式庫)。它採用 X.500 風格的資料模型,但**後端儲存**通常是自家的 **LMDB(back-mdb)** 等資料庫,而**不是** X.500 的 DSA。 👉 更精準的說法是: **「LDAP 採用(相容)X.500 的目錄資訊模型與命名規則,但在協定與網路堆疊上是獨立、輕量的設計;OpenLDAP 則是 LDAPv3 的實作。」** <br> --- <br> ### OpenLDAP 後端使用的資料庫名稱叫什麼? - ### gpt-5 (2025/09/23) 最標準的答案是:**LMDB(Lightning Memory-Mapped Database)**。 在 OpenLDAP 裡它的後端模組名稱是 **back-mdb**,設定中會看到 `database mdb` 或 `olcDatabase={N}mdb`。 (補充:早期曾用 **Berkeley DB**(`back-bdb` / `back-hdb`),現已淘汰或不建議使用。) <br> --- <br> ### 使用 Java 程式語言角度定義 OpenLDAP - ### Question 站在 Java 程式語言角度,可以說是: `class OpenLDAP extends X.500 implements LDAP` 這樣嗎? - ### gpt-5 (2025/09/23) 幾乎,但不精確。用 Java 類比,**OpenLDAP 不是「X.500 的一種」**,而是**遵循 LDAPv3 協定、並採用(對齊)X.500 風格的資料/命名模型**。因此不該 `extends X.500`,比較合理是**實作協定、組合資料模型**: ```java // 「協定」→ 介面 interface LdapV3Server { void bind(...); void search(...); void modify(...); // ... } // 「資料/命名模型」→ 類或元件(組合,而不是繼承) final class X500NamingModel { // DN, RDN, objectClass, attributeType, matchingRules ... } // (可選)通用目錄伺服器基底 abstract class DirectoryServer { // 共通伺服器行為(儲存、索引、ACL、後端等) } final class OpenLdapServer extends DirectoryServer implements LdapV3Server { private final X500NamingModel model; // has-a(組合) // ... 實作 bind/search/modify 等 LDAPv3 行為 } ``` 要點: * `implements LDAP` ✅(協定 = 介面) * `extends X.500` ❌(X.500 是資料/命名模型與一組標準,不是「OpenLDAP 的父類別」。OpenLDAP **不是** X.500 的一種) * 正確心智模型:**OpenLDAP is-a 目錄伺服器**,**implements LDAPv3 協定**,**has-a X.500 風格模型(或符合 RFC 4512 的 LDAP 資料/架構模型)**。 <br> --- <br> ### LDAPv3 的 Java 介面雛形 - ### Question ``` interface LdapV3Server { void bind(...); void search(...); void modify(...); // ... } ``` 請補全所有常見概念 API - ### gpt-5 (2025/09/23) 下面給你一份「概念完整、可實作」的 Java 介面雛形,把 LDAPv3 常見操作都補齊(bind/unbind、search、add/modify/delete、modifyDN、compare、abandon、extended ops、StartTLS、常用 extended:WhoAmI、PasswordModify、Cancel),並把搜尋參數、控制碼(Controls)等也一併建模。 (解說用 zh-TW;程式碼內註解用 English,符合你的偏好。) ```java import java.time.Duration; import java.util.*; import java.util.concurrent.CompletableFuture; // Core interface for an LDAPv3 connection/session. public interface LdapV3Server extends AutoCloseable { /* ========================= * Connection / Session * ========================= */ void connect(String host, int port, Duration timeout); @Override void close(); // Unbind/close /* StartTLS (RFC 4511 §4.14 via ExtendedOp "1.3.6.1.4.1.1466.20037") */ ExtendedResult startTLS(TLSOptions options, Control... controls); ExtendedResult stopTLS(); // convenience /* ========================= * Bind / Unbind * ========================= */ BindResult bindSimple(String bindDN, char[] password, Control... controls); BindResult bindSasl(String mechanism, byte[] clientCreds, Map<String, ?> props, Control... controls); void unbind(); // closes the session /* ========================= * Search (RFC 4511 §4.5) * ========================= */ // 1) Callback streaming (best for large result sets) SearchDone search(SearchRequest req, SearchEntryHandler onEntry, SearchRefHandler onReference); // 2) Iterator-style convenience SearchResult searchAll(SearchRequest req); // 3) Return first matched entry (convenience) Optional<SearchResultEntry> searchOne(SearchRequest req); /* ========================= * CRUD-like ops * ========================= */ // Add (RFC 4511 §4.7) LdapResult add(Entry entry, Control... controls); // Modify (RFC 4511 §4.6) LdapResult modify(DN dn, List<Modification> modifications, Control... controls); // Delete (RFC 4511 §4.8) LdapResult delete(DN dn, Control... controls); // Modify DN / Rename (RFC 4511 §4.9) LdapResult modifyDN(DN dn, RDN newRdn, boolean deleteOldRdn, DN newSuperior /* nullable */, Control... controls); // Compare (RFC 4511 §4.10) CompareResult compare(DN dn, String attribute, byte[] assertionValue, Control... controls); /* ========================= * Operation Management * ========================= */ // Abandon (RFC 4511 §4.11) – cancel in-flight request by its messageID void abandon(int messageId); // Extended Operation (RFC 4511 §4.12) ExtendedResult extended(ExtendedRequest request, Control... controls); /* Common Extended Ops */ // Who Am I? (RFC 4532) ExtendedResult whoAmI(Control... controls); // Password Modify (RFC 3062) ExtendedResult passwordModify(PasswordModifyRequest req, Control... controls); // Cancel (RFC 3909) – graceful cancel (different from Abandon) ExtendedResult cancel(int messageId); /* ========================= * Controls (request/response) * ========================= */ void setDefaultRequestControls(Control... controls); List<Control> getResponseControls(); // from last op (thread-scope or returned in results) /* ========================= * Async variants (optional) * ========================= */ CompletableFuture<SearchResult> searchAllAsync(SearchRequest req); CompletableFuture<LdapResult> addAsync(Entry entry, Control... controls); CompletableFuture<LdapResult> modifyAsync(DN dn, List<Modification> modifications, Control... controls); CompletableFuture<LdapResult> deleteAsync(DN dn, Control... controls); CompletableFuture<LdapResult> modifyDNAsync(DN dn, RDN newRdn, boolean deleteOldRdn, DN newSuperior, Control... controls); CompletableFuture<CompareResult> compareAsync(DN dn, String attribute, byte[] assertionValue, Control... controls); CompletableFuture<ExtendedResult> extendedAsync(ExtendedRequest request, Control... controls); /* ========================= * Types (minimal conceptual model) * ========================= */ // Distinguished Name (DN) / Relative DN (RDN) record DN(String value) {} record RDN(String value) {} // Entry & Attribute record Entry(DN dn, List<Attribute> attributes) {} record Attribute(String type, List<byte[]> values) {} // bytes to keep neutral (UTF-8, binary, etc.) // Search request (all knobs per RFC 4511 §4.5.1) record SearchRequest( DN base, SearchScope scope, DerefAliases derefAliases, int sizeLimit, // 0 = no limit (server-side policy may apply) int timeLimitSeconds, // 0 = no limit boolean typesOnly, Filter filter, List<String> attributes, // requested attribute types (empty = all user attrs) List<Control> controls ) {} enum SearchScope { BASE_OBJECT, SINGLE_LEVEL, WHOLE_SUBTREE, SUBORDINATE_SUBTREE /* RFC 3673 */ } enum DerefAliases { NEVER_DEREF_ALIASES, DEREF_IN_SEARCHING, DEREF_FINDING_BASE_OBJ, DEREF_ALWAYS } // Filter (string or structured AST) sealed interface Filter permits Filter.And, Filter.Or, Filter.Not, Filter.Present, Filter.Equals, Filter.GreaterOrEqual, Filter.LessOrEqual, Filter.Approx, Filter.Substring, Filter.Extensible { record And(List<Filter> children) implements Filter {} record Or(List<Filter> children) implements Filter {} record Not(Filter child) implements Filter {} record Present(String attr) implements Filter {} record Equals(String attr, byte[] value) implements Filter {} record GreaterOrEqual(String attr, byte[] value) implements Filter {} record LessOrEqual(String attr, byte[] value) implements Filter {} record Approx(String attr, byte[] value) implements Filter {} record Substring(String attr, String initial /*nullable*/, List<String> any, String fin /*nullable*/) implements Filter {} record Extensible(String attr /*nullable*/, String matchingRule /*nullable*/, boolean dnAttributes, byte[] value) implements Filter {} static Filter fromString(String rfc4515) { /* parse impl */ throw new UnsupportedOperationException(); } } // Modify item (RFC 4511 §4.6) enum ModificationOp { ADD, DELETE, REPLACE } record Modification(ModificationOp op, Attribute attribute) {} // Results enum ResultCode { SUCCESS, OPERATIONS_ERROR, PROTOCOL_ERROR, TIME_LIMIT_EXCEEDED, SIZE_LIMIT_EXCEEDED, COMPARE_FALSE, COMPARE_TRUE, AUTH_METHOD_NOT_SUPPORTED, STRONG_AUTH_REQUIRED, REFERRAL, ADMIN_LIMIT_EXCEEDED, UNAVAILABLE_CRITICAL_EXTENSION, CONFIDENTIALITY_REQUIRED, SASL_BIND_IN_PROGRESS, NO_SUCH_ATTRIBUTE, UNDEFINED_ATTRIBUTE_TYPE, INAPPROPRIATE_MATCHING, CONSTRAINT_VIOLATION, ATTRIBUTE_OR_VALUE_EXISTS, INVALID_ATTRIBUTE_SYNTAX, NO_SUCH_OBJECT, ALIAS_PROBLEM, INVALID_DN_SYNTAX, ALIAS_DEREF_PROBLEM, INAPPROPRIATE_AUTHENTICATION, INVALID_CREDENTIALS, INSUFFICIENT_ACCESS_RIGHTS, BUSY, UNAVAILABLE, UNWILLING_TO_PERFORM, LOOP_DETECT, NAMING_VIOLATION, OBJECT_CLASS_VIOLATION, NOT_ALLOWED_ON_NON_LEAF, NOT_ALLOWED_ON_RDN, ENTRY_ALREADY_EXISTS, OBJECT_CLASS_MODS_PROHIBITED, AFFECTS_MULTIPLE_DSAS, OTHER } record LdapResult(ResultCode code, String matchedDN, String diagnosticMessage, List<String> referrals, List<Control> responseControls) {} record BindResult(LdapResult result, byte[] serverSaslCreds /*nullable*/) {} record CompareResult(LdapResult result, boolean matched) {} // Search responses (entries, references, done) record SearchResultEntry(Entry entry, List<Control> controls) {} record SearchReference(List<String> uris, List<Control> controls) {} record SearchDone(LdapResult result) {} record SearchResult(List<SearchResultEntry> entries, List<SearchReference> references, LdapResult done) {} // Controls (RFC 4511 §4.1.12); use OID + value for extensibility record Control(String oid, boolean critical, byte[] value /* BER-encoded */) { // Common controls helpers (paged results RFC 2696, server-side sort RFC 2891, etc.) could be added here static Control pagedResults(int pageSize, byte[] cookie) { return new Control("1.2.840.113556.1.4.319", false, /*encode*/ null); } static Control serverSideSort(byte[] sortKeysBer) { return new Control("1.2.840.113556.1.4.473", true, sortKeysBer); } } /* Extended Ops plumbing */ record ExtendedRequest(String requestNameOid, byte[] requestValue /*nullable*/) {} record ExtendedResult(LdapResult result, String responseNameOid /*nullable*/, byte[] responseValue /*nullable*/) {} /* Common Extended Requests */ record PasswordModifyRequest(String userIdentityDN /*nullable*/, char[] oldPass /*nullable*/, char[] newPass /*nullable*/) {} record TLSOptions(boolean requireHostnameVerification) {} /* Streaming callbacks */ @FunctionalInterface interface SearchEntryHandler { void onEntry(SearchResultEntry entry); } @FunctionalInterface interface SearchRefHandler { void onReference(SearchReference ref); } } ``` ### 小重點(為什麼這些是「常見概念 API」) * **Bind / Unbind / StartTLS**:會話建立與加密協商的基本面。 * **Search / Add / Modify / Delete / ModifyDN / Compare**:RFC 4511 規範的六大核心操作。 * **Abandon / Cancel / Extended**:中止請求與擴充能力;WhoAmI、PasswordModify 都是常用 extended。 * **Controls**:分頁(RFC 2696)、排序(RFC 2891)等「請求/回應控制碼」是實務必備。 * **Filter/Scope/Deref/Limit**:把搜尋的所有旋鈕(RFC 4511 §4.5.1)完整建模,避免日後打補丁。 如果你要,我也可以把這份介面拆成「LdapConnection(有狀態)」+「LdapClient(工廠)」兩層,或補上幾個常用 Control/Extended 的 BER 編碼 helper 範例。 <br> {%hackmd vaaMgNRPS4KGJDSFG0ZE0w %}