openldapのtranslucentモジュールの活用

openldapのtranslucent機能は、他のLDAPサーバーを親機として、その情報に自分が付与したい情報を透過的に追加するための手法です。

その追加分のデータを格納するのが、今回設定するopenldapサーバーとなります。

# 環境

* Ubuntu 16.04 LTS
* slapd deb package (openldap-server 2.4.42+dfsg-2ubuntu3.2)
* ldap関連のユーティリティ: ldap-utils deb package

事前に、次のようなコマンドラインでパッケージを導入しておきます。

```bash
$ sudo apt-get install slapd ldap-utils
```

またTLSによるLDAPS(636)は考慮していません。経路の暗号化がないことを前提にldapsearchなどのコマンドを実行しています。

加えて、ldapsearchのread権限を行使する場合には、bindDNは必要ないことを前提としています。LDAPの面倒なところでもありますが、LDAPでは権限の付与には柔軟があります。設定によって、自分のエントリ以外は閲覧できない、自分のエントリを確認するためにパスワード(bindDN,bindPW)が必要、というケースもあります。ここでは、一部のパスワードを除いてほぼ全てのエントリが閲覧できることを前提としています。

またActive Directory(AD)も考慮していません。

## Ansibleによる設定例

参考のためgithubでtranslucent機能を有効にするansible roleを公開しています。

