PythonプログラムからOpenLDAPに接続＆ユーザ情報を取得する

# はじめに

これまでJupyterHub／JupyterLabを利用した分析環境をVirtualBoxもしくはWSL2で整備してきたが、共通して認証にはOpenLDAPを使ってきた。

自宅PCでは自ずと筆者のみの利用ではあるが、会社（組織）で利用するならばユーザ管理をする必要がある。
どれだけのユーザがいて、いつパスワード変更をしたか、etc.

というわけで、OpenLDAPに登録したユーザの情報を取得するプログラムを書いてみようと思う。
なぜプログラムか？かというと、OpenLDAP操作用のWebUIにはFusionDirectoryやphpLDAPadminがあるものの、全ユーザの情報を一覧で取得するのはちょっと面倒と思った次第でして。

そこで、本稿では、**「PythonプログラムからOpenLDAPに接続＆ユーザ情報を取得する方法」**を順に紹介してゆく。

## 本稿で紹介すること

- OpenLDAPの起動
- PythonプログラムでOpenLDAPへの接続
- PythonプログラムでOpenLDAPからユーザ情報の取得

## 本稿で紹介しないこと

- WSL2のインストール
- Ubuntuのインストール（From Microsoft Store）　※Ubuntu 18.04.5 LTSを使用
- Dockerのインストール　※Docker Community Edition 20.10.8を使用
- Pythonライブラリ（LDAP3）の全般
- OpenLDAP操作用のWebUI（FusionDirectoryおよびphpLDAPadmin）の全般

:::note info
過去の記事を参照されたし。
:::

