Prechádzať zdrojové kódy

AMBARI-15554. Ambari LDAP integration cannot handle LDAP directories with multiple entries for the same user. (stoader)

Toader, Sebastian 9 rokov pred
rodič
commit
71b4c624fb
29 zmenil súbory, kde vykonal 1816 pridanie a 41 odobranie
  1. 3 0
      ambari-server/conf/unix/log4j.properties
  2. 1 0
      ambari-server/pom.xml
  3. 137 0
      ambari-server/src/main/java/org/apache/ambari/server/api/UserNameOverrideFilter.java
  4. 47 0
      ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java
  5. 2 0
      ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java
  6. 214 0
      ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariAuthentication.java
  7. 38 9
      ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProvider.java
  8. 2 0
      ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthoritiesPopulator.java
  9. 24 5
      ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticator.java
  10. 43 0
      ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapUtils.java
  11. 37 1
      ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AuthorizationHelper.java
  12. 51 0
      ambari-server/src/main/java/org/apache/ambari/server/security/authorization/DuplicateLdapUserFoundAuthenticationException.java
  13. 39 5
      ambari-server/src/main/java/org/apache/ambari/server/security/authorization/LdapServerProperties.java
  14. 1 0
      ambari-server/src/main/resources/webapp/WEB-INF/spring-security.xml
  15. 196 0
      ambari-server/src/test/java/org/apache/ambari/server/api/UserNameOverrideFilterTest.java
  16. 56 0
      ambari-server/src/test/java/org/apache/ambari/server/configuration/ConfigurationTest.java
  17. 87 0
      ambari-server/src/test/java/org/apache/ambari/server/security/AmbariLdapUtilsTest.java
  18. 333 0
      ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariAuthenticationTest.java
  19. 100 0
      ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProviderForDuplicateUserTest.java
  20. 48 2
      ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProviderTest.java
  21. 136 0
      ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticatorTest.java
  22. 111 5
      ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AuthorizationHelperTest.java
  23. 21 2
      ambari-server/src/test/java/org/apache/ambari/server/security/authorization/LdapServerPropertiesTest.java
  24. 42 0
      ambari-server/src/test/java/org/apache/ambari/server/security/authorization/TestAmbariLdapAuthoritiesPopulator.java
  25. 1 0
      ambari-server/src/test/resources/users.ldif
  26. 20 0
      ambari-server/src/test/resources/users_with_duplicate_uid.ldif
  27. 5 3
      ambari-web/app/controllers/login_controller.js
  28. 2 2
      ambari-web/app/router.js
  29. 19 7
      ambari-web/test/controllers/login_controller_test.js

+ 3 - 0
ambari-server/conf/unix/log4j.properties

@@ -73,3 +73,6 @@ log4j.appender.eclipselink.layout.ConversionPattern=%m%n
 log4j.logger.org.apache.hadoop.yarn.client=WARN
 log4j.logger.org.apache.slider.common.tools.SliderUtils=WARN
 log4j.logger.org.apache.ambari.server.security.authorization=WARN
+
+log4j.logger.org.apache.ambari.server.security.authorization.AuthorizationHelper=INFO
+log4j.logger.org.apache.ambari.server.security.authorization.AmbariLdapBindAuthenticator=INFO

+ 1 - 0
ambari-server/pom.xml

@@ -293,6 +293,7 @@
             <exclude>src/test/resources/TestAmbaryServer.samples/**</exclude>
             <exclude>src/test/resources/*.txt</exclude>
             <exclude>src/test/resources/users_for_dn_with_space.ldif</exclude>
+            <exclude>src/test/resources/users_with_duplicate_uid.ldif</exclude>
 
             <!--Velocity log -->
             <exclude>**/velocity.log*</exclude>

+ 137 - 0
ambari-server/src/main/java/org/apache/ambari/server/api/UserNameOverrideFilter.java

@@ -0,0 +1,137 @@
+/**
+ * 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.ambari.server.api;
+
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+
+import org.apache.ambari.server.security.authorization.AuthorizationHelper;
+
+/**
+ * This filter overrides usernames found in request url.
+ */
+public class UserNameOverrideFilter implements Filter {
+
+  // Regex for extracting user name component from the user related api request Uris
+  private final static Pattern USER_NAME_IN_URI_REGEXP = Pattern.compile("(?<pre>.*/users/)(?<username>[^/]+)(?<post>(/.*)?)");
+
+  /**
+   * Called by the web container to indicate to a filter that it is
+   * being placed into service.
+   *
+   * <p>The servlet container calls the init
+   * method exactly once after instantiating the filter. The init
+   * method must complete successfully before the filter is asked to do any
+   * filtering work.
+   *
+   * <p>The web container cannot place the filter into service if the init
+   * method either
+   * <ol>
+   * <li>Throws a ServletException
+   * <li>Does not return within a time period defined by the web container
+   * </ol>
+   *
+   * @param filterConfig
+   */
+  @Override
+  public void init(FilterConfig filterConfig) throws ServletException {
+
+  }
+
+  /**
+   * The <code>doFilter</code> method of the Filter is called by the
+   * container each time a request/response pair is passed through the
+   * chain due to a client request for a resource at the end of the chain.
+   * The FilterChain passed in to this method allows the Filter to pass
+   * on the request and response to the next entity in the chain.
+   *
+   * Verify if this a user related request by checking that the Uri of the request contains
+   * username and resolves the username to actual ambari user name if username
+   * is a login alias.
+   *
+   * @param request
+   * @param response
+   * @param chain
+   */
+  @Override
+  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+    if (request instanceof HttpServletRequest) {
+      final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
+      Matcher userNameMatcher = getUserNameMatcher(httpServletRequest.getRequestURI());
+
+      if (userNameMatcher.find()) {
+        String userNameFromUri = URLDecoder.decode(userNameMatcher.group("username"), "UTF-8");
+        final String userName = AuthorizationHelper.resolveLoginAliasToUserName(userNameFromUri);
+
+        if (!userNameFromUri.equals(userName)) {
+          final String requestUriOverride = String.format("%s%s%s", userNameMatcher.group("pre"), userName, userNameMatcher.group("post"));
+
+          request = new HttpServletRequestWrapper(httpServletRequest) {
+            @Override
+            public String getRequestURI() {
+              return requestUriOverride;
+            }
+          };
+
+        }
+      }
+    }
+
+    chain.doFilter(request, response);
+  }
+
+  /**
+   * Returns a {@link Matcher} created from {@link #USER_NAME_IN_URI_REGEXP} for the
+   * provided requestUri.
+   * @param requestUri the Uri the Matcher is created for.
+   * @return the matcher
+   */
+  protected Matcher getUserNameMatcher(String requestUri) {
+    return USER_NAME_IN_URI_REGEXP.matcher(requestUri);
+  }
+
+  /**
+   * Called by the web container to indicate to a filter that it is being
+   * taken out of service.
+   *
+   * <p>This method is only called once all threads within the filter's
+   * doFilter method have exited or after a timeout period has passed.
+   * After the web container calls this method, it will not call the
+   * doFilter method again on this instance of the filter.
+   *
+   * <p>This method gives the filter an opportunity to clean up any
+   * resources that are being held (for example, memory, file handles,
+   * threads) and make sure that any persistent state is synchronized
+   * with the filter's current state in memory.
+   */
+  @Override
+  public void destroy() {
+
+  }
+}

+ 47 - 0
ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java

