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 %}