※1: [Windows10のPCに分析環境（VirtualBox／Vagrant＋Kubernetes＋JupyterHub／JupyterLab）を作ってみた](https://qiita.com/Blaster36/items/c3abf6650c9cc025a684)
※2: [Windows10のPCに分析環境（VirtualBox／Vagrant＋Docker＋JupyterHub／JupyterLab）を作ってみた](https://qiita.com/Blaster36/items/bcbd1a2bd82bdfcc555e)
※3: [GPU搭載WindowsのWSL2でNGCカタログのコンテナイメージを使う](https://qiita.com/Blaster36/items/6dce4cf0b4ada88ef639)
※4: [SeleniumとMeCabを使えるJupyterLabコンテナイメージを作る](https://qiita.com/Blaster36/items/4bf54b129f506f814a6c)

# ステップ紹介
大きく、3ステップです。
1. OpenLDAPの起動
2. PythonプログラムでOpenLDAPへの接続
3. PythonプログラムでOpenLDAPからユーザ情報の取得

## 事前準備

:::note info
以下、筆者の環境です。
:::

```shell:
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.5 LTS"
```

```shell:
$ docker version
Client: Docker Engine - Community
 Version:           20.10.8
 API version:       1.41
 Go version:        go1.16.6
 Git commit:        3967b7d
 Built:             Fri Jul 30 19:54:08 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.8
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.16.6
  Git commit:       75249d8
  Built:            Fri Jul 30 19:52:16 2021
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.4.9
  GitCommit:        e25210fe30a0a703442421b0f60afac609f950a3
 nvidia:
  Version:          1.0.1
  GitCommit:        v1.0.1-0-g4144b63
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
```

:::note warn
本稿では<b>hrektts/fusiondirectory-openldap:1.1.9-1.2-1</b>を取得＆起動し、ユーザ管理に利用するものとします。
また、過去の記事(<a href=https://qiita.com/Blaster36/items/4bf54b129f506f814a6c>※4</a>)でビルドした<b>jupyter/minimal-notebook:9e8682c9ea54-MeCab-Slum</b>を起動し、Pythonプログラムの実行に利用するものとします。
:::

```shell:
$ docker images
REPOSITORY                         TAG                       IMAGE ID       CREATED         SIZE
jupyterhub/jupyterhub              1.0-gpu                   e7dd44e871bd   3 days ago      420MB
jupyter/minimal-notebook           9e8682c9ea54-MeCab-Slum   a2645c7160be   4 days ago      3.92GB
jupyter/minimal-notebook           9e8682c9ea54-MeCab        d0e1a0e79bc8   4 days ago      3.55GB
postgres                           9.6                       131ab388dac1   4 months ago    200MB
nvcr.io/nvidia/pytorch             20.11-py3-gpu             dd53c8b61c4b   5 months ago    15.3GB
nvcr.io/nvidia/tensorflow          20.11-tf2-py3-gpu         f6aa8098fc48   5 months ago    12.3GB
adminer                            4.8.1                     4727f36d62d9   5 months ago    90MB
nvcr.io/nvidia/tensorflow          21.07-tf2-py3             887093b5693e   5 months ago    11.1GB
nvcr.io/nvidia/pytorch             21.07-py3                 7beec3ff8d35   5 months ago    15GB
nvcr.io/nvidia/tensorflow          20.11-tf2-py3             98a7952f7f9c   14 months ago   11.6GB
nvcr.io/nvidia/pytorch             20.11-py3                 ae35b2b3cad1   14 months ago   13.2GB
jupyterhub/jupyterhub              1.0                       c399e04fda3c   2 years ago     283MB
osixia/phpldapadmin                0.9.0                     78148b61fdb5   2 years ago     302MB
jupyter/minimal-notebook           2343e33dec46              c3bbd3471e39   3 years ago     2.72GB
jupyter/minimal-notebook           9e8682c9ea54              400c44c4a7a7   3 years ago     2.79GB
hrektts/fusiondirectory-openldap   1.1.9-1.2-1               7f2e4370509d   4 years ago     226MB
hrektts/fusiondirectory            0.2.0                     b56f1086a08d   4 years ago     345MB
```

## 1. OpenLDAPの起動

:::note info
本ステップは、WSL上で実行
:::

以下のリンクに記載された手順を見本とし、起動します。
[hrektts/fusiondirectory-openldap](https://hub.docker.com/r/hrektts/fusiondirectory-openldap)
[hrektts/fusiondirectory](https://hub.docker.com/r/hrektts/fusiondirectory)
[osixia/phpldapadmin](https://hub.docker.com/r/osixia/phpldapadmin)

以下、環境設定（環境変数の定義）を含む起動コマンドです。

```shell:
$ docker run --name ldap -p 389:389 \
  -e LDAP_ORGANISATION="OpenLDAP for ldapauthenticator.LDAPAuthenticator" \
  -e LDAP_DOMAIN="hoge.com" \
  -e LDAP_ADMIN_PASSWORD="ldapadminpwd" \
  -e FD_ADMIN_PASSWORD="fdadminpwd" \
  -d hrektts/fusiondirectory-openldap:1.1.9-1.2-1
```

ちなみに、FusionDirectoryの起動コマンドはこちら。
Webブラウザで http://127.0.0.1/fd/ にアクセスし、Usernameに「fd-admin」、Passwordに上述の「**FD_ADMIN_PASSWORD**」に指定した文字列でログインできればOKです。

:::note warn
本稿では、予めFusionDirectoryを操作し、「jhub」OUを作成＆3ユーザを作成するものとします。
:::

```shell:
$ docker run --name fd -p 80:80 --link ldap:ldap \
  -d hrektts/fusiondirectory:0.2.0
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/74e927b6-9918-7a4e-7029-dbe6a86d2e5a.png)

ちなみに、phpLDAPadminの起動コマンドはこちら。
Webブラウザで http://127.0.0.1:8080 にアクセスし、Login DNに「cn=admin,dc=hoge,dc=com」、Passwordに上述の「**LDAP_ADMIN_PASSWORD**」に指定した文字列でログインできればOKです。

```shell:
$ docker run --name phpldapadmin \
  --hostname phpldapadmin-service \
  --link ldap:ldap-host \
  --env PHPLDAPADMIN_LDAP_HOSTS=ldap-host \
  --restart=always \
  --publish=8443:443 \
  --publish=8080:80 \
  --env PHPLDAPADMIN_HTTPS=false \
  --detach osixia/phpldapadmin:0.9.0
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/8ad42921-27b3-61c6-1152-2872c56a0864.png)

## 2. PythonプログラムでOpenLDAPへの接続

:::note info
本ステップは、JupyterLab上で実行
:::

以下、PythonとPIPのVer情報です。

```shell:
$ python -V
Python 3.6.6
$ python -m pip --version
pip 21.2.2 from /opt/conda/lib/python3.6/site-packages/pip (python 3.6)
```

以下、必要なPythonライブラリのインストールコマンドです。(PIPが古くなってますね、、、気になる方はUpgradeをば。）

```shell:
$ python -m pip install ldap3
Requirement already satisfied: ldap3 in /opt/conda/lib/python3.6/site-packages (2.7)
Requirement already satisfied: pyasn1>=0.1.8 in /opt/conda/lib/python3.6/site-packages (from ldap3) (0.4.8)
WARNING: You are using pip version 21.2.2; however, version 21.3.1 is available.
You should consider upgrading via the '/opt/conda/bin/python -m pip install --upgrade pip' command.
```

以下、OpenLDAPへの接続Code例です。

:::note warn
OpenLDAPのIPアドレス、ポート番号、SSL接続有無は、読者の環境に合わせて書き換えてください。
:::

```python:
from ldap3 import Server, Connection, ALL, NTLM, SUBTREE, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES
server = Server('${OpenLDAPのIPアドレス}', get_info=ALL, port=389, use_ssl=False)
conn = Connection(server, user='cn=admin,dc=hoge,dc=com', password='ldapadminpwd', auto_bind=True)
```

接続確立後、接続に指定したuserのDN文字列が表示できればOKです。

```python:
conn.extend.standard.who_am_i()
# 'dn:cn=admin,dc=hoge,dc=com'
```

## 3. PythonプログラムでOpenLDAPからユーザ情報の取得

:::note info
本ステップは、JupyterLab上で実行
:::

以下、OpenLDAPからユーザ情報の取得Code例です。

:::note warn
OpenLDAPの階層（OU）は、読者の環境に合わせて書き換えてください。
:::

LDAPクエリを実行し、Trueが表示できればOKです。

```python:
conn.search('ou=people,ou=jhub,dc=hoge,dc=com', '(objectclass=person)', attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES], paged_size=100, search_scope=SUBTREE)
# True
```

検索結果をPrintしてみます。

```python:
for entry in sorted(conn.entries):
    print(entry.entry_dn)
    print(entry.entry_raw_attribute)
```

今回は、「jhub」OUの配下の全ユーザの情報が出力されます。

```python:
uid=admin,ou=people,ou=jhub,dc=hoge,dc=com
<bound method EntryBase.entry_raw_attribute of DN: uid=admin,ou=people,ou=jhub,dc=hoge,dc=com - STATUS: Read - READ TIME: 2022-01-10T12:51:16.304321
    cn: admin jupyterhub
    createTimestamp: 2022-01-10 12:43:49+00:00
    creatorsName: cn=admin,dc=hoge,dc=com
    entryCSN: 20220110124409.081861Z#000000#000#000000
    entryUUID: b44f782e-065e-103c-9bc3-61c9f2a130f0
    givenName: admin
    hasSubordinates: False
    modifiersName: cn=admin,dc=hoge,dc=com
    modifyTimestamp: 2022-01-10 12:44:09+00:00
    objectClass: inetOrgPerson
                 organizationalPerson
                 person
    sn: jupyterhub
    structuralObjectClass: inetOrgPerson
    subschemaSubentry: cn=Subschema
    uid: admin
    userPassword: b'{SSHA}cnvbA3tVbuxbCZYyafChFw6qV3ZLlfv/'
>
uid=user1,ou=people,ou=jhub,dc=hoge,dc=com
<bound method EntryBase.entry_raw_attribute of DN: uid=user1,ou=people,ou=jhub,dc=hoge,dc=com - STATUS: Read - READ TIME: 2022-01-10T12:51:16.303493
    cn: user1 jupyterhub
    createTimestamp: 2022-01-10 12:41:50+00:00
    creatorsName: cn=admin,dc=hoge,dc=com
    entryCSN: 20220110124150.915437Z#000000#000#000000
    entryUUID: 6d5d2ba0-065e-103c-9bbf-61c9f2a130f0
    givenName: user1
    hasSubordinates: False
    modifiersName: cn=admin,dc=hoge,dc=com
    modifyTimestamp: 2022-01-10 12:41:50+00:00
    objectClass: inetOrgPerson
                 organizationalPerson
                 person
    sn: jupyterhub
    structuralObjectClass: inetOrgPerson
    subschemaSubentry: cn=Subschema
    uid: user1
    userPassword: b'{SSHA}VAHOD9cryu8XfEcX/Q99/k2Tvoqg8E/B'
>
uid=user2,ou=people,ou=jhub,dc=hoge,dc=com
<bound method EntryBase.entry_raw_attribute of DN: uid=user2,ou=people,ou=jhub,dc=hoge,dc=com - STATUS: Read - READ TIME: 2022-01-10T12:51:16.303761
    cn: user2 jupyterhub
    createTimestamp: 2022-01-10 12:42:22+00:00
    creatorsName: cn=admin,dc=hoge,dc=com
    entryCSN: 20220110124222.193645Z#000000#000#000000
    entryUUID: 8001d8a0-065e-103c-9bc0-61c9f2a130f0
    givenName: user2
    hasSubordinates: False
    modifiersName: cn=admin,dc=hoge,dc=com
    modifyTimestamp: 2022-01-10 12:42:22+00:00
    objectClass: inetOrgPerson
                 organizationalPerson
                 person
    sn: jupyterhub
    structuralObjectClass: inetOrgPerson
    subschemaSubentry: cn=Subschema
    uid: user2
    userPassword: b'{SSHA}JTT6fLPXEsdfOLgPNI4WSBIKUjJ6uEL+'
>
uid=user3,ou=people,ou=jhub,dc=hoge,dc=com
<bound method EntryBase.entry_raw_attribute of DN: uid=user3,ou=people,ou=jhub,dc=hoge,dc=com - STATUS: Read - READ TIME: 2022-01-10T12:51:16.304017
    cn: user3 jupyterhub
    createTimestamp: 2022-01-10 12:42:40+00:00
    creatorsName: cn=admin,dc=hoge,dc=com
    entryCSN: 20220110124240.888741Z#000000#000#000000
    entryUUID: 8b267dd0-065e-103c-9bc1-61c9f2a130f0
    givenName: user3
    hasSubordinates: False
    modifiersName: cn=admin,dc=hoge,dc=com
    modifyTimestamp: 2022-01-10 12:42:40+00:00
    objectClass: inetOrgPerson
                 organizationalPerson
                 person
    sn: jupyterhub
    structuralObjectClass: inetOrgPerson
    subschemaSubentry: cn=Subschema
    uid: user3
    userPassword: b'{SSHA}qCTxg7K1DaelVbKfkL/1twiQEPCcCpxD'
>
```

# もう一息！

以下、取得済みのユーザ情報のCSVファイル出力Code例です。

:::note info
3列構成（"uid":ユーザ名、"createTimestamp":登録日時、"modifyTimestamp":更新日時≒<b><font color="Red">最終パスワード変更日時</font></b>）です。
:::

```python:
import codecs

f = codecs.open('users.csv', 'w', 'utf-8')
f.write('"uid","createTimestamp","modifyTimestamp"\n')
for entry in sorted(conn.entries):
    f.write('%s,%s,%s\n' %(str(entry['uid']), str(entry['createTimestamp']), str(entry['modifyTimestamp'])))
f.close()
```

パスワード変更をすると、"modifyTimestamp"の値が更新されます。

```python:users.csv
"uid","createTimestamp","modifyTimestamp"
admin,2022-01-10 12:43:49+00:00,2022-01-10 14:38:53+00:00
user1,2022-01-10 12:41:50+00:00,2022-01-10 12:41:50+00:00
user2,2022-01-10 12:42:22+00:00,2022-01-10 12:42:22+00:00
user3,2022-01-10 12:42:40+00:00,2022-01-10 12:42:40+00:00
```

JupyterLabのCSTTableのビューだと、以下のような見た目になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/5b71bbc4-7692-b74f-d52a-7b9e6974923e.png)

# まとめ

とりあえずは、PythonプログラムからOpenLDAPに接続＆ユーザ情報を取得することができました。
OpenLDAPに保存されている他の属性値も出力すれば、ユーザ情報の一覧化には重宝しそうな印象です。