/*
* Password Management Servlets (PWM)
* http://www.pwm-project.org
*
* Copyright (c) 2006-2009 Novell, Inc.
* Copyright (c) 2009-2016 The PWM Project
*
* This program 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 2 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package password.pwm.ldap;
import com.google.gson.reflect.TypeToken;
import com.novell.ldapchai.provider.ChaiProvider;
import password.pwm.AppProperty;
import password.pwm.PwmApplication;
import password.pwm.config.option.DataStorageMethod;
import password.pwm.config.profile.LdapProfile;
import password.pwm.error.ErrorInformation;
import password.pwm.error.PwmError;
import password.pwm.error.PwmException;
import password.pwm.error.PwmUnrecoverableException;
import password.pwm.health.HealthRecord;
import password.pwm.svc.PwmService;
import password.pwm.util.java.JsonUtil;
import password.pwm.util.logging.PwmLogger;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class LdapConnectionService implements PwmService {
private static final PwmLogger LOGGER = PwmLogger.forClass(LdapConnectionService.class);
private final Map<LdapProfile, Map<Integer,ChaiProvider>> proxyChaiProviders = new ConcurrentHashMap<>();
private final Map<LdapProfile, ErrorInformation> lastLdapErrors = new ConcurrentHashMap<>();
private PwmApplication pwmApplication;
private STATUS status = STATUS.NEW;
private int connectionsPerProfile = 1;
private final AtomicInteger slotIncrementer = new AtomicInteger(0);
public STATUS status()
{
return status;
}
public void init(final PwmApplication pwmApplication)
throws PwmException
{
this.pwmApplication = pwmApplication;
// read the lastLoginTime
this.lastLdapErrors.putAll(readLastLdapFailure(pwmApplication));
connectionsPerProfile = maxSlotsPerProfile(pwmApplication);
LOGGER.trace("allocating " + connectionsPerProfile + " ldap proxy connections per profile");
for (final LdapProfile ldapProfile: pwmApplication.getConfig().getLdapProfiles().values()) {
proxyChaiProviders.put(ldapProfile, new ConcurrentHashMap<>());
}
status = STATUS.OPEN;
}
public void close()
{
status = STATUS.CLOSED;
LOGGER.trace("closing ldap proxy connections");
for (final LdapProfile ldapProfile : proxyChaiProviders.keySet()) {
for (final int slot : proxyChaiProviders.get(ldapProfile).keySet()) {
final ChaiProvider existingProvider = proxyChaiProviders.get(ldapProfile).get(slot);
try {
existingProvider.close();
} catch (Exception e) {
LOGGER.error("error closing ldap proxy connection: " + e.getMessage(), e);
}
}
}
proxyChaiProviders.clear();
}
public List<HealthRecord> healthCheck()
{
return null;
}
public ServiceInfo serviceInfo()
{
return new ServiceInfo(Collections.singletonList(DataStorageMethod.LDAP));
}
public ChaiProvider getProxyChaiProvider(final String identifier)
throws PwmUnrecoverableException
{
final LdapProfile ldapProfile = pwmApplication.getConfig().getLdapProfiles().get(identifier);
return getProxyChaiProvider(ldapProfile);
}
public ChaiProvider getProxyChaiProvider(final LdapProfile identifier)
throws PwmUnrecoverableException
{
final int slot = nextSlot();
final ChaiProvider proxyChaiProvider = proxyChaiProviders.get(identifier).get(slot);
if (proxyChaiProvider != null) {
return proxyChaiProvider;
}
final LdapProfile ldapProfile = identifier == null
? pwmApplication.getConfig().getDefaultLdapProfile()
: identifier;
if (ldapProfile == null) {
final String errorMsg = "unknown ldap profile requested connection: " + identifier;
throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_NO_LDAP_CONNECTION,errorMsg));
}
try {
final ChaiProvider newProvider = LdapOperationsHelper.openProxyChaiProvider(
null,
ldapProfile,
pwmApplication.getConfig(),
pwmApplication.getStatisticsManager()
);
proxyChaiProviders.get(identifier).put(slot, newProvider);
return newProvider;
} catch (PwmUnrecoverableException e) {
setLastLdapFailure(ldapProfile,e.getErrorInformation());
throw e;
} catch (Exception e) {
final String errorMsg = "unexpected error creating new proxy ldap connection: " + e.getMessage();
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN, errorMsg);
LOGGER.error(errorInformation);
throw new PwmUnrecoverableException(errorInformation);
}
}
public void setLastLdapFailure(final LdapProfile ldapProfile, final ErrorInformation errorInformation) {
lastLdapErrors.put(ldapProfile, errorInformation);
final HashMap<String,ErrorInformation> outputMap = new HashMap<>();
for (final LdapProfile loopProfile : lastLdapErrors.keySet()) {
outputMap.put(loopProfile.getIdentifier(), lastLdapErrors.get(loopProfile));
}
final String jsonString = JsonUtil.serialize(outputMap);
pwmApplication.writeAppAttribute(PwmApplication.AppAttribute.LAST_LDAP_ERROR, jsonString);
}
public Map<LdapProfile,ErrorInformation> getLastLdapFailure() {
return Collections.unmodifiableMap(lastLdapErrors);
}
public Instant getLastLdapFailureTime(final LdapProfile ldapProfile) {
final ErrorInformation errorInformation = lastLdapErrors.get(ldapProfile);
if (errorInformation != null) {
return errorInformation.getDate();
}
return null;
}
private static Map<LdapProfile,ErrorInformation> readLastLdapFailure(final PwmApplication pwmApplication) {
String lastLdapFailureStr = null;
try {
lastLdapFailureStr = pwmApplication.readAppAttribute(PwmApplication.AppAttribute.LAST_LDAP_ERROR, String.class);
if (lastLdapFailureStr != null && lastLdapFailureStr.length() > 0) {
final Map<String, ErrorInformation> fromJson = JsonUtil.deserialize(lastLdapFailureStr,new TypeToken<Map<String, ErrorInformation>>() {});
final Map<LdapProfile, ErrorInformation> returnMap = new HashMap<>();
for (final String id : fromJson.keySet()) {
final LdapProfile ldapProfile = pwmApplication.getConfig().getLdapProfiles().get(id);
if (ldapProfile != null) {
returnMap.put(ldapProfile, fromJson.get(id));
}
}
return returnMap;
}
} catch (Exception e) {
LOGGER.error("unexpected error loading cached lastLdapFailure statuses: " + e.getMessage() + ", input=" + lastLdapFailureStr);
}
return Collections.emptyMap();
}
private int nextSlot() {
return slotIncrementer.getAndUpdate(operand -> {
operand++;
if (operand >= connectionsPerProfile) {
operand = 0;
}
return operand;
});
}
private int maxSlotsPerProfile(final PwmApplication pwmApplication) {
final int maxConnections = Integer.parseInt(pwmApplication.getConfig().readAppProperty(AppProperty.LDAP_PROXY_MAX_CONNECTIONS));
final int perProfile = Integer.parseInt(pwmApplication.getConfig().readAppProperty(AppProperty.LDAP_PROXY_CONNECTION_PER_PROFILE));
final int profileCount = pwmApplication.getConfig().getLdapProfiles().size();
if ((perProfile * profileCount) >= maxConnections) {
final int adjustedConnections = Math.min(1, (maxConnections / profileCount));
LOGGER.warn("connections per profile (" + perProfile + ") multiplied by number of profiles ("
+ profileCount + ") exceeds max connections (" + maxConnections + "), will limit to " + adjustedConnections);
return adjustedConnections;
}
return perProfile;
}
}