1. 程式人生 > >如何使用Python連線ldap

如何使用Python連線ldap

如何使用Python連線ldap

好多使用ldap認證的軟體都是Python的,比如superset和airflow, 好吧,他們都是airbnb家的。在配置ldap的時候可能會出現認證失敗,你不知道是因為什麼導致配置失敗的。所以,就要
跟蹤原始碼,看看內部怎麼認證實現的。

ldap介紹和使用安裝參見: https://www.cnblogs.com/woshimrf/p/ldap.html

登入的原始碼參見: https://github.com/apache/airflow/blob/70e937a8d8ff308a9fb9055ceb7ef2c034200b36/airflow/contrib/auth/backends/ldap_auth.py#L191

具體來實現如下:

為了模擬環境,我們使用docker-python。基於Debian Python3: https://github.com/Ryan-Miao/docker-china-source/tree/master/docker-python

啟動

docker run -it ryan/python:3 /bin/bash

下載ldap3

pip install ldap3

測試連線

root@5edee218d962:/# python
Python 3.7.4 (default, Jul 13 2019, 14:20:24) 
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from ldap3 import Server, Connection, ALL, NTLM, ALL_ATTRIBUTES, LEVEL, SUBTREE
>>> server = Server('172.17.0.2', get_info=ALL)
>>> conn = Connection(server, 'cn=admin,dc=demo,dc=com', 'admin', auto_bind=True)
>>> conn.extend.standard.who_am_i()
'dn:cn=admin,dc=demo,dc=com'

測試登入部分

登入原始碼如下:

@staticmethod
    def try_login(username, password):
        conn = get_ldap_connection(configuration.conf.get("ldap", "bind_user"),
                                   configuration.conf.get("ldap", "bind_password"))

        search_filter = "(&({0})({1}={2}))".format(
            configuration.conf.get("ldap", "user_filter"),
            configuration.conf.get("ldap", "user_name_attr"),
            username
        )

        search_scope = LEVEL
        if configuration.conf.has_option("ldap", "search_scope"):
            if configuration.conf.get("ldap", "search_scope") == "SUBTREE":
                search_scope = SUBTREE
            else:
                search_scope = LEVEL

        # todo: BASE or ONELEVEL?

        res = conn.search(configuration.conf.get("ldap", "basedn"), search_filter, search_scope=search_scope)

        # todo: use list or result?
        if not res:
            log.info("Cannot find user %s", username)
            raise AuthenticationError("Invalid username or password")

        entry = conn.response[0]

        conn.unbind()

        if 'dn' not in entry:
            # The search filter for the user did not return any values, so an
            # invalid user was used for credentials.
            raise AuthenticationError("Invalid username or password")

        try:
            conn = get_ldap_connection(entry['dn'], password)
        except KeyError:
            log.error("""
            Unable to parse LDAP structure. If you're using Active Directory
            and not specifying an OU, you must set search_scope=SUBTREE in airflow.cfg.
            %s
            """, traceback.format_exc())
            raise LdapException(
                "Could not parse LDAP structure. "
                "Try setting search_scope in airflow.cfg, or check logs"
            )

        if not conn:
            log.info("Password incorrect for user %s", username)
            raise AuthenticationError("Invalid username or password")

第一步: 獲取連線

from ldap3 import Server, Connection, ALL, NTLM, ALL_ATTRIBUTES, LEVEL, SUBTREE


server = Server('172.17.0.2:389', get_info=ALL)
conn = Connection(server, 'cn=admin,dc=demo,dc=com', 'admin', auto_bind=True)
conn.extend.standard.who_am_i()

第二步: 根據filter search使用者。 這裡我們的配置檔案如下:

[ldap]
# set this to ldaps://<your.ldap.server>:<port>
uri = ldap://172.17.0.2:389
user_filter = objectClass=inetOrgPerson
user_name_attr = sn
group_member_attr = memberOf
superuser_filter =
data_profiler_filter =
bind_user = cn=admin,dc=demo,dc=com
bind_password = admin
basedn = dc=demo,dc=com
cacert =
search_scope = SUBTREE

