/*
* LinShare is an open source filesharing software, part of the LinPKI software
* suite, developed by Linagora.
*
* Copyright (C) 2015 LINAGORA
*
* 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, provided you comply with the Additional Terms applicable for
* LinShare software by Linagora pursuant to Section 7 of the GNU Affero General
* Public License, subsections (b), (c), and (e), pursuant to which you must
* notably (i) retain the display of the “LinShare™” trademark/logo at the top
* of the interface window, the display of the “You are using the Open Source
* and free version of LinShare™, powered by Linagora © 2009–2015. Contribute to
* Linshare R&D by subscribing to an Enterprise offer!” infobox and in the
* e-mails sent with the Program, (ii) retain all hypertext links between
* LinShare and linshare.org, between linagora.com and Linagora, and (iii)
* refrain from infringing Linagora intellectual property rights over its
* trademarks and commercial brands. Other Additional Terms apply, see
* <http://www.linagora.com/licenses/> for more details.
*
* 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 and
* its applicable Additional Terms for LinShare along with this program. If not,
* see <http://www.gnu.org/licenses/> for the GNU Affero General Public License
* version 3 and <http://www.linagora.com/licenses/> for the Additional Terms
* applicable to LinShare software.
*/
package org.linagora.linshare.ldap;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.HasControls;
import org.linagora.linshare.core.domain.entities.UserLdapPattern;
import org.linagora.linshare.core.domain.entities.Internal;
import org.linagora.linshare.core.domain.entities.LdapConnection;
import org.linagora.linshare.core.domain.entities.LdapAttribute;
import org.linagora.linshare.core.domain.entities.User;
import org.linid.dm.authorization.lql.JScriptEvaluator;
import org.linid.dm.authorization.lql.LqlRequestCtx;
import org.linid.dm.authorization.lql.dnlist.IDnList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ldap.NameNotFoundException;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.ldap.authentication.BindAuthenticator;
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
import org.springframework.security.ldap.search.LdapUserSearch;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Maps;
public class JScriptLdapQuery {
/** Attributes **/
// Logger
private static final Logger logger = LoggerFactory.getLogger(JScriptLdapQuery.class);
private JScriptEvaluator evaluator;
private String baseDn;
private UserLdapPattern domainPattern;
private LqlRequestCtx lqlctx;
private IDnList dnList;
private BeanInfo beanInfo;
private Pattern cleaner;
/**
* @param jScriptEvaluator
* @param ldapJndiService
* @param ldapConnection
* @param baseDn
* @param domainPattern
*/
public JScriptLdapQuery(LqlRequestCtx ctx, String baseDn, UserLdapPattern domainPattern, IDnList dnList) throws NamingException, IOException {
super();
this.lqlctx = ctx;
this.evaluator = JScriptEvaluator.getInstance(ctx.getLdapCtx(), dnList);
this.baseDn = baseDn;
this.domainPattern = domainPattern;
this.cleaner = Pattern.compile("[;,!|*()&]");
try {
this.beanInfo = Introspector.getBeanInfo(Internal.class);
} catch (IntrospectionException e) {
logger.error("Introspection of Internal user class impossible.");
logger.debug("message : " + e.getMessage());
}
}
public String cleanLdapInputPattern(String pattern) {
return cleaner.matcher(pattern).replaceAll("");
}
/** Methods **/
public List<String> evaluate(String lqlExpression) throws NamingException {
try {
Date date_before = new Date();
evaluator = JScriptEvaluator.getInstance(lqlctx.getLdapCtx(), dnList);
List<String> evalToStringList = evaluator.evalToStringList(lqlExpression, lqlctx.getVariables());
if (logger.isDebugEnabled()) {
Date date_after = new Date();
logger.debug("diff : " + String.valueOf(date_after.getTime() - date_before.getTime()));
}
return evalToStringList;
} catch (IOException e) {
try {
lqlctx.renewLdapCtx();
} catch (NamingException e1) {
return null;
}
return evaluate(lqlExpression);
}
}
private void logLqlQuery(String command, String pattern) {
if (logger.isDebugEnabled()) {
logger.debug("lql command " + command);
logger.debug("pattern: " + pattern);
String cmd = command.replaceAll("\"[ ]*[+][ ]*pattern[ ]*[+][ ]*\"", pattern);
cmd = cmd.replaceAll("\"[ ]*[+][ ]*mail[ ]*[+][ ]*\"", pattern);
logger.debug("ldap filter : " + cmd);
}
}
private void logLqlQuery(String command, String mail, String first_name, String last_name) {
if (logger.isDebugEnabled()) {
logger.debug("lql command " + command);
logger.debug("first_name: " + first_name);
logger.debug("last_name: " + last_name);
String cmd = command.replaceAll("\"[ ]*[+][ ]*last_name[ ]*[+][ ]*\"", last_name);
cmd = cmd.replaceAll("\"[ ]*[+][ ]*first_name[ ]*[+][ ]*\"", first_name);
if (mail != null) {
cmd = cmd.replaceAll("\"[ ]*[+][ ]*mail[ ]*[+][ ]*\"", mail);
}
logger.debug("ldap filter : " + cmd);
}
}
private void logLqlQuery(String command, String first_name, String last_name) {
logLqlQuery(command, null, first_name, last_name);
}
/**
*
* @param pattern
* : could be first name, last name, or mail fragment.
* @return
* @throws NamingException
*/
public List<User> complete(String pattern) throws NamingException {
// Getting lql expression for completion
String command = domainPattern.getAutoCompleteCommandOnAllAttributes();
pattern = addExpansionCharacters(pattern);
// Setting lql query parameters
Map<String, Object> vars = lqlctx.getVariables();
vars.put("pattern", pattern);
if (logger.isDebugEnabled())
logLqlQuery(command, pattern);
// searching ldap directory with pattern
List<String> dnResultList = this.evaluate(command);
// Building user list from dn (getting needed attributes)
return dnListToUsersList(dnResultList, true);
}
/**
* Looking for a user using first name and last name. Only for auto-complete.
* @param first_name
* @param last_name
* @return
* @throws NamingException
*/
public List<User> complete(String first_name, String last_name) throws NamingException {
// Getting lql expression for completion
String command = domainPattern.getAutoCompleteCommandOnFirstAndLastName();
first_name = addExpansionCharacters(first_name);
last_name = addExpansionCharacters(last_name);
// Setting lql query parameters
Map<String, Object> vars = lqlctx.getVariables();
vars.put("first_name", first_name);
vars.put("last_name", last_name);
logLqlQuery(command, first_name, last_name);
// searching ldap directory with pattern
List<String> dnResultList = this.evaluate(command);
return dnListToUsersList(dnResultList, true);
}
/**
* This function build user list from input dn list
* @param dnResultList
* : // list of dn without baseDn used by the previous search.
* @param completionMode
* : completion mode return a user object with only mail,
* first name, and last name set. Otherwise all defined attributes
* will be search and set (mail, firstname, lastname, uid, ...)
* @return List of user
*/
private List<User> dnListToUsersList(List<String> dnResultList, boolean completionMode) {
ControlContext controlContext = initControlContext(completionMode);
// converting resulting dn to User object
List<User> users = new ArrayList<User>();
for (String dn : dnResultList) {
logger.debug("current dn: " + dn);
Date date_before = new Date();
User user = null;
try {
user = dnToUser(dn, controlContext.getLdapDbAttributes(), controlContext.getSearchControls());
} catch (NamingException e) {
logger.error(e.getMessage());
logger.debug(e.toString());
}
Date date_after = new Date();
logger.debug("fin dnToUser : " + String.valueOf(date_after.getTime() - date_before.getTime()) + " milliseconds.");
if (user != null) {
users.add(user);
}
}
return users;
}
/**
* This function build user from input dn
* @param dn
* @param completionMode
* @return
* @throws NamingException
*/
private User dnToUser(String dn, boolean completionMode) throws NamingException {
ControlContext controlContext = initControlContext(completionMode);
return dnToUser(dn, controlContext.getLdapDbAttributes(), controlContext.getSearchControls());
}
private ControlContext initControlContext(boolean completionMode) {
// Initialization of bean introspection
if (beanInfo == null) {
logger.error("Introspection of Internal user class impossible. Bean inspector is not initialised.");
return null;
}
// Get only ldap attributes needed for completion
Map<String, LdapAttribute> ldapDbAttributes;
if (completionMode) {
// Get only ldap attributes needed for completion
ldapDbAttributes = getLdapDbAttributeForCompletion();
} else {
// Get only ldap attributes
ldapDbAttributes = getLdapDbAttribute();
}
// String list of ldap attributes
Collection<String> ldapAttrList = getLdapAttrList(ldapDbAttributes);
// ldapContext ldapCtx, String base, String filter, int scope)
SearchControls scs = new SearchControls();
scs.setSearchScope(SearchControls.OBJECT_SCOPE);
// Attributes to retrieve from ldap.
logger.debug("ldap attributes to retrieve : " + ldapAttrList.toString());
scs.setReturningAttributes(ldapAttrList.toArray(new String[ldapAttrList.size()]));
return new ControlContext(ldapDbAttributes, scs);
}
private User dnToUser(String dn, Map<String, LdapAttribute> ldapDbAttributes, SearchControls scs) throws NamingException {
// returned value
User user = new Internal();
NamingEnumeration<SearchResult> results = lqlctx.getLdapCtx().search(dn, "(objectclass=*)", scs);
Integer cpt = new Integer(0);
while (results != null && results.hasMore()) {
cpt += 1;
SearchResult entry = (SearchResult) results.next();
logger.debug("processing result : " + cpt);
// Handle the entry's response controls (if any)
if (entry instanceof HasControls) {
Control[] controls = ((HasControls) entry).getControls();
if (logger.isDebugEnabled()) {
logger.debug("entry name has controls " + controls.toString());
}
}
// setting ldap attributes to user object.
for (String dbAttrKey : ldapDbAttributes.keySet()) {
LdapAttribute dbAttr = ldapDbAttributes.get(dbAttrKey);
String ldapAttrName = dbAttr.getAttribute();
Attribute ldapAttr = entry.getAttributes().get(ldapAttrName);
if (logger.isDebugEnabled()) {
logger.debug("field = " + dbAttrKey + ", ldap attribute = " + ldapAttrName);
}
boolean isNull = false;
String value = null;
try {
// ldapAttr and value can be null. ldapAttr.get() can raise
// NPE.
value = (String) ldapAttr.get();
if (logger.isDebugEnabled()) {
String size = null;
if(ldapAttr != null) size = String.valueOf(ldapAttr.size());
logger.debug("count of attribute values for : '" + ldapAttrName + "' :" + size);
}
} catch (NullPointerException e) {
isNull = true;
}
if (value == null)
isNull = true;
if (isNull) {
if (dbAttr.getSystem()) {
logger.error("Can not convert dn : '" + dn +"' to an user object.");
logger.error("The field '" + dbAttrKey + "' (ldap attribute : '" + ldapAttrName + "') must exist in your ldap directory, it is required by the system.");
return null;
} else {
if (logger.isDebugEnabled())
logger.debug("The field '" + dbAttrKey + "' (ldap attribute : '" + ldapAttrName + "') is null.");
continue;
}
} else {
logger.debug("value : " + value);
// updating user property with current attribute value
if (!setUserAttribute(user, dbAttrKey, value)) {
logger.error("Can not convert dn : '" + dn +"' to an user object.");
logger.error("Can not set the field '" + dbAttrKey + "' (ldap attribute : '" + ldapAttrName + "') with value : " + value);
return null;
}
}
}
}
return user;
}
/**
* Convert database LDAP attributes map to a attribute name list.
*
* @param ldapDbAttributes
* : map of database LDAP attributes
* @return List of attribute names.
*/
private Collection<String> getLdapAttrList(Map<String, LdapAttribute> ldapDbAttributes) {
Collection<String> ldapAttrList = Maps.transformValues(ldapDbAttributes, new Function<LdapAttribute, String>() {
public String apply(LdapAttribute input) {
return input.getAttribute();
}
}).values();
return ldapAttrList;
}
/**
* Filtering database LDAP attributes map to get only attributes needed for
* completion.
*
* @return
*/
private Map<String, LdapAttribute> getLdapDbAttributeForCompletion() {
Map<String, LdapAttribute> dbAttributes = domainPattern.getAttributes();
Predicate<LdapAttribute> completionFilter = new Predicate<LdapAttribute>() {
public boolean apply(LdapAttribute attr) {
if (attr.getEnable()) {
// Is attribute needed for completion ?
return attr.getCompletion();
}
return false;
}
};
Map<String, LdapAttribute> filterValues = Maps.filterValues(dbAttributes, completionFilter);
return filterValues;
}
/**
* Filtering database LDAP attributes map to get only attributes needed for
* build a user.
*
* @return
*/
private Map<String, LdapAttribute> getLdapDbAttribute() {
Map<String, LdapAttribute> dbAttributes = domainPattern.getAttributes();
Predicate<LdapAttribute> completionFilter = new Predicate<LdapAttribute>() {
public boolean apply(LdapAttribute attr) {
return attr.getEnable();
}
};
Map<String, LdapAttribute> filterValues = Maps.filterValues(dbAttributes, completionFilter);
return filterValues;
}
private boolean setUserAttribute(User user, String attr_key, String curValue) {
for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
Method userSetter = pd.getWriteMethod();
String method = UserLdapPattern.USER_METHOD_MAPPING.get(attr_key);
if (userSetter != null && method.equals(userSetter.getName())) {
try {
userSetter.invoke(user, curValue);
return true;
} catch (Exception e) {
logger.error("Introspection : can not call method '" + userSetter.getName() + "' on User object.");
logger.debug("message : " + e.getMessage());
break;
}
}
}
return false;
}
/**
* This method allow to search a user from part of his mail or/and first
* name or/and last name
*
* @param mail
* @param first_name
* @param last_name
* @return
* @throws NamingException
*/
public List<User> searchUser(String mail, String first_name, String last_name) throws NamingException {
// Getting lql expression for completion
String command = domainPattern.getSearchUserCommand();
mail = addExpansionCharacters(mail);
first_name = addExpansionCharacters(first_name);
last_name = addExpansionCharacters(last_name);
// Setting lql query parameters
Map<String, Object> vars = lqlctx.getVariables();
vars.put("mail", mail);
vars.put("first_name", first_name);
vars.put("last_name", last_name);
if (logger.isDebugEnabled())
logLqlQuery(command, mail, first_name, last_name);
// searching ldap directory with pattern
List<String> dnResultList = this.evaluate(command);
return dnListToUsersList(dnResultList, false);
}
/**
* This method allow to find a user from his mail (entire mail, not a
* fragment).
*
* @param mail
* @return a user
* @throws NamingException
*/
public User findUser(String mail) throws NamingException {
// Getting lql expression for completion
String command = domainPattern.getSearchUserCommand();
if (mail == null || mail.length() < 1) {
return null;
}
String first_name = "*";
String last_name = "*";
// Setting lql query parameters
Map<String, Object> vars = lqlctx.getVariables();
vars.put("mail", cleanLdapInputPattern(mail));
vars.put("first_name", first_name);
vars.put("last_name", last_name);
if (logger.isDebugEnabled())
logLqlQuery(command, mail, first_name, last_name);
// searching ldap directory with pattern
List<String> dnResultList = this.evaluate(command);
if (dnResultList.size() == 1) {
return dnToUser(dnResultList.get(0), false);
} else if (dnResultList.size() > 1) {
logger.error("mail must be unique ! " + mail);
}
return null;
}
/**
* test if a user exists using his mail. (entire mail, not a fragment)
*
* @param mail
* @return
* @throws NamingException
*/
public Boolean isUserExist(String mail) throws NamingException {
if (mail == null || mail.length() < 1) {
return false;
}
String first_name = "*";
String last_name = "*";
// Getting lql expression for completion
String command = domainPattern.getSearchUserCommand();
// Setting lql query parameters
Map<String, Object> vars = lqlctx.getVariables();
vars.put("mail", cleanLdapInputPattern(mail));
vars.put("first_name", first_name);
vars.put("last_name", last_name);
if (logger.isDebugEnabled())
logLqlQuery(command, mail, first_name, last_name);
// searching LDAP directory with pattern
List<String> dnResultList = this.evaluate(command);
if (dnResultList != null && !dnResultList.isEmpty()) {
if (dnResultList.size() == 1) {
return true;
}
logger.error("Multiple results found for mail : " + mail);
}
return false;
}
/**
* Ldap Authentification method
*
* @param login
* @param userPasswd
* @return
* @throws NamingException
*/
public User auth(LdapConnection ldapConnection, String login, String userPasswd) throws NamingException {
String command = domainPattern.getAuthCommand();
Map<String, Object> vars = lqlctx.getVariables();
vars.put("login", cleanLdapInputPattern(login));
if (logger.isDebugEnabled())
logLqlQuery(command, login);
// searching ldap directory with pattern
// InvalidSearchFilterException
List<String> dnResultList = this.evaluate(command);
if (dnResultList == null || dnResultList.size() < 1) {
throw new NameNotFoundException("No user found for login: " + login);
} else if (dnResultList.size() > 1) {
logger.error("The authentification query had returned more than one user !!!");
return null;
}
LdapContextSource ldapContextSource = new LdapContextSource();
ldapContextSource.setUrl(ldapConnection.getProviderUrl());
String securityPrincipal = ldapConnection.getSecurityPrincipal();
if (securityPrincipal != null) {
ldapContextSource.setUserDn(securityPrincipal);
}
String securityCredentials = ldapConnection.getSecurityCredentials();
if (securityCredentials != null) {
ldapContextSource.setPassword(securityCredentials);
}
String userDn = dnResultList.get(0);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDn, userPasswd);
BindAuthenticator authenticator = new BindAuthenticator(ldapContextSource);
String searchFilter= "(objectClass=*)";
String localBaseDn = userDn + "," + baseDn;
LdapUserSearch userSearch = new FilterBasedLdapUserSearch(localBaseDn, searchFilter, ldapContextSource);
authenticator.setUserSearch(userSearch);
try {
ldapContextSource.afterPropertiesSet();
authenticator.authenticate(authentication);
} catch (BadCredentialsException e) {
logger.debug("auth failed : BadCredentialsException(" + userDn + ")");
throw e;
} catch (Exception e) {
logger.error("auth failed for unexpected exception: " + e.getMessage(), e);
throw e;
}
return dnToUser(userDn, false);
}
/**
* search an user for Ldap Authentification process.
*
* @param login
* @param userPasswd
* @return
* @throws NamingException
*/
public User searchForAuth(LdapConnection ldapConnection, String login) throws NamingException {
String command = domainPattern.getAuthCommand();
Map<String, Object> vars = lqlctx.getVariables();
vars.put("login", cleanLdapInputPattern(login));
if (logger.isDebugEnabled())
logLqlQuery(command, login);
// searching ldap directory with pattern
// InvalidSearchFilterException
List<String> dnResultList = this.evaluate(command);
if (dnResultList == null || dnResultList.size() < 1) {
return null;
} else if (dnResultList.size() > 1) {
logger.error("The authentification query had returned more than one user !!!");
return null;
}
String userDn = dnResultList.get(0);
return dnToUser(userDn, false);
}
/**
* This method is designed to add expansion characters to the input string.
*
* @param string
* : any string
* @return
*/
private String addExpansionCharacters(String string) {
if (string == null || string.length() < 1) {
string = "*";
} else {
string = cleanLdapInputPattern(string);
string = "*" + string.trim() + "*";
}
return string;
}
}