/*
* 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.event;
import org.apache.commons.csv.CSVPrinter;
import password.pwm.AppProperty;
import password.pwm.PwmApplication;
import password.pwm.PwmApplicationMode;
import password.pwm.PwmConstants;
import password.pwm.bean.EmailItemBean;
import password.pwm.bean.SessionLabel;
import password.pwm.bean.UserInfoBean;
import password.pwm.config.Configuration;
import password.pwm.config.PwmSetting;
import password.pwm.config.option.DataStorageMethod;
import password.pwm.config.option.UserEventStorageMethod;
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.PwmSession;
import password.pwm.svc.PwmService;
import password.pwm.util.LocaleHelper;
import password.pwm.util.java.JavaHelper;
import password.pwm.util.java.JsonUtil;
import password.pwm.util.java.StringUtil;
import password.pwm.util.java.TimeDuration;
import password.pwm.util.localdb.LocalDB;
import password.pwm.util.logging.PwmLogger;
import password.pwm.util.macro.MacroMachine;
import java.io.IOException;
import java.io.OutputStream;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class AuditService implements PwmService {
private static final PwmLogger LOGGER = PwmLogger.forClass(AuditService.class);
private STATUS status = STATUS.NEW;
private AuditSettings settings;
private ServiceInfo serviceInfo = new ServiceInfo(Collections.emptyList());
private SyslogAuditService syslogManager;
private ErrorInformation lastError;
private UserHistoryStore userHistoryStore;
private AuditVault auditVault;
private PwmApplication pwmApplication;
public AuditService() {
}
public STATUS status() {
return status;
}
public void init(final PwmApplication pwmApplication) throws PwmException {
this.status = STATUS.OPENING;
this.pwmApplication = pwmApplication;
settings = new AuditSettings(pwmApplication.getConfig());
if (pwmApplication.getApplicationMode() == null || pwmApplication.getApplicationMode() == PwmApplicationMode.READ_ONLY) {
this.status = STATUS.CLOSED;
LOGGER.warn("unable to start - Application is in read-only mode");
return;
}
if (pwmApplication.getLocalDB() == null || pwmApplication.getLocalDB().status() != LocalDB.Status.OPEN) {
this.status = STATUS.CLOSED;
LOGGER.warn("unable to start - LocalDB is not available");
return;
}
final String syslogConfigString = pwmApplication.getConfig().readSettingAsString(PwmSetting.AUDIT_SYSLOG_SERVERS);
if (syslogConfigString != null && !syslogConfigString.isEmpty()) {
try {
syslogManager = new SyslogAuditService(pwmApplication);
} catch (Exception e) {
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_SYSLOG_WRITE_ERROR, "startup error: " + e.getMessage());
LOGGER.error(errorInformation.toDebugStr());
}
}
{
final UserEventStorageMethod userEventStorageMethod = pwmApplication.getConfig().readSettingAsEnum(PwmSetting.EVENTS_USER_STORAGE_METHOD, UserEventStorageMethod.class);
final String debugMsg;
final DataStorageMethod storageMethodUsed;
switch (userEventStorageMethod) {
case AUTO:
if (pwmApplication.getConfig().hasDbConfigured()) {
debugMsg = "starting using auto-configured data store, Remote Database selected";
this.userHistoryStore = new DatabaseUserHistory(pwmApplication);
storageMethodUsed = DataStorageMethod.DB;
} else {
debugMsg = "starting using auto-configured data store, LDAP selected";
this.userHistoryStore = new LdapXmlUserHistory(pwmApplication);
storageMethodUsed = DataStorageMethod.LDAP;
}
break;
case DATABASE:
this.userHistoryStore = new DatabaseUserHistory(pwmApplication);
debugMsg = "starting using Remote Database data store";
storageMethodUsed = DataStorageMethod.DB;
break;
case LDAP:
this.userHistoryStore = new LdapXmlUserHistory(pwmApplication);
debugMsg = "starting using LocalDB data store";
storageMethodUsed = DataStorageMethod.LDAP;
break;
default:
lastError = new ErrorInformation(PwmError.ERROR_UNKNOWN,"unknown storageMethod selected: " + userEventStorageMethod);
status = STATUS.CLOSED;
return;
}
LOGGER.info(debugMsg);
serviceInfo = new ServiceInfo(Collections.singletonList(storageMethodUsed));
}
{
final TimeDuration maxRecordAge = new TimeDuration(pwmApplication.getConfig().readSettingAsLong(PwmSetting.EVENTS_AUDIT_MAX_AGE) * 1000);
final long maxRecords = pwmApplication.getConfig().readSettingAsLong(PwmSetting.EVENTS_AUDIT_MAX_EVENTS);
final AuditVault.Settings settings = new AuditVault.Settings(
maxRecords,
maxRecordAge
);
if (pwmApplication.getLocalDB() != null && pwmApplication.getApplicationMode() != PwmApplicationMode.READ_ONLY) {
if (maxRecords < 1) {
LOGGER.debug("localDB audit vault will remain closed due to max records setting");
pwmApplication.getLocalDB().truncate(LocalDB.DB.AUDIT_EVENTS);
} else {
auditVault = new LocalDbAuditVault();
auditVault.init(pwmApplication, pwmApplication.getLocalDB(), settings);
}
} else {
LOGGER.debug("localDB audit vault will remain closed due to application mode");
}
}
this.status = STATUS.OPEN;
}
@Override
public void close() {
if (syslogManager != null) {
syslogManager.close();
}
this.status = STATUS.CLOSED;
}
@Override
public List<HealthRecord> healthCheck() {
if (status != STATUS.OPEN) {
return Collections.emptyList();
}
final List<HealthRecord> healthRecords = new ArrayList<>();
if (syslogManager != null) {
healthRecords.addAll(syslogManager.healthCheck());
}
if (lastError != null) {
healthRecords.add(new HealthRecord(HealthStatus.WARN, HealthTopic.Audit, lastError.toDebugStr()));
}
return healthRecords;
}
public Iterator<AuditRecord> readVault() {
return auditVault.readVault();
}
public List<UserAuditRecord> readUserHistory(final PwmSession pwmSession)
throws PwmUnrecoverableException
{
return readUserHistory(pwmSession.getUserInfoBean());
}
public List<UserAuditRecord> readUserHistory(final UserInfoBean userInfoBean)
throws PwmUnrecoverableException
{
return userHistoryStore.readUserHistory(userInfoBean);
}
protected void sendAsEmail(final AuditRecord record)
throws PwmUnrecoverableException
{
if (record == null || record.getEventCode() == null) {
return;
}
if (settings.getAlertFromAddress() == null || settings.getAlertFromAddress().length() < 1) {
return;
}
switch (record.getEventCode().getType()) {
case SYSTEM:
for (final String toAddress : settings.getSystemEmailAddresses()) {
sendAsEmail(pwmApplication, null, record, toAddress, settings.getAlertFromAddress());
}
break;
case USER:
case HELPDESK:
for (final String toAddress : settings.getUserEmailAddresses()) {
sendAsEmail(pwmApplication, null, record, toAddress, settings.getAlertFromAddress());
}
break;
default:
JavaHelper.unhandledSwitchStatement(record.getEventCode().getType());
}
}
private static void sendAsEmail(
final PwmApplication pwmApplication,
final SessionLabel sessionLabel,
final AuditRecord record,
final String toAddress,
final String fromAddress
)
throws PwmUnrecoverableException
{
final MacroMachine macroMachine = MacroMachine.forNonUserSpecific(pwmApplication, sessionLabel);
String subject = macroMachine.expandMacros(pwmApplication.getConfig().readAppProperty(AppProperty.AUDIT_EVENTS_EMAILSUBJECT));
subject = subject.replace("%EVENT%", record.getEventCode().getLocalizedString(pwmApplication.getConfig(), PwmConstants.DEFAULT_LOCALE));
final String body;
{
final String jsonRecord = JsonUtil.serialize(record);
final Map<String,Object> mapRecord = JsonUtil.deserializeMap(jsonRecord);
body = StringUtil.mapToString(mapRecord, "=", "\n");
}
final EmailItemBean emailItem = new EmailItemBean(toAddress, fromAddress, subject, body, null);
pwmApplication.getEmailQueue().submitEmail(emailItem, null, macroMachine);
}
public int vaultSize() {
if (status != STATUS.OPEN || auditVault == null) {
return -1;
}
return auditVault.size();
}
public Instant eldestVaultRecord() {
if (status != STATUS.OPEN || auditVault == null) {
return null;
}
return auditVault.oldestRecord();
}
public String sizeToDebugString() {
return auditVault.sizeToDebugString();
}
public void submit(final AuditEvent auditEvent, final UserInfoBean userInfoBean, final PwmSession pwmSession)
throws PwmUnrecoverableException
{
final AuditRecordFactory auditRecordFactory = new AuditRecordFactory(pwmApplication, pwmSession.getSessionManager().getMacroMachine(pwmApplication));
final UserAuditRecord auditRecord = auditRecordFactory.createUserAuditRecord(auditEvent, userInfoBean, pwmSession);
submit(auditRecord);
}
public void submit(final AuditRecord auditRecord)
throws PwmUnrecoverableException
{
final String jsonRecord = JsonUtil.serialize(auditRecord);
if (status != STATUS.OPEN) {
LOGGER.debug("discarding audit event (AuditManager is not open); event=" + jsonRecord);
return;
}
if (auditRecord.getEventCode() == null) {
LOGGER.error("discarding audit event, missing event type; event=" + jsonRecord);
return;
}
if (!settings.getPermittedEvents().contains(auditRecord.getEventCode())) {
LOGGER.debug("discarding event, " + auditRecord.getEventCode() + " are being ignored; event=" + jsonRecord);
return;
}
// add to debug log
LOGGER.info("audit event: " + jsonRecord);
// add to audit db
if (auditVault != null) {
try {
auditVault.add(auditRecord);
} catch (PwmOperationalException e) {
LOGGER.warn("discarding audit event due to storage error: " + e.getMessage());
}
}
// email alert
sendAsEmail(auditRecord);
// add to user history record
if (auditRecord instanceof UserAuditRecord) {
if (settings.getUserStoredEvents().contains(auditRecord.getEventCode())) {
final String perpetratorDN = ((UserAuditRecord) auditRecord).getPerpetratorDN();
if (!StringUtil.isEmpty(perpetratorDN)) {
userHistoryStore.updateUserHistory((UserAuditRecord) auditRecord);
} else {
LOGGER.trace("skipping update of user history, audit record does not have a perpetratorDN: " + JsonUtil.serialize(auditRecord));
}
}
}
// send to syslog
if (syslogManager != null) {
try {
syslogManager.add(auditRecord);
} catch (PwmOperationalException e) {
lastError = e.getErrorInformation();
}
}
}
public int outputVaultToCsv(final OutputStream outputStream, final Locale locale, final boolean includeHeader)
throws IOException
{
final Configuration config = null;
final CSVPrinter csvPrinter = JavaHelper.makeCsvPrinter(outputStream);
csvPrinter.printComment(" " + PwmConstants.PWM_APP_NAME + " audit record output ");
csvPrinter.printComment(" " + JavaHelper.toIsoDate(Instant.now()));
if (includeHeader) {
final List<String> headers = new ArrayList<>();
headers.add("Type");
headers.add(LocaleHelper.getLocalizedMessage(locale,"Field_Audit_EventCode",config,password.pwm.i18n.Admin.class));
headers.add(LocaleHelper.getLocalizedMessage(locale,"Field_Audit_Timestamp",config,password.pwm.i18n.Admin.class));
headers.add(LocaleHelper.getLocalizedMessage(locale,"Field_Audit_GUID",config,password.pwm.i18n.Admin.class));
headers.add(LocaleHelper.getLocalizedMessage(locale,"Field_Audit_Message",config,password.pwm.i18n.Admin.class));
headers.add(LocaleHelper.getLocalizedMessage(locale,"Field_Audit_Instance",config,password.pwm.i18n.Admin.class));
headers.add(LocaleHelper.getLocalizedMessage(locale,"Field_Audit_PerpetratorID",config,password.pwm.i18n.Admin.class));
headers.add(LocaleHelper.getLocalizedMessage(locale,"Field_Audit_PerpetratorDN",config,password.pwm.i18n.Admin.class));
headers.add(LocaleHelper.getLocalizedMessage(locale,"Field_Audit_TargetID",config,password.pwm.i18n.Admin.class));
headers.add(LocaleHelper.getLocalizedMessage(locale,"Field_Audit_TargetDN",config,password.pwm.i18n.Admin.class));
headers.add(LocaleHelper.getLocalizedMessage(locale,"Field_Audit_SourceAddress",config,password.pwm.i18n.Admin.class));
headers.add(LocaleHelper.getLocalizedMessage(locale,"Field_Audit_SourceHost",config,password.pwm.i18n.Admin.class));
csvPrinter.printRecord(headers);
}
int counter = 0;
for (final Iterator<AuditRecord> recordIterator = readVault(); recordIterator.hasNext(); ) {
final AuditRecord loopRecord = recordIterator.next();
counter++;
final List<String> lineOutput = new ArrayList<>();
lineOutput.add(loopRecord.getEventCode().getType().toString());
lineOutput.add(loopRecord.getEventCode().toString());
lineOutput.add(JavaHelper.toIsoDate(loopRecord.getTimestamp()));
lineOutput.add(loopRecord.getGuid());
lineOutput.add(loopRecord.getMessage() == null ? "" : loopRecord.getMessage());
if (loopRecord instanceof SystemAuditRecord) {
lineOutput.add(((SystemAuditRecord)loopRecord).getInstance());
}
if (loopRecord instanceof UserAuditRecord) {
lineOutput.add(((UserAuditRecord)loopRecord).getPerpetratorID());
lineOutput.add(((UserAuditRecord)loopRecord).getPerpetratorDN());
lineOutput.add("");
lineOutput.add("");
lineOutput.add(((UserAuditRecord)loopRecord).getSourceAddress());
lineOutput.add(((UserAuditRecord)loopRecord).getSourceHost());
}
if (loopRecord instanceof HelpdeskAuditRecord) {
lineOutput.add(((HelpdeskAuditRecord)loopRecord).getPerpetratorID());
lineOutput.add(((HelpdeskAuditRecord)loopRecord).getPerpetratorDN());
lineOutput.add(((HelpdeskAuditRecord)loopRecord).getTargetID());
lineOutput.add(((HelpdeskAuditRecord)loopRecord).getTargetDN());
lineOutput.add(((HelpdeskAuditRecord)loopRecord).getSourceAddress());
lineOutput.add(((HelpdeskAuditRecord)loopRecord).getSourceHost());
}
csvPrinter.printRecord(lineOutput);
}
csvPrinter.flush();
return counter;
}
public ServiceInfo serviceInfo()
{
return serviceInfo;
}
public int syslogQueueSize() {
return syslogManager != null ? syslogManager.queueSize() : 0;
}
}