/*
* Password Management Servlets (PWM)
* http://www.pwm-project.org
*
* Copyright (c) 2006-2009 Novell, Inc.
* Copyright (c) 2009-2017 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.svc.intruder;
import password.pwm.AppProperty;
import password.pwm.PwmApplication;
import password.pwm.bean.EmailItemBean;
import password.pwm.bean.SessionLabel;
import password.pwm.bean.UserIdentity;
import password.pwm.bean.UserInfoBean;
import password.pwm.config.Configuration;
import password.pwm.config.FormConfiguration;
import password.pwm.config.PwmSetting;
import password.pwm.config.option.DataStorageMethod;
import password.pwm.config.option.IntruderStorageMethod;
import password.pwm.error.ErrorInformation;
import password.pwm.error.PwmError;
import password.pwm.error.PwmException;
import password.pwm.error.PwmOperationalException;
import password.pwm.error.PwmUnrecoverableException;
import password.pwm.health.HealthRecord;
import password.pwm.health.HealthStatus;
import password.pwm.health.HealthTopic;
import password.pwm.http.PwmRequest;
import password.pwm.http.PwmSession;
import password.pwm.ldap.LdapUserDataReader;
import password.pwm.ldap.UserStatusReader;
import password.pwm.svc.PwmService;
import password.pwm.svc.event.AuditEvent;
import password.pwm.svc.event.AuditRecordFactory;
import password.pwm.svc.event.SystemAuditRecord;
import password.pwm.svc.event.UserAuditRecord;
import password.pwm.svc.stats.Statistic;
import password.pwm.svc.stats.StatisticsManager;
import password.pwm.util.DataStore;
import password.pwm.util.DataStoreFactory;
import password.pwm.util.LocaleHelper;
import password.pwm.util.db.DatabaseDataStore;
import password.pwm.util.db.DatabaseTable;
import password.pwm.util.java.ClosableIterator;
import password.pwm.util.java.JavaHelper;
import password.pwm.util.java.JsonUtil;
import password.pwm.util.java.TimeDuration;
import password.pwm.util.localdb.LocalDB;
import password.pwm.util.localdb.LocalDBDataStore;
import password.pwm.util.logging.PwmLogger;
import password.pwm.util.macro.MacroMachine;
import password.pwm.util.secure.PwmRandom;
import java.io.Serializable;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
// ------------------------------ FIELDS ------------------------------
public class IntruderManager implements Serializable, PwmService {
private static final PwmLogger LOGGER = PwmLogger.forClass(IntruderManager.class);
private PwmApplication pwmApplication;
private STATUS status = STATUS.NEW;
private ErrorInformation startupError;
private Timer timer;
private final Map<RecordType, RecordManager> recordManagers = new HashMap<>();
private ServiceInfo serviceInfo = new ServiceInfo(Collections.<DataStorageMethod>emptyList());
public IntruderManager() {
for (final RecordType recordType : RecordType.values()) {
recordManagers.put(recordType, new StubRecordManager());
}
}
@Override
public STATUS status() {
return status;
}
@Override
public void init(final PwmApplication pwmApplication)
throws PwmException
{
this.pwmApplication = pwmApplication;
final Configuration config = pwmApplication.getConfig();
status = STATUS.OPENING;
if (pwmApplication.getLocalDB() == null || pwmApplication.getLocalDB().status() != LocalDB.Status.OPEN) {
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_SERVICE_NOT_AVAILABLE,"unable to start IntruderManager, LocalDB unavailable");
LOGGER.error(errorInformation.toDebugStr());
startupError = errorInformation;
status = STATUS.CLOSED;
return;
}
if (!pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.INTRUDER_ENABLE)) {
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_SERVICE_NOT_AVAILABLE,"intruder module not enabled");
LOGGER.error(errorInformation.toDebugStr());
status = STATUS.CLOSED;
return;
}
final DataStore dataStore;
{
final IntruderStorageMethod intruderStorageMethod = pwmApplication.getConfig().readSettingAsEnum(PwmSetting.INTRUDER_STORAGE_METHOD, IntruderStorageMethod.class);
final String debugMsg;
final DataStorageMethod storageMethodUsed;
switch (intruderStorageMethod) {
case AUTO:
dataStore = DataStoreFactory.autoDbOrLocalDBstore(pwmApplication, DatabaseTable.INTRUDER, LocalDB.DB.INTRUDER);
if (dataStore instanceof DatabaseDataStore) {
debugMsg = "starting using auto-configured data store, Remote Database selected";
storageMethodUsed = DataStorageMethod.DB;
} else {
debugMsg = "starting using auto-configured data store, LocalDB selected";
storageMethodUsed = DataStorageMethod.LOCALDB;
}
break;
case DATABASE:
dataStore = new DatabaseDataStore(pwmApplication.getDatabaseAccessor(), DatabaseTable.INTRUDER);
debugMsg = "starting using Remote Database data store";
storageMethodUsed = DataStorageMethod.DB;
break;
case LOCALDB:
dataStore = new LocalDBDataStore(pwmApplication.getLocalDB(), LocalDB.DB.INTRUDER);
debugMsg = "starting using LocalDB data store";
storageMethodUsed = DataStorageMethod.LOCALDB;
break;
default:
startupError = new ErrorInformation(PwmError.ERROR_UNKNOWN,"unknown storageMethod selected: " + intruderStorageMethod);
status = STATUS.CLOSED;
return;
}
LOGGER.info(debugMsg);
serviceInfo = new ServiceInfo(Collections.singletonList(storageMethodUsed));
}
final RecordStore recordStore;
{
recordStore = new DataStoreRecordStore(dataStore, this);
final String threadName = JavaHelper.makeThreadName(pwmApplication, this.getClass()) + " timer";
timer = new Timer(threadName, true);
final long maxRecordAge = Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.INTRUDER_RETENTION_TIME_MS));
final long cleanerRunFrequency = Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.INTRUDER_CLEANUP_FREQUENCY_MS));
timer.schedule(new TimerTask() {
@Override
public void run() {
try {
recordStore.cleanup(new TimeDuration(maxRecordAge));
} catch (Exception e) {
LOGGER.error("error cleaning recordStore: " + e.getMessage(),e);
}
}
},1000,cleanerRunFrequency);
}
try {
{
final IntruderSettings settings = new IntruderSettings();
settings.setCheckCount((int)config.readSettingAsLong(PwmSetting.INTRUDER_USER_MAX_ATTEMPTS));
settings.setResetDuration(new TimeDuration(1000 * config.readSettingAsLong(PwmSetting.INTRUDER_USER_RESET_TIME)));
settings.setCheckDuration(new TimeDuration(1000 * config.readSettingAsLong(PwmSetting.INTRUDER_USER_CHECK_TIME)));
if (settings.getCheckCount() == 0 || settings.getCheckDuration().getTotalMilliseconds() == 0 || settings.getResetDuration().getTotalMilliseconds() == 0) {
LOGGER.info("intruder user checking will remain disabled due to configuration settings");
} else {
recordManagers.put(RecordType.USERNAME, new RecordManagerImpl(RecordType.USERNAME, recordStore, settings));
recordManagers.put(RecordType.USER_ID, new RecordManagerImpl(RecordType.USER_ID, recordStore, settings));
}
}
{
final IntruderSettings settings = new IntruderSettings();
settings.setCheckCount((int)config.readSettingAsLong(PwmSetting.INTRUDER_ATTRIBUTE_MAX_ATTEMPTS));
settings.setResetDuration(new TimeDuration(1000 * config.readSettingAsLong(PwmSetting.INTRUDER_ATTRIBUTE_RESET_TIME)));
settings.setCheckDuration(new TimeDuration(1000 * config.readSettingAsLong(PwmSetting.INTRUDER_ATTRIBUTE_CHECK_TIME)));
if (settings.getCheckCount() == 0 || settings.getCheckDuration().getTotalMilliseconds() == 0 || settings.getResetDuration().getTotalMilliseconds() == 0) {
LOGGER.info("intruder user checking will remain disabled due to configuration settings");
} else {
recordManagers.put(RecordType.ATTRIBUTE, new RecordManagerImpl(RecordType.ATTRIBUTE, recordStore, settings));
}
}
{
final IntruderSettings settings = new IntruderSettings();
settings.setCheckCount((int)config.readSettingAsLong(PwmSetting.INTRUDER_TOKEN_DEST_MAX_ATTEMPTS));
settings.setResetDuration(new TimeDuration(1000 * config.readSettingAsLong(PwmSetting.INTRUDER_TOKEN_DEST_RESET_TIME)));
settings.setCheckDuration(new TimeDuration(1000 * config.readSettingAsLong(PwmSetting.INTRUDER_TOKEN_DEST_CHECK_TIME)));
if (settings.getCheckCount() == 0 || settings.getCheckDuration().getTotalMilliseconds() == 0 || settings.getResetDuration().getTotalMilliseconds() == 0) {
LOGGER.info("intruder user checking will remain disabled due to configuration settings");
} else {
recordManagers.put(RecordType.TOKEN_DEST, new RecordManagerImpl(RecordType.TOKEN_DEST, recordStore, settings));
}
}
{
final IntruderSettings settings = new IntruderSettings();
settings.setCheckCount((int)config.readSettingAsLong(PwmSetting.INTRUDER_ADDRESS_MAX_ATTEMPTS));
settings.setResetDuration(new TimeDuration(1000 * config.readSettingAsLong(PwmSetting.INTRUDER_ADDRESS_RESET_TIME)));
settings.setCheckDuration(new TimeDuration(1000 * config.readSettingAsLong(PwmSetting.INTRUDER_ADDRESS_CHECK_TIME)));
if (settings.getCheckCount() == 0 || settings.getCheckDuration().getTotalMilliseconds() == 0 || settings.getResetDuration().getTotalMilliseconds() == 0) {
LOGGER.info("intruder address checking will remain disabled due to configuration settings");
} else {
recordManagers.put(RecordType.ADDRESS, new RecordManagerImpl(RecordType.ADDRESS, recordStore, settings));
}
}
status = STATUS.OPEN;
} catch (Exception e) {
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_SERVICE_NOT_AVAILABLE,"unexpected error starting intruder manager: " + e.getMessage());
LOGGER.error(errorInformation.toDebugStr());
startupError = errorInformation;
close();
}
}
public void clear() {
}
@Override
public void close() {
status = STATUS.CLOSED;
if (timer != null) {
timer.cancel();
timer = null;
}
}
@Override
public List<HealthRecord> healthCheck() {
if (startupError != null && status != STATUS.OPEN) {
return Collections.singletonList(new HealthRecord(HealthStatus.WARN, HealthTopic.Application,"unable to start: " + startupError.toDebugStr()));
}
return Collections.emptyList();
}
public void check(final RecordType recordType, final String subject)
throws PwmUnrecoverableException
{
if (recordType == null) {
throw new IllegalArgumentException("recordType is required");
}
if (subject == null || subject.length() < 1) {
return;
}
final RecordManager manager = recordManagers.get(recordType);
final boolean locked = manager.checkSubject(subject);
if (locked) {
switch (recordType) {
case ADDRESS:
throw new PwmUnrecoverableException(PwmError.ERROR_INTRUDER_ADDRESS);
case ATTRIBUTE:
throw new PwmUnrecoverableException(PwmError.ERROR_INTRUDER_ATTR_SEARCH);
case TOKEN_DEST:
throw new PwmUnrecoverableException(PwmError.ERROR_INTRUDER_TOKEN_DEST);
case USER_ID:
case USERNAME:
throw new PwmUnrecoverableException(PwmError.ERROR_INTRUDER_USER);
default:
JavaHelper.unhandledSwitchStatement(recordType);
}
}
}
public void clear(final RecordType recordType, final String subject)
throws PwmUnrecoverableException
{
if (recordType == null) {
throw new IllegalArgumentException("recordType is required");
}
if (subject == null || subject.length() < 1) {
return;
}
final RecordManager manager = recordManagers.get(recordType);
manager.clearSubject(subject);
}
public void mark(final RecordType recordType, final String subject, final SessionLabel sessionLabel)
throws PwmUnrecoverableException
{
if (recordType == null) {
throw new IllegalArgumentException("recordType is required");
}
if (subject == null || subject.length() < 1) {
return;
}
if (recordType == RecordType.ADDRESS) {
try {
final InetAddress inetAddress = InetAddress.getByName(subject);
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress()) {
LOGGER.debug("disregarding local address intruder attempt from: " + subject);
return;
}
} catch(Exception e) {
LOGGER.error("error examining address: " + subject);
}
}
final RecordManager manager = recordManagers.get(recordType);
manager.markSubject(subject);
if (recordType == RecordType.USER_ID) {
final UserIdentity userIdentity = UserIdentity.fromKey(subject, pwmApplication);
final UserAuditRecord auditRecord = new AuditRecordFactory(pwmApplication).createUserAuditRecord(
AuditEvent.INTRUDER_USER_ATTEMPT,
userIdentity,
sessionLabel
);
pwmApplication.getAuditManager().submit(auditRecord);
} else { // send intruder attempt audit event
final Map<String,Object> messageObj = new LinkedHashMap<>();
messageObj.put("type", recordType);
messageObj.put("subject", subject);
final String message = JsonUtil.serializeMap(messageObj);
final SystemAuditRecord auditRecord = new AuditRecordFactory(pwmApplication).createSystemAuditRecord(AuditEvent.INTRUDER_ATTEMPT,message);
pwmApplication.getAuditManager().submit(auditRecord);
}
try {
check(recordType, subject);
} catch (PwmUnrecoverableException e) {
if (!manager.isAlerted(subject) ) {
if (recordType == RecordType.USER_ID) {
final UserIdentity userIdentity = UserIdentity.fromKey(subject, pwmApplication);
final UserAuditRecord auditRecord = new AuditRecordFactory(pwmApplication).createUserAuditRecord(
AuditEvent.INTRUDER_USER_LOCK,
userIdentity,
sessionLabel
);
pwmApplication.getAuditManager().submit(auditRecord);
sendAlert(manager.readIntruderRecord(subject), sessionLabel);
} else { // send intruder attempt lock event
final Map<String,Object> messageObj = new LinkedHashMap<>();
messageObj.put("type", recordType);
messageObj.put("subject", subject);
final String message = JsonUtil.serializeMap(messageObj);
final SystemAuditRecord auditRecord = new AuditRecordFactory(pwmApplication).createSystemAuditRecord(AuditEvent.INTRUDER_LOCK,message);
pwmApplication.getAuditManager().submit(auditRecord);
}
manager.markAlerted(subject);
final StatisticsManager statisticsManager = pwmApplication.getStatisticsManager();
if (statisticsManager != null && statisticsManager.status() == STATUS.OPEN) {
statisticsManager.incrementValue(Statistic.INTRUDER_ATTEMPTS);
statisticsManager.updateEps(Statistic.EpsType.INTRUDER_ATTEMPTS,1);
statisticsManager.incrementValue(recordType.getLockStatistic());
}
}
throw e;
}
delayPenalty(manager.readIntruderRecord(subject), sessionLabel == null ? null : sessionLabel);
}
private void delayPenalty(final IntruderRecord intruderRecord, final SessionLabel sessionLabel) {
int points = 0;
if (intruderRecord != null) {
points += intruderRecord.getAttemptCount();
long delayPenalty = Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.INTRUDER_MIN_DELAY_PENALTY_MS)); // minimum
delayPenalty += points * Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.INTRUDER_DELAY_PER_COUNT_MS));
delayPenalty += PwmRandom.getInstance().nextInt((int)Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.INTRUDER_DELAY_MAX_JITTER_MS))); // add some randomness;
delayPenalty = delayPenalty > Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.INTRUDER_MAX_DELAY_PENALTY_MS)) ? Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.INTRUDER_MAX_DELAY_PENALTY_MS)) : delayPenalty;
LOGGER.trace(sessionLabel, "delaying response " + delayPenalty + "ms due to intruder record: " + JsonUtil.serialize(intruderRecord));
JavaHelper.pause(delayPenalty);
}
}
private void sendAlert(final IntruderRecord intruderRecord, final SessionLabel sessionLabel) {
if (intruderRecord == null) {
return;
}
if (intruderRecord.getType() == RecordType.USER_ID) {
try {
final UserIdentity identity = UserIdentity.fromDelimitedKey(intruderRecord.getSubject());
sendIntruderNoticeEmail(pwmApplication, sessionLabel, identity);
} catch (PwmUnrecoverableException e) {
LOGGER.error("unable to send intruder mail, can't read userDN/ldapProfile from stored record: " + e.getMessage());
}
}
}
public List<Map<String,Object>> getRecords(final RecordType recordType, final int maximum)
throws PwmOperationalException
{
final RecordManager manager = recordManagers.get(recordType);
final ArrayList<Map<String,Object>> returnList = new ArrayList<>();
ClosableIterator<IntruderRecord> theIterator = null;
try {
theIterator = manager.iterator();
while (theIterator.hasNext() && returnList.size() < maximum) {
final IntruderRecord intruderRecord = theIterator.next();
if (intruderRecord != null && intruderRecord.getType() == recordType) {
final Map<String, Object> rowData = new HashMap<>();
rowData.put("subject", intruderRecord.getSubject());
rowData.put("timestamp", intruderRecord.getTimeStamp());
rowData.put("count", String.valueOf(intruderRecord.getAttemptCount()));
try {
check(recordType, intruderRecord.getSubject());
rowData.put("status", "watching");
} catch (PwmException e) {
rowData.put("status", "locked");
}
returnList.add(rowData);
}
}
} finally {
if (theIterator != null) {
theIterator.close();
}
}
return returnList;
}
public Convenience convenience() {
return new Convenience();
}
public class Convenience {
protected Convenience() {
}
public void markAddressAndSession(final PwmSession pwmSession)
throws PwmUnrecoverableException
{
if (pwmSession != null) {
final String subject = pwmSession.getSessionStateBean().getSrcAddress();
pwmSession.getSessionStateBean().incrementIntruderAttempts();
mark(RecordType.ADDRESS, subject, pwmSession.getLabel());
}
}
public void checkAddressAndSession(final PwmSession pwmSession)
throws PwmUnrecoverableException
{
if (pwmSession != null) {
final String subject = pwmSession.getSessionStateBean().getSrcAddress();
check(RecordType.ADDRESS, subject);
final int maxAllowedAttempts = (int)pwmApplication.getConfig().readSettingAsLong(PwmSetting.INTRUDER_SESSION_MAX_ATTEMPTS);
if (maxAllowedAttempts != 0 && pwmSession.getSessionStateBean().getIntruderAttempts() > maxAllowedAttempts) {
throw new PwmUnrecoverableException(PwmError.ERROR_INTRUDER_SESSION);
}
}
}
public void clearAddressAndSession(final PwmSession pwmSession)
throws PwmUnrecoverableException
{
if (pwmSession != null) {
final String subject = pwmSession.getSessionStateBean().getSrcAddress();
clear(RecordType.ADDRESS, subject);
pwmSession.getSessionStateBean().clearIntruderAttempts();
}
}
public void markUserIdentity(final UserIdentity userIdentity, final SessionLabel sessionLabel)
throws PwmUnrecoverableException
{
if (userIdentity != null) {
final String subject = userIdentity.toDelimitedKey();
mark(RecordType.USER_ID, subject, sessionLabel);
}
}
public void markUserIdentity(final UserIdentity userIdentity, final PwmSession pwmSession)
throws PwmUnrecoverableException
{
if (userIdentity != null) {
final String subject = userIdentity.toDelimitedKey();
mark(RecordType.USER_ID, subject, pwmSession.getLabel());
}
}
public void checkUserIdentity(final UserIdentity userIdentity)
throws PwmUnrecoverableException
{
if (userIdentity != null) {
final String subject = userIdentity.toDelimitedKey();
check(RecordType.USER_ID, subject);
}
}
public void clearUserIdentity(final UserIdentity userIdentity)
throws PwmUnrecoverableException
{
if (userIdentity != null) {
final String subject = userIdentity.toDelimitedKey();
clear(RecordType.USER_ID, subject);
}
}
public void markAttributes(final Map<FormConfiguration, String> formValues, final PwmSession pwmSession)
throws PwmUnrecoverableException
{
final List<String> subjects = attributeFormToList(formValues);
for (final String subject : subjects) {
mark(RecordType.ATTRIBUTE, subject, pwmSession.getLabel());
}
}
public void clearAttributes(final Map<FormConfiguration, String> formValues)
throws PwmUnrecoverableException
{
final List<String> subjects = attributeFormToList(formValues);
for (final String subject : subjects) {
clear(RecordType.ATTRIBUTE, subject);
}
}
public void checkAttributes(final Map<FormConfiguration, String> formValues)
throws PwmUnrecoverableException
{
final List<String> subjects = attributeFormToList(formValues);
for (final String subject : subjects) {
check(RecordType.ATTRIBUTE, subject);
}
}
private List<String> attributeFormToList(final Map<FormConfiguration, String> formValues) {
final List<String> returnList = new ArrayList<>();
if (formValues != null) {
for (final FormConfiguration formConfiguration : formValues.keySet()) {
final String value = formValues.get(formConfiguration);
if (value != null && value.length() > 0) {
returnList.add(formConfiguration.getName() + ":" + value);
}
}
}
return returnList;
}
}
private static void sendIntruderNoticeEmail(
final PwmApplication pwmApplication,
final SessionLabel sessionLabel,
final UserIdentity userIdentity
)
{
final Locale locale = LocaleHelper.getLocaleForSessionID(pwmApplication, sessionLabel.getSessionID());
final Configuration config = pwmApplication.getConfig();
final EmailItemBean configuredEmailSetting = config.readSettingAsEmail(PwmSetting.EMAIL_INTRUDERNOTICE, locale);
if (configuredEmailSetting == null) {
return;
}
try {
final UserStatusReader userStatusReader = new UserStatusReader(pwmApplication, null);
final UserInfoBean userInfoBean = userStatusReader.populateUserInfoBean(
locale,
userIdentity
);
final MacroMachine macroMachine = new MacroMachine(
pwmApplication,
sessionLabel,
userInfoBean,
null,
LdapUserDataReader.appProxiedReader(pwmApplication, userIdentity));
pwmApplication.getEmailQueue().submitEmail(configuredEmailSetting, userInfoBean, macroMachine);
} catch (PwmUnrecoverableException e) {
LOGGER.error("error reading user info while sending intruder notice for user " + userIdentity + ", error: " + e.getMessage());
}
}
public ServiceInfo serviceInfo()
{
return serviceInfo;
}
public int countForNetworkEndpointInRequest(final PwmRequest pwmRequest) {
final String srcAddress = pwmRequest.getPwmSession().getSessionStateBean().getSrcAddress();
if (srcAddress == null || srcAddress.isEmpty()) {
return 0;
}
final IntruderRecord intruderRecord = recordManagers.get(RecordType.ADDRESS).readIntruderRecord(srcAddress);
if (intruderRecord == null) {
return 0;
}
return intruderRecord.getAttemptCount();
}
}