Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id D2FB7200BC5 for ; Tue, 22 Nov 2016 21:57:52 +0100 (CET) Received: by cust-asf.ponee.io (Postfix) id D1897160B0C; Tue, 22 Nov 2016 20:57:52 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id 670CB160AF1 for ; Tue, 22 Nov 2016 21:57:51 +0100 (CET) Received: (qmail 73504 invoked by uid 500); 22 Nov 2016 20:57:50 -0000 Mailing-List: contact commits-help@ambari.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: ambari-dev@ambari.apache.org Delivered-To: mailing list commits@ambari.apache.org Received: (qmail 73495 invoked by uid 99); 22 Nov 2016 20:57:50 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 22 Nov 2016 20:57:50 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 7F848DFFAB; Tue, 22 Nov 2016 20:57:50 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: rlevas@apache.org To: commits@ambari.apache.org Message-Id: X-Mailer: ASF-Git Admin Mailer Subject: ambari git commit: AMBARI-18938. NPE when authenticating via a Centrify LDAP proxy (rlevas) Date: Tue, 22 Nov 2016 20:57:50 +0000 (UTC) archived-at: Tue, 22 Nov 2016 20:57:53 -0000 Repository: ambari Updated Branches: refs/heads/branch-2.5 38834dd5d -> 809d4c4f0 AMBARI-18938. NPE when authenticating via a Centrify LDAP proxy (rlevas) Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/809d4c4f Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/809d4c4f Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/809d4c4f Branch: refs/heads/branch-2.5 Commit: 809d4c4f0a1de4100f2d33ef6a0df2d366b6ae2e Parents: 38834dd Author: Robert Levas Authored: Tue Nov 22 15:57:34 2016 -0500 Committer: Robert Levas Committed: Tue Nov 22 15:57:34 2016 -0500 ---------------------------------------------------------------------- .../AmbariLdapAuthenticationProvider.java | 23 +- .../AmbariLdapBindAuthenticator.java | 233 ++++++++++++++++--- .../AmbariLdapBindAuthenticatorTest.java | 226 +++++++++++------- 3 files changed, 354 insertions(+), 128 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/809d4c4f/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProvider.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProvider.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProvider.java index 6905757..b5776a3 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProvider.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProvider.java @@ -1,4 +1,4 @@ -/** +/* * 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 @@ -24,7 +24,6 @@ import org.apache.ambari.server.configuration.Configuration; import org.apache.ambari.server.orm.dao.UserDAO; import org.apache.ambari.server.orm.entities.UserEntity; import org.apache.ambari.server.security.ClientSecurityType; -import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.IncorrectResultSizeDataAccessException; @@ -72,17 +71,21 @@ public class AmbariLdapAuthenticationProvider implements AuthenticationProvider return new AmbariAuthentication(auth, userId); } catch (AuthenticationException e) { - LOG.debug("Got exception during LDAP authentification attempt", e); + LOG.debug("Got exception during LDAP authentication attempt", e); // Try to help in troubleshooting Throwable cause = e.getCause(); - if (cause != null) { - // Below we check the cause of an AuthenticationException . If it is - // caused by another AuthenticationException, than probably - // the problem is with LDAP ManagerDN/password - if ((cause != e) && (cause instanceof - org.springframework.ldap.AuthenticationException)) { + if ((cause != null) && (cause != e)) { + // Below we check the cause of an AuthenticationException to see what the actual cause is + // and then send an appropriate message to the caller. + if (cause instanceof org.springframework.ldap.CommunicationException) { + if (LOG.isDebugEnabled()) { + LOG.warn("Failed to communicate with the LDAP server: " + cause.getMessage(), e); + } else { + LOG.warn("Failed to communicate with the LDAP server: " + cause.getMessage()); + } + } else if (cause instanceof org.springframework.ldap.AuthenticationException) { LOG.warn("Looks like LDAP manager credentials (that are used for " + - "connecting to LDAP server) are invalid.", e); + "connecting to LDAP server) are invalid.", e); } } throw new InvalidUsernamePasswordCombinationException(e); http://git-wip-us.apache.org/repos/asf/ambari/blob/809d4c4f/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticator.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticator.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticator.java index b34ef6a..b4ef889 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticator.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticator.java @@ -1,4 +1,4 @@ -/** +/* * 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 @@ -20,26 +20,36 @@ package org.apache.ambari.server.security.authorization; import java.util.List; +import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; import org.apache.ambari.server.configuration.Configuration; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ldap.core.AttributesMapper; +import org.springframework.ldap.core.ContextSource; +import org.springframework.ldap.core.DirContextAdapter; import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.DistinguishedName; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.ldap.support.LdapUtils; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.ldap.authentication.BindAuthenticator; +import org.springframework.security.ldap.authentication.AbstractLdapAuthenticator; +import org.springframework.security.ldap.search.LdapUserSearch; /** * An authenticator which binds as a user and checks if user should get ambari * admin authorities according to LDAP group membership */ -public class AmbariLdapBindAuthenticator extends BindAuthenticator { +public class AmbariLdapBindAuthenticator extends AbstractLdapAuthenticator { private static final Logger LOG = LoggerFactory.getLogger(AmbariLdapBindAuthenticator.class); private Configuration configuration; @@ -55,9 +65,14 @@ public class AmbariLdapBindAuthenticator extends BindAuthenticator { @Override public DirContextOperations authenticate(Authentication authentication) { - DirContextOperations user = super.authenticate(authentication); - LdapServerProperties ldapServerProperties = - configuration.getLdapServerProperties(); + if (!(authentication instanceof UsernamePasswordAuthenticationToken)) { + LOG.info("Unexpected authentication token type encountered ({}) - failing authentication.", authentication.getClass().getName()); + throw new BadCredentialsException("Unexpected authentication token type encountered."); + } + + DirContextOperations user = authenticate((UsernamePasswordAuthenticationToken) authentication); + + LdapServerProperties ldapServerProperties = configuration.getLdapServerProperties(); if (StringUtils.isNotEmpty(ldapServerProperties.getAdminGroupMappingRules())) { setAmbariAdminAttr(user, ldapServerProperties); } @@ -65,9 +80,13 @@ public class AmbariLdapBindAuthenticator extends BindAuthenticator { // Users stored locally in ambari are matched against LDAP users by the ldap attribute configured to be used as user name. // (e.g. uid, sAMAccount -> ambari user name ) String ldapUserName = user.getStringAttribute(ldapServerProperties.getUsernameAttribute()); - String loginName = authentication.getName(); // user login name the user has logged in + String loginName = authentication.getName(); // user login name the user has logged in - if (!ldapUserName.equals(loginName)) { + if (ldapUserName == null) { + LOG.warn("The user data does not contain a value for {}.", ldapServerProperties.getUsernameAttribute()); + } else if (ldapUserName.isEmpty()) { + LOG.warn("The user data contains an empty value for {}.", ldapServerProperties.getUsernameAttribute()); + } else if (!ldapUserName.equals(loginName)) { // if authenticated user name is different from ldap user name than user has logged in // with a login name that is different (e.g. user principal name) from the ambari user name stored in // ambari db. In this case add the user login name as login alias for ambari user name. @@ -76,11 +95,10 @@ public class AmbariLdapBindAuthenticator extends BindAuthenticator { // If the ldap username needs to be processed (like converted to all lowercase characters), // process it before setting it in the session via AuthorizationHelper#addLoginNameAlias String processedLdapUserName; - if(ldapServerProperties.isForceUsernameToLowercase()) { + if (ldapServerProperties.isForceUsernameToLowercase()) { processedLdapUserName = ldapUserName.toLowerCase(); LOG.info("Forcing ldap username to be lowercase characters: {} ==> {}", ldapUserName, processedLdapUserName); - } - else { + } else { processedLdapUserName = ldapUserName; } @@ -91,10 +109,169 @@ public class AmbariLdapBindAuthenticator extends BindAuthenticator { } /** - * Checks weather user is a member of ambari administrators group in LDAP. If - * yes, sets user's ambari_admin attribute to true - * @param user - * @return + * Authenticates a user with a configured LDAP server using the user's username and password. + *

+ * To authenticate a user: + *

    + *
  1. + * The LDAP server is queried for the relevant user object where the + * supplied username matches the configured LDAP attribute that represents the user's username + *
    • Example: (&(uid=user1)(objectClass=posixAccount))
    + *
  2. + *
  3. + * If found, the distinguished name (DN) of the user object is obtained from returned data and then + * used, along with the supplied password to perform an LDAP bind (see {@link #bind(DirContextOperations, String)}) + *
  4. + *
+ *

+ * Failure to authenticate will result in a {@link BadCredentialsException} to be thrown. + * + * @param authentication the credentials to use for authentication + * @return the authenticated user details + * @see #bind(DirContextOperations, String) + */ + private DirContextOperations authenticate(UsernamePasswordAuthenticationToken authentication) { + DirContextOperations user = null; + + String username = authentication.getName(); + Object credentials = authentication.getCredentials(); + String password = (credentials instanceof String) ? (String) credentials : null; + + if (StringUtils.isEmpty(username)) { + LOG.debug("Empty username encountered - failing authentication."); + throw new BadCredentialsException("Empty username encountered."); + } + + LOG.debug("Authenticating {}", username); + + if (StringUtils.isEmpty(password)) { + LOG.debug("Empty password encountered - failing authentication."); + throw new BadCredentialsException("Empty password encountered."); + } + + LdapUserSearch userSearch = getUserSearch(); + if (userSearch == null) { + LOG.debug("The user search facility has not been set - failing authentication."); + throw new BadCredentialsException("The user search facility has not been set."); + } else { + if (LOG.isTraceEnabled()) { + LOG.trace("Searching for user with username {}: {}", username, userSearch.toString()); + } + + // Find the user data where the supplied username matches the value of the configured LDAP + // attribute for the user's username. If a user is found, use the DN fro the returned data + // and the supplied password to attempt authentication. + DirContextOperations userFromSearch = userSearch.searchForUser(username); + + if (userFromSearch == null) { + LOG.debug("LDAP user object not found for {}", username); + } else { + LOG.debug("Found LDAP user for {}: {}", username, userFromSearch.getDn()); + user = bind(userFromSearch, password); + + // If trace enabled, log the user's LDAP attributes. + if (LOG.isTraceEnabled()) { + Attributes attributes = user.getAttributes(); + if (attributes != null) { + StringBuilder builder = new StringBuilder(); + NamingEnumeration ids = attributes.getIDs(); + try { + while (ids.hasMore()) { + String id = ids.next(); + builder.append("\n\t"); + builder.append(attributes.get(id)); + } + } catch (NamingException e) { + // Ignore this... + } + LOG.trace("User Attributes: {}", builder); + } else { + LOG.trace("User Attributes: not available"); + } + } + } + } + + // If a user was not authenticated, thrown a BadCredentialsException, else return the user data + if (user == null) { + LOG.debug("Invalid credentials for {} - failing authentication.", username); + throw new BadCredentialsException("Invalid credentials."); + } else { + LOG.debug("Successfully authenticated {}", username); + } + + return user; + } + + /** + * Attempt to authenticate a user with the configured LDAP server by performing an LDAP bind. + *

+ * Using the distinguished name provided in the supplied user data and the supplied password, + * attempt to authenticate with the configured LDAP server. If authentication is successful, use the + * attributes from the supplied user data rather than the attributes associated with the bound context + * because some scenarios result in missing data within the bound context due to LDAP server implementations. + *

+ * If authentication is not successful, throw a {@link BadCredentialsException}. + * + * @param user the user data containing the relevant DN and associated attributes + * @param password the password + * @return the authenticated user details + * @throws BadCredentialsException if authentication fails + */ + private DirContextOperations bind(DirContextOperations user, String password) { + ContextSource contextSource = getContextSource(); + + if (contextSource == null) { + String message = "Missing ContextSource - failing authentication."; + LOG.debug(message); + throw new InternalAuthenticationServiceException(message); + } + + if (!(contextSource instanceof BaseLdapPathContextSource)) { + String message = String.format("Unexpected ContextSource type (%s) - failing authentication.", contextSource.getClass().getName()); + LOG.debug(message); + throw new InternalAuthenticationServiceException(message); + } + + BaseLdapPathContextSource baseLdapPathContextSource = (BaseLdapPathContextSource) contextSource; + DistinguishedName userDistinguishedName = new DistinguishedName(user.getDn()); + DistinguishedName fullDn = new DistinguishedName(userDistinguishedName); + fullDn.prepend(baseLdapPathContextSource.getBaseLdapPath()); + + LOG.debug("Attempting to bind as {}", fullDn); + + DirContext dirContext = null; + + try { + // Perform the authentication. The result is not used because it is expected that the supplied + // user data has all of the attributes for the authenticated user. If authentication fails, it + // expected that the supplied user data will be destroyed or orphaned. + dirContext = baseLdapPathContextSource.getContext(fullDn.toString(), password); + + // Build a new DirContextAdapter using the attributes from the passed in user details since it + // is expected these details will be more complete of querying for them from the bound context. + // Some LDAP server implementations will no return all attributes to the bound context due to + // the filter being used in the query. + return new DirContextAdapter(user.getAttributes(), userDistinguishedName, baseLdapPathContextSource.getBaseLdapPath()); + } catch (org.springframework.ldap.AuthenticationException e) { + String message = String.format("Failed to bind as %s - %s", user.getDn().toString(), e.getMessage()); + if (LOG.isTraceEnabled()) { + LOG.trace(message, e); + } else if (LOG.isDebugEnabled()) { + LOG.debug(message); + } + throw new BadCredentialsException("The username or password is incorrect."); + } finally { + LdapUtils.closeContext(dirContext); + } + } + + /** + * Checks weather user is a member of ambari administrators group in LDAP. If + * yes, sets user's ambari_admin attribute to true + * + * @param user the user details + * @return the updated user details */ private DirContextOperations setAmbariAdminAttr(DirContextOperations user, LdapServerProperties ldapServerProperties) { String baseDn = ldapServerProperties.getBaseDN().toLowerCase(); @@ -105,10 +282,10 @@ public class AmbariLdapBindAuthenticator extends BindAuthenticator { //If groupBase is set incorrectly or isn't set - search in BaseDn int indexOfBaseDn = groupBase.indexOf(baseDn); - groupBase = indexOfBaseDn <= 0 ? "" : groupBase.substring(0,indexOfBaseDn - 1); + groupBase = indexOfBaseDn <= 0 ? "" : groupBase.substring(0, indexOfBaseDn - 1); String memberValue = StringUtils.isNotEmpty(adminGroupMappingMemberAttr) - ? user.getStringAttribute(adminGroupMappingMemberAttr) : user.getNameInNamespace(); + ? user.getStringAttribute(adminGroupMappingMemberAttr) : user.getNameInNamespace(); LOG.debug("LDAP login - set '{}' as member attribute for adminGroupMappingRules", memberValue); String setAmbariAdminAttrFilter = resolveAmbariAdminAttrFilter(ldapServerProperties, memberValue); @@ -125,8 +302,8 @@ public class AmbariLdapBindAuthenticator extends BindAuthenticator { ldapTemplate.setIgnorePartialResultException(true); ldapTemplate.setIgnoreNameNotFoundException(true); - List ambariAdminGroups = ldapTemplate.search( - groupBase, setAmbariAdminAttrFilter, attributesMapper); + @SuppressWarnings("unchecked") + List ambariAdminGroups = ldapTemplate.search(groupBase, setAmbariAdminAttrFilter, attributesMapper); //user has admin role granted, if user is a member of at least 1 group, // which matches the rules in configuration @@ -141,24 +318,24 @@ public class AmbariLdapBindAuthenticator extends BindAuthenticator { String groupMembershipAttr = ldapServerProperties.getGroupMembershipAttr(); String groupObjectClass = ldapServerProperties.getGroupObjectClass(); String adminGroupMappingRules = - ldapServerProperties.getAdminGroupMappingRules(); + ldapServerProperties.getAdminGroupMappingRules(); final String groupNamingAttribute = - ldapServerProperties.getGroupNamingAttr(); + ldapServerProperties.getGroupNamingAttr(); String groupSearchFilter = ldapServerProperties.getGroupSearchFilter(); String setAmbariAdminAttrFilter; if (StringUtils.isEmpty(groupSearchFilter)) { String adminGroupMappingRegex = createAdminGroupMappingRegex(adminGroupMappingRules, groupNamingAttribute); setAmbariAdminAttrFilter = String.format("(&(%s=%s)(objectclass=%s)(|%s))", - groupMembershipAttr, - memberValue, - groupObjectClass, - adminGroupMappingRegex); + groupMembershipAttr, + memberValue, + groupObjectClass, + adminGroupMappingRegex); } else { setAmbariAdminAttrFilter = String.format("(&(%s=%s)%s)", - groupMembershipAttr, - memberValue, - groupSearchFilter); + groupMembershipAttr, + memberValue, + groupSearchFilter); } return setAmbariAdminAttrFilter; } http://git-wip-us.apache.org/repos/asf/ambari/blob/809d4c4f/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticatorTest.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticatorTest.java b/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticatorTest.java index cea4b66..aed6b57 100644 --- a/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticatorTest.java +++ b/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticatorTest.java @@ -1,5 +1,4 @@ - -/** +/* * 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 @@ -18,132 +17,179 @@ */ package org.apache.ambari.server.security.authorization; -import static org.easymock.EasyMock.eq; -import static org.easymock.EasyMock.expectLastCall; -import static org.junit.Assert.assertEquals; - +import javax.naming.NamingEnumeration; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.LdapName; import java.util.Properties; - import org.apache.ambari.server.configuration.Configuration; -import org.apache.directory.server.annotations.CreateLdapServer; -import org.apache.directory.server.annotations.CreateTransport; -import org.apache.directory.server.core.annotations.ApplyLdifFiles; -import org.apache.directory.server.core.annotations.ContextEntry; -import org.apache.directory.server.core.annotations.CreateDS; -import org.apache.directory.server.core.annotations.CreatePartition; -import org.apache.directory.server.core.integ.FrameworkRunner; -import org.easymock.EasyMockRule; -import org.easymock.Mock; -import org.easymock.MockType; -import org.junit.Before; -import org.junit.Rule; +import org.apache.commons.lang.StringUtils; +import org.easymock.EasyMockSupport; import org.junit.Test; -import org.junit.runner.RunWith; import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.DistinguishedName; import org.springframework.ldap.core.support.LdapContextSource; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; -import org.springframework.security.ldap.search.LdapUserSearch; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.anyString; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class AmbariLdapBindAuthenticatorTest extends EasyMockSupport { -@RunWith(FrameworkRunner.class) -@CreateDS(allowAnonAccess = true, - name = "AmbariLdapBindAuthenticatorTest", - partitions = { - @CreatePartition(name = "Root", - suffix = "dc=apache,dc=org", - contextEntry = @ContextEntry( - entryLdif = - "dn: dc=apache,dc=org\n" + - "dc: apache\n" + - "objectClass: top\n" + - "objectClass: domain\n\n" + - "dn: dc=ambari,dc=apache,dc=org\n" + - "dc: ambari\n" + - "objectClass: top\n" + - "objectClass: domain\n\n")) - }) -@CreateLdapServer(allowAnonymousAccess = true, - transports = {@CreateTransport(protocol = "LDAP")}) -@ApplyLdifFiles("users.ldif") -public class AmbariLdapBindAuthenticatorTest extends AmbariLdapAuthenticationProviderBaseTest { - - @Rule - public EasyMockRule mocks = new EasyMockRule(this); - - @Mock(type = MockType.NICE) - private ServletRequestAttributes servletRequestAttributes; - - @Before - public void setUp() { - resetAll(); + @Test + public void testAuthenticateWithoutLogin() throws Exception { + testAuthenticate("username", "username", false); + } + + @Test + public void testAuthenticateWithNullLDAPUsername() throws Exception { + testAuthenticate("username", null, false); } @Test public void testAuthenticateWithLoginAliasDefault() throws Exception { - testAuthenticateWithLoginAlias(false); + testAuthenticate("username", "ldapUsername", false); } @Test public void testAuthenticateWithLoginAliasForceToLower() throws Exception { - testAuthenticateWithLoginAlias(true); + testAuthenticate("username", "ldapUsername", true); } - private void testAuthenticateWithLoginAlias(boolean forceUsernameToLower) throws Exception { - // Given - - LdapContextSource ldapCtxSource = new LdapContextSource(); - ldapCtxSource.setUrls(new String[] {"ldap://localhost:" + getLdapServer().getPort()}); - ldapCtxSource.setBase("dc=ambari,dc=apache,dc=org"); - ldapCtxSource.afterPropertiesSet(); + @Test + public void testAuthenticateBadPassword() throws Exception { + String basePathString = "dc=apache,dc=org"; + String ldapUserRelativeDNString = String.format("uid=%s,ou=people,ou=dev", "ldapUsername"); + LdapName ldapUserRelativeDN = new LdapName(ldapUserRelativeDNString); + String ldapUserDNString = String.format("%s,%s", ldapUserRelativeDNString, basePathString); + DistinguishedName basePath = new DistinguishedName(basePathString); + + LdapContextSource ldapCtxSource = createMock(LdapContextSource.class); + expect(ldapCtxSource.getBaseLdapPath()) + .andReturn(basePath) + .atLeastOnce(); + expect(ldapCtxSource.getContext(ldapUserDNString, "password")) + .andThrow(new org.springframework.ldap.AuthenticationException(null)) + .once(); + + DirContextOperations searchedUserContext = createMock(DirContextOperations.class); + expect(searchedUserContext.getDn()) + .andReturn(ldapUserRelativeDN) + .atLeastOnce(); + + FilterBasedLdapUserSearch userSearch = createMock(FilterBasedLdapUserSearch.class); + expect(userSearch.searchForUser(anyString())).andReturn(searchedUserContext).once(); - Properties properties = new Properties(); - properties.setProperty(Configuration.CLIENT_SECURITY.getKey(), "ldap"); - properties.setProperty(Configuration.SERVER_PERSISTENCE_TYPE.getKey(), "in-memory"); - properties.setProperty(Configuration.METADATA_DIR_PATH.getKey(),"src/test/resources/stacks"); - properties.setProperty(Configuration.SERVER_VERSION_FILE.getKey(),"src/test/resources/version"); - properties.setProperty(Configuration.OS_VERSION.getKey(),"centos5"); - properties.setProperty(Configuration.SHARED_RESOURCES_DIR.getKey(), "src/test/resources/"); - properties.setProperty(Configuration.LDAP_BASE_DN.getKey(), "dc=ambari,dc=apache,dc=org"); - - if(forceUsernameToLower) { - properties.setProperty(Configuration.LDAP_USERNAME_FORCE_LOWERCASE.getKey(), "true"); - } + replayAll(); - Configuration configuration = new Configuration(properties); + Configuration configuration = new Configuration(); AmbariLdapBindAuthenticator bindAuthenticator = new AmbariLdapBindAuthenticator(ldapCtxSource, configuration); - - LdapUserSearch userSearch = new FilterBasedLdapUserSearch("", "(&(cn={0})(objectClass=person))", ldapCtxSource); bindAuthenticator.setUserSearch(userSearch); - // JohnSmith is a login alias for deniedUser username - String loginAlias = "JohnSmith"; - String userName = "deniedUser"; - - Authentication authentication = new UsernamePasswordAuthenticationToken(loginAlias, "password"); + try { + bindAuthenticator.authenticate(new UsernamePasswordAuthenticationToken("username", "password")); + fail("Expected thrown exception: org.springframework.security.authentication.BadCredentialsException"); + } catch (org.springframework.security.authentication.BadCredentialsException e) { + // expected + } catch (Throwable t) { + fail("Expected thrown exception: org.springframework.security.authentication.BadCredentialsException\nEncountered thrown exception " + t.getClass().getName()); + } - RequestContextHolder.setRequestAttributes(servletRequestAttributes); + verifyAll(); + } - servletRequestAttributes.setAttribute(eq(loginAlias), eq(forceUsernameToLower ? userName.toLowerCase(): userName), eq(RequestAttributes.SCOPE_SESSION)); - expectLastCall().once(); + private void testAuthenticate(String ambariUsername, String ldapUsername, boolean forceUsernameToLower) throws Exception { + String basePathString = "dc=apache,dc=org"; + String ldapUserRelativeDNString = String.format("uid=%s,ou=people,ou=dev", ldapUsername); + LdapName ldapUserRelativeDN = new LdapName(ldapUserRelativeDNString); + String ldapUserDNString = String.format("%s,%s", ldapUserRelativeDNString, basePathString); + DistinguishedName basePath = new DistinguishedName(basePathString); + + @SuppressWarnings("unchecked") + NamingEnumeration adminGroups = createMock(NamingEnumeration.class); + expect(adminGroups.hasMore()) + .andReturn(false) + .atLeastOnce(); + adminGroups.close(); + expectLastCall().atLeastOnce(); + + DirContextOperations boundUserContext = createMock(DirContextOperations.class); + expect(boundUserContext.search(eq("ou=groups"), eq("(&(member=" + ldapUserDNString + ")(objectclass=group)(|(cn=Ambari Administrators)))"), anyObject(SearchControls.class))) + .andReturn(adminGroups) + .atLeastOnce(); + boundUserContext.close(); + expectLastCall().atLeastOnce(); + + + LdapContextSource ldapCtxSource = createMock(LdapContextSource.class); + expect(ldapCtxSource.getBaseLdapPath()) + .andReturn(basePath) + .atLeastOnce(); + expect(ldapCtxSource.getContext(ldapUserDNString, "password")) + .andReturn(boundUserContext) + .once(); + expect(ldapCtxSource.getReadOnlyContext()) + .andReturn(boundUserContext) + .once(); + + Attribute uidAttribute = createMock(Attribute.class); + expect(uidAttribute.size()) + .andReturn(1) + .atLeastOnce(); + expect(uidAttribute.get()).andReturn(ldapUsername).atLeastOnce(); + + Attributes searchedAttributes = createMock(Attributes.class); + expect(searchedAttributes.get("uid")) + .andReturn(uidAttribute) + .atLeastOnce(); + + DirContextOperations searchedUserContext = createMock(DirContextOperations.class); + expect(searchedUserContext.getDn()) + .andReturn(ldapUserRelativeDN) + .atLeastOnce(); + expect(searchedUserContext.getAttributes()) + .andReturn(searchedAttributes) + .atLeastOnce(); + + FilterBasedLdapUserSearch userSearch = createMock(FilterBasedLdapUserSearch.class); + expect(userSearch.searchForUser(ambariUsername)).andReturn(searchedUserContext).once(); + + ServletRequestAttributes servletRequestAttributes = createMock(ServletRequestAttributes.class); + + if (!StringUtils.isEmpty(ldapUsername) && !ambariUsername.equals(ldapUsername)) { + servletRequestAttributes.setAttribute(eq(ambariUsername), eq(forceUsernameToLower ? ldapUsername.toLowerCase() : ldapUsername), eq(RequestAttributes.SCOPE_SESSION)); + expectLastCall().once(); + } replayAll(); - // When + RequestContextHolder.setRequestAttributes(servletRequestAttributes); - DirContextOperations user = bindAuthenticator.authenticate(authentication); + Properties properties = new Properties(); + if (forceUsernameToLower) { + properties.setProperty(Configuration.LDAP_USERNAME_FORCE_LOWERCASE.getKey(), "true"); + } + Configuration configuration = new Configuration(properties); - // Then + AmbariLdapBindAuthenticator bindAuthenticator = new AmbariLdapBindAuthenticator(ldapCtxSource, configuration); + bindAuthenticator.setUserSearch(userSearch); + DirContextOperations user = bindAuthenticator.authenticate(new UsernamePasswordAuthenticationToken(ambariUsername, "password")); verifyAll(); String ldapUserNameAttribute = configuration.getLdapServerProperties().getUsernameAttribute(); - - assertEquals(userName, user.getStringAttribute(ldapUserNameAttribute)); + assertEquals(ldapUsername, user.getStringAttribute(ldapUserNameAttribute)); } }