| /* |
| * 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.Collection; |
| import java.util.List; |
| |
| import org.apache.ambari.server.configuration.Configuration; |
| import org.apache.ambari.server.ldap.service.AmbariLdapConfigurationProvider; |
| import org.apache.ambari.server.orm.entities.UserAuthenticationEntity; |
| import org.apache.ambari.server.orm.entities.UserEntity; |
| import org.apache.ambari.server.security.ClientSecurityType; |
| import org.apache.ambari.server.security.authentication.AccountDisabledException; |
| import org.apache.ambari.server.security.authentication.AmbariAuthenticationProvider; |
| import org.apache.ambari.server.security.authentication.AmbariUserAuthentication; |
| import org.apache.ambari.server.security.authentication.AmbariUserDetails; |
| import org.apache.ambari.server.security.authentication.InvalidUsernamePasswordCombinationException; |
| import org.apache.ambari.server.security.authentication.TooManyLoginFailuresException; |
| import org.apache.commons.collections.CollectionUtils; |
| import org.apache.commons.lang.StringUtils; |
| 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.UsernamePasswordAuthenticationToken; |
| import org.springframework.security.core.Authentication; |
| import org.springframework.security.core.AuthenticationException; |
| import org.springframework.security.core.userdetails.UsernameNotFoundException; |
| import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; |
| import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; |
| import org.springframework.security.ldap.userdetails.LdapUserDetails; |
| |
| import com.google.inject.Inject; |
| |
| |
| /** |
| * Provides LDAP user authorization logic for Ambari Server |
| */ |
| public class AmbariLdapAuthenticationProvider extends AmbariAuthenticationProvider { |
| private static final String SYSTEM_PROPERTY_DISABLE_ENDPOINT_IDENTIFICATION = "com.sun.jndi.ldap.object.disableEndpointIdentification"; |
| private static Logger LOG = LoggerFactory.getLogger(AmbariLdapAuthenticationProvider.class); |
| |
| final AmbariLdapConfigurationProvider ldapConfigurationProvider; |
| |
| private AmbariLdapAuthoritiesPopulator authoritiesPopulator; |
| |
| private ThreadLocal<LdapServerProperties> ldapServerProperties = new ThreadLocal<>(); |
| private ThreadLocal<LdapAuthenticationProvider> providerThreadLocal = new ThreadLocal<>(); |
| private ThreadLocal<String> ldapUserSearchFilterThreadLocal = new ThreadLocal<>(); |
| |
| @Inject |
| public AmbariLdapAuthenticationProvider(Users users, Configuration configuration, AmbariLdapConfigurationProvider ldapConfigurationProvider, |
| AmbariLdapAuthoritiesPopulator authoritiesPopulator) { |
| super(users, configuration); |
| this.ldapConfigurationProvider = ldapConfigurationProvider; |
| this.authoritiesPopulator = authoritiesPopulator; |
| } |
| |
| @Override |
| public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
| if (isLdapEnabled()) { |
| if (authentication.getName() == null) { |
| LOG.info("Authentication failed: no username provided"); |
| throw new InvalidUsernamePasswordCombinationException(""); |
| } |
| |
| String username = authentication.getName().trim(); |
| |
| if (authentication.getCredentials() == null) { |
| LOG.info("Authentication failed: no credentials provided: {}", username); |
| throw new InvalidUsernamePasswordCombinationException(username); |
| } |
| |
| try { |
| Authentication auth = loadLdapAuthenticationProvider(username).authenticate(authentication); |
| UserEntity userEntity = getUserEntity(auth); |
| |
| if (userEntity == null) { |
| // TODO: If we were automatically importing accounts from the LDAP server, we should |
| // TODO: probably do it here. |
| LOG.debug("user not found ('{}')", username); |
| throw new InvalidUsernamePasswordCombinationException(username); |
| } else { |
| Users users = getUsers(); |
| |
| // Ensure the user is allowed to login.... |
| try { |
| users.validateLogin(userEntity, username); |
| } catch (AccountDisabledException | TooManyLoginFailuresException e) { |
| if (getConfiguration().showLockedOutUserMessage()) { |
| throw e; |
| } else { |
| // Do not give away information about the existence or status of a user |
| throw new InvalidUsernamePasswordCombinationException(username, false, e); |
| } |
| } |
| |
| AmbariUserDetails userDetails = new AmbariUserDetails(users.getUser(userEntity), null, users.getUserAuthorities(userEntity)); |
| return new AmbariUserAuthentication(null, userDetails, true); |
| } |
| } catch (AuthenticationException e) { |
| LOG.debug("Got exception during LDAP authentication attempt", e); |
| // Try to help in troubleshooting |
| Throwable cause = e.getCause(); |
| if ((cause != null) && (cause != e)) { |
| // Below we check the cause of an AuthenticationException to see what the actual cause is |
| // and then send an appropriate message to the caller. |
| if (cause instanceof org.springframework.ldap.CommunicationException) { |
| if (LOG.isDebugEnabled()) { |
| LOG.warn("Failed to communicate with the LDAP server: " + cause.getMessage(), e); |
| } else { |
| LOG.warn("Failed to communicate with the LDAP server: " + cause.getMessage()); |
| } |
| } else if (cause instanceof org.springframework.ldap.AuthenticationException) { |
| LOG.warn("Looks like LDAP manager credentials (that are used for " + |
| "connecting to LDAP server) are invalid.", e); |
| } |
| } |
| throw new InvalidUsernamePasswordCombinationException(username, e); |
| } catch (IncorrectResultSizeDataAccessException multipleUsersFound) { |
| String message = ldapConfigurationProvider.get().isLdapAlternateUserSearchEnabled() |
| ? String.format("Login Failed: Please append your domain to your username and try again. Example: %s@domain", username) |
| : "Login Failed: More than one user with that username found, please work with your Ambari Administrator to adjust your LDAP configuration"; |
| |
| throw new DuplicateLdapUserFoundAuthenticationException(message); |
| } |
| } else { |
| return null; |
| } |
| } |
| |
| @Override |
| public boolean supports(Class<?> authentication) { |
| return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); |
| } |
| |
| /** |
| * Reloads LDAP Context Source and depending objects if properties were changed |
| * |
| * @return corresponding LDAP authentication provider |
| */ |
| 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()])); |
| springSecurityContextSource.setBase(ldapServerProperties.get().getBaseDN()); |
| |
| if (!ldapServerProperties.get().isAnonymousBind()) { |
| springSecurityContextSource.setUserDn(ldapServerProperties.get().getManagerDn()); |
| springSecurityContextSource.setPassword(ldapServerProperties.get().getManagerPassword()); |
| } |
| |
| if (ldapServerProperties.get().isUseSsl() && ldapServerProperties.get().isDisableEndpointIdentification()) { |
| System.setProperty(SYSTEM_PROPERTY_DISABLE_ENDPOINT_IDENTIFICATION, "true"); |
| LOG.info("Disabled endpoint identification"); |
| } else { |
| System.clearProperty(SYSTEM_PROPERTY_DISABLE_ENDPOINT_IDENTIFICATION); |
| LOG.info("Removed endpoint identification disabling"); |
| } |
| |
| try { |
| springSecurityContextSource.afterPropertiesSet(); |
| } catch (Exception e) { |
| LOG.error("LDAP Context Source not loaded ", e); |
| throw new UsernameNotFoundException("LDAP Context Source not loaded", e); |
| } |
| |
| //TODO change properties |
| String userSearchBase = ldapServerProperties.get().getUserSearchBase(); |
| FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch(userSearchBase, ldapUserSearchFilter, springSecurityContextSource); |
| |
| AmbariLdapBindAuthenticator bindAuthenticator = new AmbariLdapBindAuthenticator(springSecurityContextSource, ldapConfigurationProvider.get()); |
| bindAuthenticator.setUserSearch(userSearch); |
| |
| LdapAuthenticationProvider authenticationProvider = new LdapAuthenticationProvider(bindAuthenticator, authoritiesPopulator); |
| providerThreadLocal.set(authenticationProvider); |
| } |
| |
| ldapUserSearchFilterThreadLocal.set(ldapUserSearchFilter); |
| |
| return providerThreadLocal.get(); |
| } |
| |
| |
| /** |
| * Check if LDAP authentication is enabled in server properties |
| * |
| * @return true if enabled |
| */ |
| boolean isLdapEnabled() { |
| return getConfiguration().getClientSecurityType() == ClientSecurityType.LDAP; |
| } |
| |
| /** |
| * Reloads LDAP Server properties from configuration |
| * |
| * @return true if properties were reloaded |
| */ |
| private boolean reloadLdapServerProperties() { |
| LdapServerProperties properties = ldapConfigurationProvider.get().getLdapServerProperties(); |
| if (!properties.equals(ldapServerProperties.get())) { |
| LOG.info("Reloading properties"); |
| ldapServerProperties.set(properties); |
| return true; |
| } |
| return false; |
| } |
| |
| |
| private String getLdapUserSearchFilter(String userName) { |
| return ldapServerProperties.get() |
| .getUserSearchFilter(ldapConfigurationProvider.get().isLdapAlternateUserSearchEnabled() && AmbariLdapUtils.isUserPrincipalNameFormat(userName)); |
| } |
| |
| /** |
| * Gets the {@link UserEntity} related to the authentication information |
| * <p> |
| * First the DN is retrieved from the user authentication information and a {@link UserAuthenticationEntity} |
| * is queried for where the type value is LDAP and key value case-insensitively matches the DN. |
| * If a record is found, the related {@link UserEntity} is returned. |
| * <p> |
| * Else, a {@link UserEntity} with the user name is queried. If one is found and it has a |
| * {@link UserAuthenticationEntity} where the type value is LDAP and key is empty, the related |
| * {@link UserEntity} is returned |
| * <p> |
| * Else, <code>null</code> is returned. |
| * |
| * @param authentication the user's authentication data |
| * @return a {@link UserEntity} |
| */ |
| private UserEntity getUserEntity(Authentication authentication) { |
| UserEntity userEntity = null; |
| |
| // Find user with the matching DN |
| String dn = getUserDN(authentication); |
| if (!StringUtils.isEmpty(dn)) { |
| userEntity = getUserEntityForDN(dn); |
| } |
| |
| // If a user was not found with the exact authentication properties (LDAP/dn), look up the user |
| // using the configured LDAP username attribute and ensure that user has an empty-keyed LDAP |
| // authentication entity record. |
| if (userEntity == null) { |
| String userName = AuthorizationHelper.resolveLoginAliasToUserName(authentication.getName()); |
| userEntity = getUsers().getUserEntity(userName); |
| |
| if (userEntity != null) { |
| Collection<UserAuthenticationEntity> authenticationEntities = getAuthenticationEntities(userEntity, UserAuthenticationType.LDAP); |
| UserEntity _userEntity = userEntity; // Hold on to the user entity value for now. |
| userEntity = null; // Guilty until proven innocent |
| |
| if (!CollectionUtils.isEmpty(authenticationEntities)) { |
| for (UserAuthenticationEntity entity : authenticationEntities) { |
| if (StringUtils.isEmpty(entity.getAuthenticationKey())) { |
| // Proven innocent! |
| userEntity = _userEntity; |
| break; |
| } |
| } |
| } |
| } |
| } |
| |
| return userEntity; |
| } |
| |
| /** |
| * Given a DN from the LDAP server, find the owning UserEntity. |
| * <p> |
| * DNs are case sensitive. Internally they are execpted to be stored as the bytes of the lowercase |
| * string. |
| * <p> |
| * DN's are expected to be unique across all {@link UserAuthenticationEntity} records for type |
| * UserAuthenticationType.LDAP. |
| * |
| * @param dn the DN to search for |
| * @return a {@link UserEntity}, if found |
| */ |
| private UserEntity getUserEntityForDN(String dn) { |
| Collection<UserAuthenticationEntity> authenticationEntities = getAuthenticationEntities(UserAuthenticationType.LDAP, StringUtils.lowerCase(dn)); |
| return ((authenticationEntities == null) || (authenticationEntities.size() != 1)) |
| ? null |
| : authenticationEntities.iterator().next().getUser(); |
| } |
| |
| /** |
| * Given the authentication object, attempt to retrieve the user's DN value from it. |
| * |
| * @param authentication the authentication data |
| * @return the relative DN; else <code>null</code> if not available |
| */ |
| private String getUserDN(Authentication authentication) { |
| Object objectPrincipal = (authentication == null) ? null : authentication.getPrincipal(); |
| if (objectPrincipal instanceof LdapUserDetails) { |
| return ((LdapUserDetails) objectPrincipal).getDn(); |
| } |
| |
| return null; |
| } |
| } |