Zeppelin 集成 LDAP(FreeIPA)

前言

本篇主要介紹Zeppelin集成LDAP認(rèn)證的方法。

LDAP服務(wù)配置我們采用FreeIPA。FreeIPA是一個集成安全信息管理解決方案,包含Linux用戶系統(tǒng)、LDAP、Kerberos、Dogtag等認(rèn)證系統(tǒng)。FreeIPA將這些認(rèn)證系統(tǒng)的用戶信息統(tǒng)一。FreeIPA還提供了web界面和命令行的操作方式。比直接配置OpenLDAP服務(wù)方便了許多。

環(huán)境信息如下:

  • OS: CentOS 7.4
  • Zeppelin: 0.10.1
  • FreeIPA: 4.6.8

FreeIPA安裝

  1. 配置本機hostname和hosts:
hostnamectl set-hostname test.paultech.com

然后配置hosts文件:

{ip} test.paultech.com

注意:hostname必須和本機host一致,否則后面執(zhí)行ipa-server-install的時候會出現(xiàn)錯誤。

  1. 安裝ipa-server
yum install ipa-server
  1. 配置ipa-server。在shell執(zhí)行:
ipa-server-install

ipa-server-install是一個向?qū)桨惭b配置工具。需要回答如下問題:

# 是否需要集成的DNS
Do you want to configure integrated DNS (BIND)? [no]: no
# 輸入hostname
Server host name [test.paultech.com]:
# 確認(rèn)domain name
Please confirm the domain name [paultech.com]:
# 確認(rèn)Kerberos的realm name
Please provide a realm name [PAULTECH.COM]:
# 設(shè)置LDAP管理員用戶的密碼
Directory Manager password:
Password (confirm):
# 設(shè)置IPA admin賬戶(管理員)的密碼
IPA admin password:
Password (confirm):

注意: 正常來說這里應(yīng)該不會有錯誤。然而實際安裝環(huán)境有差異,可能會遇到各種各樣的問題。具體錯誤和解決版本參見:安裝FreeIPA以及應(yīng)用時報錯匯總 - 尹正杰 - 博客園 (cnblogs.com)。本文作者在安裝的時候遇到如下兩個問題:

  • Command '/bin/systemctl start certmonger.service' returned non-zero exit status 1

    解決這個問題需要執(zhí)行:

    systemctl restart dbus.socket
    systemctl restart dbus.service
    
  • ipa server install ended with "CA did not start in 300s"

    執(zhí)行:

    yum install -y ipa-server-dns
    

    還有需要注意的是,如果配置步驟中遇到錯誤,每次解決后都需要執(zhí)行如下命令卸載配置:

    ipa-server-install --uninstall
    

    等待卸載掉原有的安裝配置之后,重新執(zhí)行ipa-server-install。

  1. 驗證安裝。如果上面的步驟順利執(zhí)行完畢,可執(zhí)行下方命令驗證安裝。
kinit admin
# 然后輸入安裝時候配置的admin密碼

# 查看Kerberos是否認(rèn)證成功
klist

# 查看所有用戶信息
ipa user-find --all

如果能夠打出user信息,說明安裝成功。

  1. 登錄ipa-server web頁面。訪問:https://test.paultech.com/ipa/ui。需要提前在訪問端機器配置hosts。填寫之前配置的IPA admin用戶和密碼,成功進入IPA管理頁面。

注意:如果admin登錄用戶密碼錯誤次數(shù)太多,admin用戶會被鎖定。web頁面,kinitipa命令均無法操作。在/var/log/httpd/error_log中會發(fā)現(xiàn)如下異常:

freeipa DatabaseError: Server is unwilling to perform: Too many failed logins

解決方法是解鎖admin用戶:

ipa user-unlock admin

FreeIPA命令操作

使用ipactl命令

ipactl命令控制IPA服務(wù)啟動,停止操作:

ipactl start
ipactl stop
ipactl restart
ipactl status

使用ipa命令

ipa命令調(diào)用之前必須Kerberos認(rèn)證為管理員,命令如下:

kinit admin
# 輸入管理員密碼

使用ipa命令查詢用戶和組的詳細(xì)信息。后面配置Zeppelin的時候需要用到。

ipa user-find --all
ipa group-find --all

