LDAPのことを意識させずに操作できるPYTHON用クラス

# はじめに
pythonでLDAPを操作する方法を3回に分けてまとめてきました。複数人で開発する場合は全員がldap3のライブラリやLDAPのことについて理解する必要がありますが、それには時間と労力がかかります。そこでLDAPはツリー構造のデータであるということだけを知っていれば誰でも使えるようにクラスを作成しました。

# 環境
 - python:3.6.5
 - ldap3:2.7
 - イメージ:osixia/openldap 

# メリット
 - LDAPのことを意識せずに使える
 - ルールを加えられる(dc=xxxの後はouにするなど)
 - ぱっと見で分かりやすい

# 今回のクラスを使用した例
まずは、今回のクラスを使用した場合にどのようにLDAPを操作するのかを記載します。使用するときはクラス名を変えるとより分かりやすくなるため、この後の説明で出てくるDomainクラスをAddress、OrganizationをOwner、CommonをPetに変更することでAddress -> Owner -> Petの階層であることが意識できます。

``` main.py

from ldap_obj.Address import Address

# 場所の追加
address = Address('localhost', 'LdapPass', 'admin', 'sample-ldap')
address.set_item('tokyo')
address.save()

# 人の追加
owner = address.create_owner()
owner.set_item('sato')
owner.save()

# ペット(たま)の追加
pet_tama = owner.create_pet()
pet_tama.set_item('tama')
pet_tama.save({'sn': 'sato', 'st': 'tama', 'title': 'cat'})

# ペット(ぽち)の追加
pet_pocho = owner.create_pet()
pet_pocho.set_item('pochi')
pet_pocho.save({'sn': 'sato', 'st': 'pochi', 'title': 'dog'})

# titleを検索条件にしてdomain層からcnの値を取得する
address.set_filter('title')
print(address.get_common())

print('***********************')
# Addressを生成
address_get = Address('localhost', 'LdapPass', 'admin', 'sample-ldap')
address_get.set_item('tokyo')
# Address -> Ownerの生成
owner_get = address_get.create_owner()
owner_get.set_item('sato')
# Address -> Owner -> Petの生成
pet_get = owner_get.create_pet()
pet_get.set_item('tama')
print(pet_get.get_common())

```

# LDAP用クラス
LDAP用クラスは各階層ごとにクラスを作成します。

## ディレクトリ構成

```
\--
  |--ldap_obj\
  |    |--BaseClass.py (基準のクラス)
  |    |--CommonClass.py (cnのクラス)
  |    |--DomainClass.py (dcのクラス)
  |    |--OrganizationClass.py (ouのクラス)
  |
  |--main.py
```

## 基準のクラス
基準のクラスは基本的にldap3のラッパーになります。特に複雑なことはしていません。コントラクタでは接続に必要なクラスの生成をして、get_xxx()では_read_ldap()にオブジェクトクラスを渡してLDAPから情報を取得しています。オブジェクトクラスが増えた場合はこの関数が増えます。

``` BaseClass.py

from ldap3 import Server, Connection, ObjectDef, Reader

class BaseClass(object):

    def __init__(self, host, passwd, user, top_domain, dn=None):
        self.passwd = passwd
        self.user = user
        self.host = host
        self.top_domain = top_domain
        self.filter = None

        if (dn):
            self.dn = dn
        else:
            self.dn = 'dc=' + top_domain
        
        self.server = Server(self.host)
        self.conn = Connection(self.host, 'cn={},dc={}'.format(user, top_domain),  password=passwd)

    def set_filter(self, filter):
        self.filter = filter
    
    def get_domain(self):
        return self._read_ldap('domain', self.filter)

    def get_organizational(self):
        return self._read_ldap('organizationalUnit', self.filter)

    def get_common(self):
        return self._read_ldap('inetOrgPerson', self.filter)

    def get_domain_dict(self):
        return self._read_ldap_dict('domain', self.filter)

    def get_organizational_dict(self):
        return self._read_ldap_dict('organizationalUnit', self.filter)

    def get_common_dict(self):
        return self._read_ldap_dict('inetOrgPerson', self.filter)

    def _read_ldap(self, object_type, search_attr = None):
        data_list = []
        self.conn.bind()
        obj_dn = ObjectDef(object_type, self.conn)
        data_reader = Reader(self.conn, obj_dn, self.dn)
        data_reader.search(search_attr)
        for data in data_reader:
            data_list.append(data)

        data_reader.reset()
        self.conn.unbind()
        return data_list

    def _read_ldap_dict(self, object_type, search_attr = None):
        data_list = []
        self.conn.bind()
        obj_dn = ObjectDef(object_type, self.conn)
        data_reader = Reader(self.conn, obj_dn, self.dn)
        data_reader.search(search_attr)
        for data in data_reader:
            data_list.append(data.entry_attributes_as_dict)

        data_reader.reset()
        self.conn.unbind()
        return data_list

```

## 拡張クラス
上の基準のクラスを継承したクラスをそれぞれの階層ごとに作成していきます。

