/*
* 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.ldap;
import password.pwm.AppProperty;
import password.pwm.PwmApplication;
import password.pwm.PwmConstants;
import password.pwm.bean.SessionLabel;
import password.pwm.bean.UserIdentity;
import password.pwm.config.PwmSetting;
import password.pwm.config.option.PasswordSyncCheckMode;
import password.pwm.error.PwmUnrecoverableException;
import password.pwm.i18n.Display;
import password.pwm.util.LocaleHelper;
import password.pwm.util.ProgressInfo;
import password.pwm.util.java.JsonUtil;
import password.pwm.util.java.Percent;
import password.pwm.util.java.TimeDuration;
import password.pwm.util.logging.PwmLogger;
import password.pwm.util.operations.PasswordUtility;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
public class PasswordChangeProgressChecker {
private static final PwmLogger LOGGER = PwmLogger.forClass(PasswordChangeProgressChecker.class);
public static final String PROGRESS_KEY_REPLICATION = "replication";
private final ProgressRecord completedReplicationRecord;
private final PwmApplication pwmApplication;
private final UserIdentity userIdentity;
private final SessionLabel pwmSession;
private final Locale locale;
private final PasswordSyncCheckMode passwordSyncCheckMode;
public PasswordChangeProgressChecker(
final PwmApplication pwmApplication,
final UserIdentity userIdentity,
final SessionLabel sessionLabel,
final Locale locale
)
{
this.pwmApplication = pwmApplication;
this.pwmSession = sessionLabel;
this.userIdentity = userIdentity;
this.locale = locale == null ? PwmConstants.DEFAULT_LOCALE : locale;
if (pwmApplication == null) {
throw new IllegalArgumentException("pwmApplication cannot be null");
}
passwordSyncCheckMode = pwmApplication.getConfig().readSettingAsEnum(PwmSetting.PASSWORD_SYNC_ENABLE_REPLICA_CHECK,PasswordSyncCheckMode.class);
completedReplicationRecord = makeReplicaProgressRecord(Percent.ONE_HUNDRED);
}
public static class PasswordChangeProgress implements Serializable {
private boolean complete;
private BigDecimal percentComplete;
private Collection<ProgressRecord> messages;
private String elapsedSeconds;
private String estimatedRemainingSeconds;
public static final PasswordChangeProgress COMPLETE = new PasswordChangeProgress(
true,
Percent.ONE_HUNDRED.asBigDecimal(2),
Collections.emptyList(),
"",
""
);
public PasswordChangeProgress(
final boolean complete,
final BigDecimal percentComplete,
final Collection<ProgressRecord> messages,
final String elapsedSeconds,
final String estimatedRemainingSeconds
)
{
this.complete = complete;
this.percentComplete = percentComplete;
this.messages = messages;
this.elapsedSeconds = elapsedSeconds;
this.estimatedRemainingSeconds = estimatedRemainingSeconds;
}
public boolean isComplete()
{
return complete;
}
public BigDecimal getPercentComplete()
{
return percentComplete;
}
public Collection<ProgressRecord> getItemCompletion()
{
return messages;
}
}
public static class ProgressRecord implements Serializable {
private String key;
private String label;
private BigDecimal percentComplete;
private boolean complete;
private boolean show;
}
public static class ProgressTracker implements Serializable {
private Instant beginTime = Instant.now();
private Instant lastReplicaCheckTime;
private final Map<String,ProgressRecord> itemCompletions = new HashMap<>();
public Instant getBeginTime()
{
return beginTime;
}
public Instant getLastReplicaCheckTime()
{
return lastReplicaCheckTime;
}
public Map<String, ProgressRecord> getItemCompletions()
{
return itemCompletions;
}
}
public PasswordChangeProgress figureProgress(
final ProgressTracker tracker
) {
if (tracker == null) {
throw new IllegalArgumentException("tracker cannot be null");
}
final Map<String,ProgressRecord> newItemProgress = new LinkedHashMap<>();
newItemProgress.putAll(tracker.itemCompletions);
if (tracker.beginTime == null || Instant.now().isAfter(maxCompletionTime(tracker))) {
return PasswordChangeProgress.COMPLETE;
}
newItemProgress.putAll(figureItemProgresses(tracker));
final Instant estimatedCompletion = figureEstimatedCompletion(tracker, newItemProgress.values());
final long elapsedMs = TimeDuration.fromCurrent(tracker.beginTime).getTotalMilliseconds();
final long remainingMs = TimeDuration.fromCurrent(estimatedCompletion).getTotalMilliseconds();
final Percent percentage;
if (Instant.now().isAfter(estimatedCompletion)) {
percentage = Percent.ONE_HUNDRED;
} else {
final long totalMs = new TimeDuration(tracker.beginTime,estimatedCompletion).getTotalMilliseconds();
percentage = new Percent(elapsedMs,totalMs + 1);
}
tracker.itemCompletions.putAll(newItemProgress);
return new PasswordChangeProgress(
percentage.isComplete(),
percentage.asBigDecimal(2),
newItemProgress.values(),
new TimeDuration(elapsedMs).asLongString(locale),
new TimeDuration(remainingMs).asLongString(locale)
);
}
private Map<String,ProgressRecord> figureItemProgresses(
final ProgressTracker tracker
)
{
final Map<String,ProgressRecord> returnValue = new LinkedHashMap<>();
{ // figure replication progress
final ProgressRecord replicationProgress = figureReplicationStatusCompletion(tracker);
if (replicationProgress != null) {
returnValue.put(replicationProgress.key, replicationProgress);
}
}
{ // random
/*
final long randMs = PwmRandom.getInstance().nextInt(90 * 1000) + 30 * 1000;
//final long randMs = 75 * 1000;
final long elapsedMs = TimeDuration.fromCurrent(tracker.beginTime).getTotalMilliseconds();
final Percent percent = new Percent(elapsedMs,randMs);
final ProgressRecord record = new ProgressRecord();
record.key = "randomItem";
record.label = "Random Replication Delay " + randMs + "ms";
record.percentComplete = percent.asBigDecimal();
record.complete = percent.isComplete();
record.show = true;
returnValue.put(record.key, record);
*/
}
// insert more checks here @todo
return returnValue;
}
public Instant maxCompletionTime(final ProgressTracker tracker) {
final TimeDuration maxWait = new TimeDuration(pwmApplication.getConfig().readSettingAsLong(PwmSetting.PASSWORD_SYNC_MAX_WAIT_TIME) * 1000);
return Instant.ofEpochMilli(tracker.beginTime.toEpochMilli() + maxWait.getTotalMilliseconds());
}
private Instant minCompletionTime(final ProgressTracker tracker) {
final TimeDuration minWait = new TimeDuration(pwmApplication.getConfig().readSettingAsLong(PwmSetting.PASSWORD_SYNC_MIN_WAIT_TIME) * 1000);
return Instant.ofEpochMilli(tracker.beginTime.toEpochMilli() + minWait.getTotalMilliseconds());
}
private Instant figureEstimatedCompletion(
final ProgressTracker tracker,
final Collection<ProgressRecord> progressRecords
) {
final Instant minCompletionTime = minCompletionTime(tracker);
final Instant maxCompletionTime = maxCompletionTime(tracker);
final Instant estimatedCompletion;
{
final BigDecimal pctComplete = figureAverageProgress(progressRecords);
LOGGER.trace(pwmSession, "percent complete: " + pctComplete);
final ProgressInfo progressInfo = new ProgressInfo(tracker.beginTime, 100, pctComplete.longValue());
final Instant actualEstimate = progressInfo.estimatedCompletion();
if (actualEstimate.isBefore(minCompletionTime)) {
estimatedCompletion = minCompletionTime;
} else if (actualEstimate.isAfter(maxCompletionTime)) {
estimatedCompletion = maxCompletionTime;
} else {
estimatedCompletion = actualEstimate;
}
}
return estimatedCompletion;
}
private BigDecimal figureAverageProgress(final Collection<ProgressRecord> progressRecords) {
int items = 0;
BigDecimal sum = BigDecimal.ZERO;
if (progressRecords != null) {
for (final ProgressRecord progress : progressRecords) {
if (progress.percentComplete != null) {
items++;
sum = sum.add(progress.percentComplete);
}
}
}
if (items > 0) {
return sum.divide(new BigDecimal(items), MathContext.DECIMAL32).setScale(2,RoundingMode.UP);
}
return Percent.ONE_HUNDRED.asBigDecimal(2);
}
private ProgressRecord figureReplicationStatusCompletion(
final ProgressTracker tracker
) {
final long initDelayMs = Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.LDAP_PASSWORD_REPLICA_CHECK_INIT_DELAY_MS));
final long cycleDelayMs = Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.LDAP_PASSWORD_REPLICA_CHECK_CYCLE_DELAY_MS));
final TimeDuration initialReplicaDelay = new TimeDuration(initDelayMs);
final TimeDuration cycleReplicaDelay = new TimeDuration(cycleDelayMs);
if (passwordSyncCheckMode == PasswordSyncCheckMode.DISABLED) {
LOGGER.trace(pwmSession, "skipping replica sync check, disabled");
return tracker.itemCompletions.get(PROGRESS_KEY_REPLICATION);
}
if (tracker.itemCompletions.containsKey(PROGRESS_KEY_REPLICATION)) {
if (tracker.itemCompletions.get(PROGRESS_KEY_REPLICATION).complete) {
LOGGER.trace(pwmSession, "skipping replica sync check, replica sync completed previously");
return tracker.itemCompletions.get(PROGRESS_KEY_REPLICATION);
}
}
if (tracker.lastReplicaCheckTime == null) {
if (TimeDuration.fromCurrent(tracker.beginTime).isShorterThan(initialReplicaDelay)) {
LOGGER.trace(pwmSession, "skipping replica sync check, initDelay has not yet passed");
return null;
}
} else if (TimeDuration.fromCurrent(tracker.lastReplicaCheckTime).isShorterThan(cycleReplicaDelay)) {
LOGGER.trace(pwmSession, "skipping replica sync check, cycleDelay has not yet passed");
return null;
}
tracker.lastReplicaCheckTime = Instant.now();
LOGGER.trace(pwmSession, "beginning password replication time check for " + userIdentity.toDelimitedKey());
try {
final Map<String,Instant> checkResults = PasswordUtility.readIndividualReplicaLastPasswordTimes(pwmApplication,
pwmSession, userIdentity);
if (checkResults.size() <= 1) {
LOGGER.trace("only one replica returned data, marking as complete");
return completedReplicationRecord;
} else {
final HashSet<Instant> tempHashSet = new HashSet<>();
int duplicateValues = 0;
for (final String replicaUrl : checkResults.keySet()) {
final Instant date = checkResults.get(replicaUrl);
if (tempHashSet.contains(date)) {
duplicateValues++;
} else {
tempHashSet.add(date);
}
}
final Percent pctComplete = new Percent(duplicateValues + 1, checkResults.size());
final ProgressRecord progressRecord = makeReplicaProgressRecord(pctComplete);
LOGGER.trace("read password replication sync status as: " + JsonUtil.serialize(progressRecord));
return progressRecord;
}
} catch (PwmUnrecoverableException e) {
LOGGER.error(pwmSession, "error during password replication status check: " + e.getMessage());
}
return null;
}
private ProgressRecord makeReplicaProgressRecord(final Percent pctComplete) {
final ProgressRecord progressRecord = new ProgressRecord();
progressRecord.key = PROGRESS_KEY_REPLICATION;
progressRecord.complete = pctComplete.isComplete();
progressRecord.percentComplete = pctComplete.asBigDecimal(2);
progressRecord.show = passwordSyncCheckMode == PasswordSyncCheckMode.ENABLED_SHOW;
progressRecord.label = LocaleHelper.getLocalizedMessage(
locale,
"Display_PasswordReplicationStatus",
pwmApplication.getConfig(),
Display.class,
new String[]{pctComplete.pretty()}
);
return progressRecord;
}
}