/*
* 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.List;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.orm.dao.UserDAO;
import org.apache.ambari.server.orm.entities.UserEntity;
import org.apache.ambari.server.security.ClientSecurityType;
import org.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;
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 com.google.inject.Inject;
/**
* Provides LDAP user authorization logic for Ambari Server
*/
public class AmbariLdapAuthenticationProvider implements AuthenticationProvider {
Logger LOG = LoggerFactory.getLogger(AmbariLdapAuthenticationProvider.class);
Configuration configuration;
private AmbariLdapAuthoritiesPopulator authoritiesPopulator;
private UserDAO userDAO;
private ThreadLocal<LdapServerProperties> ldapServerProperties = new ThreadLocal<>();
private ThreadLocal<LdapAuthenticationProvider> providerThreadLocal = new ThreadLocal<>();
private ThreadLocal<String> ldapUserSearchFilterThreadLocal = new ThreadLocal<>();
@Inject
public AmbariLdapAuthenticationProvider(Configuration configuration,
AmbariLdapAuthoritiesPopulator authoritiesPopulator, UserDAO userDAO) {
this.configuration = configuration;
this.authoritiesPopulator = authoritiesPopulator;
this.userDAO = userDAO;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (isLdapEnabled()) {
String username = getUserName(authentication);
try {
Authentication auth = loadLdapAuthenticationProvider(username).authenticate(authentication);
Integer userId = getUserId(auth);
return new AmbariAuthentication(auth, userId);
} 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(e);
} catch (IncorrectResultSizeDataAccessException multipleUsersFound) {
String message = configuration.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());
}
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, configuration);
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 configuration.getClientSecurityType() == ClientSecurityType.LDAP;
}
/**
* Extracts the user name from the passed authentication object.
* @param authentication
* @return
*/
protected String getUserName(Authentication authentication) {
UsernamePasswordAuthenticationToken userToken = (UsernamePasswordAuthenticationToken)authentication;
return userToken.getName();
}
/**
* Reloads LDAP Server properties from configuration
*
* @return true if properties were reloaded
*/
private boolean reloadLdapServerProperties() {
LdapServerProperties properties = configuration.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(configuration.isLdapAlternateUserSearchEnabled() && AmbariLdapUtils.isUserPrincipalNameFormat(userName));
}
private Integer getUserId(Authentication authentication) {
String userName = AuthorizationHelper.resolveLoginAliasToUserName(authentication.getName());
UserEntity userEntity = userDAO.findLdapUserByName(userName);
// lookup is case insensitive, so no need for string comparison
if (userEntity == null) {
LOG.info("user not found ('{}')", userName);
throw new InvalidUsernamePasswordCombinationException();
}
if (!userEntity.getActive()) {
LOG.debug("User account is disabled ('{}')", userName);
throw new InvalidUsernamePasswordCombinationException();
}
return userEntity.getUserId();
}
}