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()) {