/* * 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 password.pwm.AppProperty; import password.pwm.PwmApplication; import password.pwm.error.PwmException; import password.pwm.svc.PwmService; import password.pwm.util.TransactionSizeCalculator; import password.pwm.util.java.JavaHelper; import password.pwm.util.java.JsonUtil; import password.pwm.util.java.Percent; import password.pwm.util.java.TimeDuration; import password.pwm.util.localdb.LocalDB; import password.pwm.util.localdb.LocalDBException; import password.pwm.util.localdb.LocalDBStoredQueue; import password.pwm.util.logging.PwmLogger; import java.time.Instant; import java.util.Iterator; import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class LocalDbAuditVault implements AuditVault { private static final PwmLogger LOGGER = PwmLogger.forClass(LocalDbAuditVault.class); private LocalDBStoredQueue auditDB; private Settings settings; private Instant oldestRecord; private int maxBulkRemovals = 105; private ScheduledExecutorService executorService; private volatile PwmService.STATUS status = PwmService.STATUS.NEW; public LocalDbAuditVault( ) throws LocalDBException { } public void init( final PwmApplication pwmApplication, final LocalDB localDB, final Settings settings ) throws PwmException { this.settings = settings; this.auditDB = LocalDBStoredQueue.createLocalDBStoredQueue(pwmApplication, localDB, LocalDB.DB.AUDIT_EVENTS); this.maxBulkRemovals = Integer.parseInt(pwmApplication.getConfig().readAppProperty(AppProperty.AUDIT_EVENTS_LOCALDB_MAX_BULK_REMOVALS)); readOldestRecord(); executorService = Executors.newSingleThreadScheduledExecutor( JavaHelper.makePwmThreadFactory( JavaHelper.makeThreadName(pwmApplication,this.getClass()) + "-", true )); status = PwmService.STATUS.OPEN; executorService.scheduleWithFixedDelay(new TrimmerThread(), 0, 10, TimeUnit.MINUTES); } public void close() { executorService.shutdown(); status = PwmService.STATUS.CLOSED; } public PwmService.STATUS getStatus() { return status; } @Override public Instant oldestRecord() { return oldestRecord; } @Override public int size() { return auditDB.size(); } public Iterator<AuditRecord> readVault() { return new IteratorWrapper(auditDB.descendingIterator()); } private static class IteratorWrapper implements Iterator<AuditRecord> { private Iterator<String> innerIter; private IteratorWrapper(final Iterator<String> innerIter) { this.innerIter = innerIter; } @Override public boolean hasNext() { return innerIter.hasNext(); } @Override public AuditRecord next() { final String value = innerIter.next(); return deSerializeRecord(value); } @Override public void remove() { throw new UnsupportedOperationException(); } } @Override public String sizeToDebugString() { final long storedEvents = this.size(); final long maxEvents = settings.getMaxRecordCount(); final Percent percent = new Percent(storedEvents, maxEvents); return storedEvents + " / " + maxEvents + " (" + percent.pretty(2) + ")"; } private static AuditRecord deSerializeRecord(final String input) { final Map<String,String> tempMap = JsonUtil.deserializeStringMap(input); String errorMsg = ""; try { if (tempMap != null) { final String eventCode = tempMap.get("eventCode"); if (eventCode != null && eventCode.length() > 0) { final AuditEvent event; try { event = AuditEvent.valueOf(eventCode); } catch (IllegalArgumentException e) { errorMsg = "error de-serializing audit record: " + e.getMessage(); LOGGER.error(errorMsg); return null; } final Class clazz = event.getType().getDataClass(); final com.google.gson.reflect.TypeToken typeToken = com.google.gson.reflect.TypeToken.get(clazz); return JsonUtil.deserialize(input, typeToken); } } } catch (Exception e) { errorMsg = e.getMessage(); } LOGGER.debug("unable to deserialize stored record '" + input + "', error: " + errorMsg); return null; } public void add(final AuditRecord record) { if (record == null) { return; } final String jsonRecord = JsonUtil.serialize(record); auditDB.addLast(jsonRecord); if (auditDB.size() > settings.getMaxRecordCount()) { removeRecords(1); } } private void readOldestRecord() { if (auditDB != null && !auditDB.isEmpty()) { final String stringFirstRecord = auditDB.getFirst(); final UserAuditRecord firstRecord = JsonUtil.deserialize(stringFirstRecord, UserAuditRecord.class); oldestRecord = firstRecord.getTimestamp(); } } private void removeRecords(final int count) { auditDB.removeFirst(count); readOldestRecord(); } private class TrimmerThread implements Runnable { // keep transaction duration around 100ms if possible. final TransactionSizeCalculator transactionSizeCalculator = new TransactionSizeCalculator( new TransactionSizeCalculator.SettingsBuilder() .setDurationGoal(new TimeDuration(101, TimeUnit.MILLISECONDS)) .setMaxTransactions(5003) .setMinTransactions(3) .createSettings() ); @Override public void run() { long startTime = System.currentTimeMillis(); while (trim(transactionSizeCalculator.getTransactionSize()) && status == PwmService.STATUS.OPEN ) { final long executeTime = System.currentTimeMillis() - startTime; transactionSizeCalculator.recordLastTransactionDuration(executeTime); transactionSizeCalculator.pause(); startTime = System.currentTimeMillis(); } } private boolean trim(final int maxRemovals) { if (auditDB.isEmpty()) { return false; } if (auditDB.size() > settings.getMaxRecordCount() + maxRemovals) { removeRecords(maxRemovals); return true; } int workActions = 0; while (oldestRecord != null && workActions < maxRemovals && !auditDB.isEmpty() && status == PwmService.STATUS.OPEN ) { if (TimeDuration.fromCurrent(oldestRecord).isLongerThan(settings.getMaxRecordAge())) { removeRecords(1); workActions++; } else { break; } } return workActions > 0; } } }