### Domainクラス
dcの値が必要なため```set_item()```でdnの文字列にdcの値を追加しています。このself.dnを使用して基準のクラスの```get_xxx()```で情報を取得したり、```save()```で追加します。```create_organization()```はou用のクラスを生成して返却しています。今回の例は、dc=xxx,ou=yyyとしてほしいのでou用の生成関数しかないですがdc=xxx,dc=yyyとしたいときはdc用の生成関数を同じように作成すればできます。

``` DomainClass.py

from ldap_obj.BaseClass import BaseClass
from ldap_obj.OrganizationClass import OrganizationClass

class DomainClass(BaseClass):
         
    def set_item(self, item):
        self.dn = 'dc=' + item + ',' + self.dn

    def create_organization(self):
        return OrganizationClass(self.host, self.passwd, self.user, self.top_domain, self.dn)

    def save(self):
        self.conn.bind()
        result = self.conn.add(self.dn, 'domain')
        self.conn.unbind()
        return result
```

### Organizationクラス
Domainクラスから生成したOrganizationクラスにはself.dnにcnまでのパスが入っているので```set_item()```でouの値を追加しています。このself.dnを使用して基準のクラスの```get_xxx()```で情報を取得したり、```save()```で追加します。```create_common()```はcn用のクラスを生成して返却しています。今回の例は、dc=xxx,ou=yyy,cn=zzzzとしてほしいのでcn用の生成関数しかないですが他の構成にしたいときはそれ用の生成関数を作成します。

``` OrganizationClass.py

from ldap_obj.BaseClass import BaseClass
from ldap_obj.CommonClass import CommonClass

class OrganizationClass(BaseClass):

    def set_item(self, item):
        self.dn = 'ou=' + item + ',' + self.dn

    def create_common(self):
        return CommonClass(self.host, self.passwd, self.user, self.top_domain, self.dn)

    def save(self):
        self.conn.bind()
        result = self.conn.add(self.dn, 'organizationalUnit')
        self.conn.unbind()
        return result

```

### Commonクラス
Organizationクラスから生成したCommonクラスにはself.dnにouまでのパスが入っているので```set_item()```でcnの値を追加しています。このself.dnを使用して基準のクラスの```get_xxx()```で情報を取得したり、```save()```で追加します。今回の例は、このcnで最後なので生成関数はありませんがさらに階層が深い場合はそれ用の生成関数を作成します。

``` CommonClass.py

from ldap_obj.BaseClass import BaseClass

class CommonClass(BaseClass):

    def set_item(self, item):
        self.dn = 'cn=' + item + ',' + self.dn

    def save(self, attr_dict):
        self.conn.bind()
        result = self.conn.add(self.dn, 'inetOrgPerson', attr_dict)
        self.conn.unbind()
        return result
```

## 呼び出し元メイン関数
これらのクラスの使い方は、最初にDomainClassを生成してからdcの値を入れます、次に生成したDomainClassの```create_organization()```を使用してOrganizationクラスを生成してouの値をいれます。commonの生成は生成したOrganizationクラスの```create_common()```を使用してcnの値をいれます。それぞれの生成クラスで取得関数や追加関数を使用してLDAPの操作をします。


``` main.py

from ldap_obj.DomainClass import DomainClass

domain = DomainClass('localhost', 'LdapPass', 'admin', 'sample-ldap')
domain.set_item('sample-component')
domain_item_list = domain.get_domain()
for domain_item in domain_item_list:
    print(domain_item)

print("=====================")

domain.set_filter('st')
domain_item_list = domain.get_common()
for domain_item in domain_item_list:
    print(domain_item)

print("=====================")

organization = domain.create_organization()
organization.set_item('sample-unit')
organization_list = organization.get_organizational()
for organization_item in organization_list:
    print(organization_item)

print("=====================")

common = organization.create_common()
common.set_item('sample-name')
common_list = common.get_common()
for common_item in common_list:
    print(common_item)

print("***********************************")

new_domain = DomainClass('localhost', 'LdapPass', 'admin', 'sample-ldap')
new_domain.set_item('new-component')
print(new_domain.save())
print("=====================")

new_organization = new_domain.create_organization()
new_organization.set_item('new-organization')
print(new_organization.save())
print("=====================")

new_common = new_organization.create_common()
new_common.set_item('new-common')
print(new_common.save({'st':'new-st', 'sn': 'new-sn'}))
print("=====================")

new_common_list = new_common.get_common()
for common_item in new_common_list:
    print(common_item)
```

# おわりに
極力LDAPの要素を排して操作できるようなクラスを作ってみました。ldap3のライブラリを知っているとクラス経由の操作は面倒に思えますが実際にソースを見てみるとぱっと見で何をしているのかがわかりやすくなりました。今回は取得と追加の機能を持たせましたが、削除と移動と更新は基準のクラスにldap3を使用する関数を追加すれば良いと思います。ここまで分かりやすくすればデータの格納の候補として使用にも躊躇がなくなるのではないかと思います。