From d6a7cea1fe9555fc29f53a228ac1fbc320139ca3 Mon Sep 17 00:00:00 2001 From: gss2002 Date: Tue, 8 Nov 2016 13:14:46 -0500 Subject: [PATCH] ZEPPELIN-1472 - LdapRealm Additions based on Knox LdapRealm and support of using roles with LdapRealms. Also adjusted to use className and not actual name of the realm in shiro.ini. As using realmName in code could cause problems for people who want to use alternate names. Also migrated the LdapGroupRealm and ActiveDirectoryRealm to org.apache.zeppelin.realm packages per a recommendation. --- .../ActiveDirectoryGroupRealm.java | 2 +- .../{server => realm}/LdapGroupRealm.java | 2 +- .../org/apache/zeppelin/realm/LdapRealm.java | 842 ++++++++++++++++++ .../org/apache/zeppelin/rest/GetUserList.java | 71 +- .../apache/zeppelin/rest/SecurityRestApi.java | 19 +- .../apache/zeppelin/utils/SecurityUtils.java | 13 +- 6 files changed, 937 insertions(+), 12 deletions(-) rename zeppelin-server/src/main/java/org/apache/zeppelin/{server => realm}/ActiveDirectoryGroupRealm.java (99%) rename zeppelin-server/src/main/java/org/apache/zeppelin/{server => realm}/LdapGroupRealm.java (98%) create mode 100644 zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/server/ActiveDirectoryGroupRealm.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ActiveDirectoryGroupRealm.java similarity index 99% rename from zeppelin-server/src/main/java/org/apache/zeppelin/server/ActiveDirectoryGroupRealm.java rename to zeppelin-server/src/main/java/org/apache/zeppelin/realm/ActiveDirectoryGroupRealm.java index 0c7c56d640e..d89719cdf47 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/server/ActiveDirectoryGroupRealm.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ActiveDirectoryGroupRealm.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.zeppelin.server; +package org.apache.zeppelin.realm; import org.apache.commons.lang.StringUtils; import org.apache.hadoop.conf.Configuration; diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/server/LdapGroupRealm.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapGroupRealm.java similarity index 98% rename from zeppelin-server/src/main/java/org/apache/zeppelin/server/LdapGroupRealm.java rename to zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapGroupRealm.java index e53027c906c..4133ce055e3 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/server/LdapGroupRealm.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapGroupRealm.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.zeppelin.server; +package org.apache.zeppelin.realm; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java new file mode 100644 index 00000000000..34dcaa4c7e6 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java @@ -0,0 +1,842 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.zeppelin.realm; + +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.SimpleAuthenticationInfo; +import org.apache.shiro.authc.credential.HashedCredentialsMatcher; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.authz.SimpleAuthorizationInfo; +import org.apache.shiro.crypto.hash.DefaultHashService; +import org.apache.shiro.crypto.hash.Hash; +import org.apache.shiro.crypto.hash.HashRequest; +import org.apache.shiro.crypto.hash.HashService; +import org.apache.shiro.realm.ldap.JndiLdapRealm; +import org.apache.shiro.realm.ldap.LdapContextFactory; +import org.apache.shiro.realm.ldap.LdapUtils; +import org.apache.shiro.subject.MutablePrincipalCollection; +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.util.StringUtils; +import org.mortbay.log.Log; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.naming.AuthenticationException; +import javax.naming.Context; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.PartialResultException; +import javax.naming.SizeLimitExceededException; +import javax.naming.directory.Attribute; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.Control; +import javax.naming.ldap.LdapContext; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.PagedResultsControl; + + +/** + * Implementation of {@link org.apache.shiro.realm.ldap.JndiLdapRealm} that also + * returns each user's groups. This implementation is heavily based on + * org.apache.isis.security.shiro.IsisLdapRealm. + * + *

This implementation saves looked up ldap groups in Shiro Session to make them + * easy to be looked up outside of this object + * + *

Sample config for shiro.ini: + * + *

[main] + * ldapRealm=org.apache.zeppelin.realm.LdapRealm + * ldapRealm.contextFactory=$ldapGroupContextFactory + * ldapRealm.contextFactory.authenticationMechanism=simple + * ldapRealm.contextFactory.url=ldap://localhost:33389 + * ldapRealm.userDnTemplate=uid={0},ou=people,dc=hadoop,dc=apache,dc=org + * # Ability to set ldap paging Size if needed default is 100 + * ldapRealm.pagingSize = 200 + * ldapRealm.authorizationEnabled=true + * ldapRealm.contextFactory.systemAuthenticationMechanism=simple + * ldapRealm.searchBase=dc=hadoop,dc=apache,dc=org + * ldapRealm.userSearchBase = dc=hadoop,dc=apache,dc=org + * ldapRealm.groupSearchBase = ou=groups,dc=hadoop,dc=apache,dc=org + * ldapRealm.groupObjectClass=groupofnames + * # Allow userSearchAttribute to be customized + * ldapRealm.userSearchAttributeName = sAMAccountName + * ldapRealm.memberAttribute=member + * # force usernames returned from ldap to lowercase useful for AD + * ldapRealm.userLowerCase = true + * # ability set searchScopes subtree (default), one, base + * ldapRealm.userSearchScope = subtree; + * ldapRealm.groupSearchScope = subtree; + * ldapRealm.memberAttributeValueTemplate=cn={0},ou=people,dc=hadoop,dc=apache, + * dc=org + * ldapRealm.contextFactory.systemUsername=uid=guest,ou=people,dc=hadoop,dc= + * apache,dc=org + * ldapRealm.contextFactory.systemPassword=S{ALIAS=ldcSystemPassword} [urls] + * **=authcBasic + * + *

# optional mapping from physical groups to logical application roles + * ldapRealm.rolesByGroup = \ LDN_USERS: user_role,\ NYK_USERS: user_role,\ + * HKG_USERS: user_role,\ GLOBAL_ADMIN: admin_role,\ DEMOS: self-install_role + * + *

ldapRealm.permissionsByRole=\ user_role = *:ToDoItemsJdo:*:*,\ + * *:ToDoItem:*:*; \ self-install_role = *:ToDoItemsFixturesService:install:* ; + * \ admin_role = * + * + *

securityManager.realms = $ldapRealm + * + */ +public class LdapRealm extends JndiLdapRealm { + + private static final SearchControls SUBTREE_SCOPE = new SearchControls(); + private static final SearchControls ONELEVEL_SCOPE = new SearchControls(); + private static final SearchControls OBJECT_SCOPE = new SearchControls(); + private static final String SUBJECT_USER_ROLES = "subject.userRoles"; + private static final String SUBJECT_USER_GROUPS = "subject.userGroups"; + private static final String MEMBER_URL = "memberUrl"; + private static final String POSIX_GROUP = "posixGroup"; + + private static Pattern TEMPLATE_PATTERN = Pattern.compile("\\{(\\d+?)\\}"); + private static String DEFAULT_PRINCIPAL_REGEX = "(.*)"; + private static final String MEMBER_SUBSTITUTION_TOKEN = "{0}"; + private static final String HASHING_ALGORITHM = "SHA-1"; + private static final Logger log = LoggerFactory.getLogger(LdapRealm.class); + + + static { + SUBTREE_SCOPE.setSearchScope(SearchControls.SUBTREE_SCOPE); + ONELEVEL_SCOPE.setSearchScope(SearchControls.ONELEVEL_SCOPE); + OBJECT_SCOPE.setSearchScope(SearchControls.OBJECT_SCOPE); + } + + private String searchBase; + private String userSearchBase; + private int pagingSize = 100; + private boolean userLowerCase; + private String principalRegex = DEFAULT_PRINCIPAL_REGEX; + private Pattern principalPattern = Pattern.compile(DEFAULT_PRINCIPAL_REGEX); + private String userDnTemplate = "{0}"; + private String userSearchFilter = null; + private String userSearchAttributeTemplate = "{0}"; + private String userSearchScope = "subtree"; + private String groupSearchScope = "subtree"; + + + private String groupSearchBase; + + private String groupObjectClass = "groupOfNames"; + + // typical value: member, uniqueMember, meberUrl + private String memberAttribute = "member"; + + private String groupIdAttribute = "cn"; + + private String memberAttributeValuePrefix = "uid={0}"; + private String memberAttributeValueSuffix = ""; + + private final Map rolesByGroup = new LinkedHashMap(); + private final Map> permissionsByRole = + new LinkedHashMap>(); + + private boolean authorizationEnabled; + + private String userSearchAttributeName; + private String userObjectClass = "person"; + + private HashService hashService = new DefaultHashService(); + + public LdapRealm() { + HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(HASHING_ALGORITHM); + setCredentialsMatcher(credentialsMatcher); + } + + @Override + + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) + throws org.apache.shiro.authc.AuthenticationException { + try { + return super.doGetAuthenticationInfo(token); + } catch (org.apache.shiro.authc.AuthenticationException ae) { + throw ae; + } + } + + /** + * Get groups from LDAP. + * + * @param principals + * the principals of the Subject whose AuthenticationInfo should + * be queried from the LDAP server. + * @param ldapContextFactory + * factory used to retrieve LDAP connections. + * @return an {@link AuthorizationInfo} instance containing information + * retrieved from the LDAP server. + * @throws NamingException + * if any LDAP errors occur during the search. + */ + @Override + protected AuthorizationInfo queryForAuthorizationInfo(final PrincipalCollection principals, + final LdapContextFactory ldapContextFactory) throws NamingException { + if (!isAuthorizationEnabled()) { + return null; + } + final Set roleNames = getRoles(principals, ldapContextFactory); + if (log.isDebugEnabled()) { + log.debug("RolesNames Authorization: " + roleNames); + } + SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(roleNames); + Set stringPermissions = permsFor(roleNames); + simpleAuthorizationInfo.setStringPermissions(stringPermissions); + return simpleAuthorizationInfo; + } + + private Set getRoles(PrincipalCollection principals, + final LdapContextFactory ldapContextFactory) + throws NamingException { + final String username = (String) getAvailablePrincipal(principals); + + LdapContext systemLdapCtx = null; + try { + systemLdapCtx = ldapContextFactory.getSystemLdapContext(); + return rolesFor(principals, username, systemLdapCtx, ldapContextFactory); + } catch (AuthenticationException ae) { + ae.printStackTrace(); + return Collections.emptySet(); + } finally { + LdapUtils.closeContext(systemLdapCtx); + } + } + + private Set rolesFor(PrincipalCollection principals, + String userNameIn, final LdapContext ldapCtx, + final LdapContextFactory ldapContextFactory) throws NamingException { + final Set roleNames = new HashSet<>(); + final Set groupNames = new HashSet<>(); + final String userName; + if (getUserLowerCase()) { + log.debug("userLowerCase true"); + userName = userNameIn.toLowerCase(); + } else { + userName = userNameIn; + } + + String userDn; + if (userSearchAttributeName == null || userSearchAttributeName.isEmpty()) { + // memberAttributeValuePrefix and memberAttributeValueSuffix + // were computed from memberAttributeValueTemplate + userDn = memberAttributeValuePrefix + userName + memberAttributeValueSuffix; + } else { + userDn = getUserDn(userName); + } + + // Activate paged results + int pageSize = getPagingSize(); + if (log.isDebugEnabled()) { + log.debug("Ldap PagingSize: " + pageSize); + } + int numResults = 0; + byte[] cookie = null; + try { + ldapCtx.addToEnvironment(Context.REFERRAL, "ignore"); + + ldapCtx.setRequestControls(new Control[]{new PagedResultsControl(pageSize, + Control.NONCRITICAL)}); + + do { + // 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; + SearchControls searchControls = getGroupSearchControls(); + try { + searchResultEnum = ldapCtx.search( + getGroupSearchBase(), + "objectClass=" + groupObjectClass, + searchControls); + + while (searchResultEnum != null && searchResultEnum.hasMore()) { + // searchResults contains all the groups in search scope + numResults++; + final SearchResult group = searchResultEnum.next(); + addRoleIfMember(userDn, group, roleNames, groupNames, ldapContextFactory); + } + } catch (PartialResultException e) { + log.debug("Ignoring PartitalResultException"); + } finally { + if (searchResultEnum != null) { + searchResultEnum.close(); + } + } + // Re-activate paged results + ldapCtx.setRequestControls(new Control[]{new PagedResultsControl(pageSize, + cookie, Control.CRITICAL)}); + } while (cookie != null); + } catch (SizeLimitExceededException e) { + log.info("Only retrieved first " + numResults + + " groups due to SizeLimitExceededException."); + } catch (IOException e) { + log.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 + SecurityUtils.getSubject().getSession().setAttribute(SUBJECT_USER_ROLES, roleNames); + SecurityUtils.getSubject().getSession().setAttribute(SUBJECT_USER_GROUPS, groupNames); + if (!groupNames.isEmpty() && (principals instanceof MutablePrincipalCollection)) { + ((MutablePrincipalCollection) principals).addAll(groupNames, getName()); + } + if (log.isDebugEnabled()) { + log.debug("User RoleNames: " + userName + "::" + roleNames); + } + return roleNames; + } + + 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 { + LdapName userLdapDn = new LdapName(userDn); + Attribute attribute = group.getAttributes().get(getGroupIdAttribute()); + String groupName = attribute.get().toString(); + + attributeEnum = group.getAttributes().getAll(); + while (attributeEnum.hasMore()) { + final Attribute attr = attributeEnum.next(); + if (!memberAttribute.equalsIgnoreCase(attr.getID())) { + continue; + } + ne = attr.getAll(); + while (ne.hasMore()) { + String attrValue = ne.next().toString(); + if (memberAttribute.equalsIgnoreCase(MEMBER_URL)) { + 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 { + if (groupObjectClass.equalsIgnoreCase(POSIX_GROUP)) { + attrValue = memberAttributeValuePrefix + attrValue + memberAttributeValueSuffix; + } + 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(); + } + } + } + } + + public Map getListRoles() { + Map groupToRoles = getRolesByGroup(); + Map roles = new HashMap<>(); + for (Map.Entry entry : groupToRoles.entrySet()){ + roles.put(entry.getValue(), entry.getKey()); + } + return roles; + } + + private String roleNameFor(String groupName) { + return !rolesByGroup.isEmpty() ? rolesByGroup.get(groupName) : groupName; + } + + private Set permsFor(Set roleNames) { + Set perms = new LinkedHashSet(); // preserve order + for (String role : roleNames) { + List permsForRole = permissionsByRole.get(role); + if (log.isDebugEnabled()) { + log.debug("PermsForRole: " + role); + log.debug("PermByRole: " + permsForRole); + } + if (permsForRole != null) { + perms.addAll(permsForRole); + } + } + return perms; + } + + public String getSearchBase() { + return searchBase; + } + + public void setSearchBase(String searchBase) { + this.searchBase = searchBase; + } + + public String getUserSearchBase() { + return (userSearchBase != null && !userSearchBase.isEmpty()) ? userSearchBase : searchBase; + } + + public void setUserSearchBase(String userSearchBase) { + this.userSearchBase = userSearchBase; + } + + public int getPagingSize() { + return pagingSize; + } + + public void setPagingSize(int pagingSize) { + this.pagingSize = pagingSize; + } + + public String getGroupSearchBase() { + return (groupSearchBase != null && !groupSearchBase.isEmpty()) ? groupSearchBase : searchBase; + } + + public void setGroupSearchBase(String groupSearchBase) { + this.groupSearchBase = groupSearchBase; + } + + public String getGroupObjectClass() { + return groupObjectClass; + } + + public void setGroupObjectClass(String groupObjectClassAttribute) { + this.groupObjectClass = groupObjectClassAttribute; + } + + public String getMemberAttribute() { + return memberAttribute; + } + + public void setMemberAttribute(String memberAttribute) { + this.memberAttribute = memberAttribute; + } + + public String getGroupIdAttribute() { + return groupIdAttribute; + } + + public void setGroupIdAttribute(String groupIdAttribute) { + this.groupIdAttribute = groupIdAttribute; + } + + /** + * Set Member Attribute Template for LDAP. + * + * @param template + * DN template to be used to query ldap. + * @throws IllegalArgumentException + * if template is empty or null. + */ + 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; + } + + public void setRolesByGroup(Map rolesByGroup) { + this.rolesByGroup.putAll(rolesByGroup); + } + + public Map getRolesByGroup() { + return rolesByGroup; + } + + public void setPermissionsByRole(String permissionsByRoleStr) { + permissionsByRole.putAll(parsePermissionByRoleString(permissionsByRoleStr)); + } + + public Map> getPermissionsByRole() { + return permissionsByRole; + } + + public boolean isAuthorizationEnabled() { + return authorizationEnabled; + } + + public void setAuthorizationEnabled(boolean authorizationEnabled) { + this.authorizationEnabled = authorizationEnabled; + } + + public String getUserSearchAttributeName() { + return userSearchAttributeName; + } + + /** + * Set User Search Attribute Name for LDAP. + * + * @param userSearchAttributeName + * userAttribute to search ldap. + */ + public void setUserSearchAttributeName(String userSearchAttributeName) { + if (userSearchAttributeName != null) { + userSearchAttributeName = userSearchAttributeName.trim(); + } + this.userSearchAttributeName = userSearchAttributeName; + } + + public String getUserObjectClass() { + return userObjectClass; + } + + public void setUserObjectClass(String userObjectClass) { + this.userObjectClass = userObjectClass; + } + + private Map> parsePermissionByRoleString(String permissionsByRoleStr) { + Map> perms = new HashMap>(); + + // split by semicolon ; then by eq = then by comma , + StringTokenizer stSem = new StringTokenizer(permissionsByRoleStr, ";"); + while (stSem.hasMoreTokens()) { + String roleAndPerm = stSem.nextToken(); + StringTokenizer stEq = new StringTokenizer(roleAndPerm, "="); + if (stEq.countTokens() != 2) { + continue; + } + String role = stEq.nextToken().trim(); + String perm = stEq.nextToken().trim(); + StringTokenizer stCom = new StringTokenizer(perm, ","); + List permList = new ArrayList(); + while (stCom.hasMoreTokens()) { + permList.add(stCom.nextToken().trim()); + } + perms.put(role, permList); + } + return perms; + } + + boolean isUserMemberOfDynamicGroup(LdapName userLdapDn, String memberUrl, + final LdapContextFactory ldapContextFactory) throws NamingException { + + // ldap://host:port/dn?attributes?scope?filter?extensions + + if (memberUrl == null) { + return false; + } + String[] tokens = memberUrl.split("\\?"); + if (tokens.length < 4) { + return false; + } + + String searchBaseString = tokens[0].substring(tokens[0].lastIndexOf("/") + 1); + String searchScope = tokens[2]; + String searchFilter = tokens[3]; + + LdapName searchBaseDn = new LdapName(searchBaseString); + + // do scope test + if (searchScope.equalsIgnoreCase("base")) { + log.debug("DynamicGroup SearchScope base"); + return false; + } + if (!userLdapDn.toString().endsWith(searchBaseDn.toString())) { + return false; + } + if (searchScope.equalsIgnoreCase("one") && (userLdapDn.size() != searchBaseDn.size() - 1)) { + log.debug("DynamicGroup SearchScope one"); + return false; + } + // search for the filter, substituting base with userDn + // search for base_dn=userDn, scope=base, filter=filter + LdapContext systemLdapCtx = null; + systemLdapCtx = ldapContextFactory.getSystemLdapContext(); + boolean member = false; + NamingEnumeration searchResultEnum = null; + try { + searchResultEnum = systemLdapCtx.search(userLdapDn, searchFilter, + searchScope.equalsIgnoreCase("sub") ? SUBTREE_SCOPE : ONELEVEL_SCOPE); + if (searchResultEnum.hasMore()) { + return true; + } + } finally { + try { + if (searchResultEnum != null) { + searchResultEnum.close(); + } + } finally { + LdapUtils.closeContext(systemLdapCtx); + } + } + return member; + } + + public String getPrincipalRegex() { + return principalRegex; + } + + /** + * Set Regex for Principal LDAP. + * + * @param regex + * regex to use to search for principal in shiro. + */ + public void setPrincipalRegex(String regex) { + if (regex == null || regex.trim().isEmpty()) { + principalPattern = Pattern.compile(DEFAULT_PRINCIPAL_REGEX); + principalRegex = DEFAULT_PRINCIPAL_REGEX; + } else { + regex = regex.trim(); + Pattern pattern = Pattern.compile(regex); + principalPattern = pattern; + principalRegex = regex; + } + } + + public String getUserSearchAttributeTemplate() { + return userSearchAttributeTemplate; + } + + public void setUserSearchAttributeTemplate(final String template) { + this.userSearchAttributeTemplate = (template == null ? null : template.trim()); + } + + public String getUserSearchFilter() { + return userSearchFilter; + } + + public void setUserSearchFilter(final String filter) { + this.userSearchFilter = (filter == null ? null : filter.trim()); + } + + public boolean getUserLowerCase() { + return userLowerCase; + } + + public void setUserLowerCase(boolean userLowerCase) { + this.userLowerCase = userLowerCase; + } + + public String getUserSearchScope() { + return userSearchScope; + } + + public void setUserSearchScope(final String scope) { + this.userSearchScope = (scope == null ? null : scope.trim().toLowerCase()); + } + + public String getGroupSearchScope() { + return groupSearchScope; + } + + public void setGroupSearchScope(final String scope) { + this.groupSearchScope = (scope == null ? null : scope.trim().toLowerCase()); + } + + private SearchControls getUserSearchControls() { + SearchControls searchControls = SUBTREE_SCOPE; + if ("onelevel".equalsIgnoreCase(userSearchScope)) { + searchControls = ONELEVEL_SCOPE; + } else if ("object".equalsIgnoreCase(userSearchScope)) { + searchControls = OBJECT_SCOPE; + } + return searchControls; + } + + private SearchControls getGroupSearchControls() { + SearchControls searchControls = SUBTREE_SCOPE; + if ("onelevel".equalsIgnoreCase(groupSearchScope)) { + searchControls = ONELEVEL_SCOPE; + } else if ("object".equalsIgnoreCase(groupSearchScope)) { + searchControls = OBJECT_SCOPE; + } + return searchControls; + } + + @Override + public void setUserDnTemplate(final String template) throws IllegalArgumentException { + userDnTemplate = template; + } + + private Matcher matchPrincipal(final String principal) { + Matcher matchedPrincipal = principalPattern.matcher(principal); + if (!matchedPrincipal.matches()) { + throw new IllegalArgumentException("Principal " + + principal + " does not match " + principalRegex); + } + return matchedPrincipal; + } + + /** + * Returns the LDAP User Distinguished Name (DN) to use when acquiring an + * {@link javax.naming.ldap.LdapContext LdapContext} from the + * {@link LdapContextFactory}. + *

+ * If the the {@link #getUserDnTemplate() userDnTemplate} property has been + * set, this implementation will construct the User DN by substituting the + * specified {@code principal} into the configured template. If the + * {@link #getUserDnTemplate() userDnTemplate} has not been set, the method + * argument will be returned directly (indicating that the submitted + * authentication token principal is the User DN). + * + * @param principal + * the principal to substitute into the configured + * {@link #getUserDnTemplate() userDnTemplate}. + * @return the constructed User DN to use at runtime when acquiring an + * {@link javax.naming.ldap.LdapContext}. + * @throws IllegalArgumentException + * if the method argument is null or empty + * @throws IllegalStateException + * if the {@link #getUserDnTemplate userDnTemplate} has not been + * set. + * @see LdapContextFactory#getLdapContext(Object, Object) + */ + @Override + protected String getUserDn(final String principal) throws IllegalArgumentException, + IllegalStateException { + String userDn; + Matcher matchedPrincipal = matchPrincipal(principal); + String userSearchBase = getUserSearchBase(); + String userSearchAttributeName = getUserSearchAttributeName(); + + // If not searching use the userDnTemplate and return. + if ((userSearchBase == null || userSearchBase.isEmpty()) || (userSearchAttributeName == null + && userSearchFilter == null && !"object".equalsIgnoreCase(userSearchScope))) { + userDn = expandTemplate(userDnTemplate, matchedPrincipal); + if (log.isDebugEnabled()) { + log.debug("LDAP UserDN and Principal: " + userDn + "," + principal); + } + return userDn; + } + + // Create the searchBase and searchFilter from config. + String searchBase = expandTemplate(getUserSearchBase(), matchedPrincipal); + String searchFilter = null; + if (userSearchFilter == null) { + if (userSearchAttributeName == null) { + searchFilter = String.format("(objectclass=%1$s)", getUserObjectClass()); + } else { + searchFilter = String.format("(&(objectclass=%1$s)(%2$s=%3$s))", getUserObjectClass(), + userSearchAttributeName, expandTemplate(getUserSearchAttributeTemplate(), + matchedPrincipal)); + } + } else { + searchFilter = expandTemplate(userSearchFilter, matchedPrincipal); + } + SearchControls searchControls = getUserSearchControls(); + + // Search for userDn and return. + LdapContext systemLdapCtx = null; + NamingEnumeration searchResultEnum = null; + try { + systemLdapCtx = getContextFactory().getSystemLdapContext(); + if (log.isDebugEnabled()) { + log.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(); + userDn = searchResult.getNameInNamespace(); + if (log.isDebugEnabled()) { + log.debug("UserDN Returned,Principal: " + userDn + "," + principal); + } + return userDn; + } else { + throw new IllegalArgumentException("Illegal principal name: " + principal); + } + } catch (AuthenticationException ne) { + ne.printStackTrace(); + 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); + } + } + } + + @Override + protected AuthenticationInfo createAuthenticationInfo(AuthenticationToken token, + Object ldapPrincipal, + Object ldapCredentials, LdapContext ldapContext) throws NamingException { + HashRequest.Builder builder = new HashRequest.Builder(); + Hash credentialsHash = hashService + .computeHash(builder.setSource(token.getCredentials()) + .setAlgorithmName(HASHING_ALGORITHM).build()); + return new SimpleAuthenticationInfo(token.getPrincipal(), + credentialsHash.toHex(), credentialsHash.getSalt(), + getName()); + } + + private static final String expandTemplate(final String template, final Matcher input) { + String output = template; + Matcher matcher = TEMPLATE_PATTERN.matcher(output); + while (matcher.find()) { + String lookupStr = matcher.group(1); + int lookupIndex = Integer.parseInt(lookupStr); + String lookupValue = input.group(lookupIndex); + output = matcher.replaceFirst(lookupValue == null ? "" : lookupValue); + matcher = TEMPLATE_PATTERN.matcher(output); + } + return output; + } +} diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/GetUserList.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/GetUserList.java index f1a895c8bcf..f0e37404a19 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/GetUserList.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/GetUserList.java @@ -24,7 +24,8 @@ import org.apache.shiro.realm.ldap.JndiLdapRealm; import org.apache.shiro.realm.text.IniRealm; import org.apache.shiro.util.JdbcUtils; -import org.apache.zeppelin.server.ActiveDirectoryGroupRealm; +import org.apache.zeppelin.realm.ActiveDirectoryGroupRealm; +import org.apache.zeppelin.realm.LdapRealm; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -114,8 +115,76 @@ public List getUserList(JndiLdapRealm r, String searchText) { } catch (Exception e) { LOG.error("Error retrieving User list from Ldap Realm", e); } + LOG.info("UserList: " + userList); return userList; } + + /** + * function to extract users from Zeppelin LdapRealm + */ + public List getUserList(LdapRealm r, String searchText) { + List userList = new ArrayList<>(); + if (LOG.isDebugEnabled()) { + LOG.debug("SearchText: " + searchText); + } + String userAttribute = r.getUserSearchAttributeName(); + String userSearchRealm = r.getUserSearchBase(); + String userObjectClass = r.getUserObjectClass(); + JndiLdapContextFactory CF = (JndiLdapContextFactory) r.getContextFactory(); + try { + LdapContext ctx = CF.getSystemLdapContext(); + SearchControls constraints = new SearchControls(); + constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); + String[] attrIDs = {userAttribute}; + constraints.setReturningAttributes(attrIDs); + NamingEnumeration result = ctx.search(userSearchRealm, "(&(objectclass=" + + userObjectClass + ")(" + + userAttribute + "=" + searchText + "))", constraints); + while (result.hasMore()) { + Attributes attrs = ((SearchResult) result.next()).getAttributes(); + if (attrs.get(userAttribute) != null) { + String currentUser; + if (r.getUserLowerCase()) { + LOG.debug("userLowerCase true"); + currentUser = ((String) attrs.get(userAttribute).get()).toLowerCase(); + } else { + LOG.debug("userLowerCase false"); + currentUser = (String) attrs.get(userAttribute).get(); + } + if (LOG.isDebugEnabled()) { + LOG.debug("CurrentUser: " + currentUser); + } + userList.add(currentUser.trim()); + } + } + } catch (Exception e) { + LOG.error("Error retrieving User list from Ldap Realm", e); + } + return userList; + } + + /*** + * Get user roles from shiro.ini for Zeppelin LdapRealm + * @param r + * @return + */ + public List getRolesList(LdapRealm r) { + List roleList = new ArrayList<>(); + Map roles = r.getListRoles(); + if (roles != null) { + Iterator it = roles.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry pair = (Map.Entry) it.next(); + if (LOG.isDebugEnabled()) { + LOG.debug("RoleKeyValue: " + pair.getKey() + + " = " + pair.getValue()); + } + roleList.add((String) pair.getKey()); + } + } + return roleList; + } + public List getUserList(ActiveDirectoryGroupRealm r, String searchText) { List userList = new ArrayList<>(); diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java index 7af52c8c8ba..742af9e5241 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java @@ -25,7 +25,8 @@ import org.apache.shiro.realm.text.IniRealm; import org.apache.zeppelin.annotation.ZeppelinApi; import org.apache.zeppelin.conf.ZeppelinConfiguration; -import org.apache.zeppelin.server.ActiveDirectoryGroupRealm; +import org.apache.zeppelin.realm.ActiveDirectoryGroupRealm; +import org.apache.zeppelin.realm.LdapRealm; import org.apache.zeppelin.server.JsonResponse; import org.apache.zeppelin.ticket.TicketContainer; import org.apache.zeppelin.utils.SecurityUtils; @@ -105,16 +106,22 @@ public Response getUserList(@PathParam("searchText") final String searchText) { if (realmsList != null) { for (Iterator iterator = realmsList.iterator(); iterator.hasNext(); ) { Realm realm = iterator.next(); - String name = realm.getName(); - if (name.equals("iniRealm")) { + String name = realm.getClass().getName(); + if (LOG.isDebugEnabled()) { + LOG.debug("RealmClass.getName: " + name); + } + if (name.equals("org.apache.shiro.realm.text.IniRealm")) { usersList.addAll(getUserListObj.getUserList((IniRealm) realm)); rolesList.addAll(getUserListObj.getRolesList((IniRealm) realm)); - } else if (name.equals("ldapRealm")) { + } else if (name.equals("org.apache.zeppelin.realm.LdapGroupRealm")) { usersList.addAll(getUserListObj.getUserList((JndiLdapRealm) realm, searchText)); - } else if (name.equals("activeDirectoryRealm")) { + } else if (name.equals("org.apache.zeppelin.realm.LdapRealm")) { + usersList.addAll(getUserListObj.getUserList((LdapRealm) realm, searchText)); + rolesList.addAll(getUserListObj.getRolesList((LdapRealm) realm)); + } else if (name.equals("org.apache.zeppelin.realm.ActiveDirectoryGroupRealm")) { usersList.addAll(getUserListObj.getUserList((ActiveDirectoryGroupRealm) realm, searchText)); - } else if (name.equals("jdbcRealm")) { + } else if (name.equals("org.apache.shiro.realm.jdbc.JdbcRealm")) { usersList.addAll(getUserListObj.getUserList((JdbcRealm) realm)); } } diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java b/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java index 186a324052f..6385a630e0f 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java @@ -34,6 +34,10 @@ import org.apache.shiro.util.ThreadContext; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.realm.LdapRealm; +import org.mortbay.log.Log; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.common.collect.Sets; @@ -45,6 +49,7 @@ public class SecurityUtils { private static final String ANONYMOUS = "anonymous"; private static final HashSet EMPTY_HASHSET = Sets.newHashSet(); private static boolean isEnabled = false; + private static final Logger log = LoggerFactory.getLogger(SecurityUtils.class); public static void initSecurityManager(String shiroPath) { IniSecurityManagerFactory factory = new IniSecurityManagerFactory("file:" + shiroPath); @@ -119,13 +124,15 @@ public static HashSet getRoles() { Collection realmsList = SecurityUtils.getRealmsList(); for (Iterator iterator = realmsList.iterator(); iterator.hasNext(); ) { Realm realm = iterator.next(); - String name = realm.getName(); - if (name.equals("iniRealm")) { + String name = realm.getClass().getName(); + if (name.equals("org.apache.shiro.realm.text.IniRealm")) { allRoles = ((IniRealm) realm).getIni().get("roles"); break; + } else if (name.equals("org.apache.zeppelin.realm.LdapRealm")) { + allRoles = ((LdapRealm) realm).getListRoles(); + break; } } - if (allRoles != null) { Iterator it = allRoles.entrySet().iterator(); while (it.hasNext()) {