前言
本篇主要介紹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安裝
- 配置本機hostname和hosts:
hostnamectl set-hostname test.paultech.com
然后配置hosts文件:
{ip} test.paultech.com
注意:hostname必須和本機host一致,否則后面執(zhí)行
ipa-server-install的時候會出現(xiàn)錯誤。
- 安裝
ipa-server
yum install ipa-server
- 配置
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。
- 驗證安裝。如果上面的步驟順利執(zhí)行完畢,可執(zhí)行下方命令驗證安裝。
kinit admin
# 然后輸入安裝時候配置的admin密碼
# 查看Kerberos是否認(rèn)證成功
klist
# 查看所有用戶信息
ipa user-find --all
如果能夠打出user信息,說明安裝成功。
- 登錄ipa-server web頁面。訪問:
https://test.paultech.com/ipa/ui。需要提前在訪問端機器配置hosts。填寫之前配置的IPA admin用戶和密碼,成功進入IPA管理頁面。
注意:如果admin登錄用戶密碼錯誤次數(shù)太多,admin用戶會被鎖定。web頁面,kinit和ipa命令均無法操作。在/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]表示用戶必須同事具有admin和zeppelinadmin角色才有權(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();
}
}
}
}