AMBARI-15554. Ambari LDAP integration cannot handle LDAP directories with multiple entries for the same user. (stoader)
Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/71b4c624 Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/71b4c624 Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/71b4c624 Branch: refs/heads/trunk Commit: 71b4c624fb219bb1626c238322bda6c2e5589f72 Parents: d0672c2 Author: Toader, Sebastian <stoa...@hortonworks.com> Authored: Mon Mar 28 13:04:17 2016 +0200 Committer: Toader, Sebastian <stoa...@hortonworks.com> Committed: Mon Mar 28 13:04:17 2016 +0200 ---------------------------------------------------------------------- ambari-server/conf/unix/log4j.properties | 3 + ambari-server/pom.xml | 1 + .../server/api/UserNameOverrideFilter.java | 137 ++++++++ .../server/configuration/Configuration.java | 47 +++ .../ambari/server/controller/AmbariServer.java | 2 + .../authorization/AmbariAuthentication.java | 214 ++++++++++++ .../AmbariLdapAuthenticationProvider.java | 47 ++- .../AmbariLdapAuthoritiesPopulator.java | 2 + .../AmbariLdapBindAuthenticator.java | 29 +- .../security/authorization/AmbariLdapUtils.java | 43 +++ .../authorization/AuthorizationHelper.java | 38 ++- ...ateLdapUserFoundAuthenticationException.java | 51 +++ .../authorization/LdapServerProperties.java | 44 ++- .../webapp/WEB-INF/spring-security.xml | 1 + .../server/api/UserNameOverrideFilterTest.java | 196 +++++++++++ .../server/configuration/ConfigurationTest.java | 56 ++++ .../server/security/AmbariLdapUtilsTest.java | 87 +++++ .../authorization/AmbariAuthenticationTest.java | 333 +++++++++++++++++++ ...henticationProviderForDuplicateUserTest.java | 100 ++++++ .../AmbariLdapAuthenticationProviderTest.java | 50 ++- .../AmbariLdapBindAuthenticatorTest.java | 136 ++++++++ .../authorization/AuthorizationHelperTest.java | 116 ++++++- .../authorization/LdapServerPropertiesTest.java | 23 +- .../TestAmbariLdapAuthoritiesPopulator.java | 42 +++ ambari-server/src/test/resources/users.ldif | 1 + .../resources/users_with_duplicate_uid.ldif | 20 ++ ambari-web/app/controllers/login_controller.js | 8 +- ambari-web/app/router.js | 4 +- .../test/controllers/login_controller_test.js | 26 +- 29 files changed, 1816 insertions(+), 41 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/conf/unix/log4j.properties ---------------------------------------------------------------------- diff --git a/ambari-server/conf/unix/log4j.properties b/ambari-server/conf/unix/log4j.properties index 2ee32d4..ab3ea0b 100644 --- a/ambari-server/conf/unix/log4j.properties +++ b/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 http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/pom.xml ---------------------------------------------------------------------- diff --git a/ambari-server/pom.xml b/ambari-server/pom.xml index 1e44517..857e554 100644 --- a/ambari-server/pom.xml +++ b/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> http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/main/java/org/apache/ambari/server/api/UserNameOverrideFilter.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/api/UserNameOverrideFilter.java b/ambari-server/src/main/java/org/apache/ambari/server/api/UserNameOverrideFilter.java new file mode 100644 index 0000000..04de12c --- /dev/null +++ b/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() { + + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java b/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java index bf18325..1d30f1c 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java +++ b/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. us...@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. us...@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( http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java index 076f850..4b769e3 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java +++ b/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); http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariAuthentication.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariAuthentication.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariAuthentication.java new file mode 100644 index 0000000..98b97b2 --- /dev/null +++ b/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; + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProvider.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProvider.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProvider.java index 20cf2fd..7b2a95c 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProvider.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProvider.java @@ -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(); } @@ -136,6 +148,16 @@ public class AmbariLdapAuthenticationProvider implements AuthenticationProvider } /** + * 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 * * @return true if properties were reloaded @@ -149,4 +171,11 @@ public class AmbariLdapAuthenticationProvider implements AuthenticationProvider } return false; } + + + private String getLdapUserSearchFilter(String userName) { + return ldapServerProperties.get() + .getUserSearchFilter(AmbariLdapUtils.isUserPrincipalNameFormat(userName)); + } + } http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthoritiesPopulator.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthoritiesPopulator.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthoritiesPopulator.java index fc7f73a..7df8dc3 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthoritiesPopulator.java +++ b/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; http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticator.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticator.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticator.java index ed68c01..c63ea92 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticator.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticator.java @@ -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; } /** http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapUtils.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapUtils.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapUtils.java new file mode 100644 index 0000000..ffebd45 --- /dev/null +++ b/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(); + } + + +} http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AuthorizationHelper.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AuthorizationHelper.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AuthorizationHelper.java index 0c675b8..15ef8d0 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AuthorizationHelper.java +++ b/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; + } } http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/DuplicateLdapUserFoundAuthenticationException.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/DuplicateLdapUserFoundAuthenticationException.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/DuplicateLdapUserFoundAuthenticationException.java new file mode 100644 index 0000000..dd5c754 --- /dev/null +++ b/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); + } + + +} http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/LdapServerProperties.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/LdapServerProperties.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/LdapServerProperties.java index 8eeaf35..17432d0 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/LdapServerProperties.java +++ b/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); + } + } http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/main/resources/webapp/WEB-INF/spring-security.xml ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/resources/webapp/WEB-INF/spring-security.xml b/ambari-server/src/main/resources/webapp/WEB-INF/spring-security.xml index 3bbc785..8b44b94 100644 --- a/ambari-server/src/main/resources/webapp/WEB-INF/spring-security.xml +++ b/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> http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/test/java/org/apache/ambari/server/api/UserNameOverrideFilterTest.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/test/java/org/apache/ambari/server/api/UserNameOverrideFilterTest.java b/ambari-server/src/test/java/org/apache/ambari/server/api/UserNameOverrideFilterTest.java new file mode 100644 index 0000000..1e0fe90 --- /dev/null +++ b/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("testloginal...@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("testloginal...@testdomain.com"))).andReturn("testuser1"); + + PowerMock.replay(AuthorizationHelper.class); + replayAll(); + + // When + filter.doFilter(userRelatedRequest, response, filterChain); + + // Then + HttpServletRequest updatedRequest = (HttpServletRequest)requestCapture.getValue(); + assertEquals("testloginal...@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(); + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/test/java/org/apache/ambari/server/configuration/ConfigurationTest.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/test/java/org/apache/ambari/server/configuration/ConfigurationTest.java b/ambari-server/src/test/java/org/apache/ambari/server/configuration/ConfigurationTest.java index 3ecb5aa..99ec786 100644 --- a/ambari-server/src/test/java/org/apache/ambari/server/configuration/ConfigurationTest.java +++ b/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); + } + } http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/test/java/org/apache/ambari/server/security/AmbariLdapUtilsTest.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/test/java/org/apache/ambari/server/security/AmbariLdapUtilsTest.java b/ambari-server/src/test/java/org/apache/ambari/server/security/AmbariLdapUtilsTest.java new file mode 100644 index 0000000..b2778ae --- /dev/null +++ b/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); + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariAuthenticationTest.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariAuthenticationTest.java b/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariAuthenticationTest.java new file mode 100644 index 0000000..19656b1 --- /dev/null +++ b/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(); + } + + +} http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProviderForDuplicateUserTest.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProviderForDuplicateUserTest.java b/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProviderForDuplicateUserTest.java new file mode 100644 index 0000000..f5d1412 --- /dev/null +++ b/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 + + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProviderTest.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProviderTest.java b/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProviderTest.java index d48be85..b26494c 100644 --- a/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProviderTest.java +++ b/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("allowedu...@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("missingloginal...@ambari.apache.org", "password"); + + + // When + authenticationProvider.authenticate(authentication); + + // Then + // BadCredentialsException should be thrown due to no user with 'missingloginal...@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("allowedu...@ambari.apache.org", "bad_password"); + + + // When + authenticationProvider.authenticate(authentication); + + // Then + // BadCredentialsException should be thrown due to wrong password + } } http://git-wip-us.apache.org/repos/asf/ambari/blob/71b4c624/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticatorTest.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticatorTest.java b/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapBindAuthenticatorTest.java new file mode 100644 index 0000000..27e62e2 --- /dev/null +++ b/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)); + } +}