/**
* This file is part of lavagna.
*
* lavagna is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* lavagna 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with lavagna. If not, see <http://www.gnu.org/licenses/>.
*/
package io.lavagna.service;
import io.lavagna.model.Key;
import io.lavagna.model.Pair;
import io.lavagna.service.LdapConnection.InitialDirContextCloseable;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Service;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static java.lang.String.format;
import static java.util.EnumSet.of;
import static java.util.Objects.requireNonNull;
@Service
public class Ldap {
private static final Logger LOG = LogManager.getLogger();
private final ConfigurationRepository configurationRepository;
private final LdapConnection ldapConnection;
public Ldap(ConfigurationRepository configurationRepository, LdapConnection ldapConnection) {
this.configurationRepository = configurationRepository;
this.ldapConnection = ldapConnection;
}
public boolean authenticate(String username, String password) {
Map<Key, String> conf = configurationRepository
.findConfigurationFor(of(Key.LDAP_SERVER_URL, Key.LDAP_MANAGER_DN, Key.LDAP_MANAGER_PASSWORD,
Key.LDAP_USER_SEARCH_BASE, Key.LDAP_USER_SEARCH_FILTER));
String providerUrl = requireNonNull(conf.get(Key.LDAP_SERVER_URL));
String ldapManagerDn = requireNonNull(conf.get(Key.LDAP_MANAGER_DN));
String ldapManagerPwd = requireNonNull(conf.get(Key.LDAP_MANAGER_PASSWORD));
String base = requireNonNull(conf.get(Key.LDAP_USER_SEARCH_BASE));
String filter = requireNonNull(conf.get(Key.LDAP_USER_SEARCH_FILTER));
//
return authenticateWithParams(providerUrl, ldapManagerDn, ldapManagerPwd, base, filter, username, password)
.getFirst();
}
public Pair<Boolean, List<String>> authenticateWithParams(String providerUrl, String ldapManagerDn,
String ldapManagerPwd, String base, String filter, String username, String password) {
requireNonNull(username);
requireNonNull(password);
List<String> msgs = new ArrayList<>();
msgs.add(format("connecting to %s with managerDn %s", providerUrl, ldapManagerDn));
try (InitialDirContextCloseable dctx = ldapConnection.context(providerUrl, ldapManagerDn, ldapManagerPwd)) {
msgs.add(format("connected [ok]"));
msgs.add(format("now searching user \"%s\" with base %s and filter %s", username, base, filter));
SearchControls sc = new SearchControls();
sc.setReturningAttributes(null);
sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
List<SearchResult> srs = Ldap.search(dctx, base,
new MessageFormat(filter).format(new Object[] { Ldap.escapeLDAPSearchFilter(username) }), sc);
if (srs.size() != 1) {
String msg = format("error for username \"%s\" we have %d results instead of 1 [error]", username,
srs.size());
msgs.add(msg);
LOG.info(msg, username, srs.size());
return Pair.Companion.of(false, msgs);
}
msgs.add("user found, now will connect with given password [ok]");
SearchResult sr = srs.get(0);
try (InitialDirContextCloseable uctx = ldapConnection.context(providerUrl, sr.getNameInNamespace(),
password)) {
msgs.add("user authenticated, everything seems ok [ok]");
return Pair.Companion.of(true, msgs);
} catch (NamingException e) {
String msg = format("error while checking with username \"%s\" with message: %s [error]", username,
e.getMessage());
msgs.add(msg);
LOG.info(msg, e);
return Pair.Companion.of(false, msgs);
}
} catch (Throwable e) {
String errMsg = format(
"error while opening the connection with message: %s [error], check the logs for a more complete trace",
e.getMessage());
msgs.add(errMsg);
msgs.add("Full stacktrace is:");
msgs.add(ExceptionUtils.getStackTrace(e));
LOG.error(errMsg, e);
return Pair.Companion.of(false, msgs);
}
}
private static List<SearchResult> search(DirContext dctx, String base, String filter, SearchControls sc)
throws NamingException {
List<SearchResult> res = new ArrayList<>();
NamingEnumeration<SearchResult> search = dctx.search(base, filter, sc);
while (search.hasMore()) {
res.add(search.next());
}
return res;
}
// imported from
// https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java .
// Checked against spring implementation too...
private static final String escapeLDAPSearchFilter(String filter) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < filter.length(); i++) {
char curChar = filter.charAt(i);
switch (curChar) {
case '\\':
sb.append("\\5c");
break;
case '*':
sb.append("\\2a");
break;
case '(':
sb.append("\\28");
break;
case ')':
sb.append("\\29");
break;
case '\u0000':
sb.append("\\00");
break;
default:
sb.append(curChar);
}
}
return sb.toString();
}
}