/*
* This file is part of LibrePlan
*
* Copyright (C) 2011 ComtecSF S.L.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.libreplan.web.users.services;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.libreplan.business.common.IAdHocTransactionService;
import org.libreplan.business.common.IOnTransaction;
import org.libreplan.business.common.daos.IConfigurationDAO;
import org.libreplan.business.common.entities.ConfigurationRolesLDAP;
import org.libreplan.business.common.entities.LDAPConfiguration;
import org.libreplan.business.common.exceptions.InstanceNotFoundException;
import org.libreplan.business.users.daos.IUserDAO;
import org.libreplan.business.users.entities.User;
import org.libreplan.business.users.entities.UserRole;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.transaction.annotation.Transactional;
/**
* An extending from AbstractUserDetailsAuthenticationProvider class which is
* used to implement the authentication against LDAP.
*
* This provider implements the process explained in
* <https://wiki.libreplan.org/twiki/bin/view/LibrePlan/AnA04S06LdapAuthentication>
*
* At this time it authenticates user against LDAP and then searches it in BD to
* use the BD user in application.
*
* @author Ignacio Diaz Teijido <ignacio.diaz@comtecsf.es>
* @author Cristina Alvarino Perez <cristina.alvarino@comtecsf.es>
*
*/
// TODO resolve deprecated methods
public class LDAPCustomAuthenticationProvider
extends AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider {
@Autowired
private IAdHocTransactionService transactionService;
@Autowired
private IConfigurationDAO configurationDAO;
@Autowired
private IUserDAO userDAO;
private LDAPConfiguration configuration;
/** Template to search in LDAP */
private LdapTemplate ldapTemplate;
private UserDetailsService userDetailsService;
private DBPasswordEncoderService passwordEncoderService;
private static final String COLON = ":";
private static final String USER_ID_SUBSTITUTION = "[USER_ID]";
private static final Log LOG = LogFactory.getLog(LDAPCustomAuthenticationProvider.class);
/**
* LDAP role matching could be configured using an asterix (*) to specify all users or groups
*/
private static final String WILDCHAR_ALL = "*";
@Override
protected void additionalAuthenticationChecks(UserDetails arg0, UsernamePasswordAuthenticationToken arg1) {
// No needed at this time
}
@Transactional(readOnly = true)
@Override
public UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) {
String clearPassword = authentication.getCredentials().toString();
if ( StringUtils.isBlank(username) || StringUtils.isBlank(clearPassword) ) {
throw new BadCredentialsException("Username and password can not be empty");
}
String encodedPassword = passwordEncoderService.encodePassword(clearPassword, username);
User user = getUserFromDB(username);
// If user != null then exists in LibrePlan
if ( null != user && user.isLibrePlanUser() ) {
// is a LibrePlan user, then we must authenticate against DB
return authenticateInDatabase(username, user, encodedPassword);
}
// If it's a LDAP or null user, then we must authenticate against LDAP
// Load LDAPConfiguration properties
configuration = loadLDAPConfiguration();
if ( configuration.getLdapAuthEnabled() ) {
// Sets the new context to ldapTemplate
ldapTemplate.setContextSource(loadLDAPContext());
try {
// Test authentication for user against LDAP
if ( authenticateAgainstLDAP(username, clearPassword) ) {
// Authentication against LDAP was ok
if ( null == user ) {
// User does not exist in LibrePlan must be imported
user = createLDAPUserWithRoles(username, encodedPassword);
} else {
// Update password
if ( configuration.isLdapSavePasswordsDB() ) {
user.setPassword(encodedPassword);
}
// Update roles from LDAP
setRoles(user);
}
saveUserOnTransaction(user);
return loadUserDetails(username);
} else {
throw new BadCredentialsException("User is not in LDAP.");
}
} catch (Exception e) {
// This exception captures when LDAP authentication is not possible
LOG.info("LDAP not reachable. Trying to authenticate against database.", e);
}
}
// LDAP is not enabled we must check if the LDAP user is in DB
return authenticateInDatabase(username, user, encodedPassword);
}
private UserDetails loadUserDetails(String username) {
return getUserDetailsService().loadUserByUsername(username);
}
private void setRoles(User user) {
if ( configuration.getLdapSaveRolesDB() ) {
user.clearRoles();
List<String> roles = getMatchedRoles(configuration, user.getLoginName());
for (String role : roles) {
user.addRole(UserRole.valueOf(UserRole.class, role));
}
}
}
private User createLDAPUserWithRoles(String username, String encodedPassword) {
User user = User.create();
user.setLoginName(username);
String newEncodedPassword = encodedPassword;
// we must check if it is needed to save LDAP passwords in DB
if ( !configuration.isLdapSavePasswordsDB() ) {
newEncodedPassword = null;
}
user.setPassword(newEncodedPassword);
user.setLibrePlanUser(false);
user.setDisabled(false);
setRoles(user);
return user;
}
private LDAPConfiguration loadLDAPConfiguration() {
return transactionService.runOnReadOnlyTransaction(new IOnTransaction<LDAPConfiguration>() {
@Override
public LDAPConfiguration execute() {
return configurationDAO.getConfiguration().getLdapConfiguration();
}
});
}
private User getUserFromDB(String username) {
final String usernameInserted = username;
return transactionService.runOnReadOnlyTransaction(new IOnTransaction<User>() {
@Override
public User execute() {
try {
return userDAO.findByLoginName(usernameInserted);
} catch (InstanceNotFoundException e) {
LOG.info("User " + usernameInserted + " not found in database.");
return null;
}
}
});
}
private LDAPCustomContextSource loadLDAPContext() {
// Establishes the context for LDAP connection.
LDAPCustomContextSource context = (LDAPCustomContextSource) ldapTemplate.getContextSource();
context.setUrl(configuration.getLdapHost() + COLON + configuration.getLdapPort());
context.setBase(configuration.getLdapBase());
context.setUserDn(configuration.getLdapUserDn());
context.setPassword(configuration.getLdapPassword());
try {
context.afterPropertiesSet();
} catch (Exception e) {
// This exception will be never reached if the LDAP properties are well-formed.
LOG.error("There is a problem in LDAP connection: ", e);
}
return context;
}
private boolean authenticateAgainstLDAP(String username, String clearPassword) {
return ldapTemplate.authenticate(
DistinguishedName.EMPTY_PATH,
new EqualsFilter(configuration.getLdapUserId(), username).toString(),
clearPassword);
}
private void saveUserOnTransaction(User user) {
final User librePlanUser = user;
transactionService.runOnTransaction(new IOnTransaction<Void>() {
@Override
public Void execute() {
userDAO.save(librePlanUser);
return null;
}
});
}
private UserDetails authenticateInDatabase(String username, User user, String encodedPassword) {
if ( null != user && null != user.getPassword() && encodedPassword.equals(user.getPassword()) ) {
return loadUserDetails(username);
} else {
throw new BadCredentialsException("Credentials are not the same as in database.");
}
}
@SuppressWarnings("unchecked")
private List<String> getRolesUsingNodeStrategy(
Set<ConfigurationRolesLDAP> rolesLdap, String queryRoles, final LDAPConfiguration configuration) {
String roleProperty = configuration.getLdapRoleProperty();
List<String> rolesReturn = new ArrayList<>();
for (ConfigurationRolesLDAP roleLDAP : rolesLdap) {
if ( roleLDAP.getRoleLdap().equals(WILDCHAR_ALL) ) {
rolesReturn.add(roleLDAP.getRoleLibreplan());
continue;
}
// We must make a search for each role-matching in nodes
List<Attribute> resultsSearch = new ArrayList<>();
resultsSearch.addAll(ldapTemplate.search(
DistinguishedName.EMPTY_PATH,
new EqualsFilter(roleProperty, roleLDAP.getRoleLdap()).toString(),
new AttributesMapper() {
@Override
public Object mapFromAttributes(Attributes attributes) throws NamingException {
return attributes.get(configuration.getLdapUserId());
}
}));
for (Attribute atrib : resultsSearch) {
if ( atrib.contains(queryRoles) ) {
rolesReturn.add(roleLDAP.getRoleLibreplan());
}
}
}
return rolesReturn;
}
private List<String> getRolesUsingBranchStrategy(
Set<ConfigurationRolesLDAP> rolesLdap, String queryRoles, LDAPConfiguration configuration) {
String roleProperty = configuration.getLdapRoleProperty();
String groupsPath = configuration.getLdapGroupPath();
List<String> rolesReturn = new ArrayList<>();
for (ConfigurationRolesLDAP roleLdap : rolesLdap) {
if ( roleLdap.getRoleLdap().equals(WILDCHAR_ALL) ) {
rolesReturn.add(roleLdap.getRoleLibreplan());
continue;
}
// We must make a search for each role matching
DirContextAdapter adapter = null;
try {
adapter = (DirContextAdapter) ldapTemplate.lookup(roleLdap.getRoleLdap() + "," + groupsPath);
} catch (org.springframework.ldap.NamingException ne) {
LOG.error(ne.getMessage());
}
if ( adapter != null && adapter.attributeExists(roleProperty) ) {
Attributes atrs = adapter.getAttributes();
if ( atrs.get(roleProperty).contains(queryRoles) ) {
rolesReturn.add(roleLdap.getRoleLibreplan());
}
}
}
return rolesReturn;
}
private List<String> getMatchedRoles(LDAPConfiguration configuration, String username) {
String queryRoles = configuration.getLdapSearchQuery().replace(USER_ID_SUBSTITUTION, username);
Set<ConfigurationRolesLDAP> rolesLdap = configuration.getConfigurationRolesLdap();
try {
if ( !configuration.getLdapGroupStrategy() ) {
// The LDAP has a node strategy for groups, we must check the roleProperty in user node
return getRolesUsingNodeStrategy(rolesLdap, queryRoles, configuration);
} else {
// The LDAP has a branch strategy for groups we must check if the user is in one of the groups
return getRolesUsingBranchStrategy(rolesLdap, queryRoles, configuration);
}
} catch (Exception e) {
LOG.error("Configuration of LDAP role-matching is wrong. Please check it.", e);
return Collections.emptyList();
}
}
public DBPasswordEncoderService getPasswordEncoderService() {
return passwordEncoderService;
}
public void setPasswordEncoderService(DBPasswordEncoderService passwordEncoderService) {
this.passwordEncoderService = passwordEncoderService;
}
public LdapTemplate getLdapTemplate() {
return ldapTemplate;
}
public void setLdapTemplate(LdapTemplate ldapTemplate) {
this.ldapTemplate = ldapTemplate;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
}