* [https://github.com/YasuhiroABE/ansible-test-transldap](https://github.com/YasuhiroABE/ansible-test-transldap)

metaモジュールを利用した例については、別に公開しています。

* [https://github.com/YasuhiroABE/ansible-test-metaldap](https://github.com/YasuhiroABE/ansible-test-metaldap)

この2つのモジュールは別のサーバーに適用することを前提としています。suffixが重複する場合には同一サーバーで両方を適用することはテストしていないので注意してください。

## LDAP関連の用語
|用語|意味|
|---|---|
|エントリ(Entry)|ツリー構造でいうところのノード|
|属性(Attribute)|エントリに記録される個々の内容(例 name=foo bar)|
|DN(Distinguished Name)|エントリを特定する目的で、エントリに必ず1つ設定されている特別な属性｜
|bindDN|LDAPサーバーに接続(bind)するために必要なDNのこと。IDに相当する。パスワードはbindPW等と呼ばれる。これが必要かどうかはサーバーの管理者の設定次第。いくつかのOSSの設定ではbindDNが必須となっているものがあり、今回のようにbindDNが不要な設定のサーバーに接続する場合には、自分のDNとパスワードを登録する必要があるなど、面倒になる場合がある。|
|LDIF(LDAP Interchange Format)|LDAP専用のフォーマット。機能は違うが、位置付けだけならRDBMSに対するSQLのようなものとの理解が一番早いと思われる。|
|objectClass|属性(attribute)をまとめているもの。/etc/ldap/schema/*.schemaファイルを眺めると、属性タイプ(attributetype)とobjectclassの定義が確認できます。|

LDAPはまず,**dn:**で始まる行がデータの先頭にあって、それが特別な意味を持っていることを理解していればLDAPの利用はあまり怖くないと思います。

# 想定する追加するLDAP上のエントリ、属性について

現在の(親)LDAPサーバーは、posixAccout, posixGroupのobjectclassを中心とした基本的なアカウント情報を保持しているだけです。inetOrgPersonなどは指定されていません。

1. ここに部屋の番号や電話番号、base64encodedな顔画像ファイルといったビジネスに必要な情報を追加するための方法をまとめていきます。

2. さらに、メーリングリストのメンバーはファイルで登録されています。これを読み込んで、posixGroupとは別に、メーリングリスト名をエントリとして、memberOf属性を持つ、グループの作成を目指します。

# ドキュメントの少ないopenldapのtranslucent機能に関する資料

translucentを利用する時の、ベストな参考書は、openldapのソースにあるtests/ディレクトリ内にある、translucent機能を検証するための一連のスクリプトやLDIFファイルです。

tests/scripts/test034-translucent では、$URI2 に対して、translucent機能をテストするための設定変更が行なわれています。

この他に[MLにもtranslucentを利用した例](http://www.openldap.org/lists/openldap-technical/201205/msg00125.html)がアーカイブにあります。

# 準備作業
Ubuntuでのopenldapはデフォルトで、dc=のdomainなDBが設定されています。対話的なインストールを行なうとパスワードを聞かれたりもします。これを設定変更して使っても良いのですが、一度まっさらな状態にするために、情報を消してしまいます。

まずはslapdプロセスを停止します。
```$ sudo systemctl stop slapd.service```

で、次のファイルを全て消しています。

```
'/etc/ldap/slapd.d/cn=config/olcDatabase={1}mdb.ldif'
'/etc/ldap/slapd.d/cn=config/olcDatabase={1}mdb'
'/var/lib/ldap/data.mdb'
'/var/lib/ldap/lock.mdb'
```

この情報を削除するだけではcn=configに設定されるモジュールなどの情報は削除できません。
/etc/ldap/slapd.dディレクトリを再作成させるのは```$ sudo dpkg-reconfigure slapd```で可能ですが、インストール時に聴かれなかった質問が表示されるので```$ sudo apt-get remove slapd && sudo dpkg --purge slapd```などの方法で削除してから、再インストールしても良いと思います。

いずれにしても、これらのファイルを消してから、slapdを起動します。

```bash
$ sudo systemctl start slapd.service
```

無事に終ると、エントリがなにもない事がslapcatで確認できます。

```bash
$ sudo slapcat
Available database(s) do not allow slapcat
```

ここまでで、事前準備は終りです。

# translucent機能を有効にして、親LDAPサーバーに透過的に接続する

## ベースとなるMDBの追加

translucentを利用するために、データを格納するバックエンドDBを作成しておきます。
この中で、olcSuffix, olcRootDN, olcRootPW は任意に変更する必要があります。olcSuffixは親LDAPサーバーと同じにしています。olcRootDNはolcSuffix配下にある必要があるので、一般的なc=adminを追加したエントリとしています。olcDbDirectoryはUbuntu/Debian以外であれば、変更する必要があるかもしれません。

```bash:filename.ldif
dn: olcDatabase=mdb,cn=config
objectClass: olcDatabaseConfig
objectClass: olcMdbConfig
olcDatabase: mdb
OlcDbMaxSize: 1073741824
olcSuffix: dc=example,dc=com
olcRootDN: cn=admin,dc=example,dc=com
olcRootPW: plainpasswordtext
olcDbDirectory: /var/lib/ldap
olcDbIndex: objectClass eq
olcAccess: to * by * read
```

これのファイルを適当な名前(e.g. *filename.ldif*)で保存し、実行します。

```bash
$ sudo ldapadd -Q -Y EXTERNAL -H ldapi:/// -f filename.ldif
``` 

## モジュールの追加
olcOverlay=translucent を設定する必要があり、モジュールとして、/usr/lib/ldap/translucent.so をslapdが読み込んでいる必要があります。

親側のLDAPサーバーがppolicyを要求するので、追加しています。

```bash:filename.ldif
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: back_ldap
-
add: olcModuleLoad
olcModuleLoad: translucent
-
add: olcModuleLoad
olcModuleLoad: ppolicy
```

適当なファイル(e.g. *filename.ldif*)に保存して、既存の設定を変更するようにしています。

```bash
$ sudo ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f filename.ldif
```

## schemaファイルの追加

例えば、次のように、schema定義が格納されているLDIFファイルを使い、objectclassを追加することでこのLDAPサーバーで利用できるエントリ、属性を増やします。

```bash
$ sudo ldapadd -Q -Y EXTERNAL -H ldapi:/// -f "/etc/ldap/schema/inetorgperson.ldif"
```

## 親LDAPサーバーのデータに追加したい属性の追加

```bash:filename.ldif
dn: olcOverlay=translucent,olcDatabase={1}mdb,cn=config
objectClass: olcOverlayConfig
objectClass: olcTranslucentConfig
olcOverlay: translucent
olcTranslucentLocal: jpegPhoto,roomNumber,telephoneNumber
```

前回と同様に、適当なファイル(e.g. *filename.ldif*)に保存して、slapdに設定を行ないます。

```bash
$ sudo ldapadd -Q -Y EXTERNAL -H ldapi:/// -f filename.ldif
```

ここではldapaddコマンドを使っています。ldapmodifyコマンドでも"-a"オプションを付けることで、内部ではldapaddが呼ばれるようになります。

## 親LDAPサーバー上の情報を追加

LDIFファイルに各DBのインデックス(e.g. {0},{1})は、環境によって変化します。
必要に応じて```sudo slapcat```で、インデックスの数字が違わないか確認することが必要です。

```bash:filename.ldif
dn: olcDatabase=ldap,olcOverlay={0}translucent,olcDatabase={1}mdb,cn=config
objectClass: olcLDAPConfig
objectClass: olcTranslucentDatabase
olcDatabase: ldap
olcDbURI: ldap://192.168.100.10:389/
```

これも、適当なファイル(e.g. *filename.ldif*)に保存して、slapdに設定を行ないます。

```bash
$ sudo ldapadd -Q -Y EXTERNAL -H ldapi:/// -f filename.ldif
```


## 動作確認
ここまでの段階で、親LDAPサーバーにリクエストをパススルーするだけのslapdが立ち上がっているはずです。

ldapsearchを使って、トップレベル直下のエントリを眺めてみます。

```bash
$ ldapsearch -x -b 'dc=example,dc=com' -s one

dn: ou=people,dc=example,dc=com
objectClass: top
...

dn: ou=group,dc=example,dc=com
objectClass: top
...
```

この実行結果は具体的なエントリ情報ではなく、トップレベルから一段下がって、UserやGroup情報を格納するための構造を示しています。

# 独自属性の追加
ここまで問題がなければ、独自の属性を追加していきます。

例： roomNumber属性の追加
今回はjpegPhoto, roomNumber, telephoneNumber の3つの属性を、自分のlocal側に設定しています。追加・変更用のスキーマを実行しますが、その際にはldapadd,ldapmodifyコマンドはバックエンドDB作成時に登録したolcRootDN,olcRootPWを使用してバインドします。

## 追加前に現在の状況の確認

例えば、親LDAPサーバーに格納されている私のエントリは、次のDNで表されています。

```bash
$ ldapsearch -x -b ou=People,dc=example,dc=com '(uid=yasu-abe)'
dn: uid=yasu-abe,ou=People,dc=example,dc=com
uid: yasu-abe
gecos: Yasuhiro Abe
...
```

ここにroomNumber属性を追加します。

## 追加用LDIFファイルの準備

追加用のLDIFは、追加をするDNと追加する属性が定義されているobjectClassを指定します。

```bash:filename.ldif
dn: uid=yasu-abe,ou=People,dc=example,dc=com
objectClass: inetOrgPerson
roomNumber: RQ147
```

この内容のLDIFを適当な名前(e.g. filename.ldif)でファイルにして、実行します。

```bash
$ ldapadd -x -D cn=admin,dc=example,dc=com -W -f filename.ldif
```

-Dオプションに指定するのは、普通はolcRootDN。-Wを付けるとパスワードを尋ねられるので、olcRootPWに指定したパスワードをタイプします。

既にあるエントリでも、実際に格納される手元のLDAPサーバーでは新規に追加する内容なので、ldapaddを実行します。

次のようなエントリでも追加は可能です。

```bash:filename.ldif
dn: uid=yasu-abe,ou=People,dc=example,dc=com
changetype: modify
add: telephoneNumber
telephoneNumber: ext.3154
```

```bash
$ ldapmodify -x -D cn=admin,dc=example,dc=com -W -f filename.ldif
```

仮に、親LDAPへの変更であるかのように、gecosフィールドを変更してみます。

```bash:filename.ldif
dn: uid=yasu-abe,ou=People,dc=example,dc=com
changetype: modify
replace: gecos
gecos: Y.ABE
```

```bash
$ ldapmodify -x -D cn=admin,dc=example,dc=com -W -f filename.ldif
Enter LDAP Password: 
modifying entry "uid=yasu-abe,ou=People,dc=example,dc=com"
```

実際にldapsearchで検索をすると以下のようになります。

```bash
$ ldapsearch -x -b ou=People,dc=example,dc=com '(uid=yasu-abe)'
dn: uid=yasu-abe,ou=People,dc=example,dc=com
uid: yasu-abe
...
gecos: Y.ABE
...
telephoneNumber: ext.3154
roomNumber: RQ147
...
```

translucentを構成すると、親ディレクトリの内容も手元で上書きできるので非常に便利です。
ここまでで、基本的な確認作業は終了です。

# ldapadd(ldapmodify -a) or ldapmodify 

LDAPに関連する情報を扱う場合に、人間であれば**追加**か**既存情報の修正**のいずれかか、操作を迷うことはないはずです。しかし、LDAPの場合、実践的にはldapsearchかldapmodifyを実行して終了時コード($?)を確認して処理を分岐させる、追加でldapaddを実行する、といった対応を取ることが必要になります。

場合によっては、処理の流れによって追加か変更かLDIFファイルを個別に生成する仕組みが必要になる場合もあります。

translucentを使う場合は、olcTranslucentLocalに指定したエントリは、存在しないように見えても、ldapmodifyで更新(replace)扱いにすることができます。

まだ追加していないjpegPhotoを以下のような方法で追加することができます。ここでは、事前に適当なサイズのJPEGファイルを/tmp/a.jpgに配置しています。

```bash:filename.ldif
dn: uid=yasu-abe,ou=People,dc=example,dc=com
changetype: modify
replace: jpegPhoto
jpegPhoto: <file:///tmp/a.jpeg
```

```bash
$ ldapmodify -x -D cn=admin,dc=example,dc=com -W -f filename.ldif
...
$ ldapsearch -x -b ou=People,dc=example,dc=com '(uid=yasu-abe)'
dn: uid=yasu-abe,ou=People,dc=example,dc=com
...
jpegPhoto:: PGZpbGU6Ly8vdG1wL2EuanBlZw==
...
```

# translucentな情報と並列に独自のディレクトリを追加する

translucentを使う理由に、既存LDAPの情報を活用しつつ、独自に拡張したい用途があります。メーリングリストでも、独自エントリを追加しようとして、できないという質問がありました。→ [translucent overlay and local objects?](http://openldap-technical.openldap.narkive.com/N3VRt8MI/translucent-overlay-and-local-objects)

今回は、下記のように元のou=people,...やou=group,...といったエントリの他に、ou=maillist,...といったエントリを独自に設けたいと考えています。

* ou=people,dc=example,dc=com (親LDAPの情報にtranslucentで独自属性の追加)
* ou=group,dc=example,dc=com (親LDAPの情報をそのまま利用)
* ou=maillist,dc=example,dc=com (親LDAPにはまったく登録されていない独自情報)

この問題点は、translucentの設定でsuffixとしてdc=example,dc=comを使うと、このLDAPサーバーには、より長い、ou=maillist,dc=example,dc=comのようなsuffixを持つバックエンドDBを追加することはできません。
実際にはou=maillist,dc=example,dc=comを追加することはできますが、反応はしない、そういう動きになります。


# 対応策の検討
最初から、ou=people,ou=group,ou=maillistを平行に、3つのバックエンドDBを準備する方法が順当な方法だと思います。

ここでは、rootDNとして、dc=example,dc=comを利用した、translucentの設定を使っているので、無理やり、対応策を考えてみました。

translucentなLDAPサーバーを立てて、それがsuffixとして、dc=example,dc=comを利用しているとします。
このままでは、このLDAPサーバーにou=maillist,dc=example,dc=comを構築することはできません。

そのため、別のLDAPサーバー(親LDAPサーバー、translucentなLDAPサーバー、に加える3台目)を立てて、ou=maillist,...なsuffixを処理するバックエンドと、新たに、ou=proxy,dc=example,dc=comをRootDNとするmetaバックエンドを加えて次のようなディレクトリを構築します。

* ou=people,ou=proxy,dc=example,dc=com
* ou=group,ou=proxy,dc=example,dc=com
* ou=maillist,ou=proxy,dc=example,dc=com

## ou=maillist,dc=example,dc=comディレクトリの構築

まず通常のディレクトリを構築します。

```bash:filename.ldif
dn: olcDatabase=mdb,cn=config
objectClass: olcDatabaseConfig
objectClass: olcMdbConfig
olcDatabase: mdb
OlcDbMaxSize: 1073741824
olcSuffix: ou=maillist,dc=example,dc=com
olcRootDN: cn=admin,ou=maillist,dc=example,dc=com
olcRootPW: secretpasswd
olcDbDirectory: /var/lib/ldap
olcDbIndex: objectClass eq
olcAccess: to * by * read
```

```bash
$ ldapadd -Q -Y EXTERNAL -H ldapi:/// -f filename.ldif
```

個々のエントリを加えていく前にトップレベルのエントリを作成します。

```bash:filename.ldif
dn: ou=maillist,dc=example,dc=com
objectClass: organizationalUnit
ou: maillist
```

```bash
$ ldapadd -x -D cn=admin,ou=maillist,dc=example,dc=com -W -f filename.ldif
```

ここから、メーリングリストの内容を加えていきます。今回はエントリに含めるユーザーのDNはmetaディレクトリに統合されているエントリで記述していきます。

```bash:filename.ldif
dn: cn=ml01,ou=maillist,dc=example,dc=com
objectClass: groupOfNames
cn: ml01
member: uid=yasu-abe,ou=people,ou=proxy,dc=example,dc=com
member: uid=user01,ou=people,ou=proxy,dc=example,dc=com

dn: cn=ml02,ou=maillist,dc=example,dc=com
objectClass: groupOfNames
cn: ml02
member: uid=yasu-abe,ou=people,ou=proxy,dc=example,dc=com
member: uid=user02,ou=people,ou=proxy,dc=example,dc=com
```

```bash
$ ldapadd -x -D cn=admin,ou=maillist,dc=example,dc=com -W -F filename.ldif
```

## ou=proxy,dc=example,dc=comディレクトリの構築

ここまでで作成してきた、2つのLDAPサーバーを統合していきます。

まずはモジュールの準備。

```bash:filename.ldif
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: back_ldap
-
add: olcModuleLoad
olcModuleLoad: back_meta
```

```bash
$ ldapadd -Q -Y EXTERNAL -H ldapi:/// -f filename.ldif
```

次にディレクトリ情報を加えていきます。

```bash:filename.ldif
dn: olcDatabase=meta,cn=config
objectClass: olcDatabaseConfig
objectClass: olcMetaConfig
olcDatabase: meta
olcSuffix: ou=opmgw,{{ openldap_basedn }}
olcAccess: to * by * read

dn: olcMetaSub={0}uri,olcDatabase={1}meta,cn=config
objectClass: olcMetaTargetConfig
olcMetaSub: {0}uri
olcDbURI: ldap:///ou=maillist,ou=proxy,dc=example,dc=com
olcDbRewrite: {0}suffixmassage "ou=ML,ou=proxy,dc=example,dc=com" "ou=maillist,dc=example,dc=com"

dn: olcMetaSub={1}uri,olcDatabase={1}meta,cn=config
objectClass: olcMetaTargetConfig
olcMetaSub: {1}uri
olcDbURI: ldap://transldap.example.com/ou=people,ou=proxy,dc=example,dc=com
olcDbRewrite: {0}suffixmassage "ou=people,ou=proxy,dc=example,dc=com" "ou=people,dc=example,dc=com"

dn: olcMetaSub={2}uri,olcDatabase={1}meta,cn=config
objectClass: olcMetaTargetConfig
olcMetaSub: {2}uri
olcDbURI: ldap://transldap.example.com/ou=group,ou=proxy,dc=example,dc=com
olcDbRewrite: {0}suffixmassage "ou=group,ou=proxy,dc=example,dc=com" "ou=group,dc=example,dc=com"
```

```bash
$ ldapadd -Q -Y EXTERNAL -H ldapi:/// -f filename.ldif
```

ou=maillist,dc=example,dc=comはローカルサーバーにあるのでホスト名の指定をolcDbURIで行なっていません。その他のディレクトリは適宜、追加しています。

この作業では、[OpenLDAPでメタディレクトリを利用した複数のデータベースの統合管理 ](https://stpr18.blogspot.jp/2017/02/openldap.html)の記事を参考にしていますが、olcAccessの行が不足しています。

olcAccessで必要な権限を付与していない場合は、tcpdump(wireshark)などで確認できますが、エントリ情報を収集した上で、クライアント側では結果が表示されないのに、検索が正常に終了したようにみえます。

# まとめ

translucentやback_metaモジュールを利用した既存LDAPサーバーの活用は、需要はありそうですが、あまり例はないのかなと思っています。

むかし、ブログでUTF-8な日本語情報を加えた時にSAFE-STRING扱いで、base64でエンコードされてしまった事がありましたが、慣れないとldapsearchの結果は不思議にみえるかもしれないのに、資料は不足していると思ったことがありました。

とはいえ、LDAPは統合認証基盤としては必須で、1組織内で1ユーザーに複数のパスワードを配布するような愚行は避けなければいけないですし、うまくLDAPとつきあっていきたいと思いました。

以上