基本上FreeIPA web頁面的操作都能夠通過ipa命令的方式實現(xiàn)。其他使用方式到具體用到的時候再補充。

Zeppelin 使用LDAP

Zeppelin的認(rèn)證配置依賴Shiro。認(rèn)證配置文件位于${ZEPPELIN_HOME/conf/shiro.ini.

LdapGroupRealm配置

LdapGroupRealm是一種簡化方式的LDAP用戶和角色綁定配置方式。它讀取LDAP目錄searchBase下所有objectClass為groupOfNames,并且member屬性包含user DN的條目的cn屬性值,就是這個用戶綁定的角色名。具體獲取用戶綁定角色的方式參見附錄LdapGroupRealm讀取用戶對應(yīng)role的原理。

我們編輯${ZEPPELIN_HOME/conf/shiro.ini,配置LDAP相關(guān)內(nèi)容:

### A sample for configuring LDAP Directory Realm
ldapRealm = org.apache.zeppelin.realm.LdapGroupRealm
## search base for ldap groups (only relevant for LdapGroupRealm):
ldapRealm.contextFactory.environment[ldap.searchBase] = dc=paultech,dc=com
ldapRealm.contextFactory.url = ldap://192.168.1.100:389
ldapRealm.userDnTemplate = uid={0},cn=users,cn=accounts,dc=paultech,dc=com
ldapRealm.contextFactory.authenticationMechanism = simple
ldapRealm.contextFactory.systemUsername = uid=admin,cn=users,cn=accounts,dc=paultech,dc=com
ldapRealm.contextFactory.systemPassword = 123456

配置項含義如下:

  • ldapRealm.contextFactory.environment[ldap.searchBase]需要寫search base。具有這些條件的用戶才會被Zeppelin搜索到。這里的LDAP信息需要通過ipa user-find --all命令查看。
  • ldapRealm.contextFactory.url: LDAP服務(wù)器的訪問URL。
  • ldapRealm.userDnTemplate需要配置如何將Zeppelin的user映射為LDAP user的dn。例如對于用戶paul而言,uid={0},cn=users,cn=accounts,dc=paultech,dc=com模板會被映射成dn為uid=paul,cn=users,cn=accounts,dc=paultech,dc=com
  • ldapRealm.contextFactory.authenticationMechanism: 認(rèn)證機制,這里使用簡單認(rèn)證。
  • ldapRealm.contextFactory.systemUsername/systemPassword: LDAP服務(wù)的管理員賬戶和密碼。

接下來配置用戶角色和權(quán)限,找到配置文件中[roles]部分:

[roles]
admin = *
zeppelinadmin = *

[urls]
/api/version = anon
/api/cluster/address = anon
# Allow all authenticated users to restart interpreters on a notebook page.
# Comment out the following line if you would like to authorize only admin users to restart interpreters.
/api/interpreter/setting/restart/** = authc
/api/interpreter/** = authc, roles[zeppelinadmin]
/api/notebook-repositories/** = authc, roles[zeppelinadmin]
/api/configurations/** = authc, roles[zeppelinadmin]
/api/credential/** = authc, roles[zeppelinadmin]
/api/admin/** = authc, roles[zeppelinadmin]

上面的配置文件中我們新定義了一個zeppelinadmin角色,該角色擁有Zeppelin管理員的權(quán)限。

注意,[urls]部分API權(quán)限表達式的roles可以配置多個角色,例如roles[admin, zeppelinadmin]表示用戶必須同事具有adminzeppelinadmin角色才有權(quán)限。如果想要實現(xiàn)“用戶具有如下角色之一”就有權(quán)限這種配置呢?可以按照如下方式配置:

[main]
anyofrolesuser = org.apache.zeppelin.utils.AnyOfRolesUserAuthorizationFilter

[urls]
/api/interpreter/** = authc, anyofrolesuser[admin, user1]
/api/configurations/** = authc, roles[admin]
/api/credential/** = authc, roles[admin]

到此為止我們已經(jīng)完成了Zeppelin LDAP的集成和角色權(quán)限的對應(yīng)關(guān)系配置。那么用戶和角色的對應(yīng)關(guān)系在哪里配置?我們繼續(xù)下一節(jié),綁定用戶和角色。

FreeIPA 綁定用戶和角色

創(chuàng)建用戶

依次點擊IPA web頁面中的身份,用戶,活躍用戶,然后點擊右側(cè)表格上方的添加。設(shè)置登錄名,姓名和密碼之后點擊添加,用戶創(chuàng)建完畢。

創(chuàng)建角色并綁定角色到用戶

依次點擊IPA服務(wù)器 -> Role-Based Access Control,點擊表格右側(cè)上方的添加。新建一個名字為zeppelinadmin的角色。然后打開這個角色,在用戶標(biāo)簽中,點擊添加,選擇上一步創(chuàng)建好的用戶。到這里用戶已經(jīng)成功綁定到zeppelinadmin角色。

Zeppelin登陸LDAP用戶

重啟Zeppelin服務(wù)后,在web頁面使用上面步驟創(chuàng)建用戶的登錄名和密碼登錄。

登錄成功后可以看到Zeppelin server有類似如下日志:

INFO [2022-08-09 01:53:57,311] ({qtp823723302-12} LoginRestApi.java[postLogin]:249) - {"status":"OK","message":"","body":{"principal":"paul","ticket":"1789ef4d-4f19-4534-8f9b-c351ded0b7fb","roles":"[\"zeppelinadmin\"]"}}

如果看到獲取到用戶的角色正確,說明上述配置無誤。Zeppelin成功獲取到用戶對應(yīng)的角色。

LdapRealm配置(可選)

前面的LdapGroupRealm為我們預(yù)定義了用戶和角色的對應(yīng)管理查找邏輯。如果我們的LDAP不是這么存儲對應(yīng)關(guān)系的,也就是說需要支持自定義的查找邏輯,這該怎么辦?

Zeppelin提供了更為靈活的LdapRealm配置方式,但是配置項也更為復(fù)雜。

接下來是一個例子。我們的組為:

dn: cn=zeppelinadmin,ou=roles,dc=paultech,dc=com
member: uid=paul,ou=People,dc=paultech,dc=com
objectClass: groupOfNames
objectClass: top
cn: zeppelinadmin

groupOfNames是用戶組常見的組的objectClass。它包含一個重要屬性member,存儲了屬于這個組的用戶DN。

還有一種常見的組的objectClass是posixGroup。它的屬性為memberUid,只保存屬于這個組用戶的uid信息,而不是DN。

用戶為:

dn: uid=paul,ou=People,dc=paultech,dc=com
uid: paul
cn: paul
objectClass: account
objectClass: posixAccount
objectClass: top
objectClass: shadowAccount
userPassword:: xxxxxx
shadowLastChange: 19206
shadowMin: 0
shadowMax: 99999
shadowWarning: 7
loginShell: /bin/bash
uidNumber: 11107
gidNumber: 11107
homeDirectory: /home/paul

這個例子符合LdapGroupRealm的解析方式,但為了演示我們使用LdapRealm方式配置。具體配置和解釋如下:

# 啟用LdapRealm配置方式
ldapRealm = org.apache.zeppelin.realm.LdapRealm

# 使用簡單認(rèn)證
ldapRealm.contextFactory.authenticationMechanism = simple
# 配置LDAP服務(wù)器訪問URL
ldapRealm.contextFactory.url = ldap://10.180.210.127:389
# 配置user DN的模板
ldapRealm.userDnTemplate = uid={0},ou=People,dc=paultech,dc=com
# 分頁大小,默認(rèn)為100
ldapRealm.pagingSize = 200
# 啟用認(rèn)證
ldapRealm.authorizationEnabled = true
# 指定searchBase,通常為LDAP目錄根節(jié)點
ldapRealm.searchBase = dc=paultech,dc=com
# 查找用戶條目的根節(jié)點,所有用戶必須在該節(jié)點下存儲
ldapRealm.userSearchBase = ou=People,dc=paultech,dc=com
# 查找組(角色)條目的根節(jié)點,所有用戶組信息必須在改條目下存儲
ldapRealm.groupSearchBase = ou=roles,dc=paultech,dc=com
# 所有組條目的objectClass屬性值。默認(rèn)為groupOfNames。常用的也有posixGroup
ldapRealm.groupObjectClass = groupOfNames
# 和前面配置二選一,也可以指定查找關(guān)聯(lián)group的查詢表達式
# ldapRealm.groupSearchFilter = (&(objectClass=groupOfNames)(member=uid={0},ou=People,dc=paultech,dc=com))
# 如果配置了此選項,就不再使用memberAttribute方式獲取用戶組
# 例如下面配置,而是使用memberUid=用戶名方式來搜索用戶所屬的組。也就是說用戶組要包含'memberUid=用戶名'鍵值對
# ldapRealm.userSearchAttributeName = memberUid
# 配置member屬性的名字。比如說groupOfName對象是通過member來保存屬于中各組對象的,這里就配置為member
ldapRealm.memberAttribute = member
# member屬性值的模板,對于groupOfNames條目,它的member保存了屬于這個組的user的DN,所以這里配置userDnTemplate
ldapRealm.memberAttributeValueTemplate=uid={0},ou=People,dc=paultech,dc=com
# 強制將用戶名小寫
ldapRealm.userLowerCase = true
# user和group的查找范圍,可以配置subtree(默認(rèn)),one, base。一般用subtree,查找對應(yīng)searchBase及其各級子條目
ldapRealm.userSearchScope = subtree;
ldapRealm.groupSearchScope = subtree;
# LDAP管理員的DN和密碼
ldapRealm.contextFactory.systemUsername = cn=manager,dc=paultech,dc=com
ldapRealm.contextFactory.systemPassword = 123456
# enable support for nested groups using the LDAP_MATCHING_RULE_IN_CHAIN operator
# OpenLDAP不支持LDAP_MATCHING_RULE_IN_CHAIN operator,這里禁用
ldapRealm.groupSearchEnableMatchingRuleInChain = false
# 配置LDAP組(角色)和Zeppelin角色的對應(yīng)關(guān)系
# 例如下面的配置,如果根據(jù)前面查找規(guī)則找到某個用戶對應(yīng)的組名為zeppelinadmin,那么它對應(yīng)Zeppelin內(nèi)部的角色名為admin
# zeppelin角色的權(quán)限和訪問控制在[roles]和[urls]部分配置
ldapRealm.rolesByGroup = zeppelinadmin: admin

更為詳細(xì)的LdapRealm獲取用戶匹配組的方式,請見附錄LdapRealm讀取用戶對應(yīng)role的原理。

附錄

LdapGroupRealm讀取用戶對應(yīng)role的原理

上面章節(jié)我們使用FreeIPA幫忙綁定LDAP用戶和角色,Zeppelin可以識別成功。那么問題來了,Zeppelin是如何查找用戶對應(yīng)的角色的?如果不使用FreeIPA,只用手工方式配置LDAP,我們怎么把用戶和對應(yīng)的角色綁定在一起?接下來我們一起揭曉這個謎題。

我們從源代碼入手,分析LdapGroupRealm根據(jù)登錄用戶名獲取所屬角色的核心邏輯getRoleNamesForUser方法。代碼和解釋如下所示:

public Set<String> getRoleNamesForUser(String username, LdapContext ldapContext,
                                       String userDnTemplate) {
    try {
        Set<String> roleNames = new LinkedHashSet<>();

        // 不僅查找searchBase,還查找searchBase的子目錄
        // searchBase為查找的跟目錄,例如dc=paultech,dc=com。
        SearchControls searchCtls = new SearchControls();
        searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);

        // 組裝ldapsearch 過濾器
        // 這里需要兩個條件都滿足
        // 1. objectClass=groupOfNames,必須為groupOfNames類型
        // 2. member為userDnTemplate,前面例子中配置的是uid={0},cn=users,cn=accounts,dc=paultech,dc=com
        String searchFilter = "(&(objectClass=groupOfNames)(member=" + userDnTemplate + "))";
        Object[] searchArguments = new Object[]{username};

        // 查找符合條件的條目
        // 相當(dāng)于執(zhí)行l(wèi)dapsearch -D "cn=manager,dc=paultech,dc=com" -w password -b dc=paultech,dc=com -s sub '(&(objectClass=groupOfNames)(member=uid=paul,cn=users,cn=accounts,dc=paultech,dc=com))'
        NamingEnumeration<?> answer = ldapContext.search(
            String.valueOf(ldapContext.getEnvironment().get("ldap.searchBase")),
            searchFilter,
            searchArguments,
            searchCtls);

        // 遍歷搜索結(jié)果
        while (answer.hasMoreElements()) {
            SearchResult sr = (SearchResult) answer.next();
            Attributes attrs = sr.getAttributes();
            if (attrs != null) {
                // 遍歷所有屬性
                NamingEnumeration<?> ae = attrs.getAll();
                while (ae.hasMore()) {
                    Attribute attr = (Attribute) ae.next();
                    // 找到名字為cn的屬性,它的屬性值就是用戶對應(yīng)的角色,保存起來
                    if (attr.getID().equals("cn")) {
                        roleNames.add((String) attr.get());
                    }
                }
            }
        }
        return roleNames;

    } catch (Exception e) {
        LOGGER.error("Error", e);
    }

    return new HashSet<>();
}

通過上面分析我們發(fā)現(xiàn),比如用戶名為paul,searchBase為dc=paultech,dc=com,userDnTemplate為uid={0},cn=users,cn=accounts,dc=paultech,dc=com,Zeppelin查找用戶組相當(dāng)如執(zhí)行如下命令:

ldapsearch -D "cn=manager,dc=paultech,dc=com" -w password -b dc=paultech,dc=com -s sub '(&(objectClass=groupOfNames)(member=uid=paul,cn=users,cn=accounts,dc=paultech,dc=com))'

即查找objectClass為groupOfNames,同時member屬性值為uid=paul,cn=users,cn=accounts,dc=paultech,dc=com的條目,獲取它的cn屬性值為用戶對應(yīng)的role。我們可以查看下LDAP目錄其中的內(nèi)容,驗證下FreeIPA創(chuàng)建的角色是不是和這個邏輯相匹配。分析到這里,相信大家即便不用FreeIPA,也能夠配置用戶和角色的對應(yīng)關(guān)系了。

LdapRealm讀取用戶對應(yīng)role的原理

核心rolesFor方法

我們直接從核心方法rolesFor入手:

protected Set<String> rolesFor(PrincipalCollection principals, String userNameIn,
                               final LdapContext ldapCtx, final LdapContextFactory ldapContextFactory, Session session)
    throws NamingException {
    final Set<String> roleNames = new HashSet<>();
    final Set<String> groupNames = new HashSet<>();
    final String userName;
    // 對應(yīng)配置ldapRealm.userLowerCase
    // 如果配置了true,將用戶名轉(zhuǎn)換為小寫
    if (getUserLowerCase()) {
        LOGGER.debug("userLowerCase true");
        userName = userNameIn.toLowerCase();
    } else {
        userName = userNameIn;
    }

    // 從用戶名獲取需要搜索用戶DN,這個方法很重要,流程也較長,放在后面分析
    String userDn = getUserDnForSearch(userName);

    // Activate paged results
    // 對應(yīng)配置ldapRealm.pagingSize
    int pageSize = getPagingSize();
    LOGGER.debug("Ldap PagingSize: {}", pageSize);
    int numResults = 0;
    try {
        ldapCtx.addToEnvironment(Context.REFERRAL, "ignore");

        ldapCtx.setRequestControls(new Control[]{new PagedResultsControl(pageSize,
                                                                         Control.NONCRITICAL)});

        // ldapsearch -h localhost -p 33389 -D
        // uid=guest,ou=people,dc=hadoop,dc=apache,dc=org -w guest-password
        // -b dc=hadoop,dc=apache,dc=org -s sub '(objectclass=*)'
        NamingEnumeration<SearchResult> searchResultEnum = null;
        // 對應(yīng)配置ldapRealm.groupSearchScope
        SearchControls searchControls = getGroupSearchControls();
        try {
            // 對應(yīng)配置ldapRealm.groupSearchEnableMatchingRuleInChain
            if (groupSearchEnableMatchingRuleInChain) {
                // groupObjectClass對應(yīng)配置ldapRealm.groupObjectClass
                // memberAttribute對應(yīng)配置ldapRealm.memberAttribute
                // 搜索filter相當(dāng)于
                // (&(objectClass=groupObjectClass)(member:1.2.840.113556.1.4.1941:=userDN))
                searchResultEnum = ldapCtx.search(
                    getGroupSearchBase(),
                    String.format(
                        MATCHING_RULE_IN_CHAIN_FORMAT, groupObjectClass, memberAttribute, userDn),
                    searchControls);
                // 遍歷結(jié)果
                while (searchResultEnum != null && searchResultEnum.hasMore()) {
                    // searchResults contains all the groups in search scope
                    numResults++;
                    final SearchResult group = searchResultEnum.next();
                    // 獲取查詢到的group的cn屬性,就是匹配的組名
                    Attribute attribute = group.getAttributes().get(getGroupIdAttribute());
                    String groupName = attribute.get().toString();

                    // 查找組名對應(yīng)的zeppelin角色名
                    // 對應(yīng)配置ldapRealm.rolesByGroup
                    String roleName = roleNameFor(groupName);
                    // 如果沒找到對應(yīng)zeppelin角色名,則直接使用組名
                    if (roleName != null) {
                        roleNames.add(roleName);
                    } else {
                        roleNames.add(groupName);
                    }
                }
            } else {
                // 如果沒啟用ldapRealm.groupSearchEnableMatchingRuleInChain
                // 則按照objectClass查找匹配的組信息
                // 這里查找objectclass為groupObjectClass的組信息。
                String searchFilter = String.format("(objectclass=%1$s)", groupObjectClass);

                // If group search filter is defined in Shiro config, then use it
                // 如果配置了ldapRealm.groupSearchFilter
                // 則放棄上面的搜索方式,使用自定義的search filter
                if (groupSearchFilter != null) {
                    // 使用用戶名替換掉模板中的占位符'{0}'
                    searchFilter = expandTemplate(groupSearchFilter, userName);
                }
                LOGGER.debug("Group SearchBase|SearchFilter|GroupSearchScope: " + "{}|{}|{}",
                             getGroupSearchBase(), searchFilter, groupSearchScope);
                searchResultEnum = ldapCtx.search(
                    getGroupSearchBase(),
                    searchFilter,
                    searchControls);
                while (searchResultEnum != null && searchResultEnum.hasMore()) {
                    // searchResults contains all the groups in search scope
                    numResults++;
                    final SearchResult group = searchResultEnum.next();
                    // 判斷如果group中包含這個用戶,則將這個組對應(yīng)的role加入roleNames集合
                    // 邏輯在后面分析
                    addRoleIfMember(userDn, group, roleNames, groupNames, ldapContextFactory);
                }
            }
        } catch (PartialResultException e) {
            LOGGER.debug("Ignoring PartitalResultException");
        } finally {
            if (searchResultEnum != null) {
                searchResultEnum.close();
            }
        }
        // Re-activate paged results
        ldapCtx.setRequestControls(new Control[]{new PagedResultsControl(pageSize,
                                                                         null, Control.CRITICAL)});
    } catch (SizeLimitExceededException e) {
        LOGGER.info("Only retrieved first {} groups due to SizeLimitExceededException.", numResults);
    } catch (IOException e) {
        LOGGER.error("Unabled to setup paged results");
    }
    // save role names and group names in session so that they can be
    // easily looked up outside of this object
    session.setAttribute(SUBJECT_USER_ROLES, roleNames);
    session.setAttribute(SUBJECT_USER_GROUPS, groupNames);
    if (!groupNames.isEmpty() && (principals instanceof MutablePrincipalCollection)) {
        ((MutablePrincipalCollection) principals).addAll(groupNames, getName());
    }
    LOGGER.debug("User RoleNames: {}::{}", userName, roleNames);
    return roleNames;
}

getUserDnForSearch

getUserDnForSearch獲取搜索匹配組時候用的user DN。代碼如下所示:

protected String getUserDnForSearch(String userName) {
    // 對應(yīng)配置ldapRealm.userSearchAttributeName
    if (userSearchAttributeName == null || userSearchAttributeName.isEmpty()) {
        // memberAttributeValuePrefix and memberAttributeValueSuffix
        // were computed from memberAttributeValueTemplate
        return memberDn(userName);
    } else {
        return getUserDn(userName);
    }
}

可以看到如果配置了ldapRealm.memberAttribute,使用getUserDn(userName)獲取user DN,否則使用memberDn(userName)。

首先分析memberDn方法:

private String memberDn(String attrValue) {
    return memberAttributeValuePrefix + attrValue + memberAttributeValueSuffix;
}

這個方法使用前綴+用戶名+后綴的方式拼接user DN。那么前綴和后綴是什么時候配置的?答案在setMemberAttributeValueTemplate方法。該方法對應(yīng)的配置項為ldapRealm.memberAttributeValueTemplate

public void setMemberAttributeValueTemplate(String template) {
    if (!StringUtils.hasText(template)) {
        String msg = "User DN template cannot be null or empty.";
        throw new IllegalArgumentException(msg);
    }
    int index = template.indexOf(MEMBER_SUBSTITUTION_TOKEN);
    if (index < 0) {
        String msg = "Member attribute value template must contain the '" + MEMBER_SUBSTITUTION_TOKEN
            + "' replacement token to understand how to " + "parse the group members.";
        throw new IllegalArgumentException(msg);
    }
    String prefix = template.substring(0, index);
    String suffix = template.substring(prefix.length() + MEMBER_SUBSTITUTION_TOKEN.length());
    this.memberAttributeValuePrefix = prefix;
    this.memberAttributeValueSuffix = suffix;
}

MEMBER_SUBSTITUTION_TOKEN的值為{0}這個方法的含義是找到ldapRealm.memberAttributeValueTemplate中的{0}。它前面的字符串設(shè)置為memberAttributeValuePrefix,后面的設(shè)置為memberAttributeValueSuffix。到這里memberDn相關(guān)邏輯就分析完了。

我們繼續(xù)分析getUserDn方法:

protected String getUserDn(final String principal) throws IllegalArgumentException,
IllegalStateException {
    String userDn;
    // 通過自定義正則表達式轉(zhuǎn)換用戶principal名,默認(rèn)是取全部名字作為principal
    // 對應(yīng)配置項ldapRealm.principalRegex
    String matchedPrincipal = matchPrincipal(principal);
    // 獲取ldapRealm.userSearchBase
    String userSearchBase = getUserSearchBase();
    // 獲取ldapRealm.userSearchAttributeName
    String userSearchAttributeName = getUserSearchAttributeName();

    // If not searching use the userDnTemplate and return.
    // 如果沒配置userSearchBase或userSearchAttributeName等
    // 使用userDnTemplate補全user DN并返回
    if ((userSearchBase == null || userSearchBase.isEmpty()) || (userSearchAttributeName == null
                                                                 && userSearchFilter == null && !"object".equalsIgnoreCase(userSearchScope))) {
        userDn = expandTemplate(userDnTemplate, matchedPrincipal);
        LOGGER.debug("LDAP UserDN and Principal: {},{}", userDn, principal);
        return userDn;
    }

    // Create the searchBase and searchFilter from config.
    // 獲取用戶的searchBase
    String searchBase = expandTemplate(getUserSearchBase(), matchedPrincipal);
    String searchFilter;
    // userSearchFilter對應(yīng)配置項ldapRealm.userSearchFilter
    if (userSearchFilter == null) {
        if (userSearchAttributeName == null) {
            // 使用指定objectclass作為filter
            // userObjectClass對應(yīng)配置項為ldap.userObjectClass,默認(rèn)為person
            searchFilter = String.format("(objectclass=%1$s)", getUserObjectClass());
        } else {
            // 除了使用objectclass作為filter外,還添加條件必須具有屬性和值:
            // userSearchAttributeName=userSearchAttributeTemplate使用principal替換掉占位符的值。
            // userSearchAttributeTemplate默認(rèn)為{0},對應(yīng)配置項為ldapRealm.userSearchAttributeTemplate
            searchFilter = String.format("(&(objectclass=%1$s)(%2$s=%3$s))", getUserObjectClass(),
                                         userSearchAttributeName, expandTemplate(getUserSearchAttributeTemplate(),
                                                                                 matchedPrincipal));
        }
    } else {
        // 如果配置了自定義userSearchFilter,則使用這個
        searchFilter = expandTemplate(userSearchFilter, matchedPrincipal);
    }
    // 獲取用戶搜索范圍
    SearchControls searchControls = getUserSearchControls();

    // Search for userDn and return.
    LdapContext systemLdapCtx = null;
    NamingEnumeration<SearchResult> searchResultEnum = null;
    try {
        systemLdapCtx = getContextFactory().getSystemLdapContext();
        LOGGER.debug("SearchBase,SearchFilter,UserSearchScope: {},{},{}", searchBase, searchFilter, userSearchScope);
        // 執(zhí)行搜索
        searchResultEnum = systemLdapCtx.search(searchBase, searchFilter, searchControls);
        // SearchResults contains all the entries in search scope
        if (searchResultEnum.hasMore()) {
            SearchResult searchResult = searchResultEnum.next();
            // 獲取DN作為userDn返回
            userDn = searchResult.getNameInNamespace();
            LOGGER.debug("UserDN Returned,Principal: {},{}", userDn, principal);
            return userDn;
        } else {
            throw new IllegalArgumentException("Illegal principal name: " + principal);
        }
    } catch (AuthenticationException ne) {
        LOGGER.error("AuthenticationException in getUserDn", ne);
        throw new IllegalArgumentException("Illegal principal name: " + principal);
    } catch (NamingException ne) {
        throw new IllegalArgumentException("Hit NamingException: " + ne.getMessage());
    } finally {
        try {
            if (searchResultEnum != null) {
                searchResultEnum.close();
            }
        } catch (NamingException ne) {
            // Ignore exception on close.
        } finally {
            LdapUtils.closeContext(systemLdapCtx);
        }
    }
}

addRoleIfMember

addRoleIfMember方法判斷查找出的組是否包含userSearchDn。如果是的話,找出對應(yīng)的zeppelin角色。代碼如下:

private void addRoleIfMember(final String userDn, final SearchResult group,
                             final Set<String> roleNames, final Set<String> groupNames,
                             final LdapContextFactory ldapContextFactory) throws NamingException {
    NamingEnumeration<? extends Attribute> attributeEnum = null;
    NamingEnumeration<?> ne = null;
    try {
        // 封裝userSearchDn為LdapName對象
        LdapName userLdapDn = new LdapName(userDn);
        // 根據(jù)組的cn屬性名,獲取groupName
        Attribute attribute = group.getAttributes().get(getGroupIdAttribute());
        String groupName = attribute.get().toString();

        // 遍歷組的所有屬性
        attributeEnum = group.getAttributes().getAll();
        while (attributeEnum.hasMore()) {
            final Attribute attr = attributeEnum.next();
            // 只處理和memberAttribute名字相同的屬性
            if (!memberAttribute.equalsIgnoreCase(attr.getID())) {
                continue;
            }
            // memberAttribute鍵值對可能有多個,遍歷他們
            ne = attr.getAll();
            while (ne.hasMore()) {
                String attrValue = ne.next().toString();
                // 如果memberAttribute配置的是memberUrl
                if (memberAttribute.equalsIgnoreCase(MEMBER_URL)) {
                    // 根據(jù)memberUrl,檢查用戶是否屬于動態(tài)組,邏輯暫不分析
                    boolean dynamicGroupMember = isUserMemberOfDynamicGroup(userLdapDn, attrValue,
                                                                            ldapContextFactory);
                    if (dynamicGroupMember) {
                        groupNames.add(groupName);
                        String roleName = roleNameFor(groupName);
                        if (roleName != null) {
                            roleNames.add(roleName);
                        } else {
                            roleNames.add(groupName);
                        }
                    }
                } else {
                    // posix groups' members don' include the entire dn
                    // 如果groupObjectClass配置的是posixGroup
                    // posixGroup的member屬性不配置user的DN,通常為user的uid
                    // 這里需要把它轉(zhuǎn)化為user DN
                    if (groupObjectClass.equalsIgnoreCase(POSIX_GROUP)) {
                        attrValue = memberDn(attrValue);
                    }
                    // 如果memberAttribute屬性讀取到的user DN和方法傳入的userDn相同,說明這個組包含該user
                    // 獲取對應(yīng)的zeppelin角色名之后加入到roleNames集合中
                    if (userLdapDn.equals(new LdapName(attrValue))) {
                        groupNames.add(groupName);
                        String roleName = roleNameFor(groupName);
                        if (roleName != null) {
                            roleNames.add(roleName);
                        } else {
                            roleNames.add(groupName);
                        }
                        break;
                    }
                }
            }
        }
    } finally {
        try {
            if (attributeEnum != null) {
                attributeEnum.close();
            }
        } finally {
            if (ne != null) {
                ne.close();
            }
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容