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安装

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

然后配置hosts文件:

{ip} test.paultech.com

注意:hostname必须和本机host一致,否则后面执行ipa-server-install的时候会出现错误。

  1. 安装ipa-server
yum install ipa-server
  1. 配置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

  1. 验证安装。如果上面的步骤顺利执行完毕,可执行下方命令验证安装。
kinit admin
# 然后输入安装时候配置的admin密码

# 查看Kerberos是否认证成功
klist

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

如果能够打出user信息,说明安装成功。

  1. 登录ipa-server web页面。访问:https://test.paultech.com/ipa/ui。需要提前在访问端机器配置hosts。填写之前配置的IPA admin用户和密码,成功进入IPA管理页面。

注意:如果admin登录用户密码错误次数太多,admin用户会被锁定。web页面,kinitipa命令均无法操作。在/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]表示用户必须同事具有adminzeppelinadmin角色才有权限。如果想要实现“用户具有如下角色之一”就有权限这种配置呢?可以按照如下方式配置:

[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 getRoleNamesForUser(String username, LdapContext ldapContext,
                                       String userDnTemplate) {
    try {
        Set 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 rolesFor(PrincipalCollection principals, String userNameIn,
                               final LdapContext ldapCtx, final LdapContextFactory ldapContextFactory, Session session)
    throws NamingException {
    final Set roleNames = new HashSet<>();
    final Set 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 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 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 roleNames, final Set groupNames,
                             final LdapContextFactory ldapContextFactory) throws NamingException {
    NamingEnumeration 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();
            }
        }
    }
}

你可能感兴趣的:(Zeppelin 集成 LDAP(FreeIPA))