/*
* 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 com.novell.ldapchai.ChaiUser;
import com.novell.ldapchai.exception.ChaiOperationException;
import com.novell.ldapchai.exception.ChaiUnavailableException;
import com.novell.ldapchai.util.ConfigObjectRecord;
import org.jdom2.CDATA;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import password.pwm.PwmApplication;
import password.pwm.bean.UserIdentity;
import password.pwm.bean.UserInfoBean;
import password.pwm.config.PwmSetting;
import password.pwm.config.profile.LdapProfile;
import password.pwm.error.ErrorInformation;
import password.pwm.error.PwmError;
import password.pwm.error.PwmUnrecoverableException;
import password.pwm.util.logging.PwmLogger;
import java.io.IOException;
import java.io.Serializable;
import java.io.StringReader;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* Wrapper class to handle user event history.
*
* @author Jason D. Rivard
*/
class LdapXmlUserHistory implements UserHistoryStore, Serializable {
// ------------------------------ FIELDS ------------------------------
private static final PwmLogger LOGGER = PwmLogger.forClass(LdapXmlUserHistory.class);
private static final String XML_ATTR_TIMESTAMP = "timestamp";
private static final String XML_ATTR_TRANSACTION = "eventCode";
private static final String XML_ATTR_SRC_IP = "srcIP";
private static final String XML_ATTR_SRC_HOST = "srcHost";
private static final String XML_NODE_ROOT = "history";
private static final String XML_NODE_RECORD = "record";
private static final String COR_RECORD_ID = "0001";
// -------------------------- STATIC METHODS --------------------------
private final PwmApplication pwmApplication;
LdapXmlUserHistory(final PwmApplication pwmApplication) {
this.pwmApplication = pwmApplication;
}
public void updateUserHistory(final UserAuditRecord auditRecord)
throws PwmUnrecoverableException
{
try {
updateUserHistoryImpl(auditRecord);
} catch (ChaiUnavailableException e) {
throw new PwmUnrecoverableException(PwmError.forChaiError(e.getErrorCode()));
}
}
void updateUserHistoryImpl(final UserAuditRecord auditRecord)
throws PwmUnrecoverableException, ChaiUnavailableException
{
// user info
final UserIdentity userIdentity;
if (auditRecord instanceof HelpdeskAuditRecord && auditRecord.getType() == AuditEvent.Type.HELPDESK) {
final HelpdeskAuditRecord helpdeskAuditRecord = (HelpdeskAuditRecord)auditRecord;
userIdentity = new UserIdentity(helpdeskAuditRecord.getTargetDN(),helpdeskAuditRecord.getTargetLdapProfile());
} else {
userIdentity = new UserIdentity(auditRecord.getPerpetratorDN(),auditRecord.getPerpetratorLdapProfile());
}
final ChaiUser theUser = pwmApplication.getProxiedChaiUser(userIdentity);
// settings
final String corRecordIdentifer = COR_RECORD_ID;
final LdapProfile ldapProfile = userIdentity.getLdapProfile(pwmApplication.getConfig());
final String corAttribute = ldapProfile.readSettingAsString(PwmSetting.EVENTS_LDAP_ATTRIBUTE);
// quit if settings no good;
if (corAttribute == null || corAttribute.length() < 1) {
LOGGER.debug("no user event log attribute configured, skipping write of log data");
return;
}
// read current value;
final StoredHistory storedHistory;
final ConfigObjectRecord theCor;
final List corList;
try {
corList = ConfigObjectRecord.readRecordFromLDAP(theUser, corAttribute, corRecordIdentifer, null, null);
} catch (Exception e) {
final String errorMsg = "error reading LDAP user event history for user " + userIdentity.toDisplayString() + ", error: " + e.getMessage();
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN, errorMsg);
LOGGER.error(errorInformation.toDebugStr(),e);
throw new PwmUnrecoverableException(errorInformation, e);
}
try {
if (!corList.isEmpty()) {
theCor = (ConfigObjectRecord) corList.get(0);
} else {
theCor = ConfigObjectRecord.createNew(theUser, corAttribute, corRecordIdentifer, null, null);
}
storedHistory = StoredHistory.fromXml(theCor.getPayload());
} catch (Exception e) {
LOGGER.error("ldap error writing user event log: " + e.getMessage());
return;
}
// add next record to blob
final StoredEvent storedEvent = StoredEvent.fromAuditRecord(auditRecord);
storedHistory.addEvent(storedEvent);
// trim the blob.
final int maxUserEvents = (int) pwmApplication.getConfig().readSettingAsLong(PwmSetting.EVENTS_LDAP_MAX_EVENTS);
storedHistory.trim(maxUserEvents);
// write the blob.
try {
theCor.updatePayload(storedHistory.toXml());
} catch (ChaiOperationException e) {
LOGGER.error("ldap error writing user event log: " + e.getMessage());
}
}
public List<UserAuditRecord> readUserHistory(final UserInfoBean userInfoBean)
throws PwmUnrecoverableException
{
try {
final ChaiUser theUser = pwmApplication.getProxiedChaiUser(userInfoBean.getUserIdentity());
final StoredHistory storedHistory = readUserHistory(pwmApplication, userInfoBean.getUserIdentity(), theUser);
return storedHistory.asAuditRecords(userInfoBean);
} catch (ChaiUnavailableException e) {
throw new PwmUnrecoverableException(PwmError.forChaiError(e.getErrorCode()));
}
}
private StoredHistory readUserHistory(
final PwmApplication pwmApplication,
final UserIdentity userIdentity,
final ChaiUser chaiUser
)
throws ChaiUnavailableException, PwmUnrecoverableException {
final String corRecordIdentifer = COR_RECORD_ID;
final LdapProfile ldapProfile = userIdentity.getLdapProfile(pwmApplication.getConfig());
final String corAttribute = ldapProfile.readSettingAsString(PwmSetting.EVENTS_LDAP_ATTRIBUTE);
if (corAttribute == null || corAttribute.length() < 1) {
LOGGER.trace("no user event log attribute configured, skipping read of log data");
return new StoredHistory();
}
try {
final List corList = ConfigObjectRecord.readRecordFromLDAP(chaiUser, corAttribute, corRecordIdentifer, null, null);
if (!corList.isEmpty()) {
final ConfigObjectRecord theCor = (ConfigObjectRecord) corList.get(0);
return StoredHistory.fromXml(theCor.getPayload());
}
} catch (ChaiOperationException e) {
LOGGER.error("ldap error reading user event log: " + e.getMessage());
}
return new StoredHistory();
}
private static class StoredHistory {
private final LinkedList<StoredEvent> records = new LinkedList<>();
public void addEvent(final StoredEvent storedEvent) {
records.add(storedEvent);
}
public void trim(final int size) {
while (records.size() > size) {
records.removeFirst();
}
}
public List<UserAuditRecord> asAuditRecords(final UserInfoBean userInfoBean) {
final List<UserAuditRecord> returnList = new LinkedList<>();
for (final StoredEvent loopEvent : records) {
returnList.add(loopEvent.asAuditRecord(userInfoBean));
}
return Collections.unmodifiableList(returnList);
}
public String toXml() {
final Element rootElement = new Element(XML_NODE_ROOT);
for (final StoredEvent loopEvent : records) {
if (loopEvent.getAuditEvent() != null) {
final Element hrElement = new Element(XML_NODE_RECORD);
hrElement.setAttribute(XML_ATTR_TIMESTAMP, String.valueOf(loopEvent.getTimestamp()));
hrElement.setAttribute(XML_ATTR_TRANSACTION, loopEvent.getAuditEvent().getMessage().getKey());
if (loopEvent.getSourceAddress() != null && loopEvent.getSourceAddress().length() > 0) {
hrElement.setAttribute(XML_ATTR_SRC_IP,loopEvent.getSourceAddress());
}
if (loopEvent.getSourceHost() != null && loopEvent.getSourceHost().length() > 0) {
hrElement.setAttribute(XML_ATTR_SRC_HOST,loopEvent.getSourceHost());
}
if (loopEvent.getMessage() != null) {
hrElement.setContent(new CDATA(loopEvent.getMessage()));
}
rootElement.addContent(hrElement);
}
}
final Document doc = new Document(rootElement);
final XMLOutputter outputter = new XMLOutputter();
outputter.setFormat(Format.getCompactFormat());
return outputter.outputString(doc);
}
public static StoredHistory fromXml(final String input) {
final StoredHistory returnHistory = new StoredHistory();
if (input == null || input.length() < 1) {
return returnHistory;
}
try {
final SAXBuilder builder = new SAXBuilder();
final Document doc = builder.build(new StringReader(input));
final Element rootElement = doc.getRootElement();
for (final Element hrElement : rootElement.getChildren(XML_NODE_RECORD)) {
final long timeStamp = hrElement.getAttribute(XML_ATTR_TIMESTAMP).getLongValue();
final String transactionCode = hrElement.getAttribute(XML_ATTR_TRANSACTION).getValue();
final AuditEvent eventCode = AuditEvent.forKey(transactionCode);
final String srcAddr = hrElement.getAttribute(XML_ATTR_SRC_IP) != null ? hrElement.getAttribute(XML_ATTR_SRC_IP).getValue() : "";
final String srcHost = hrElement.getAttribute(XML_ATTR_SRC_HOST) != null ? hrElement.getAttribute(XML_ATTR_SRC_HOST).getValue() : "";
final String message = hrElement.getText();
final StoredEvent storedEvent = new StoredEvent(eventCode,timeStamp,message,srcAddr,srcHost);
returnHistory.addEvent(storedEvent);
}
} catch (JDOMException e) {
LOGGER.error("error parsing user event history record: " + e.getMessage());
} catch (IOException e) {
LOGGER.error("error parsing user event history record: " + e.getMessage());
}
return returnHistory;
}
}
private static class StoredEvent implements Serializable {
private AuditEvent auditEvent;
private long timestamp;
private String message;
private String sourceAddress;
private String sourceHost;
private StoredEvent(final AuditEvent auditEvent, final long timestamp, final String message, final String sourceAddress, final String sourceHost) {
this.auditEvent = auditEvent;
this.timestamp = timestamp;
this.message = message;
this.sourceAddress = sourceAddress;
this.sourceHost = sourceHost;
}
public AuditEvent getAuditEvent() {
return auditEvent;
}
public long getTimestamp() {
return timestamp;
}
public String getMessage() {
return message;
}
public String getSourceAddress() {
return sourceAddress;
}
public String getSourceHost() {
return sourceHost;
}
public static StoredEvent fromAuditRecord(final UserAuditRecord auditRecord) {
return new StoredEvent(
auditRecord.getEventCode(),
auditRecord.getTimestamp().toEpochMilli(),
auditRecord.getMessage(),
auditRecord.getSourceAddress(),
auditRecord.getSourceHost()
);
}
public UserAuditRecord asAuditRecord(final UserInfoBean userInfoBean) {
return new UserAuditRecord(
Instant.ofEpochMilli(this.getTimestamp()),
this.getAuditEvent(),
null,
null,
userInfoBean.getUserIdentity().getUserDN(),
this.getMessage(),
this.getSourceAddress(),
this.getSourceHost()
);
}
}
// -------------------------- INNER CLASSES --------------------------
}