原始碼就是拼接filter, 最後變成(&(objectClass=inetOrgPerson)(sn=ryanmiao)), 然後search scope.


>>> with Connection(server, 'cn=admin,dc=demo,dc=com', 'admin') as conn:
...         conn.search('dc=demo,dc=com', '(&(objectClass=inetOrgPerson)(cn=hr-ryan))', search_scope=SUBTREE)
...         entry = conn.entries[0]
... 
True
>>> entry
DN: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com - STATUS: Read - READ TIME: 2019-08-19T12:58:34.966181

第三步: 從上一步得到dn,然後根據使用者輸入的密碼,再次連線, 不拋異常就證明密碼正確

>>> conn = Connection(server, 'cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com', '123456', auto_bind=True)
>>> 
>>> conn = Connection(server, 'cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com', '1234567', auto_bind=True)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.7/site-packages/ldap3/core/connection.py", line 325, in __init__
    self.do_auto_bind()
  File "/usr/local/lib/python3.7/site-packages/ldap3/core/connection.py", line 353, in do_auto_bind
    raise LDAPBindError(self.last_error)
ldap3.core.exceptions.LDAPBindError: automatic bind not successful - invalidCredentials

測試分組

ldap提供了分組,配置見前文。我們採用ldap統一登入之後,還要把使用者放入不同的group裡來區分許可權。比如splunk-users, splunk-admin, gitlab-admin, gitlab-user等。

根據使用者名稱和密碼,我們實現了使用者登入密碼驗證,接下來需要取到使用者所屬於的group。

原始碼如下:


def groups_user(conn, search_base, user_filter, user_name_att, username):
    search_filter = "(&({0})({1}={2}))".format(user_filter, user_name_att, username)
    try:
        memberof_attr = configuration.conf.get("ldap", "group_member_attr")
    except Exception:
        memberof_attr = "memberOf"
    res = conn.search(search_base, search_filter, attributes=[memberof_attr])
    if not res:
        log.info("Cannot find user %s", username)
        raise AuthenticationError("Invalid username or password")

    if conn.response and memberof_attr not in conn.response[0]["attributes"]:
        log.warning("""Missing attribute "%s" when looked-up in Ldap database.
        The user does not seem to be a member of a group and therefore won't see any dag
        if the option filter_by_owner=True and owner_mode=ldapgroup are set""",
                    memberof_attr)
        return []

    user_groups = conn.response[0]["attributes"][memberof_attr]

    regex = re.compile("cn=([^,]*).*", re.IGNORECASE)
    groups_list = []
    try:
        groups_list = [regex.search(i).group(1) for i in user_groups]
    except IndexError:
        log.warning("Parsing error when retrieving the user's group(s)."
                    " Check if the user belongs to at least one group"
                    " or if the user's groups name do not contain special characters")

    return groups_list

同樣,第一步,連線,第二步search,但需要返回欄位memberof。注意我們前面配置了group_member_attr=memberof

>>> with Connection(server, 'cn=admin,dc=demo,dc=com', 'admin') as conn:
...         conn.search('dc=demo,dc=com', '(&(objectClass=inetOrgPerson)(sn=hr-ryan))', attributes=['memberof'])
...         entry = conn.entries[0]
...         entry
... 
True
DN: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com - STATUS: Read - READ TIME: 2019-08-19T13:05:20.592805
    memberOf: cn=g-admin,ou=Group,dc=demo,dc=com
              cn=g-users,ou=Group,dc=demo,dc=com
              cn=g-a,ou=Group,dc=demo,dc=com

如果取不到group會報錯。

以上就差不多是airflow的ldap配置原理了。其他雷同,不一樣的地方也許是在filter的地方,我們找對應軟體的原始碼look一下就ok了