Zeppelin 集成 LDAP(FreeIPA)
前言
本篇主要介绍Zeppelin集成LDAP认证的方法。
LDAP服务配置我们采用FreeIPA。FreeIPA是一个集成安全信息管理解决方案,包含Linux用户系统、LDAP、Kerberos、Dogtag等认证系统。FreeIPA将这些认证系统的用户信息统一。FreeIPA还提供了web界面和命令行的操作方式。比直接配置OpenLDAP服务方便了许多。
环境信息如下:
- 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一致,否则后面执行
ipa-server-install
的时候会出现错误。
- 安装
ipa-server
yum install ipa-server
- 配置
ipa-server
。在shell执行:
ipa-server-install
ipa-server-install
是一个向导式安装配置工具。需要回答如下问题:
# 是否需要集成的DNS
Do you want to configure integrated DNS (BIND)? [no]: no
# 输入hostname
Server host name [test.paultech.com]:
# 确认domain name
Please confirm the domain name [paultech.com]:
# 确认Kerberos的realm name
Please provide a realm name [PAULTECH.COM]:
# 设置LDAP管理员用户的密码
Directory Manager password:
Password (confirm):
# 设置IPA admin账户(管理员)的密码
IPA admin password:
Password (confirm):
注意: 正常来说这里应该不会有错误。然而实际安装环境有差异,可能会遇到各种各样的问题。具体错误和解决版本参见:安装FreeIPA以及应用时报错汇总 - 尹正杰 - 博客园 (cnblogs.com)。本文作者在安装的时候遇到如下两个问题:
-
Command '/bin/systemctl start certmonger.service' returned non-zero exit status 1
解决这个问题需要执行:
systemctl restart dbus.socket systemctl restart dbus.service
-
ipa server install ended with "CA did not start in 300s"
执行:
yum install -y ipa-server-dns
还有需要注意的是,如果配置步骤中遇到错误,每次解决后都需要执行如下命令卸载配置:
ipa-server-install --uninstall
等待卸载掉原有的安装配置之后,重新执行
ipa-server-install
。
- 验证安装。如果上面的步骤顺利执行完毕,可执行下方命令验证安装。
kinit admin
# 然后输入安装时候配置的admin密码
# 查看Kerberos是否认证成功
klist
# 查看所有用户信息
ipa user-find --all
如果能够打出user信息,说明安装成功。
- 登录ipa-server web页面。访问:
https://test.paultech.com/ipa/ui
。需要提前在访问端机器配置hosts。填写之前配置的IPA admin用户和密码,成功进入IPA管理页面。
注意:如果admin登录用户密码错误次数太多,admin用户会被锁定。web页面,kinit
和ipa
命令均无法操作。在/var/log/httpd/error_log
中会发现如下异常:
freeipa DatabaseError: Server is unwilling to perform: Too many failed logins
解决方法是解锁admin用户:
ipa user-unlock admin
FreeIPA命令操作
使用ipactl命令
ipactl
命令控制IPA服务启动,停止操作:
ipactl start
ipactl stop
ipactl restart
ipactl status
使用ipa命令
ipa
命令调用之前必须Kerberos认证为管理员,命令如下:
kinit admin
# 输入管理员密码
使用ipa
命令查询用户和组的详细信息。后面配置Zeppelin的时候需要用到。
ipa user-find --all
ipa group-find --all
基本上FreeIPA web页面的操作都能够通过ipa命令的方式实现。其他使用方式到具体用到的时候再补充。
Zeppelin 使用LDAP
Zeppelin的认证配置依赖Shiro。认证配置文件位于${ZEPPELIN_HOME/conf/shiro.ini
.
LdapGroupRealm配置
LdapGroupRealm
是一种简化方式的LDAP用户和角色绑定配置方式。它读取LDAP目录searchBase下所有objectClass为groupOfNames
,并且member属性包含user DN的条目的cn属性值,就是这个用户绑定的角色名。具体获取用户绑定角色的方式参见附录LdapGroupRealm读取用户对应role的原理
。
我们编辑${ZEPPELIN_HOME/conf/shiro.ini
,配置LDAP相关内容:
### 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服务器的访问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: 认证机制,这里使用简单认证。
- ldapRealm.contextFactory.systemUsername/systemPassword: LDAP服务的管理员账户和密码。
接下来配置用户角色和权限,找到配置文件中[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管理员的权限。
注意,[urls]
部分API权限表达式的roles可以配置多个角色,例如roles[admin, zeppelinadmin]
表示用户必须同事具有admin
和zeppelinadmin
角色才有权限。如果想要实现“用户具有如下角色之一”就有权限这种配置呢?可以按照如下方式配置:
[main]
anyofrolesuser = org.apache.zeppelin.utils.AnyOfRolesUserAuthorizationFilter
[urls]
/api/interpreter/** = authc, anyofrolesuser[admin, user1]
/api/configurations/** = authc, roles[admin]
/api/credential/** = authc, roles[admin]
到此为止我们已经完成了Zeppelin LDAP的集成和角色权限的对应关系配置。那么用户和角色的对应关系在哪里配置?我们继续下一节,绑定用户和角色。
FreeIPA 绑定用户和角色
创建用户
依次点击IPA web页面中的身份
,用户
,活跃用户
,然后点击右侧表格上方的添加
。设置登录名,姓名和密码之后点击添加,用户创建完毕。
创建角色并绑定角色到用户
依次点击IPA服务器
-> Role-Based Access Control
,点击表格右侧上方的添加
。新建一个名字为zeppelinadmin
的角色。然后打开这个角色,在用户标签中,点击添加
,选择上一步创建好的用户。到这里用户已经成功绑定到zeppelinadmin
角色。
Zeppelin登陆LDAP用户
重启Zeppelin服务后,在web页面使用上面步骤创建用户的登录名和密码登录。
登录成功后可以看到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成功获取到用户对应的角色。
LdapRealm配置(可选)
前面的LdapGroupRealm
为我们预定义了用户和角色的对应管理查找逻辑。如果我们的LDAP不是这么存储对应关系的,也就是说需要支持自定义的查找逻辑,这该怎么办?
Zeppelin提供了更为灵活的LdapRealm
配置方式,但是配置项也更为复杂。
接下来是一个例子。我们的组为:
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
# 使用简单认证
ldapRealm.contextFactory.authenticationMechanism = simple
# 配置LDAP服务器访问URL
ldapRealm.contextFactory.url = ldap://10.180.210.127:389
# 配置user DN的模板
ldapRealm.userDnTemplate = uid={0},ou=People,dc=paultech,dc=com
# 分页大小,默认为100
ldapRealm.pagingSize = 200
# 启用认证
ldapRealm.authorizationEnabled = true
# 指定searchBase,通常为LDAP目录根节点
ldapRealm.searchBase = dc=paultech,dc=com
# 查找用户条目的根节点,所有用户必须在该节点下存储
ldapRealm.userSearchBase = ou=People,dc=paultech,dc=com
# 查找组(角色)条目的根节点,所有用户组信息必须在改条目下存储
ldapRealm.groupSearchBase = ou=roles,dc=paultech,dc=com
# 所有组条目的objectClass属性值。默认为groupOfNames。常用的也有posixGroup
ldapRealm.groupObjectClass = groupOfNames
# 和前面配置二选一,也可以指定查找关联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(默认),one, base。一般用subtree,查找对应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角色的对应关系
# 例如下面的配置,如果根据前面查找规则找到某个用户对应的组名为zeppelinadmin,那么它对应Zeppelin内部的角色名为admin
# zeppelin角色的权限和访问控制在[roles]和[urls]部分配置
ldapRealm.rolesByGroup = zeppelinadmin: admin
更为详细的LdapRealm获取用户匹配组的方式,请见附录LdapRealm读取用户对应role的原理
。
附录
LdapGroupRealm读取用户对应role的原理
上面章节我们使用FreeIPA帮忙绑定LDAP用户和角色,Zeppelin可以识别成功。那么问题来了,Zeppelin是如何查找用户对应的角色的?如果不使用FreeIPA,只用手工方式配置LDAP,我们怎么把用户和对应的角色绑定在一起?接下来我们一起揭晓这个谜题。
我们从源代码入手,分析LdapGroupRealm
根据登录用户名获取所属角色的核心逻辑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};
// 查找符合条件的条目
// 相当于执行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))'
NamingEnumeration<?> answer = ldapContext.search(
String.valueOf(ldapContext.getEnvironment().get("ldap.searchBase")),
searchFilter,
searchArguments,
searchCtls);
// 遍历搜索结果
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的属性,它的属性值就是用户对应的角色,保存起来
if (attr.getID().equals("cn")) {
roleNames.add((String) attr.get());
}
}
}
}
return roleNames;
} catch (Exception e) {
LOGGER.error("Error", e);
}
return new HashSet<>();
}
通过上面分析我们发现,比如用户名为paul
,searchBase为dc=paultech,dc=com
,userDnTemplate为uid={0},cn=users,cn=accounts,dc=paultech,dc=com
,Zeppelin查找用户组相当如执行如下命令:
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属性值为用户对应的role。我们可以查看下LDAP目录其中的内容,验证下FreeIPA创建的角色是不是和这个逻辑相匹配。分析到这里,相信大家即便不用FreeIPA,也能够配置用户和角色的对应关系了。
LdapRealm读取用户对应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;
// 对应配置ldapRealm.userLowerCase
// 如果配置了true,将用户名转换为小写
if (getUserLowerCase()) {
LOGGER.debug("userLowerCase true");
userName = userNameIn.toLowerCase();
} else {
userName = userNameIn;
}
// 从用户名获取需要搜索用户DN,这个方法很重要,流程也较长,放在后面分析
String userDn = getUserDnForSearch(userName);
// Activate paged results
// 对应配置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;
// 对应配置ldapRealm.groupSearchScope
SearchControls searchControls = getGroupSearchControls();
try {
// 对应配置ldapRealm.groupSearchEnableMatchingRuleInChain
if (groupSearchEnableMatchingRuleInChain) {
// groupObjectClass对应配置ldapRealm.groupObjectClass
// memberAttribute对应配置ldapRealm.memberAttribute
// 搜索filter相当于
// (&(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);
// 遍历结果
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();
// 查找组名对应的zeppelin角色名
// 对应配置ldapRealm.rolesByGroup
String roleName = roleNameFor(groupName);
// 如果没找到对应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中包含这个用户,则将这个组对应的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) {
// 对应配置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
方法。该方法对应的配置项为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}
。它前面的字符串设置为memberAttributeValuePrefix
,后面的设置为memberAttributeValueSuffix
。到这里memberDn
相关逻辑就分析完了。
我们继续分析getUserDn
方法:
protected String getUserDn(final String principal) throws IllegalArgumentException,
IllegalStateException {
String userDn;
// 通过自定义正则表达式转换用户principal名,默认是取全部名字作为principal
// 对应配置项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对应配置项ldapRealm.userSearchFilter
if (userSearchFilter == null) {
if (userSearchAttributeName == null) {
// 使用指定objectclass作为filter
// userObjectClass对应配置项为ldap.userObjectClass,默认为person
searchFilter = String.format("(objectclass=%1$s)", getUserObjectClass());
} else {
// 除了使用objectclass作为filter外,还添加条件必须具有属性和值:
// userSearchAttributeName=userSearchAttributeTemplate使用principal替换掉占位符的值。
// userSearchAttributeTemplate默认为{0},对应配置项为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);
// 执行搜索
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
。如果是的话,找出对应的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);
// 根据组的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)) {
// 根据memberUrl,检查用户是否属于动态组,逻辑暂不分析
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
// 这里需要把它转化为user DN
if (groupObjectClass.equalsIgnoreCase(POSIX_GROUP)) {
attrValue = memberDn(attrValue);
}
// 如果memberAttribute属性读取到的user DN和方法传入的userDn相同,说明这个组包含该user
// 获取对应的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();
}
}
}
}