美文网首页玩转大数据Java
Zeppelin 集成 LDAP(FreeIPA)

Zeppelin 集成 LDAP(FreeIPA)

作者: AlienPaul | 来源:发表于2022-08-09 10:49 被阅读0次

    前言

    本篇主要介绍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<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();
                }
            }
        }
    }
    

    相关文章

      网友评论

        本文标题:Zeppelin 集成 LDAP(FreeIPA)

        本文链接:https://www.haomeiwen.com/subject/gnqnwrtx.html