pythonでLDAPにデータの追加取得をする

# はじめに
認証やツリー構造のデータの管理などでLDAPが使用されます。RDBと比べるとLDAPは使用される機会が少なく使用方法も異なるのでpythonでLDAPを操作する方法をまとめます。さらにLDAP認証のサンプルは色々なところで紹介されていますが、もっと簡単にするためシンプルな例を紹介します。

# 環境準備
## LDAPサーバ
LDAPサーバは、UbuntuやCentosにldapをインストールしてもできますがdockerイメージがあったため、そちらを使用します。

### dockerイメージのpull
dockerイメージは単純にpullするだけです。

```

docker pull osixia/openldap
```

### dockerイメージの起動
イメージの起動時にldapのパスワードとトップのドメイン、各ポートのマウントをしておきます。dockerネットワークを利用している場合は、ポートのマウントはせずにネットワークとIPアドレスの設定をします。

```

 docker run -p 389:389 -p 636:636 --env LDAP_DOMAIN="sample-ldap" --env LDAP_ADMIN_PASSWORD="LdapPass" --name LDAPSERVER --detach osixia/openldap
 ```

結果

```

> docker ps
CONTAINER ID        IMAGE               COMMAND                 CREATED             STATUS              PORTS               NAMES
4f6e1b4eaf29        osixia/openldap     "/container/tool/run"   2 hours ago         Up 2 hours          0.0.0.0:389->389/tcp, 0.0.0.0:636->636/tcp   LDAPSERVER
```

## Ldapライブラリのインストール
クライアントはpythonで使用するのでLDAPクライアントのライブラリ```ldap3```をpipでインストールします。

```

pip install ldap3
```

# LDAPの操作
LDAPのサーバとクライアントの用意ができたため、これから操作のソースを作成していきます。

## LDAPへのログイン
LDAPへの操作にはログインが必要なため、まずはログインをします。LDAPサーバのIPアドレスやポート番号、タイムアウト等必要な設定を追加したServerクラスを設定します。そのServerクラスを使用してConnectionクラスを生成します。この時にはLDAPサーバへ接続されず、```bind()```で始めて接続されます。dcとpasswordはdocker run時に指定した値になり、cnはデフォルトはadminになります。

``` main.py

conn = Connection(server, 'cn=admin,dc=sample-ldap',  password='LdapPass')
result = conn.bind()
print(result)
```

結果


```
> python main.py' 
True
```

bindの結果がTrueとなっているため、LDAPサーバへ接続ができたことが分かります。


## ドメイン
接続ができたため次はLDAPの追加と取得をしていきます。
LDAPはトップからdc、ou、cnの順にツリー構造で構成されているため、まずはdcから追加と取得していきます。

### ドメインの追加
サンプルコードは上のソースの続きになります。作成したコネクションの```add関数```の第一引数に追加したいdcとトップのdcをつなげた文字列を指定して、第二引数に```'domain'```を指定します。この時に第一引数の文字列のカンマの後にスペースを入れるとエラーするので注意してください。

``` main.py

# ドメインの追加
dc_result = conn.add('dc=sample-component,dc=sample-ldap', 'domain')
print(dc_result)
```

結果

```
True
```

実行結果はバインドと同じでaddの結果としてTrueが返ってきたため、追加できたことがわかります。

### ドメインの取得
サンプルコードは上のソースの続きになります。上で追加したdcを取得します。``` conn.search() ```の第一引数に調べたいLDAPのパスを指定します。第二引数は```domain```を指定します。その結果、```conn.entries ```にsample-componentの情報が取得できます。

``` main.py

# ドメインの取得
conn.search('dc=sample-component,dc=sample-ldap', '(objectclass=domain)')
print(conn.entries)
```

結果

```
True
[DN: dc=sample-component,dc=sample-ldap - STATUS: Read - READ TIME: 2020-03-25T22:35:26.491599
]
```

今回は、第二引数にdomainを指定してドメインを検索しているので対象の1個のentriesが取得できます。後ほどソースを載せますが第二引数に他の値を入れると複数取得できます。

## オーガニゼーション
LDAPのdcが追加できたので次はouの追加と取得していきます。

### オーガニゼーションの追加
サンプルコードは上のソースの続きになります。作成したコネクションの```add関数```の第一引数に追加したいouとdcをつなげた文字列を指定して、第二引数に```'organizationalUnit'```を指定します。

``` main.py

# ドメインの追加
ou_result = conn.add('ou=sample-unit,dc=sample-component,dc=sample-ldap', 'organizationalUnit')
print(ou_result)
```

結果

```
True
```

実行結果はバインドと同じでaddの結果としてTrueが返ってきたため、追加できたことがわかります。

### オーガニゼーションの取得
サンプルコードは上のソースの続きになります。上で追加したouを取得します。``` conn.search() ```の第一引数に検索したいパスを指定します。第二引数は```organizationalUnit```を指定します。その結果、```conn.entries ```にsample-unitの情報が取得できます。

