python3-ldapを触ってみた

## 事の起こり
windows7+python3.4+django1.7でLDAP連携しようとしたら、[python-ldap](http://www.python-ldap.org/)がWindows7+Python3環境で準備できなかった。
（Python3向けはコンパイルが必要なのだが、自分の環境ではコンパイルできず…Windows向けに用意されているインストーラーはPython2までしか無かった）

そこで色々調べてみたら、[python3-ldap](https://pypi.python.org/pypi/python3-ldap)なるものを発見。Pure Pythonで書かれているPython用のLDAP連携ライブラリみたい。
で、コイツを使って「とりあえず」でAD連携のバックエンドを書いてみる。

## python3-ldapをいじる
とりあえず[サンプル](http://pythonhosted.org//python3-ldap/quicktour.html)に従って、ADサーバに接続→認証するところまで書いてみる。

```py3:quickstart_python3-ldap.py
from ldap3 import Server, Connection, AUTH_SIMPLE, STRATEGY_SYNC, STRATEGY_ASYNC_THREADED, SEARCH_SCOPE_WHOLE_SUBTREE, GET_ALL_INFO

s = Server('servername', port = 389, get_info = GET_ALL_INFO)  # define an unsecure LDAP server, requesting info on DSE and schema
c = Connection(s, auto_bind = True, client_strategy = STRATEGY_SYNC, user='username', password='password', authentication=AUTH_SIMPLE, check_names=True)
```

Connection生成するときに、userとpasswordでバインド（認証）を行う。バインドに失敗すると例外が発生

```py3tb:bind_error.py3tb
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "C:\work\venv\lib\site-packages\ldap3\core\connection.py", line 243, in __init__
    raise LDAPBindError(self.last_error)
ldap3.core.exceptions.LDAPBindError: automatic bind not successful - invalidCredentials
```

バインドに成功した場合は、例外は発生せず、以降はConnectionオブジェクトを使ってldapsearchやら何やらを実行できるようになる（バインドしたユーザーの権限によるけど）。
今回は認証だけできればOKなので、Connection使って何ができるか、という部分はスルー

## 認証バックエンドを自作する
お次はDjango側のコード。python-ldapを使う場合は[django-auth-ldap](https://pythonhosted.org/django-auth-ldap/)を使えばオシマイなのだが、python3-ldapはまだ新しいライブラリなんでdjango用のライブラリは見つからず。
仕方ないので、自前で認証バックエンドのコードを書いて動かしてみる。

```py3:backends.py
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from ldap3 import Server, Connection, AUTH_SIMPLE, STRATEGY_SYNC, STRATEGY_ASYNC_THREADED, SEARCH_SCOPE_WHOLE_SUBTREE, GET_ALL_INFO

from django.conf import settings
UserModel = get_user_model()

LDAP_SERVER_HOST = 'server_name'
LDAP_SERVER_PORT = 389
ACTIVE_DIRECTORY_DOMAIN = 'domain_name'

def get_server(host='localhost', port=389) :
    return Server(host, port=port, get_info = GET_ALL_INFO)

def get_connection(server, user, password) :
    return Connection(server, auto_bind = True, client_strategy = STRATEGY_SYNC, user=user, password=password, authentication=AUTH_SIMPLE, check_names=True)

class ActiveDirectoryBackend(ModelBackend):
    def authenticate(self, username=None, password=None):
        server = get_server(host=LDAP_SERVER_HOST, port=LDAP_SERVER_PORT)
        if not server.check_availability() :
            return None

        user = '%s@%s'%(username,ACTIVE_DIRECTORY_DOMAIN)
        try :
            conn = get_connection(server, user=user, password=password)
        except :
            return None

        try :
            return UserModel.objects.get(username=username)
        except :
            return None

    def get_user(self, user_id):
        try :
            return UserModel.objects.get(pk=user_id)
        except :
            return None
```

本当はドメイン名も引数で受け取るようにしといた方がいいんだろうけど、やり方がよく分かってないので固定で。

あとはsettings.pyに自作した認証バックエンドを指定すればOK。
ホストやポート、ドメイン名も、settings.pyに移した方がいいかも。

```py3:settings.py
AUTHENTICATION_BACKENDS = (
    'python3ldap.backends.ActiveDirectoryBackend',
)
```

なお、Django側のDBにも、同じユーザー名でデータ突っ込んでおかないと認証失敗するので、お忘れなく。
（最後の最後、これでハマった）


2014/11/28：
ユーザーモデルをdjango.contrib.auth.get_user_model()で取得するよう修正。