@@ -188,6 +188,30 @@ public class Configuration {
   public static final String LDAP_GROUP_NAMING_ATTR_KEY = "authentication.ldap.groupNamingAttr";
   public static final String LDAP_GROUP_MEMEBERSHIP_ATTR_KEY = "authentication.ldap.groupMembershipAttr";
   public static final String LDAP_ADMIN_GROUP_MAPPING_RULES_KEY = "authorization.ldap.adminGroupMappingRules";
+  /**
+   * When authentication through LDAP is enabled then Ambari Server uses this filter to lookup
+   * the user in LDAP based on the provided ambari user name.
+   *
+   * If it is not set then the default {@link #LDAP_USER_SEARCH_FILTER_DEFAULT} is used.
+   */
+  public static final String LDAP_USER_SEARCH_FILTER_KEY = "authentication.ldap.userSearchFilter";
+
+  /**
+   * When authentication through LDAP is enabled there might be cases when {@link #LDAP_USER_SEARCH_FILTER_KEY}
+   * may match multiple users in LDAP. In such cases the user is prompted to provide additional info, e.g. the domain
+   * he or she wants ot log in upon login beside the username. This filter will be used by Ambari Server to lookup
+   * users in LDAP if the login name the user logs in contains additional information beside ambari user name.
+   *
+   * If it is not not set then the default {@link #LDAP_ALT_USER_SEARCH_FILTER_DEFAULT} is used.
+   *
+   * <p>
+   *   Note: Currently this filter will only be used by Ambari Server if the user login name
+   *   is in the username@domain format (e.g. user1@x.y.com) which is the userPrincipalName
+   *   format used in AD.
+   * </p>
+   */
+  public static final String LDAP_ALT_USER_SEARCH_FILTER_KEY = "authentication.ldap.alternateUserSearchFilter"; //TODO: we'll need a more generic solution to support any login name format
+
   public static final String LDAP_GROUP_SEARCH_FILTER_KEY = "authorization.ldap.groupSearchFilter";
   public static final String LDAP_REFERRAL_KEY = "authentication.ldap.referral";
   public static final String LDAP_PAGINATION_ENABLED_KEY = "authentication.ldap.pagination.enabled";
@@ -459,6 +483,25 @@ public class Configuration {
   private static final String LDAP_GROUP_NAMING_ATTR_DEFAULT = "cn";
   private static final String LDAP_GROUP_MEMBERSHIP_ATTR_DEFAULT = "member";
   private static final String LDAP_ADMIN_GROUP_MAPPING_RULES_DEFAULT = "Ambari Administrators";
+  /**
+   * When authentication through LDAP is enabled then Ambari Server uses this filter by default to lookup
+   * the user in LDAP if one not provided in the config via {@link #LDAP_USER_SEARCH_FILTER_KEY}.
+   */
+  protected static final String LDAP_USER_SEARCH_FILTER_DEFAULT = "(&({usernameAttribute}={0})(objectClass={userObjectClass}))";
+
+  /**
+   * When authentication through LDAP is enabled Ambari Server uses this filter by default to lookup
+   * the user in LDAP when the user provides beside user name additional information.
+   * This filter can be overridden through {@link #LDAP_ALT_USER_SEARCH_FILTER_KEY}.
+   *
+   * <p>
+   *   Note: Currently the use of alternate user search filter is triggered only if the user login name
+   *   is in the username@domain format (e.g. user1@x.y.com) which is the userPrincipalName
+   *   format used in AD.
+   * </p>
+   */
+  protected static final String LDAP_ALT_USER_SEARCH_FILTER_DEFAULT = "(&(userPrincipalName={0})(objectClass={userObjectClass}))"; //TODO: we'll need a more generic solution to support any login name format
+
   private static final String LDAP_GROUP_SEARCH_FILTER_DEFAULT = "";
   private static final String LDAP_REFERRAL_DEFAULT = "follow";
 
@@ -1661,6 +1704,10 @@ public class Configuration {
       getProperty(LDAP_GROUP_NAMING_ATTR_KEY, LDAP_GROUP_NAMING_ATTR_DEFAULT));
     ldapServerProperties.setAdminGroupMappingRules(properties.getProperty(
       LDAP_ADMIN_GROUP_MAPPING_RULES_KEY, LDAP_ADMIN_GROUP_MAPPING_RULES_DEFAULT));
+    ldapServerProperties.setUserSearchFilter(properties.getProperty(
+      LDAP_USER_SEARCH_FILTER_KEY, LDAP_USER_SEARCH_FILTER_DEFAULT));
+    ldapServerProperties.setAlternateUserSearchFilter(properties.getProperty(
+      LDAP_ALT_USER_SEARCH_FILTER_KEY, LDAP_ALT_USER_SEARCH_FILTER_DEFAULT));
     ldapServerProperties.setGroupSearchFilter(properties.getProperty(
       LDAP_GROUP_SEARCH_FILTER_KEY, LDAP_GROUP_SEARCH_FILTER_DEFAULT));
     ldapServerProperties.setReferralMethod(properties.getProperty(

+ 2 - 0
ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java

@@ -41,6 +41,7 @@ import org.apache.ambari.server.agent.rest.AgentResource;
 import org.apache.ambari.server.api.AmbariErrorHandler;
 import org.apache.ambari.server.api.AmbariPersistFilter;
 import org.apache.ambari.server.api.MethodOverrideFilter;
+import org.apache.ambari.server.api.UserNameOverrideFilter;
 import org.apache.ambari.server.api.rest.BootStrapResource;
 import org.apache.ambari.server.api.services.AmbariMetaInfo;
 import org.apache.ambari.server.api.services.KeyService;
@@ -363,6 +364,7 @@ public class AmbariServer {
       root.addEventListener(new RequestContextListener());
 
       root.addFilter(new FilterHolder(springSecurityFilter), "/api/*", DISPATCHER_TYPES);
+      root.addFilter(new FilterHolder(new UserNameOverrideFilter()), "/api/v1/users/*", DISPATCHER_TYPES);
 
       // session-per-request strategy for agents
       agentroot.addFilter(new FilterHolder(injector.getInstance(AmbariPersistFilter.class)), "/agent/*", DISPATCHER_TYPES);

+ 214 - 0
ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariAuthentication.java

@@ -0,0 +1,214 @@
+/**
+ * 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.ambari.server.security.authorization;
+
+import java.security.Principal;
+import java.util.Collection;
+import java.util.Objects;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.User;
+
+/**
+ * This class is a wrapper for authentication objects to
+ * provide functionality for resolving login aliases to
+ * ambari user names.
+ */
+public final class AmbariAuthentication implements Authentication {
+  private final Authentication authentication;
+  private final Object principalOverride;
+
+  public AmbariAuthentication(Authentication authentication) {
+    this.authentication = authentication;
+    this.principalOverride = getPrincipalOverride();
+  }
+
+
+
+  /**
+   * Set by an <code>AuthenticationManager</code> to indicate the authorities that the principal has been
+   * granted. Note that classes should not rely on this value as being valid unless it has been set by a trusted
+   * <code>AuthenticationManager</code>.
+   * <p>
+   * Implementations should ensure that modifications to the returned collection
+   * array do not affect the state of the Authentication object, or use an unmodifiable instance.
+   * </p>
+   *
+   * @return the authorities granted to the principal, or an empty collection if the token has not been authenticated.
+   * Never null.
+   */
+  @Override
+  public Collection<? extends GrantedAuthority> getAuthorities() {
+    return authentication.getAuthorities();
+  }
+
+  /**
+   * The credentials that prove the principal is correct. This is usually a password, but could be anything
+   * relevant to the <code>AuthenticationManager</code>. Callers are expected to populate the credentials.
+   *
+   * @return the credentials that prove the identity of the <code>Principal</code>
+   */
+  @Override
+  public Object getCredentials() {
+    return authentication.getCredentials();
+  }
+
+  /**
+   * Stores additional details about the authentication request. These might be an IP address, certificate
+   * serial number etc.
+   *
+   * @return additional details about the authentication request, or <code>null</code> if not used
+   */
+  @Override
+  public Object getDetails() {
+    return authentication.getDetails();
+  }
+
+  /**
+   * The identity of the principal being authenticated. In the case of an authentication request with username and
+   * password, this would be the username. Callers are expected to populate the principal for an authentication
+   * request.
+   * <p>
+   * The <tt>AuthenticationManager</tt> implementation will often return an <tt>Authentication</tt> containing
+   * richer information as the principal for use by the application. Many of the authentication providers will
+   * create a {@code UserDetails} object as the principal.
+   *
+   * @return the <code>Principal</code> being authenticated or the authenticated principal after authentication.
+   */
+  @Override
+  public Object getPrincipal() {
+    if (principalOverride != null) {
+      return principalOverride;
+    }
+
+    return authentication.getPrincipal();
+  }
+
+  /**
+   * Used to indicate to {@code AbstractSecurityInterceptor} whether it should present the
+   * authentication token to the <code>AuthenticationManager</code>. Typically an <code>AuthenticationManager</code>
+   * (or, more often, one of its <code>AuthenticationProvider</code>s) will return an immutable authentication token
+   * after successful authentication, in which case that token can safely return <code>true</code> to this method.
+   * Returning <code>true</code> will improve performance, as calling the <code>AuthenticationManager</code> for
+   * every request will no longer be necessary.
+   * <p>
+   * For security reasons, implementations of this interface should be very careful about returning
+   * <code>true</code> from this method unless they are either immutable, or have some way of ensuring the properties
+   * have not been changed since original creation.
+   *
+   * @return true if the token has been authenticated and the <code>AbstractSecurityInterceptor</code> does not need
+   * to present the token to the <code>AuthenticationManager</code> again for re-authentication.
+   */
+  @Override
+  public boolean isAuthenticated() {
+    return authentication.isAuthenticated();
+  }
+
+  /**
+   * See {@link #isAuthenticated()} for a full description.
+   * <p>
+   * Implementations should <b>always</b> allow this method to be called with a <code>false</code> parameter,
+   * as this is used by various classes to specify the authentication token should not be trusted.
+   * If an implementation wishes to reject an invocation with a <code>true</code> parameter (which would indicate
+   * the authentication token is trusted - a potential security risk) the implementation should throw an
+   * {@link IllegalArgumentException}.
+   *
+   * @param isAuthenticated <code>true</code> if the token should be trusted (which may result in an exception) or
+   *                        <code>false</code> if the token should not be trusted
+   * @throws IllegalArgumentException if an attempt to make the authentication token trusted (by passing
+   *                                  <code>true</code> as the argument) is rejected due to the implementation being immutable or
+   *                                  implementing its own alternative approach to {@link #isAuthenticated()}
+   */
+  @Override
+  public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
+    authentication.setAuthenticated(isAuthenticated);
+  }
+
+  /**
+   * Returns the name of this principal.
+   *
+   * @return the name of this principal.
+   */
+  @Override
+  public String getName() {
+    if (principalOverride != null)
+    {
+      if (principalOverride instanceof UserDetails) {
+        return ((UserDetails) principalOverride).getUsername();
+      }
+
+      return principalOverride.toString();
+    }
+
+    return authentication.getName();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    AmbariAuthentication that = (AmbariAuthentication) o;
+    return Objects.equals(authentication, that.authentication) &&
+      Objects.equals(principalOverride, that.principalOverride);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(authentication, principalOverride);
+  }
+
+  /**
+   * Returns a principal object that is to be used
+   * to override the original principal object
+   * returned by the inner {@link #authentication} object.
+   *
+   * <p>The purpose of overriding the origin principal is to provide
+   * and object that resolves the contained user name to ambari user name in case
+   * the original user name is a login alias.</p>
+   *
+   * @return principal override of the original one is of type {@link UserDetails},
+   * if the original one is a login alias name than the user name the login alias resolves to
+   * otherwise <code>null</code>
+   */
+  private Object getPrincipalOverride() {
+    Object principal = authentication.getPrincipal();
+
+    if (principal instanceof UserDetails) {
+      UserDetails user = (UserDetails)principal;
+
+      principal =
+        new User(
+          AuthorizationHelper.resolveLoginAliasToUserName(user.getUsername()),
+          user.getPassword(),
+          user.isEnabled(),
+          user.isAccountNonExpired(),
+          user.isCredentialsNonExpired(),
+          user.isAccountNonLocked(),
+          user.getAuthorities());
+    } else if ( !(principal instanceof Principal) && principal != null ){
+      String username = principal.toString();
+      principal = AuthorizationHelper.resolveLoginAliasToUserName(username);
+    } else {
+      principal = null;
+    }
+
+    return principal;
+  }
+}

+ 38 - 9
ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProvider.java

@@ -19,10 +19,12 @@ package org.apache.ambari.server.security.authorization;
 
 import com.google.inject.Inject;
 import java.util.List;
+
 import org.apache.ambari.server.configuration.Configuration;
 import org.apache.ambari.server.security.ClientSecurityType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.dao.IncorrectResultSizeDataAccessException;
 import org.springframework.ldap.core.support.LdapContextSource;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -45,6 +47,7 @@ public class AmbariLdapAuthenticationProvider implements AuthenticationProvider
 
   private ThreadLocal<LdapServerProperties> ldapServerProperties = new ThreadLocal<LdapServerProperties>();
   private ThreadLocal<LdapAuthenticationProvider> providerThreadLocal = new ThreadLocal<LdapAuthenticationProvider>();
+  private ThreadLocal<String> ldapUserSearchFilterThreadLocal = new ThreadLocal<>();
 
   @Inject
   public AmbariLdapAuthenticationProvider(Configuration configuration, AmbariLdapAuthoritiesPopulator authoritiesPopulator) {
@@ -54,10 +57,13 @@ public class AmbariLdapAuthenticationProvider implements AuthenticationProvider
 
   @Override
   public Authentication authenticate(Authentication authentication) throws AuthenticationException {
-
     if (isLdapEnabled()) {
+      String username = getUserName(authentication);
+
       try {
-        return loadLdapAuthenticationProvider().authenticate(authentication);
+        Authentication auth = loadLdapAuthenticationProvider(username).authenticate(authentication);
+
+        return new AmbariAuthentication(auth);
       } catch (AuthenticationException e) {
         LOG.debug("Got exception during LDAP authentification attempt", e);
         // Try to help in troubleshooting
@@ -73,6 +79,8 @@ public class AmbariLdapAuthenticationProvider implements AuthenticationProvider
           }
         }
         throw e;
+      } catch (IncorrectResultSizeDataAccessException multipleUsersFound) {
+        throw new DuplicateLdapUserFoundAuthenticationException(String.format("Login Failed: Please append your domain to your username and try again.  Example: %s@domain", username));
       }
     } else {
       return null;
@@ -89,9 +97,14 @@ public class AmbariLdapAuthenticationProvider implements AuthenticationProvider
    * Reloads LDAP Context Source and depending objects if properties were changed
    * @return corresponding LDAP authentication provider
    */
-  LdapAuthenticationProvider loadLdapAuthenticationProvider() {
-    if (reloadLdapServerProperties()) {
-      LOG.info("LDAP Properties changed - rebuilding Context");
+  LdapAuthenticationProvider loadLdapAuthenticationProvider(String userName) {
+    boolean ldapConfigPropertiesChanged = reloadLdapServerProperties();
+
+    String ldapUserSearchFilter = getLdapUserSearchFilter(userName);
+
+    if (ldapConfigPropertiesChanged|| !ldapUserSearchFilter.equals(ldapUserSearchFilterThreadLocal.get())) {
+
+      LOG.info("Either LDAP Properties or user search filter changed - rebuilding Context");
       LdapContextSource springSecurityContextSource = new LdapContextSource();
       List<String> ldapUrls = ldapServerProperties.get().getLdapUrls();
       springSecurityContextSource.setUrls(ldapUrls.toArray(new String[ldapUrls.size()]));
@@ -111,18 +124,17 @@ public class AmbariLdapAuthenticationProvider implements AuthenticationProvider
 
       //TODO change properties
       String userSearchBase = ldapServerProperties.get().getUserSearchBase();
-      String userSearchFilter = ldapServerProperties.get().getUserSearchFilter();
-
-      FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch(userSearchBase, userSearchFilter, springSecurityContextSource);
+      FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch(userSearchBase, ldapUserSearchFilter, springSecurityContextSource);
 
       AmbariLdapBindAuthenticator bindAuthenticator = new AmbariLdapBindAuthenticator(springSecurityContextSource, configuration);
       bindAuthenticator.setUserSearch(userSearch);
 
       LdapAuthenticationProvider authenticationProvider = new LdapAuthenticationProvider(bindAuthenticator, authoritiesPopulator);
-
       providerThreadLocal.set(authenticationProvider);
     }
 
+    ldapUserSearchFilterThreadLocal.set(ldapUserSearchFilter);
+
     return providerThreadLocal.get();
   }
 
@@ -135,6 +147,16 @@ public class AmbariLdapAuthenticationProvider implements AuthenticationProvider
     return configuration.getClientSecurityType() == ClientSecurityType.LDAP;
   }
 
+  /**
+   * Extracts the user name from the passed authentication object.
+   * @param authentication
+   * @return
+   */
+  protected String getUserName(Authentication authentication) {
+    UsernamePasswordAuthenticationToken userToken = (UsernamePasswordAuthenticationToken)authentication;
+    return userToken.getName();
+  }
+
   /**
    * Reloads LDAP Server properties from configuration
    *
@@ -149,4 +171,11 @@ public class AmbariLdapAuthenticationProvider implements AuthenticationProvider
     }
     return false;
   }
+
+
+  private String getLdapUserSearchFilter(String userName) {
+    return ldapServerProperties.get()
+      .getUserSearchFilter(AmbariLdapUtils.isUserPrincipalNameFormat(userName));
+  }
+
 }

+ 2 - 0
ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthoritiesPopulator.java

@@ -60,6 +60,8 @@ public class AmbariLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
 
   @Override
   public Collection<? extends GrantedAuthority> getGrantedAuthorities(DirContextOperations userData, String username) {
+    username = AuthorizationHelper.resolveLoginAliasToUserName(username);
+
     log.info("Get authorities for user " + username + " from local DB");
 
     UserEntity user;

+ 24 - 5
ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticator.java

@@ -18,7 +18,14 @@
 package org.apache.ambari.server.security.authorization;
 
 
+import java.util.List;
+
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+
 import org.apache.ambari.server.configuration.Configuration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.ldap.core.AttributesMapper;
 import org.springframework.ldap.core.DirContextOperations;
 import org.springframework.ldap.core.LdapTemplate;
@@ -26,16 +33,13 @@ import org.springframework.ldap.core.support.BaseLdapPathContextSource;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.ldap.authentication.BindAuthenticator;
 
-import java.util.*;
-import javax.naming.*;
-import javax.naming.directory.Attributes;
-
 
 /**
  * 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 {
+  private static final Logger LOG = LoggerFactory.getLogger(AmbariLdapBindAuthenticator.class);
 
   private Configuration configuration;
 
@@ -51,8 +55,23 @@ public class AmbariLdapBindAuthenticator extends BindAuthenticator {
   public DirContextOperations authenticate(Authentication authentication) {
 
     DirContextOperations user = super.authenticate(authentication);
+    setAmbariAdminAttr(user);
 
-    return setAmbariAdminAttr(user);
+    // 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(configuration.getLdapServerProperties().getUsernameAttribute());
+    String loginName  = authentication.getName(); // user login name the user has logged in
+
+    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.
+      LOG.info("User with {}='{}' logged in with login alias '{}'", ldapUserName, loginName);
+
+      AuthorizationHelper.addLoginNameAlias(ldapUserName, loginName);
+    }
+
+    return user;
   }
 
   /**

+ 43 - 0
ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapUtils.java

@@ -0,0 +1,43 @@
+/**
+ * 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.ambari.server.security.authorization;
+
+import java.util.regex.Pattern;
+
+/**
+ * Provides utility methods for LDAP related functionality
+ */
+public class AmbariLdapUtils {
+
+  /**
+   * Regexp to verify if user login name beside user contains domain information as well (User principal name format).
+   */
+  private static final Pattern UPN_FORMAT = Pattern.compile(".+@\\w+(\\.\\w+)*");
+
+  /**
+   * Returns true if the given user name contains domain name as well (e.g. username@domain)
+   * @param loginName the login name to verify if it contains domain information.
+   * @return
+   */
+  public static boolean isUserPrincipalNameFormat(String loginName) {
+    return UPN_FORMAT.matcher(loginName).matches();
+  }
+
+
+}

+ 37 - 1
ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AuthorizationHelper.java

@@ -28,6 +28,9 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
 
 import java.util.*;
 
@@ -42,7 +45,7 @@ public class AuthorizationHelper {
    * Converts collection of RoleEntities to collection of GrantedAuthorities
    */
   public Collection<GrantedAuthority> convertPrivilegesToAuthorities(Collection<PrivilegeEntity> privilegeEntities) {
-    Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>(privilegeEntities.size());
+    Set<GrantedAuthority> authorities = new HashSet<>(privilegeEntities.size());
 
     for (PrivilegeEntity privilegeEntity : privilegeEntities) {
       authorities.add(new AmbariGrantedAuthority(privilegeEntity));
@@ -247,4 +250,37 @@ public class AuthorizationHelper {
     SecurityContext context = SecurityContextHolder.getContext();
     return (context == null) ? null : context.getAuthentication();
   }
+
+  /**
+   * There are cases when users log-in with a login name that is
+   * define in LDAP and which do not correspond to the user name stored
+   * locally in ambari. These external login names act as an alias to
+   * ambari users name. This method stores in the current http session a mapping
+   * of alias user name to local ambari user name to make possible resolving
+   * login alias to ambari user name.
+   * @param ambariUserName ambari user name for which the alias is to be stored in the session
+   * @param loginAlias the alias for the ambari user name.
+   */
+  public static void addLoginNameAlias(String ambariUserName, String loginAlias) {
+    ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+    if (attr != null) {
+      LOG.info("Adding login alias '{}' for user name '{}'", loginAlias, ambariUserName);
+      attr.setAttribute(loginAlias, ambariUserName, RequestAttributes.SCOPE_SESSION);
+    }
+  }
+
+  /**
+   * Looks up the provided loginAlias in the current http session and return the ambari
+   * user name that the alias is defined for.
+   * @param loginAlias the login alias to resolve to ambari user name
+   * @return the ambari user name if the alias is found otherwise returns the passed in loginAlias
+   */
+  public static String resolveLoginAliasToUserName(String loginAlias) {
+    ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+    if (attr != null && attr.getAttribute(loginAlias, RequestAttributes.SCOPE_SESSION) != null) {
+      return (String)attr.getAttribute(loginAlias, RequestAttributes.SCOPE_SESSION);
+    }
+
+    return loginAlias;
+  }
 }

+ 51 - 0
ambari-server/src/main/java/org/apache/ambari/server/security/authorization/DuplicateLdapUserFoundAuthenticationException.java

@@ -0,0 +1,51 @@
+/**
+ * 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.ambari.server.security.authorization;
+
+
+import org.springframework.security.core.AuthenticationException;
+
+/**
+ * This exception signals that duplicate user entries were found in LDAP during authentication.
+ * The filter used to match user entry in LDAP that corresponds to the user being authenticated
+ * should be refined to match only one entry.
+ */
+public class DuplicateLdapUserFoundAuthenticationException extends AuthenticationException {
+
+  /**
+   * Constructs an {@code DuplicateLdapUserFoundAuthenticationException} with the specified message.
+   *
+   * @param msg the detail message
+   */
+  public DuplicateLdapUserFoundAuthenticationException(String msg) {
+    super(msg);
+  }
+
+  /**
+   * Constructs an {@code DuplicateLdapUserFoundAuthenticationException} with the specified message and root cause.
+   *
+   * @param msg the detail message
+   * @param t   the root cause
+   */
+  public DuplicateLdapUserFoundAuthenticationException(String msg, Throwable t) {
+    super(msg, t);
+  }
+
+
+}

+ 39 - 5
ambari-server/src/main/java/org/apache/ambari/server/security/authorization/LdapServerProperties.java

@@ -53,7 +53,8 @@ public class LdapServerProperties {
   private String userSearchBase = "";
 
   private String groupSearchFilter;
-  private static final String userSearchFilter = "(&({attribute}={0})(objectClass={userObjectClass}))";
+  private String userSearchFilter;
+  private String alternateUserSearchFilter; // alternate user search filter to be used when users use their alternate login id (e.g. User Principal Name)
 
   //LDAP pagination properties
   private boolean paginationEnabled = true;
@@ -137,10 +138,17 @@ public class LdapServerProperties {
     this.userSearchBase = userSearchBase;
   }
 
-  public String getUserSearchFilter() {
-    return userSearchFilter
-      .replace("{attribute}", usernameAttribute)
-      .replace("{userObjectClass}", userObjectClass);
+  /**
+   * Returns the LDAP filter to search users by.
+   * @param useAlternateUserSearchFilter if true than return LDAP filter that expects user name in
+   *                                  User Principal Name format to filter users constructed from {@value org.apache.ambari.server.configuration.Configuration#LDAP_ALT_USER_SEARCH_FILTER_KEY}.
+   *                                  Otherwise the filter is constructed from {@value org.apache.ambari.server.configuration.Configuration#LDAP_USER_SEARCH_FILTER_KEY}
+   * @return the LDAP filter string
+   */
+  public String getUserSearchFilter(boolean useAlternateUserSearchFilter) {
+    String filter = useAlternateUserSearchFilter ? alternateUserSearchFilter : userSearchFilter;
+
+    return resolveUserSearchFilterPlaceHolders(filter);
   }
 
   public String getUsernameAttribute() {
@@ -199,6 +207,15 @@ public class LdapServerProperties {
     this.groupSearchFilter = groupSearchFilter;
   }
 
+
+  public void setUserSearchFilter(String userSearchFilter) {
+    this.userSearchFilter = userSearchFilter;
+  }
+
+  public void setAlternateUserSearchFilter(String alternateUserSearchFilter) {
+    this.alternateUserSearchFilter = alternateUserSearchFilter;
+  }
+
   public boolean isGroupMappingEnabled() {
     return groupMappingEnabled;
   }
@@ -288,6 +305,10 @@ public class LdapServerProperties {
 
     if (paginationEnabled != that.isPaginationEnabled()) return false;
 
+    if (userSearchFilter != null ? !userSearchFilter.equals(that.userSearchFilter) : that.userSearchFilter != null) return false;
+    if (alternateUserSearchFilter != null ? !alternateUserSearchFilter.equals(that.alternateUserSearchFilter) : that.alternateUserSearchFilter != null) return false;
+
+
     return true;
   }
 
@@ -311,7 +332,20 @@ public class LdapServerProperties {
     result = 31 * result + (groupSearchFilter != null ? groupSearchFilter.hashCode() : 0);
     result = 31 * result + (dnAttribute != null ? dnAttribute.hashCode() : 0);
     result = 31 * result + (referralMethod != null ? referralMethod.hashCode() : 0);
+    result = 31 * result + (userSearchFilter != null ? userSearchFilter.hashCode() : 0);
+    result = 31 * result + (alternateUserSearchFilter != null ? alternateUserSearchFilter.hashCode() : 0);
     return result;
   }
 
+  /**
+   * Resolves known placeholders found within the given ldap user search ldap filter
+   * @param filter
+   * @return returns the filter with the resolved placeholders.
+   */
+  protected String resolveUserSearchFilterPlaceHolders(String filter) {
+    return filter
+      .replace("{usernameAttribute}", usernameAttribute)
+      .replace("{userObjectClass}", userObjectClass);
+  }
+
 }

+ 1 - 0
ambari-server/src/main/resources/webapp/WEB-INF/spring-security.xml

@@ -49,5 +49,6 @@
 
   <beans:bean id="basicFilter" class="org.springframework.security.web.authentication.www.BasicAuthenticationFilter">
     <beans:constructor-arg ref="authenticationManager"/>
+    <beans:constructor-arg ref="ambariEntryPoint"/>
   </beans:bean>
 </beans:beans>

+ 196 - 0
ambari-server/src/test/java/org/apache/ambari/server/api/UserNameOverrideFilterTest.java

@@ -0,0 +1,196 @@
+/**
+ * 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.ambari.server.api;
+
+import java.net.URLEncoder;
+import java.util.regex.Matcher;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.ambari.server.security.authorization.AuthorizationHelper;
+import org.easymock.Capture;
+import org.easymock.EasyMockRule;
+import org.easymock.EasyMockSupport;
+import org.easymock.Mock;
+import org.easymock.MockType;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.easymock.PowerMock;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.same;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(PowerMockRunner.class)               // Allow mocking static methods
+@PrepareForTest(AuthorizationHelper.class)    // This class has a static method that will be mocked
+public class UserNameOverrideFilterTest extends EasyMockSupport {
+
+  @Rule
+  public EasyMockRule mocks = new EasyMockRule(this);
+
+  @Mock(type = MockType.NICE)
+  private HttpServletRequest userRelatedRequest;
+
+  @Mock(type = MockType.NICE)
+  private ServletResponse response;
+
+  @Mock(type = MockType.NICE)
+  private FilterChain filterChain;
+
+  private UserNameOverrideFilter filter = new UserNameOverrideFilter();
+
+
+  @Test
+  public void testGetUserNameMatcherNoUserNameInUri() throws Exception {
+    // Given
+    String uri = "/aaa/bbb";
+
+    // When
+    Matcher m = filter.getUserNameMatcher(uri);
+    boolean isMatch = m.matches();
+
+    // Then
+    assertFalse(isMatch);
+  }
+
+  @Test
+  public void testGetUserNameMatcherNoPostInUri() throws Exception {
+    // Given
+    String uri = "/aaa/users/user1@domain";
+
+    // When
+    Matcher m = filter.getUserNameMatcher(uri);
+    boolean isMatch = m.find();
+
+    String pre = isMatch ? m.group("pre") : null;
+    String userName = isMatch ? m.group("username") : null;
+    String post = isMatch ? m.group("post") : null;
+
+
+    // Then
+    assertTrue(isMatch);
+
+    assertEquals("/aaa/users/", pre);
+    assertEquals("user1@domain", userName);
+    assertEquals("", post);
+  }
+
+
+
+  @Test
+  public void testGetUserNameMatcherPostInUri() throws Exception {
+    // Given
+    String uri = "/aaa/users/user1@domain/privileges";
+
+    // When
+    Matcher m = filter.getUserNameMatcher(uri);
+    boolean isMatch = m.find();
+
+    String pre = isMatch ? m.group("pre") : null;
+    String userName = isMatch ? m.group("username") : null;
+    String post = isMatch ? m.group("post") : null;
+
+
+    // Then
+    assertTrue(isMatch);
+
+    assertEquals("/aaa/users/", pre);
+    assertEquals("user1@domain", userName);
+    assertEquals("/privileges", post);
+  }
+
+  @Test
+  public void testDoFilterNoUserNameInUri() throws Exception {
+    // Given
+    expect(userRelatedRequest.getRequestURI()).andReturn("/test/test1").anyTimes();
+    filterChain.doFilter(same(userRelatedRequest), same(response));
+    expectLastCall();
+
+    replayAll();
+
+    // When
+    filter.doFilter(userRelatedRequest, response, filterChain);
+
+    // Then
+
+    verifyAll();
+  }
+
+  @Test
+  public void testDoFilterWithUserNameInUri() throws Exception {
+    // Given
+    expect(userRelatedRequest.getRequestURI()).andReturn("/test/users/testUserName/test1").anyTimes();
+
+    // filterChain should be invoked with the same req and resp as the OverrideUserName filter doesn't change these
+    filterChain.doFilter(same(userRelatedRequest), same(response));
+    expectLastCall();
+
+    replayAll();
+
+    // When
+    filter.doFilter(userRelatedRequest, response, filterChain);
+
+    // Then
+
+    verifyAll();
+  }
+
+  @Test
+  public void testDoFilterWithLoginAliasInUri() throws Exception {
+    // Given
+    expect(userRelatedRequest.getRequestURI()).andReturn(String.format("/test/users/%s/test1", URLEncoder.encode("testLoginAlias@testdomain.com", "UTF-8"))).anyTimes();
+
+    Capture<ServletRequest> requestCapture = Capture.newInstance();
+    filterChain.doFilter(capture(requestCapture), same(response));
+    expectLastCall();
+
+    PowerMock.mockStatic(AuthorizationHelper.class);
+    expect(AuthorizationHelper.resolveLoginAliasToUserName(eq("testLoginAlias@testdomain.com"))).andReturn("testuser1");
+
+    PowerMock.replay(AuthorizationHelper.class);
+    replayAll();
+
+    // When
+    filter.doFilter(userRelatedRequest, response, filterChain);
+
+    // Then
+    HttpServletRequest updatedRequest = (HttpServletRequest)requestCapture.getValue();
+    assertEquals("testLoginAlias@testdomain.com login alias in the request Uri should be resolved to testuser1 user name !", "/test/users/testuser1/test1", updatedRequest.getRequestURI());
+
+    PowerMock.verify(AuthorizationHelper.class);
+    verifyAll();
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    resetAll();
+  }
+}

+ 56 - 0
ambari-server/src/test/java/org/apache/ambari/server/configuration/ConfigurationTest.java

@@ -658,4 +658,60 @@ public class ConfigurationTest {
     Assert.assertEquals(actualCacheEnabledConfig, Configuration.SERVER_HRC_STATUS_SUMMARY_CACHE_ENABLED_DEFAULT);
   }
 
+  @Test
+  public void testLdapUserSearchFilterDefault() throws Exception {
+    // Given
+    final Properties ambariProperties = new Properties();
+    final Configuration configuration = new Configuration(ambariProperties);
+
+    // When
+    String actualLdapUserSearchFilter = configuration.getLdapServerProperties().getUserSearchFilter(false);
+
+    // Then
+    Assert.assertEquals("(&(uid={0})(objectClass=person))", actualLdapUserSearchFilter);
+  }
+
+  @Test
+  public void testLdapUserSearchFilter() throws Exception {
+    // Given
+    final Properties ambariProperties = new Properties();
+    final Configuration configuration = new Configuration(ambariProperties);
+    ambariProperties.setProperty(Configuration.LDAP_USERNAME_ATTRIBUTE_KEY, "test_uid");
+    ambariProperties.setProperty(Configuration.LDAP_USER_SEARCH_FILTER_KEY, "{usernameAttribute}={0}");
+
+    // When
+    String actualLdapUserSearchFilter = configuration.getLdapServerProperties().getUserSearchFilter(false);
+
+    // Then
+    Assert.assertEquals("test_uid={0}", actualLdapUserSearchFilter);
+  }
+
+  @Test
+  public void testAlternateLdapUserSearchFilterDefault() throws Exception {
+    // Given
+    final Properties ambariProperties = new Properties();
+    final Configuration configuration = new Configuration(ambariProperties);
+
+    // When
+    String actualLdapUserSearchFilter = configuration.getLdapServerProperties().getUserSearchFilter(true);
+
+    // Then
+    Assert.assertEquals("(&(userPrincipalName={0})(objectClass=person))", actualLdapUserSearchFilter);
+  }
+
+  @Test
+  public void testAlternatLdapUserSearchFilter() throws Exception {
+    // Given
+    final Properties ambariProperties = new Properties();
+    final Configuration configuration = new Configuration(ambariProperties);
+    ambariProperties.setProperty(Configuration.LDAP_USERNAME_ATTRIBUTE_KEY, "test_uid");
+    ambariProperties.setProperty(Configuration.LDAP_ALT_USER_SEARCH_FILTER_KEY, "{usernameAttribute}={5}");
+
+    // When
+    String actualLdapUserSearchFilter = configuration.getLdapServerProperties().getUserSearchFilter(true);
+
+    // Then
+    Assert.assertEquals("test_uid={5}", actualLdapUserSearchFilter);
+  }
+
 }

+ 87 - 0
ambari-server/src/test/java/org/apache/ambari/server/security/AmbariLdapUtilsTest.java

@@ -0,0 +1,87 @@
+/**
+ * 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.ambari.server.security;
+
+import org.apache.ambari.server.security.authorization.AmbariLdapUtils;
+import org.junit.Test;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+public class AmbariLdapUtilsTest {
+
+  @Test
+  public void testIsUserPrincipalNameFormat_True() throws Exception {
+    // Given
+    String testLoginName = "testuser@domain1.d_1.com";
+
+    // When
+    boolean isUserPrincipalNameFormat = AmbariLdapUtils.isUserPrincipalNameFormat(testLoginName);
+
+    // Then
+    assertTrue(isUserPrincipalNameFormat);
+  }
+
+  @Test
+  public void testIsUserPrincipalNameFormatMultipleAtSign_True() throws Exception {
+    // Given
+    String testLoginName = "@testuser@domain1.d_1.com";
+
+    // When
+    boolean isUserPrincipalNameFormat = AmbariLdapUtils.isUserPrincipalNameFormat(testLoginName);
+
+    // Then
+    assertTrue(isUserPrincipalNameFormat);
+  }
+
+  @Test
+  public void testIsUserPrincipalNameFormat_False() throws Exception {
+    // Given
+    String testLoginName = "testuser";
+
+    // When
+    boolean isUserPrincipalNameFormat = AmbariLdapUtils.isUserPrincipalNameFormat(testLoginName);
+
+    // Then
+    assertFalse(isUserPrincipalNameFormat);
+  }
+
+  @Test
+  public void testIsUserPrincipalNameFormatWithAtSign_False() throws Exception {
+    // Given
+    String testLoginName = "@testuser";
+
+    // When
+    boolean isUserPrincipalNameFormat = AmbariLdapUtils.isUserPrincipalNameFormat(testLoginName);
+
+    // Then
+    assertFalse(isUserPrincipalNameFormat);
+  }
+
+  @Test
+  public void testIsUserPrincipalNameFormatWithAtSign1_False() throws Exception {
+    // Given
+    String testLoginName = "testuser@";
+
+    // When
+    boolean isUserPrincipalNameFormat = AmbariLdapUtils.isUserPrincipalNameFormat(testLoginName);
+
+    // Then
+    assertFalse(isUserPrincipalNameFormat);
+  }
+}

+ 333 - 0
ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariAuthenticationTest.java

@@ -0,0 +1,333 @@
+/**
+ * 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.ambari.server.security.authorization;
+
+import java.security.Principal;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.easymock.EasyMockRule;
+import org.easymock.EasyMockSupport;
+import org.easymock.Mock;
+import org.easymock.MockType;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertSame;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.verify;
+
+public class AmbariAuthenticationTest extends EasyMockSupport {
+
+  @Rule
+  public EasyMockRule mocks = new EasyMockRule(this);
+
+  @Mock(type = MockType.NICE)
+  private ServletRequestAttributes servletRequestAttributes;
+
+  @Mock(type = MockType.NICE)
+  private Authentication testAuthentication;
+
+  @Before
+  public void setUp() {
+    resetAll();
+
+    RequestContextHolder.setRequestAttributes(servletRequestAttributes);
+
+  }
+
+  @Test
+  public void testGetPrincipalNoOverride() throws Exception {
+    // Given
+    Principal origPrincipal = new Principal() {
+      @Override
+      public String getName() {
+        return "user";
+      }
+    };
+
+    Authentication authentication = new TestingAuthenticationToken(origPrincipal, "password");
+    Authentication ambariAuthentication = new AmbariAuthentication(authentication);
+
+    // When
+    Object principal = ambariAuthentication.getPrincipal();
+
+    // Then
+    assertSame(origPrincipal, principal);
+  }
+
+
+  @Test
+  public void testGetPrincipal() throws Exception {
+    // Given
+    Authentication authentication = new TestingAuthenticationToken("user", "password");
+    Authentication ambariAuthentication = new AmbariAuthentication(authentication);
+
+    // When
+    Object principal = ambariAuthentication.getPrincipal();
+
+    // Then
+    assertEquals("user", principal);
+  }
+
+  @Test
+  public void testGetPrincipalWithLoginAlias() throws Exception {
+    // Given
+    Authentication authentication = new TestingAuthenticationToken("loginAlias", "password");
+    expect(servletRequestAttributes.getAttribute(eq("loginAlias"), eq(RequestAttributes.SCOPE_SESSION)))
+      .andReturn("user").atLeastOnce();
+
+    replayAll();
+
+    Authentication ambariAuthentication = new AmbariAuthentication(authentication);
+
+    // When
+    verifyAll();
+    Object principal = ambariAuthentication.getPrincipal();
+
+    // Then
+    assertEquals("user", principal);
+  }
+
+  @Test
+  public void testGetUserDetailPrincipal() throws Exception {
+    // Given
+    UserDetails userDetails = new User("user", "password", Collections.<GrantedAuthority>emptyList());
+    Authentication authentication = new TestingAuthenticationToken(userDetails, userDetails.getPassword());
+
+    Authentication ambariAuthentication = new AmbariAuthentication(authentication);
+
+    // When
+    Object principal = ambariAuthentication.getPrincipal();
+
+    // Then
+    assertEquals(userDetails, principal);
+  }
+
+  @Test
+  public void testGetUserDetailPrincipalWithLoginAlias() throws Exception {
+    // Given
+    UserDetails userDetails = new User("loginAlias", "password", Collections.<GrantedAuthority>emptyList());
+    Authentication authentication = new TestingAuthenticationToken(userDetails, userDetails.getPassword());
+
+    expect(servletRequestAttributes.getAttribute(eq("loginAlias"), eq(RequestAttributes.SCOPE_SESSION)))
+      .andReturn("user").atLeastOnce();
+
+    replayAll();
+
+    Authentication ambariAuthentication = new AmbariAuthentication(authentication);
+
+    // When
+    Object principal = ambariAuthentication.getPrincipal();
+
+    // Then
+    verify();
+    UserDetails expectedUserDetails = new User("user", "password", Collections.<GrantedAuthority>emptyList()); // user detail with login alias resolved
+
+    assertEquals(expectedUserDetails, principal);
+  }
+
+
+
+  @Test
+  public void testGetNameNoOverride () throws Exception {
+    // Given
+    Principal origPrincipal = new Principal() {
+      @Override
+      public String getName() {
+        return "user1";
+      }
+    };
+    Authentication authentication = new TestingAuthenticationToken(origPrincipal, "password");
+    Authentication ambariAuthentication = new AmbariAuthentication(authentication);
+
+    // When
+    String name = ambariAuthentication.getName();
+
+    // Then
+    assertEquals("user1", name);
+  }
+
+  @Test
+  public void testGetName() throws Exception {
+    // Given
+    Authentication authentication = new TestingAuthenticationToken("user", "password");
+    Authentication ambariAuthentication = new AmbariAuthentication(authentication);
+
+    // When
+    String name = ambariAuthentication.getName();
+
+    // Then
+    assertEquals("user", name);
+  }
+
+  @Test
+  public void testGetNameWithLoginAlias() throws Exception {
+    // Given
+    Authentication authentication = new TestingAuthenticationToken("loginAlias", "password");
+    expect(servletRequestAttributes.getAttribute(eq("loginAlias"), eq(RequestAttributes.SCOPE_SESSION)))
+      .andReturn("user").atLeastOnce();
+
+    replayAll();
+
+    Authentication ambariAuthentication = new AmbariAuthentication(authentication);
+
+    // When
+    String name = ambariAuthentication.getName();
+
+    // Then
+    verifyAll();
+    assertEquals("user", name);
+  }
+
+  @Test
+  public void testGetNameWithUserDetailsPrincipal() throws Exception {
+    // Given
+    UserDetails userDetails = new User("user", "password", Collections.<GrantedAuthority>emptyList());
+    Authentication authentication = new TestingAuthenticationToken(userDetails, userDetails.getPassword());
+
+    Authentication ambariAuthentication = new AmbariAuthentication(authentication);
+
+    // When
+    String name = ambariAuthentication.getName();
+
+    // Then
+    assertEquals("user", name);
+  }
+
+  @Test
+  public void testGetNameWithUserDetailsPrincipalWithLoginAlias() throws Exception {
+    // Given
+    UserDetails userDetails = new User("loginAlias", "password", Collections.<GrantedAuthority>emptyList());
+    Authentication authentication = new TestingAuthenticationToken(userDetails, userDetails.getPassword());
+
+    expect(servletRequestAttributes.getAttribute(eq("loginAlias"), eq(RequestAttributes.SCOPE_SESSION)))
+      .andReturn("user").atLeastOnce();
+
+    replayAll();
+
+    Authentication ambariAuthentication = new AmbariAuthentication(authentication);
+
+    // When
+    String name = ambariAuthentication.getName();
+
+    // Then
+    verifyAll();
+    assertEquals("user", name);
+  }
+
+  @Test
+  public void testGetAuthorities() throws Exception {
+    // Given
+    Authentication authentication = new TestingAuthenticationToken("user", "password", "test_role");
+    Authentication ambariAuthentication = new AmbariAuthentication(authentication);
+
+    // When
+    Collection<?>  grantedAuthorities =  ambariAuthentication.getAuthorities();
+
+    // Then
+    Collection<?> expectedAuthorities = authentication.getAuthorities();
+
+    assertSame(expectedAuthorities, grantedAuthorities);
+  }
+
+  @Test
+  public void testGetCredentials() throws Exception {
+    // Given
+    String passord = "password";
+    Authentication authentication = new TestingAuthenticationToken("user", passord);
+    Authentication ambariAuthentication = new AmbariAuthentication(authentication);
+
+    // When
+    Object credentials = ambariAuthentication.getCredentials();
+
+    // Then
+    assertSame(passord, credentials);
+  }
+
+  @Test
+  public void testGetDetails() throws Exception {
+    // Given
+    TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password");
+    authentication.setDetails("test auth details");
+    Authentication ambariAuthentication = new AmbariAuthentication(authentication);
+
+    // When
+    Object authDetails = ambariAuthentication.getDetails();
+
+    // Then
+    Object expecteAuthDetails = authentication.getDetails();
+
+    assertSame(expecteAuthDetails, authDetails);
+  }
+
+  @Test
+  public void testIsAuthenticated() throws Exception {
+    // Given
+    expect(testAuthentication.isAuthenticated()).andReturn(false).once();
+
+    replayAll();
+
+    Authentication ambariAuthentication = new AmbariAuthentication(testAuthentication);
+
+    // When
+    ambariAuthentication.isAuthenticated();
+
+    // Then
+    verifyAll();
+  }
+
+  @Test
+  public void setTestAuthentication() throws Exception {
+    // Given
+    testAuthentication.setAuthenticated(true);
+    expectLastCall().once();
+
+    replayAll();
+
+    Authentication ambariAuthentication = new AmbariAuthentication(testAuthentication);
+
+    // When
+    ambariAuthentication.setAuthenticated(true);
+
+    // Then
+    verifyAll();
+  }
+
+  @Test
+  public void testEquals() throws Exception {
+    EqualsVerifier.forClass(AmbariAuthentication.class)
+      .verify();
+  }
+
+
+}

+ 100 - 0
ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProviderForDuplicateUserTest.java

@@ -0,0 +1,100 @@
+/**
+ * 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.ambari.server.security.authorization;
+
+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.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+
+import com.google.inject.Inject;
+
+@RunWith(FrameworkRunner.class)
+@CreateDS(allowAnonAccess = true,
+  name = "Test",
+  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", port = 33389)})
+@ApplyLdifFiles("users_with_duplicate_uid.ldif")
+public class AmbariLdapAuthenticationProviderForDuplicateUserTest extends AmbariLdapAuthenticationProviderBaseTest {
+
+  @Rule
+  public EasyMockRule mocks = new EasyMockRule(this);
+
+  @Mock(type = MockType.NICE)
+  private AmbariLdapAuthoritiesPopulator authoritiesPopulator;
+
+  private AmbariLdapAuthenticationProvider authenticationProvider;
+
+  @Before
+  public void setUp() {
+    Properties properties = new Properties();
+    properties.setProperty(Configuration.CLIENT_SECURITY_KEY, "ldap");
+    properties.setProperty(Configuration.SERVER_PERSISTENCE_TYPE_KEY, "in-memory");
+    properties.setProperty(Configuration.METADATA_DIR_PATH,"src/test/resources/stacks");
+    properties.setProperty(Configuration.SERVER_VERSION_FILE,"src/test/resources/version");
+    properties.setProperty(Configuration.OS_VERSION_KEY,"centos5");
+    properties.setProperty(Configuration.SHARED_RESOURCES_DIR_KEY, "src/test/resources/");
+    properties.setProperty(Configuration.LDAP_BASE_DN_KEY, "dc=apache,dc=org");
+
+    Configuration configuration = new Configuration(properties);
+
+    authenticationProvider = new AmbariLdapAuthenticationProvider(configuration, authoritiesPopulator);
+  }
+
+  @Test(expected = DuplicateLdapUserFoundAuthenticationException.class)
+  public void testAuthenticateDuplicateUser() throws Exception {
+    // Given
+    Authentication authentication = new UsernamePasswordAuthenticationToken("user_dup", "password");
+
+    // When
+    authenticationProvider.authenticate(authentication);
+
+    // Then
+    // DuplicateLdapUserFoundAuthenticationException should be thrown
+
+  }
+}

+ 48 - 2
ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProviderTest.java

@@ -49,6 +49,8 @@ import org.slf4j.Logger;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
+
 import static org.easymock.EasyMock.*;
 
 import static org.junit.Assert.*;
@@ -90,6 +92,7 @@ public class AmbariLdapAuthenticationProviderTest extends AmbariLdapAuthenticati
     injector.injectMembers(this);
     injector.getInstance(GuiceJpaInitializer.class);
     configuration.setClientSecurityType(ClientSecurityType.LDAP);
+    configuration.setProperty(Configuration.LDAP_ALT_USER_SEARCH_FILTER_KEY, "(&(mail={0})(objectClass={userObjectClass}))");
   }
 
   @After
@@ -116,7 +119,7 @@ public class AmbariLdapAuthenticationProviderTest extends AmbariLdapAuthenticati
     expect(exception.getCause()).andReturn(exception).atLeastOnce();
 
     expect(provider.isLdapEnabled()).andReturn(true);
-    expect(provider.loadLdapAuthenticationProvider()).andThrow(exception);
+    expect(provider.loadLdapAuthenticationProvider("notFound")).andThrow(exception);
     // Logging call
     Logger log = createNiceMock(Logger.class);
     provider.LOG = log;
@@ -155,7 +158,7 @@ public class AmbariLdapAuthenticationProviderTest extends AmbariLdapAuthenticati
     expect(exception.getCause()).andReturn(cause).atLeastOnce();
 
     expect(provider.isLdapEnabled()).andReturn(true);
-    expect(provider.loadLdapAuthenticationProvider()).andThrow(exception);
+    expect(provider.loadLdapAuthenticationProvider("notFound")).andThrow(exception);
     // Logging call
     Logger log = createNiceMock(Logger.class);
     provider.LOG = log;
@@ -189,4 +192,47 @@ public class AmbariLdapAuthenticationProviderTest extends AmbariLdapAuthenticati
     Authentication auth = authenticationProvider.authenticate(authentication);
     Assert.assertTrue(auth == null);
   }
+
+  @Test
+  public void testAuthenticateLoginAlias() throws Exception {
+    // Given
+    assertNull("User alread exists in DB", userDAO.findLdapUserByName("allowedUser"));
+    Authentication authentication = new UsernamePasswordAuthenticationToken("allowedUser@ambari.apache.org", "password");
+
+
+    // When
+    Authentication result = authenticationProvider.authenticate(authentication);
+
+    // Then
+    assertTrue(result.isAuthenticated());
+  }
+
+  @Test(expected = BadCredentialsException.class)
+  public void testBadCredentialsForMissingLoginAlias() throws Exception {
+    // Given
+    assertNull("User alread exists in DB", userDAO.findLdapUserByName("allowedUser"));
+    Authentication authentication = new UsernamePasswordAuthenticationToken("missingloginalias@ambari.apache.org", "password");
+
+
+    // When
+    authenticationProvider.authenticate(authentication);
+
+    // Then
+    // BadCredentialsException should be thrown due to no user with 'missingloginalias@ambari.apache.org'  is found in ldap
+  }
+
+
+  @Test(expected = BadCredentialsException.class)
+  public void testBadCredentialsBadPasswordForLoginAlias() throws Exception {
+    // Given
+    assertNull("User alread exists in DB", userDAO.findLdapUserByName("allowedUser"));
+    Authentication authentication = new UsernamePasswordAuthenticationToken("allowedUser@ambari.apache.org", "bad_password");
+
+
+    // When
+    authenticationProvider.authenticate(authentication);
+
+    // Then
+    // BadCredentialsException should be thrown due to wrong password
+  }
 }

+ 136 - 0
ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticatorTest.java

@@ -0,0 +1,136 @@
+
+/**
+ * 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.ambari.server.security.authorization;
+
+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.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.ldap.core.DirContextOperations;
+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.eq;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.junit.Assert.assertEquals;
+
+
+@RunWith(FrameworkRunner.class)
+@CreateDS(allowAnonAccess = true,
+  name = "Test",
+  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", port = 33389)})
+@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 testAuthenticateWithLoginAlias() throws Exception {
+    // Given
+
+    LdapContextSource ldapCtxSource = new LdapContextSource();
+    ldapCtxSource.setUrls(new String[] {"ldap://localhost:33389"});
+    ldapCtxSource.setBase("dc=ambari,dc=apache,dc=org");
+    ldapCtxSource.afterPropertiesSet();
+
+    Properties properties = new Properties();
+    properties.setProperty(Configuration.CLIENT_SECURITY_KEY, "ldap");
+    properties.setProperty(Configuration.SERVER_PERSISTENCE_TYPE_KEY, "in-memory");
+    properties.setProperty(Configuration.METADATA_DIR_PATH,"src/test/resources/stacks");
+    properties.setProperty(Configuration.SERVER_VERSION_FILE,"src/test/resources/version");
+    properties.setProperty(Configuration.OS_VERSION_KEY,"centos5");
+    properties.setProperty(Configuration.SHARED_RESOURCES_DIR_KEY, "src/test/resources/");
+    properties.setProperty(Configuration.LDAP_BASE_DN_KEY, "dc=ambari,dc=apache,dc=org");
+
+    Configuration configuration = new Configuration(properties);
+
+    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");
+
+    RequestContextHolder.setRequestAttributes(servletRequestAttributes);
+
+    servletRequestAttributes.setAttribute(eq(loginAlias), eq(userName), eq(RequestAttributes.SCOPE_SESSION));
+    expectLastCall().once();
+
+    replayAll();
+
+    // When
+
+    DirContextOperations user = bindAuthenticator.authenticate(authentication);
+
+    // Then
+
+    verifyAll();
+
+    String ldapUserNameAttribute = configuration.getLdapServerProperties().getUsernameAttribute();
+
+    assertEquals(userName, user.getStringAttribute(ldapUserNameAttribute));
+  }
+}

+ 111 - 5
ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AuthorizationHelperTest.java

@@ -17,10 +17,6 @@
  */
 package org.apache.ambari.server.security.authorization;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -36,16 +32,40 @@ import org.apache.ambari.server.orm.entities.PrivilegeEntity;
 import org.apache.ambari.server.orm.entities.ResourceEntity;
 import org.apache.ambari.server.orm.entities.ResourceTypeEntity;
 import org.apache.ambari.server.orm.entities.RoleAuthorizationEntity;
+import org.easymock.EasyMockRule;
+import org.easymock.Mock;
+import org.easymock.MockType;
 import org.junit.Assert;
+import org.junit.Rule;
 import org.junit.Test;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
+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.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.reset;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 public class AuthorizationHelperTest {
 
+  @Rule
+  public EasyMockRule mocks = new EasyMockRule(this);
+
+  @Mock(type = MockType.NICE)
+  private ServletRequestAttributes servletRequestAttributes;
+
+
   @Test
   public void testConvertPrivilegesToAuthorities() throws Exception {
     Collection<PrivilegeEntity> privilegeEntities = new ArrayList<PrivilegeEntity>();
@@ -119,6 +139,29 @@ public class AuthorizationHelperTest {
 
   }
 
+  @Test
+  public void testLoginAliasAuthName() throws Exception {
+
+    reset(servletRequestAttributes);
+
+    RequestContextHolder.setRequestAttributes(servletRequestAttributes);
+    expect(servletRequestAttributes.getAttribute(eq("user1@domain.com"), eq(RequestAttributes.SCOPE_SESSION)))
+      .andReturn("user1").atLeastOnce(); // user1@domain.com is a login alias for user1
+
+    replay(servletRequestAttributes);
+
+    Authentication auth = new UsernamePasswordAuthenticationToken("user1@domain.com", null);
+    SecurityContextHolder.getContext().setAuthentication(new AmbariAuthentication(auth));
+
+    String user = AuthorizationHelper.getAuthenticatedName();
+    Assert.assertEquals("user1", user);
+
+    SecurityContextHolder.getContext().setAuthentication(null); // clean up security context
+
+    verify(servletRequestAttributes);
+
+  }
+
   @Test
   public void testIsAuthorized() {
     RoleAuthorizationEntity readOnlyRoleAuthorizationEntity = new RoleAuthorizationEntity();
@@ -254,7 +297,6 @@ public class AuthorizationHelperTest {
     assertTrue(AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, 1L, EnumSet.of(RoleAuthorization.AMBARI_MANAGE_USERS)));
   }
 
-  @Test
   public void testIsAuthorizedForSpecificView() {
     RoleAuthorizationEntity readOnlyRoleAuthorizationEntity = new RoleAuthorizationEntity();
     readOnlyRoleAuthorizationEntity.setAuthorizationId(RoleAuthorization.CLUSTER_VIEW_METRICS.getId());
@@ -337,6 +379,70 @@ public class AuthorizationHelperTest {
     assertTrue(AuthorizationHelper.isAuthorized(ResourceType.VIEW, 50L, permissionsViewUse));
   }
 
+  public void testAddLoginNameAlias() throws Exception {
+    // Given
+    reset(servletRequestAttributes);
+
+    RequestContextHolder.setRequestAttributes(servletRequestAttributes);
+    servletRequestAttributes.setAttribute(eq("loginAlias"), eq("user"), eq(RequestAttributes.SCOPE_SESSION));
+    expectLastCall().once();
+
+    replay(servletRequestAttributes);
+
+    // When
+    AuthorizationHelper.addLoginNameAlias("user","loginAlias");
+
+    // Then
+    verify(servletRequestAttributes);
+  }
+
+  @Test
+  public void testResolveLoginAliasToUserName() throws Exception {
+    // Given
+    reset(servletRequestAttributes);
+
+    RequestContextHolder.setRequestAttributes(servletRequestAttributes);
+
+    expect(servletRequestAttributes.getAttribute(eq("loginAlias1"), eq(RequestAttributes.SCOPE_SESSION)))
+      .andReturn("user1").atLeastOnce();
+
+    replay(servletRequestAttributes);
+
+    // When
+    String user = AuthorizationHelper.resolveLoginAliasToUserName("loginAlias1");
+
+    // Then
+    verify(servletRequestAttributes);
+
+    assertEquals("user1", user);
+  }
+
+  @Test
+  public void testResolveNoLoginAliasToUserName() throws Exception {
+    reset(servletRequestAttributes);
+
+    // No request attributes/http session available yet
+    RequestContextHolder.setRequestAttributes(null);
+    assertEquals("user", AuthorizationHelper.resolveLoginAliasToUserName("user"));
+
+
+    // request attributes available but user doesn't have any login aliases
+    RequestContextHolder.setRequestAttributes(servletRequestAttributes);
+
+    expect(servletRequestAttributes.getAttribute(eq("nosuchalias"), eq(RequestAttributes.SCOPE_SESSION)))
+      .andReturn(null).atLeastOnce();
+
+    replay(servletRequestAttributes);
+
+    // When
+    String user = AuthorizationHelper.resolveLoginAliasToUserName("nosuchalias");
+
+    // Then
+    verify(servletRequestAttributes);
+
+    assertEquals("nosuchalias", user);
+  }
+
   private class TestAuthentication implements Authentication {
     private final Collection<? extends GrantedAuthority> grantedAuthorities;
 

+ 21 - 2
ambari-server/src/test/java/org/apache/ambari/server/security/authorization/LdapServerPropertiesTest.java

@@ -77,9 +77,20 @@ public class LdapServerPropertiesTest {
 
   @Test
   public void testGetUserSearchFilter() throws Exception {
-    assertEquals(INCORRECT_USER_SEARCH_FILTER, "(&(uid={0})(objectClass=dummyObjectClass))", ldapServerProperties.getUserSearchFilter());
+    ldapServerProperties.setUserSearchFilter("(&({usernameAttribute}={0})(objectClass={userObjectClass}))");
+    assertEquals(INCORRECT_USER_SEARCH_FILTER, "(&(uid={0})(objectClass=dummyObjectClass))", ldapServerProperties.getUserSearchFilter(false));
+
+    ldapServerProperties.setUsernameAttribute("anotherName");
+    assertEquals(INCORRECT_USER_SEARCH_FILTER, "(&(anotherName={0})(objectClass=dummyObjectClass))", ldapServerProperties.getUserSearchFilter(false));
+  }
+
+  @Test
+  public void testGetAlternatUserSearchFilterForUserPrincipalName() throws Exception {
+    ldapServerProperties.setAlternateUserSearchFilter("(&({usernameAttribute}={0})(objectClass={userObjectClass}))");
+    assertEquals(INCORRECT_USER_SEARCH_FILTER, "(&(uid={0})(objectClass=dummyObjectClass))", ldapServerProperties.getUserSearchFilter(true));
+
     ldapServerProperties.setUsernameAttribute("anotherName");
-    assertEquals(INCORRECT_USER_SEARCH_FILTER, "(&(anotherName={0})(objectClass=dummyObjectClass))", ldapServerProperties.getUserSearchFilter());
+    assertEquals(INCORRECT_USER_SEARCH_FILTER, "(&(anotherName={0})(objectClass=dummyObjectClass))", ldapServerProperties.getUserSearchFilter(true));
   }
 
   @Test
@@ -92,4 +103,12 @@ public class LdapServerPropertiesTest {
     properties2.setSecondaryUrl("5.6.7.8:389");
     assertFalse("Objects are equal", properties1.equals(properties2));
   }
+
+  @Test
+  public void testResolveUserSearchFilterPlaceHolders() throws Exception {
+    String ldapUserSearchFilter = "{usernameAttribute}={0}  {userObjectClass}={1}";
+    String filter = ldapServerProperties.resolveUserSearchFilterPlaceHolders(ldapUserSearchFilter);
+
+    assertEquals("uid={0}  dummyObjectClass={1}", filter);
+  }
 }

+ 42 - 0
ambari-server/src/test/java/org/apache/ambari/server/security/authorization/TestAmbariLdapAuthoritiesPopulator.java

@@ -29,6 +29,10 @@ import org.easymock.EasyMock;
 import org.easymock.EasyMockSupport;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.easymock.PowerMock;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
 import org.springframework.ldap.core.DirContextOperations;
 
 import java.util.Collections;
@@ -36,6 +40,8 @@ import java.util.LinkedList;
 import java.util.List;
 import static org.easymock.EasyMock.*;
 
+@RunWith(PowerMockRunner.class)               // Allow mocking static methods
+@PrepareForTest(AuthorizationHelper.class)    // This class has a static method that will be mocked
 public class TestAmbariLdapAuthoritiesPopulator extends EasyMockSupport {
 
   AuthorizationHelper helper = new AuthorizationHelper();
@@ -53,6 +59,7 @@ public class TestAmbariLdapAuthoritiesPopulator extends EasyMockSupport {
   @Before
   public void setUp() throws Exception {
     resetAll();
+    PowerMock.resetAll();
   }
 
   @Test
@@ -109,4 +116,39 @@ public class TestAmbariLdapAuthoritiesPopulator extends EasyMockSupport {
     verifyAll();
   }
 
+  @Test
+  public void testGetGrantedAuthoritiesWithLoginAlias() throws Exception {
+    // Given
+    String loginAlias = "testLoginAlias@testdomain.com";
+    String ambariUserName = "user";
+
+    PowerMock.mockStatic(AuthorizationHelper.class);
+    expect(AuthorizationHelper.resolveLoginAliasToUserName(loginAlias)).andReturn(ambariUserName);
+
+    PowerMock.replay(AuthorizationHelper.class);
+
+    AmbariLdapAuthoritiesPopulator populator = createMockBuilder(AmbariLdapAuthoritiesPopulator.class)
+      .withConstructor(helper, userDAO, memberDAO, privilegeDAO).createMock();
+
+    expect(userEntity.getPrincipal()).andReturn(principalEntity);
+    expect(userEntity.getActive()).andReturn(true);
+    expect(memberDAO.findAllMembersByUser(userEntity)).andReturn(Collections.singletonList(memberEntity));
+    expect(memberEntity.getGroup()).andReturn(groupEntity);
+    expect(groupEntity.getPrincipal()).andReturn(groupPrincipalEntity);
+    List<PrincipalEntity> principalEntityList = new LinkedList<PrincipalEntity>();
+    principalEntityList.add(principalEntity);
+    principalEntityList.add(groupPrincipalEntity);
+    expect(privilegeDAO.findAllByPrincipal(principalEntityList)).andReturn(Collections.singletonList(privilegeEntity));
+
+    expect(userDAO.findLdapUserByName(ambariUserName)).andReturn(userEntity); // user should be looked up by user name instead of login alias
+
+    replayAll();
+
+    // When
+    populator.getGrantedAuthorities(userData, loginAlias);
+
+    PowerMock.verify(AuthorizationHelper.class);
+    verifyAll();
+  }
+
 }

+ 1 - 0
ambari-server/src/test/resources/users.ldif

@@ -17,6 +17,7 @@ cn: CraigWalls
 sn: Walls
 uid: allowedUser
 userPassword:password
+mail:allowedUser@ambari.apache.org
 
 dn: uid=deniedUser,ou=people,dc=ambari,dc=apache,dc=org
 objectclass:top

+ 20 - 0
ambari-server/src/test/resources/users_with_duplicate_uid.ldif

@@ -0,0 +1,20 @@
+dn: uid=user_dup,dc=apache,dc=org
+objectclass:top
+objectclass:person
+objectclass:organizationalPerson
+objectclass:inetOrgPerson
+cn: CraigWalls
+sn: Walls
+uid: user_dup
+userPassword:password
+
+dn: uid=user_dup,dc=ambari,dc=apache,dc=org
+objectclass:top
+objectclass:person
+objectclass:organizationalPerson
+objectclass:inetOrgPerson
+cn: JohnSmith
+sn: Smith
+uid: user_dup
+userPassword:password
+

+ 5 - 3
ambari-web/app/controllers/login_controller.js

@@ -36,14 +36,16 @@ App.LoginController = Em.Object.extend({
   },
 
   postLogin: function (isConnected, isAuthenticated, responseText) {
+    var errorMessage = "";
     if (!isConnected) {
       this.set('errorMessage', responseText || Em.I18n.t('login.error.bad.connection'));
     } else if (!isAuthenticated) {
-      var errorMessage = "";
-      if( responseText === "User is disabled" ){
+      if (responseText === "User is disabled") {
         errorMessage = Em.I18n.t('login.error.disabled');
-      } else {
+      } else if (responseText === "Authentication required" || Em.isNone(responseText)) {
         errorMessage = Em.I18n.t('login.error.bad.credentials');
+      } else {
+        errorMessage = responseText;
       }
       this.set('errorMessage', errorMessage);
     }

+ 2 - 2
ambari-web/app/router.js

@@ -311,9 +311,9 @@ App.Router = Em.Router.extend({
     var self = this;
     App.router.set('loginController.isSubmitDisabled', false);
     App.usersMapper.map({"items": [data]});
-    this.setUserLoggedIn(decodeURIComponent(params.loginName));
+    this.setUserLoggedIn(data.Users.user_name);
     var requestData = {
-      loginName: params.loginName,
+      loginName: data.Users.user_name,
       loginData: data
     };
     App.router.get('clusterController').loadAuthorizations().complete(function() {

+ 19 - 7
ambari-web/test/controllers/login_controller_test.js

@@ -28,16 +28,28 @@ describe('App.LoginController', function () {
 
   describe('#postLogin', function() {
     it ('Should set error connect', function() {
-      loginController.postLogin(false, false, false);
-      expect(loginController.get('errorMessage')).to.be.equal('Unable to connect to Ambari Server. Confirm Ambari Server is running and you can reach Ambari Server from this machine.');
+      loginController.postLogin(false, false, null);
+      expect(loginController.get('errorMessage')).to.be.equal(Em.I18n.t('login.error.bad.connection'));
     });
-    it ('Should set error login', function() {
+    it ('Should set error connect with specific message', function() {
+      loginController.postLogin(false, false, 'specific message');
+      expect(loginController.get('errorMessage')).to.be.equal('specific message');
+    });
+    it ('Should set error user is disabled', function() {
       loginController.postLogin(true, false, 'User is disabled');
-      expect(loginController.get('errorMessage')).to.be.equal('Unable to sign in. Invalid username/password combination.');
+      expect(loginController.get('errorMessage')).to.be.equal(Em.I18n.t('login.error.disabled'));
+    });
+    it ('Should set bad credentials error', function() {
+      loginController.postLogin(true, false, 'Authentication required');
+      expect(loginController.get('errorMessage')).to.be.equal(Em.I18n.t('login.error.bad.credentials'));
+    });
+    it ('Should set bad credentials error, empty response', function() {
+      loginController.postLogin(true, false, null);
+      expect(loginController.get('errorMessage')).to.be.equal(Em.I18n.t('login.error.bad.credentials'));
     });
-    it ('Should set error', function() {
-      loginController.postLogin(true, false, '');
-      expect(loginController.get('errorMessage')).to.be.equal('Unable to sign in. Invalid username/password combination.');
+    it ('Should set custom error', function() {
+      loginController.postLogin(true, false, 'Login Failed: Please append your domain to your username and try again.  Example: user_dup@domain');
+      expect(loginController.get('errorMessage')).to.be.equal('Login Failed: Please append your domain to your username and try again.  Example: user_dup@domain');
     });
   });