``` main.py

# オーガニゼーションの取得
conn.search('ou=sample-unit,dc=sample-component,dc=sample-ldap', '(objectclass=organizationalUnit)')
print(conn.entries)

# dc指定のオーガニゼーションの取得
conn.search('dc=sample-component,dc=sample-ldap', '(objectclass=organizationalUnit)')
print(conn.entries)


```

結果

```

[DN: ou=sample-unit,dc=sample-component,dc=sample-ldap - STATUS: Read - READ TIME: 2020-03-25T23:27:36.594396]
[DN: ou=sample-unit,dc=sample-component,dc=sample-ldap - STATUS: Read - READ TIME: 2020-03-25T23:27:36.604398
, DN: ou=sample-unit2,dc=sample-component,dc=sample-ldap - STATUS: Read - READ TIME: 2020-03-25T23:27:36.604398
]
```

第一引数にオーガニゼーションを検索している場合は対象の1個のentriesが取得できます。ドメインを検索している場合は、ドメインが含んでいるouの数だけ複数取得できます。


## コモンネーム
LDAPのouが追加できたので次はcnの追加と取得していきます。

### コモンネームの追加
サンプルコードは上のソースの続きになります。作成したコネクションの```add関数```の第一引数に追加したいcnとouとdcをつなげた文字列を指定して、第二引数に```'inetOrgPerson'```を指定して、第三引数に付加情報を指定します。

``` main.py

# ドメインの追加
cn_result = conn.add('cn=sample-name,ou=sample-unit,dc=sample-component,dc=sample-ldap', 'inetOrgPerson', {'sn':'sample'})
print(cn_result)
```

結果

```
True
```

実行結果はバインドと同じでaddの結果としてTrueが返ってきたため、追加できたことがわかります。

### コモンネームの取得
サンプルコードは上のソースの続きになります。上で追加したcnを取得します。``` conn.search() ```の第一引数に検索したいパスを指定します。第二引数は```inetOrgPerson```を指定します。その結果、```conn.entries ```にsample-nameの情報が取得できます。

``` main.py

# コモンネームの取得
conn.search('cn=sample-name,ou=sample-unit,dc=sample-component,dc=sample-ldap', '(objectclass=inetOrgPerson)')
print(conn.entries)

# ou指定のコモンネームの取得
conn.search('ou=sample-unit,dc=sample-component,dc=sample-ldap', '(objectclass=inetOrgPerson)')
print(conn.entries)

# dc指定のコモンネームの取得
conn.search('dc=sample-component,dc=sample-ldap', '(objectclass=inetOrgPerson)')
print(conn.entries)
```

結果

```

[DN: cn=sample-name,ou=sample-unit,dc=sample-component,dc=sample-ldap - STATUS: Read - READ TIME: 2020-03-25T23:36:41.125246
]
[DN: cn=sample-name,ou=sample-unit,dc=sample-component,dc=sample-ldap - STATUS: Read - READ TIME: 2020-03-25T23:36:41.156378
, DN: cn=sample-name2,ou=sample-unit,dc=sample-component,dc=sample-ldap - STATUS: Read - READ TIME: 2020-03-25T23:36:41.157365
, DN: cn=sample-name3,ou=sample-unit,dc=sample-component,dc=sample-ldap - STATUS: Read - READ TIME: 2020-03-25T23:36:41.157365
, DN: cn=sample-name1,ou=sample-unit,dc=sample-component,dc=sample-ldap - STATUS: Read - READ TIME: 2020-03-25T23:36:41.157365
]
[DN: cn=sample-name,ou=sample-unit,dc=sample-component,dc=sample-ldap - STATUS: Read - READ TIME: 2020-03-25T23:51:20.773638
, DN: cn=sample-name2,ou=sample-unit,dc=sample-component,dc=sample-ldap - STATUS: Read - READ TIME: 2020-03-25T23:51:20.773638
, DN: cn=sample-name3,ou=sample-unit,dc=sample-component,dc=sample-ldap - STATUS: Read - READ TIME: 2020-03-25T23:51:20.774650
, DN: cn=sample-name1,ou=sample-unit,dc=sample-component,dc=sample-ldap - STATUS: Read - READ TIME: 2020-03-25T23:51:20.774650
, DN: cn=sample-name,ou=sample-unit1,dc=sample-component,dc=sample-ldap - STATUS: Read - READ TIME: 2020-03-25T23:51:20.774650
]
```

第二引数にinetOrgPersonを指定してコモンネームを検索している場合は対象の1個のentriesが取得できます。オーガニゼーションやドメインを検索している場合はそれぞれが含んでいるcnの数だけ複数取得できます。

# おわりに
LDAPに関しては、LDAP認証として使用しただけなので今回のようにLDAPのドメインから追加したりそれぞれのディレクトリから検索方法を変えて検索することはしていませんでした。使用するのに癖はありますが、思った以上にシンプルな方法で値を取得できるのでツリー構造のデータならこちらの方がRDBより使いやすくなるかもしれません。次は他の操作も見ていきます。