/*
* 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.util.operations;
import com.novell.ldapchai.ChaiFactory;
import com.novell.ldapchai.ChaiPasswordPolicy;
import com.novell.ldapchai.ChaiUser;
import com.novell.ldapchai.exception.ChaiException;
import com.novell.ldapchai.exception.ChaiOperationException;
import com.novell.ldapchai.exception.ChaiPasswordPolicyException;
import com.novell.ldapchai.exception.ChaiUnavailableException;
import com.novell.ldapchai.impl.oracleds.entry.OracleDSEntries;
import com.novell.ldapchai.provider.ChaiConfiguration;
import com.novell.ldapchai.provider.ChaiProvider;
import com.novell.ldapchai.provider.ChaiProviderFactory;
import com.novell.ldapchai.provider.ChaiSetting;
import com.novell.ldapchai.util.ChaiUtility;
import password.pwm.AppProperty;
import password.pwm.Permission;
import password.pwm.PwmApplication;
import password.pwm.bean.EmailItemBean;
import password.pwm.bean.LoginInfoBean;
import password.pwm.bean.PasswordStatus;
import password.pwm.bean.SessionLabel;
import password.pwm.bean.SmsItemBean;
import password.pwm.bean.UserIdentity;
import password.pwm.bean.UserInfoBean;
import password.pwm.config.ActionConfiguration;
import password.pwm.config.Configuration;
import password.pwm.config.PwmSetting;
import password.pwm.config.UserPermission;
import password.pwm.config.option.HelpdeskClearResponseMode;
import password.pwm.config.option.MessageSendMethod;
import password.pwm.config.profile.ForgottenPasswordProfile;
import password.pwm.config.profile.HelpdeskProfile;
import password.pwm.config.profile.LdapProfile;
import password.pwm.config.profile.ProfileType;
import password.pwm.config.profile.ProfileUtility;
import password.pwm.config.profile.PwmPasswordPolicy;
import password.pwm.config.profile.PwmPasswordRule;
import password.pwm.error.ErrorInformation;
import password.pwm.error.PwmDataValidationException;
import password.pwm.error.PwmError;
import password.pwm.error.PwmOperationalException;
import password.pwm.error.PwmUnrecoverableException;
import password.pwm.http.PwmSession;
import password.pwm.ldap.LdapOperationsHelper;
import password.pwm.ldap.LdapPermissionTester;
import password.pwm.ldap.LdapUserDataReader;
import password.pwm.ldap.UserDataReader;
import password.pwm.ldap.UserStatusReader;
import password.pwm.ldap.auth.AuthenticationType;
import password.pwm.svc.cache.CacheKey;
import password.pwm.svc.cache.CachePolicy;
import password.pwm.svc.cache.CacheService;
import password.pwm.svc.event.AuditEvent;
import password.pwm.svc.event.AuditRecordFactory;
import password.pwm.svc.event.HelpdeskAuditRecord;
import password.pwm.svc.stats.Statistic;
import password.pwm.util.PasswordCharCounter;
import password.pwm.util.PasswordData;
import password.pwm.util.PostChangePasswordAction;
import password.pwm.util.PwmPasswordRuleValidator;
import password.pwm.util.java.JavaHelper;
import password.pwm.util.java.JsonUtil;
import password.pwm.util.java.TimeDuration;
import password.pwm.util.logging.PwmLogger;
import password.pwm.util.macro.MacroMachine;
import java.io.Serializable;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author Jason D. Rivard
*/
public class PasswordUtility {
private static final PwmLogger LOGGER = PwmLogger.forClass(PasswordUtility.class);
private static final String NEGATIVE_CACHE_HIT = "NEGATIVE_CACHE_HIT";
public static String sendNewPassword(
final UserInfoBean userInfoBean,
final PwmApplication pwmApplication,
final MacroMachine macroMachine,
final PasswordData newPassword,
final Locale userLocale,
final MessageSendMethod messageSendMethod
)
throws PwmOperationalException, PwmUnrecoverableException
{
final String emailAddress = userInfoBean.getUserEmailAddress();
final String smsNumber = userInfoBean.getUserSmsNumber();
String returnToAddress = emailAddress;
ErrorInformation error = null;
switch (messageSendMethod) {
case BOTH:
// Send both email and SMS, success if one of both succeeds
final ErrorInformation err1 = sendNewPasswordEmail(userInfoBean, pwmApplication, macroMachine, newPassword, emailAddress, userLocale);
final ErrorInformation err2 = sendNewPasswordSms(userInfoBean, pwmApplication, macroMachine, newPassword, smsNumber, userLocale);
if (err1 != null) {
error = err1;
returnToAddress = smsNumber;
} else if (err2 != null) {
error = err2;
}
break;
case EMAILFIRST:
// Send email first, try SMS if email is not available
error = sendNewPasswordEmail(userInfoBean, pwmApplication, macroMachine, newPassword, emailAddress, userLocale);
if (error != null) {
error = sendNewPasswordSms(userInfoBean, pwmApplication, macroMachine, newPassword, smsNumber, userLocale);
returnToAddress = smsNumber;
}
break;
case SMSFIRST:
// Send SMS first, try email if SMS is not available
error = sendNewPasswordSms(userInfoBean, pwmApplication, macroMachine, newPassword, smsNumber, userLocale);
if (error != null) {
error = sendNewPasswordEmail(userInfoBean, pwmApplication, macroMachine, newPassword, emailAddress, userLocale);
} else {
returnToAddress = smsNumber;
}
break;
case SMSONLY:
// Only try SMS
error = sendNewPasswordSms(userInfoBean, pwmApplication, macroMachine, newPassword, smsNumber, userLocale);
returnToAddress = smsNumber;
break;
case EMAILONLY:
default:
// Only try email
error = sendNewPasswordEmail(userInfoBean, pwmApplication, macroMachine, newPassword, emailAddress, userLocale);
break;
}
if (error != null) {
throw new PwmOperationalException(error);
}
return returnToAddress;
}
private static ErrorInformation sendNewPasswordSms(
final UserInfoBean userInfoBean,
final PwmApplication pwmApplication,
final MacroMachine macroMachine,
final PasswordData newPassword,
final String toNumber,
final Locale userLocale
)
throws PwmOperationalException, PwmUnrecoverableException
{
final Configuration config = pwmApplication.getConfig();
String message = config.readSettingAsLocalizedString(PwmSetting.SMS_CHALLENGE_NEW_PASSWORD_TEXT, userLocale);
if (toNumber == null || toNumber.length() < 1) {
final String errorMsg = String.format("unable to send new password email for '%s'; no SMS number available in ldap", userInfoBean.getUserIdentity());
return new ErrorInformation(PwmError.ERROR_UNKNOWN, errorMsg);
}
message = message.replace("%TOKEN%", newPassword.getStringValue());
pwmApplication.sendSmsUsingQueue(new SmsItemBean(toNumber, message), macroMachine);
LOGGER.debug(String.format("password SMS added to send queue for %s", toNumber));
return null;
}
private static ErrorInformation sendNewPasswordEmail(
final UserInfoBean userInfoBean,
final PwmApplication pwmApplication,
final MacroMachine macroMachine,
final PasswordData newPassword,
final String toAddress,
final Locale userLocale
)
throws PwmOperationalException, PwmUnrecoverableException
{
final Configuration config = pwmApplication.getConfig();
final EmailItemBean configuredEmailSetting = config.readSettingAsEmail(PwmSetting.EMAIL_SENDPASSWORD, userLocale);
if (configuredEmailSetting == null) {
final String errorMsg = "send password email contents are not configured";
return new ErrorInformation(PwmError.ERROR_UNKNOWN, errorMsg);
}
final EmailItemBean emailItemBean = new EmailItemBean(
configuredEmailSetting.getTo(),
configuredEmailSetting.getFrom(),
configuredEmailSetting.getSubject(),
configuredEmailSetting.getBodyPlain().replace("%TOKEN%", newPassword.getStringValue()),
configuredEmailSetting.getBodyHtml().replace("%TOKEN%", newPassword.getStringValue())
);
pwmApplication.getEmailQueue().submitEmail(
emailItemBean,
userInfoBean,
macroMachine
);
LOGGER.debug("new password email to " + userInfoBean.getUserIdentity() + " added to send queue for " + toAddress);
return null;
}
enum PasswordPolicySource {
MERGE,
LDAP,
PWM,
}
private PasswordUtility() {
}
/**
* This is the entry point under which all password changes are managed.
* The following is the general procedure when this method is invoked.
* <ul>
* <li> password is checked against PWM password requirement </li>
* <li> ldap password set is attempted<br/>
* <br/>if successful:
* <ul>
* <li> uiBean is updated with old and new passwords </li>
* <li> uiBean's password expire flag is set to false </li>
* <li> any configured external methods are invoked </li>
* <li> user email notification is sent </li>
* <li> return true </li>
* </ul>
* <br/>if unsuccessful
* <ul>
* <li> ssBean is updated with appropriate error </li>
* <li> return false </li>
* </ul>
* </li>
* </ul>
*
* @param newPassword the new password that is being set.
* @param pwmSession beanmanager for config and user info lookup
* @throws com.novell.ldapchai.exception.ChaiUnavailableException
* if the ldap directory is not unavailable
* @throws password.pwm.error.PwmUnrecoverableException
* if user is not authenticated
*/
public static void setActorPassword(
final PwmSession pwmSession,
final PwmApplication pwmApplication,
final PasswordData newPassword
)
throws ChaiUnavailableException, PwmUnrecoverableException, PwmOperationalException
{
final UserInfoBean uiBean = pwmSession.getUserInfoBean();
if (!pwmSession.getSessionManager().checkPermission(pwmApplication, Permission.CHANGE_PASSWORD)) {
final String errorMsg = "attempt to setActorPassword, but user does not have password change permission";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNAUTHORIZED, errorMsg);
throw new PwmOperationalException(errorInformation);
}
// double check to make sure password meets PWM rule requirements. This should
// have been done before setActorPassword() is invoked, so it should be redundant
// but we do it just in case.
try {
final PwmPasswordRuleValidator pwmPasswordRuleValidator = new PwmPasswordRuleValidator(pwmApplication,uiBean.getPasswordPolicy());
pwmPasswordRuleValidator.testPassword(newPassword,null,uiBean,pwmSession.getSessionManager().getActor(pwmApplication));
} catch (PwmDataValidationException e) {
final String errorMsg = "attempt to setActorPassword, but password does not pass local policy validator";
final ErrorInformation errorInformation = new ErrorInformation(e.getErrorInformation().getError(), errorMsg);
throw new PwmOperationalException(errorInformation);
}
// retrieve the user's old password from the userInfoBean in the session
final PasswordData oldPassword = pwmSession.getLoginInfoBean().getUserCurrentPassword();
boolean setPasswordWithoutOld = false;
if (oldPassword == null) {
if (pwmSession.getSessionManager().getActor(pwmApplication).getChaiProvider().getDirectoryVendor() == ChaiProvider.DIRECTORY_VENDOR.MICROSOFT_ACTIVE_DIRECTORY) {
setPasswordWithoutOld = true;
}
}
if (!setPasswordWithoutOld) {
// Check to make sure we actually have an old password
if (oldPassword == null) {
final String errorMsg = "cannot set password for user, old password is not available";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_WRONGPASSWORD, errorMsg);
throw new PwmOperationalException(errorInformation);
}
}
try {
final ChaiProvider provider = pwmSession.getSessionManager().getChaiProvider();
final ChaiUser theUser = ChaiFactory.createChaiUser(pwmSession.getUserInfoBean().getUserIdentity().getUserDN(), provider);
final boolean boundAsSelf = theUser.getEntryDN().equals(provider.getChaiConfiguration().getSetting(ChaiSetting.BIND_DN));
LOGGER.trace(pwmSession, "preparing to setActorPassword for '" + theUser.getEntryDN() + "', bindAsSelf=" + boundAsSelf + ", authType=" + pwmSession.getLoginInfoBean().getType());
final boolean setting_enableChange = Boolean.parseBoolean(pwmApplication.getConfig().readAppProperty(AppProperty.LDAP_PASSWORD_CHANGE_SELF_ENABLE));
if (setting_enableChange) {
if (setPasswordWithoutOld) {
theUser.setPassword(newPassword.getStringValue(), true);
} else {
theUser.changePassword(oldPassword.getStringValue(), newPassword.getStringValue());
}
} else {
LOGGER.debug(pwmSession, "skipping actual ldap password change operation due to app property " + AppProperty.LDAP_PASSWORD_CHANGE_SELF_ENABLE.getKey() + "=false");
}
} catch (ChaiPasswordPolicyException e) {
final String errorMsg = "error setting password for user '" + uiBean.getUserIdentity() + "'' " + e.toString();
final PwmError pwmError = PwmError.forChaiError(e.getErrorCode());
final ErrorInformation error = new ErrorInformation(pwmError == null ? PwmError.PASSWORD_UNKNOWN_VALIDATION : pwmError, errorMsg);
throw new PwmOperationalException(error);
} catch (ChaiOperationException e) {
final String errorMsg = "error setting password for user '" + uiBean.getUserIdentity() + "'' " + e.getMessage();
final PwmError pwmError = PwmError.forChaiError(e.getErrorCode()) == null ? PwmError.ERROR_UNKNOWN : PwmError.forChaiError(e.getErrorCode());
final ErrorInformation error = new ErrorInformation(pwmError, errorMsg);
throw new PwmOperationalException(error);
}
// at this point the password has been changed, so log it.
LOGGER.info(pwmSession, "user '" + uiBean.getUserIdentity() + "' successfully changed password");
// update the session state bean's password modified flag
pwmSession.getSessionStateBean().setPasswordModified(true);
// update the login info bean with the user's new password
pwmSession.getLoginInfoBean().setUserCurrentPassword(newPassword);
//close any outstanding ldap connections (since they cache the old password)
pwmSession.getSessionManager().updateUserPassword(pwmApplication, uiBean.getUserIdentity(), newPassword);
// clear the "requires new password flag"
uiBean.setRequiresNewPassword(false);
// mark the auth type as authenticatePd now that we have the user's natural password.
pwmSession.getLoginInfoBean().setType(AuthenticationType.AUTHENTICATED);
// update the uibean's "password expired flag".
final UserStatusReader userStatusReader = new UserStatusReader(pwmApplication, pwmSession.getLabel());
uiBean.setPasswordState(userStatusReader.readPasswordStatus(
pwmSession.getSessionManager().getActor(pwmApplication),
uiBean.getPasswordPolicy(),
uiBean,
newPassword
));
// create a proxy user object for pwm to update/read the user.
final ChaiUser proxiedUser = pwmSession.getSessionManager().getActor(pwmApplication);
// update statistics
{
pwmApplication.getStatisticsManager().incrementValue(Statistic.PASSWORD_CHANGES);
pwmApplication.getStatisticsManager().updateEps(Statistic.EpsType.PASSWORD_CHANGES,1);
final int passwordStrength = PasswordUtility.judgePasswordStrength(newPassword.getStringValue());
pwmApplication.getStatisticsManager().updateAverageValue(Statistic.AVG_PASSWORD_STRENGTH,passwordStrength);
}
// add the old password to the global history list (if the old password is known)
if (oldPassword != null && pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.PASSWORD_SHAREDHISTORY_ENABLE)) {
pwmApplication.getSharedHistoryManager().addWord(pwmSession, oldPassword.getStringValue());
}
// invoke post password change actions
invokePostChangePasswordActions(pwmSession, newPassword.getStringValue());
{ // execute configured actions
LOGGER.debug(pwmSession, "executing configured actions to user " + proxiedUser.getEntryDN());
final List<ActionConfiguration> configValues = pwmApplication.getConfig().readSettingAsAction(PwmSetting.CHANGE_PASSWORD_WRITE_ATTRIBUTES);
if (configValues != null && !configValues.isEmpty()) {
final LoginInfoBean clonedLoginInfoBean = JsonUtil.cloneUsingJson(pwmSession.getLoginInfoBean(), LoginInfoBean.class);
clonedLoginInfoBean.setUserCurrentPassword(newPassword);
final MacroMachine macroMachine = new MacroMachine(
pwmApplication,
pwmSession.getLabel(),
pwmSession.getUserInfoBean(),
clonedLoginInfoBean,
pwmSession.getSessionManager().getUserDataReader(pwmApplication)
);
final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings(pwmApplication, uiBean.getUserIdentity())
.setMacroMachine(macroMachine)
.setExpandPwmMacros(true)
.createActionExecutor();
actionExecutor.executeActions(configValues, pwmSession);
}
}
//update the current last password update field in ldap
LdapOperationsHelper.updateLastPasswordUpdateAttribute(pwmApplication, pwmSession.getLabel(), uiBean.getUserIdentity());
}
public static void helpdeskSetUserPassword(
final PwmSession pwmSession,
final ChaiUser chaiUser,
final UserIdentity userIdentity,
final PwmApplication pwmApplication,
final PasswordData newPassword
)
throws ChaiUnavailableException, PwmUnrecoverableException, PwmOperationalException
{
final SessionLabel sessionLabel = pwmSession.getLabel();
if (!pwmSession.isAuthenticated()) {
final String errorMsg = "attempt to helpdeskSetUserPassword, but user is not authenticated";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNAUTHORIZED, errorMsg);
throw new PwmOperationalException(errorInformation);
}
final HelpdeskProfile helpdeskProfile = pwmSession.getSessionManager().getHelpdeskProfile(pwmApplication);
if (helpdeskProfile == null) {
final String errorMsg = "attempt to helpdeskSetUserPassword, but user does not have helpdesk permission";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNAUTHORIZED, errorMsg);
throw new PwmOperationalException(errorInformation);
}
final boolean setting_enableChange = Boolean.parseBoolean(pwmApplication.getConfig().readAppProperty(AppProperty.LDAP_PASSWORD_CHANGE_HELPDESK_ENABLE));
try {
if (setting_enableChange) {
chaiUser.setPassword(newPassword.getStringValue());
} else {
LOGGER.debug(pwmSession, "skipping actual ldap password change operation due to app property " + AppProperty.LDAP_PASSWORD_CHANGE_HELPDESK_ENABLE.getKey() + "=false");
}
} catch (ChaiPasswordPolicyException e) {
final String errorMsg = "error setting password for user '" + chaiUser.getEntryDN() + "'' " + e.toString();
final PwmError pwmError = PwmError.forChaiError(e.getErrorCode());
final ErrorInformation error = new ErrorInformation(pwmError == null ? PwmError.PASSWORD_UNKNOWN_VALIDATION : pwmError, errorMsg);
throw new PwmOperationalException(error);
} catch (ChaiOperationException e) {
final String errorMsg = "error setting password for user '" + chaiUser.getEntryDN() + "'' " + e.getMessage();
final PwmError pwmError = PwmError.forChaiError(e.getErrorCode()) == null ? PwmError.ERROR_UNKNOWN : PwmError.forChaiError(e.getErrorCode());
final ErrorInformation error = new ErrorInformation(pwmError, errorMsg);
throw new PwmOperationalException(error);
}
// at this point the password has been changed, so log it.
LOGGER.info(sessionLabel, "user '" + pwmSession.getUserInfoBean().getUserIdentity() + "' successfully changed password for " + chaiUser.getEntryDN());
// create a proxy user object for pwm to update/read the user.
final ChaiUser proxiedUser = pwmApplication.getProxiedChaiUser(userIdentity);
// mark the event log
{
final HelpdeskAuditRecord auditRecord = new AuditRecordFactory(pwmApplication, pwmSession).createHelpdeskAuditRecord(
AuditEvent.HELPDESK_SET_PASSWORD,
pwmSession.getUserInfoBean().getUserIdentity(),
null,
userIdentity,
pwmSession.getSessionStateBean().getSrcAddress(),
pwmSession.getSessionStateBean().getSrcHostname()
);
pwmApplication.getAuditManager().submit(auditRecord);
}
// update statistics
pwmApplication.getStatisticsManager().updateEps(Statistic.EpsType.PASSWORD_CHANGES,1);
pwmApplication.getStatisticsManager().incrementValue(Statistic.HELPDESK_PASSWORD_SET);
// create a uib for end user
final UserInfoBean userInfoBean = new UserInfoBean();
final UserStatusReader userStatusReader = new UserStatusReader(pwmApplication, pwmSession.getLabel());
userStatusReader.populateUserInfoBean(
userInfoBean,
pwmSession.getSessionStateBean().getLocale(),
userIdentity,
proxiedUser.getChaiProvider()
);
{ // execute configured actions
LOGGER.debug(sessionLabel, "executing changepassword and helpdesk post password change writeAttributes to user " + userIdentity);
final List<ActionConfiguration> actions = new ArrayList<>();
actions.addAll(pwmApplication.getConfig().readSettingAsAction(PwmSetting.CHANGE_PASSWORD_WRITE_ATTRIBUTES));
actions.addAll(helpdeskProfile.readSettingAsAction(PwmSetting.HELPDESK_POST_SET_PASSWORD_WRITE_ATTRIBUTES));
if (!actions.isEmpty()) {
final LoginInfoBean loginInfoBean = new LoginInfoBean();
loginInfoBean.setUserCurrentPassword(newPassword);
final UserDataReader userDataReader = LdapUserDataReader.appProxiedReader(pwmApplication, userIdentity);
final MacroMachine macroMachine = new MacroMachine(
pwmApplication,
sessionLabel,
userInfoBean,
loginInfoBean,
userDataReader
);
final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings(pwmApplication, userIdentity)
.setMacroMachine(macroMachine)
.setExpandPwmMacros(true)
.createActionExecutor();
actionExecutor.executeActions(actions,pwmSession);
}
}
final HelpdeskClearResponseMode settingClearResponses = HelpdeskClearResponseMode.valueOf(
helpdeskProfile.readSettingAsString(PwmSetting.HELPDESK_CLEAR_RESPONSES));
if (settingClearResponses == HelpdeskClearResponseMode.yes) {
final String userGUID = LdapOperationsHelper.readLdapGuidValue(pwmApplication, sessionLabel, userIdentity, false);
pwmApplication.getCrService().clearResponses(pwmSession.getLabel(), userIdentity, proxiedUser, userGUID);
// mark the event log
final HelpdeskAuditRecord auditRecord = new AuditRecordFactory(pwmApplication, pwmSession).createHelpdeskAuditRecord(
AuditEvent.HELPDESK_CLEAR_RESPONSES,
pwmSession.getUserInfoBean().getUserIdentity(),
null,
userIdentity,
pwmSession.getSessionStateBean().getSrcAddress(),
pwmSession.getSessionStateBean().getSrcHostname()
);
pwmApplication.getAuditManager().submit(auditRecord);
}
// send email notification
sendChangePasswordHelpdeskEmailNotice(pwmSession, pwmApplication, userInfoBean);
// expire if so configured
if (helpdeskProfile.readSettingAsBoolean(PwmSetting.HELPDESK_FORCE_PW_EXPIRATION)) {
LOGGER.trace(pwmSession,"preparing to expire password for user " + userIdentity.toDisplayString());
try {
proxiedUser.expirePassword();
} catch (ChaiOperationException e) {
LOGGER.warn(pwmSession, "error while forcing password expiration for user " + userIdentity.toDisplayString() + ", error: " + e.getMessage());
}
}
// send password
final boolean sendPassword = helpdeskProfile.readSettingAsBoolean(PwmSetting.HELPDESK_SEND_PASSWORD);
if (sendPassword) {
final MessageSendMethod messageSendMethod;
{
final String profileID = ProfileUtility.discoverProfileIDforUser(pwmApplication, sessionLabel, userIdentity, ProfileType.ForgottenPassword);
final ForgottenPasswordProfile forgottenPasswordProfile = pwmApplication.getConfig().getForgottenPasswordProfiles().get(profileID);
messageSendMethod = forgottenPasswordProfile.readSettingAsEnum(PwmSetting.RECOVERY_SENDNEWPW_METHOD, MessageSendMethod.class);
}
final UserDataReader userDataReader = new LdapUserDataReader(userIdentity, chaiUser);
final LoginInfoBean loginInfoBean = new LoginInfoBean();
loginInfoBean.setUserCurrentPassword(newPassword);
final MacroMachine macroMachine = new MacroMachine(pwmApplication, pwmSession.getLabel(), userInfoBean, loginInfoBean, userDataReader);
PasswordUtility.sendNewPassword(
userInfoBean,
pwmApplication,
macroMachine,
newPassword,
pwmSession.getSessionStateBean().getLocale(),
messageSendMethod
);
}
}
public static Map<String,Instant> readIndividualReplicaLastPasswordTimes(
final PwmApplication pwmApplication,
final SessionLabel sessionLabel,
final UserIdentity userIdentity
)
throws PwmUnrecoverableException
{
final Map<String,Instant> returnValue = new LinkedHashMap<>();
final ChaiProvider chaiProvider = pwmApplication.getProxyChaiProvider(userIdentity.getLdapProfileID());
final Collection<ChaiConfiguration> perReplicaConfigs = ChaiUtility.splitConfigurationPerReplica(
chaiProvider.getChaiConfiguration(),
Collections.singletonMap(ChaiSetting.FAILOVER_CONNECT_RETRIES, "1")
);
for (final ChaiConfiguration loopConfiguration : perReplicaConfigs) {
final String loopReplicaUrl = loopConfiguration.getSetting(ChaiSetting.BIND_DN);
ChaiProvider loopProvider = null;
try {
loopProvider = ChaiProviderFactory.createProvider(loopConfiguration);
final Instant lastModifiedDate = determinePwdLastModified(pwmApplication, sessionLabel, userIdentity);
returnValue.put(loopReplicaUrl, lastModifiedDate);
} catch (ChaiUnavailableException e) {
LOGGER.error(sessionLabel, "unreachable server during replica password sync check");
e.printStackTrace();
} finally {
if (loopProvider != null) {
try {
loopProvider.close();
} catch (Exception e) {
final String errorMsg = "error closing loopProvider to " + loopReplicaUrl + " while checking individual password sync status";
LOGGER.error(sessionLabel, errorMsg);
}
}
}
}
return returnValue;
}
private static void invokePostChangePasswordActions(final PwmSession pwmSession, final String newPassword)
throws PwmUnrecoverableException
{
final List<PostChangePasswordAction> postChangePasswordActions = pwmSession.getUserSessionDataCacheBean().removePostChangePasswordActions();
if (postChangePasswordActions == null || postChangePasswordActions.isEmpty()) {
LOGGER.trace(pwmSession, "no post change password actions pending from previous operations");
return;
}
for (final PostChangePasswordAction postChangePasswordAction : postChangePasswordActions) {
try {
postChangePasswordAction.doAction(pwmSession, newPassword);
} catch (PwmUnrecoverableException e) {
LOGGER.error(pwmSession, "error during post change password action '" + postChangePasswordAction.getLabel() + "' " + e.getMessage());
throw e;
} catch (Exception e) {
LOGGER.error(pwmSession, "unexpected error during post change password action '" + postChangePasswordAction.getLabel() + "' " + e.getMessage(), e);
final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_UNKNOWN, e.getMessage());
throw new PwmUnrecoverableException(errorInfo);
}
}
}
/*
static Map<String, ReplicationStatus> checkIfPasswordIsReplicated(final ChaiUser user, final PwmSession pwmSession)
throws ChaiUnavailableException
{
final Map<String, ReplicationStatus> repStatusMap = new HashMap<String, ReplicationStatus>();
{
final ReplicationStatus repStatus = checkDirectoryReplicationStatus(user, pwmSession);
repStatusMap.put("ReplicationSync", repStatus);
}
if (ChaiProvider.DIRECTORY_VENDOR.NOVELL_EDIRECTORY == user.getChaiProvider().getDirectoryVendor()) {
}
return repStatusMap;
}
public static Map<String, ReplicationStatus> checkNovellIDMReplicationStatus(final ChaiUser chaiUser)
throws ChaiUnavailableException, ChaiOperationException
{
final Map<String,ReplicationStatus> repStatuses = new HashMap<String,ReplicationStatus>();
final Set<String> values = chaiUser.readMultiStringAttribute("DirXML-PasswordSyncStatus");
if (values != null) {
for (final String value : values) {
if (value != null && value.length() >= 62 ) {
final String guid = value.substring(0,32);
final String timestamp = value.substring(32,46);
final String status = value.substring(46,62);
final String descr = value.substring(61, value.length());
final Date dateValue = EdirEntries.convertZuluToDate(timestamp + "Z");
System.out.println("guid=" + guid + ", timestamp=" + dateValue.toString() + ", status=" + status + ", descr=" + descr);
}
}
}
return repStatuses;
}
private static ReplicationStatus checkDirectoryReplicationStatus(final ChaiUser user, final PwmSession pwmSession)
throws ChaiUnavailableException
{
boolean isReplicated = false;
try {
isReplicated = ChaiUtility.testAttributeReplication(user, pwmSession.getConfig().readSettingAsString(PwmSetting.PASSWORD_LAST_UPDATE_ATTRIBUTE), null);
Helper.pause(PwmConstants.PASSWORD_UPDATE_CYCLE_DELAY_MS);
} catch (ChaiOperationException e) {
//oh well, give up.
LOGGER.trace(pwmSession, "error during password sync check: " + e.getMessage());
}
return isReplicated ? ReplicationStatus.COMPLETE : ReplicationStatus.IN_PROGRESS;
}
enum ReplicationStatus {
IN_PROGRESS,
COMPLETE
}
*/
public static int judgePasswordStrength(
final String password
)
throws PwmUnrecoverableException
{
if (password == null || password.length() < 1) {
return 0;
}
int score = 0;
final PasswordCharCounter charCounter = new PasswordCharCounter(password);
// -- Additions --
// amount of unique chars
if (charCounter.getUniqueChars() > 7) {
score = score + 10;
}
score = score + ((charCounter.getUniqueChars()) * 3);
// Numbers
if (charCounter.getNumericCharCount() > 0) {
score = score + 8;
score = score + (charCounter.getNumericCharCount()) * 4;
}
// specials
if (charCounter.getSpecialCharsCount() > 0) {
score = score + 14;
score = score + (charCounter.getSpecialCharsCount()) * 5;
}
// mixed case
if ((charCounter.getAlphaChars() != charCounter.getUpperChars()) && (charCounter.getAlphaChars() != charCounter.getLowerChars())) {
score = score + 10;
}
// -- Deductions --
// sequential numbers
if (charCounter.getSequentialNumericChars() > 2) {
score = score - (charCounter.getSequentialNumericChars() - 1) * 4;
}
// sequential chars
if (charCounter.getSequentialRepeatedChars() > 1) {
score = score - (charCounter.getSequentialRepeatedChars()) * 5;
}
return score > 100 ? 100 : score < 0 ? 0 : score;
}
public static PwmPasswordPolicy readPasswordPolicyForUser(
final PwmApplication pwmApplication,
final SessionLabel pwmSession,
final UserIdentity userIdentity,
final ChaiUser theUser,
final Locale locale
)
throws PwmUnrecoverableException
{
final Instant startTime = Instant.now();
final PasswordPolicySource ppSource = PasswordPolicySource.valueOf(pwmApplication.getConfig().readSettingAsString(PwmSetting.PASSWORD_POLICY_SOURCE));
final PwmPasswordPolicy returnPolicy;
switch (ppSource) {
case MERGE:
final PwmPasswordPolicy pwmPolicy = determineConfiguredPolicyProfileForUser(pwmApplication,pwmSession,userIdentity,locale);
final PwmPasswordPolicy userPolicy = readLdapPasswordPolicy(pwmApplication, theUser);
LOGGER.trace(pwmSession, "read user policy for '" + theUser.getEntryDN() + "', policy: " + userPolicy.toString());
returnPolicy = pwmPolicy.merge(userPolicy);
LOGGER.debug(pwmSession, "merged user password policy of '" + theUser.getEntryDN() + "' with PWM configured policy: " + returnPolicy.toString());
break;
case LDAP:
returnPolicy = readLdapPasswordPolicy(pwmApplication, theUser);
LOGGER.debug(pwmSession, "discovered assigned password policy for " + theUser.getEntryDN() + " " + returnPolicy.toString());
break;
case PWM:
returnPolicy = determineConfiguredPolicyProfileForUser(pwmApplication,pwmSession,userIdentity,locale);
break;
default:
throw new IllegalStateException("unknown policy source defined: " + ppSource.name());
}
LOGGER.trace(pwmSession, "readPasswordPolicyForUser completed in " + TimeDuration.fromCurrent(startTime).asCompactString());
return returnPolicy;
}
public static PwmPasswordPolicy determineConfiguredPolicyProfileForUser(
final PwmApplication pwmApplication,
final SessionLabel pwmSession,
final UserIdentity userIdentity,
final Locale locale
) throws PwmUnrecoverableException {
final List<String> profiles = pwmApplication.getConfig().getPasswordProfileIDs();
if (profiles.isEmpty()) {
throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_NO_PROFILE_ASSIGNED,"no password profiles are configured"));
}
for (final String profile : profiles) {
final PwmPasswordPolicy loopPolicy = pwmApplication.getConfig().getPasswordPolicy(profile,locale);
final List<UserPermission> userPermissions = loopPolicy.getUserPermissions();
LOGGER.debug(pwmSession, "testing password policy profile '" + profile + "'");
try {
final boolean match = LdapPermissionTester.testUserPermissions(pwmApplication, pwmSession, userIdentity, userPermissions);
if (match) {
return loopPolicy;
}
} catch (PwmUnrecoverableException e) {
LOGGER.error(pwmSession,"unexpected error while testing password policy profile '" + profile + "', error: " + e.getMessage());
}
}
throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_NO_PROFILE_ASSIGNED,"no challenge profile is configured"));
}
public static PwmPasswordPolicy readLdapPasswordPolicy(
final PwmApplication pwmApplication,
final ChaiUser theUser)
throws PwmUnrecoverableException {
try {
final Map<String, String> ruleMap = new HashMap<>();
final ChaiPasswordPolicy chaiPolicy;
try {
chaiPolicy = theUser.getPasswordPolicy();
} catch (ChaiUnavailableException e) {
throw new PwmUnrecoverableException(PwmError.forChaiError(e.getErrorCode()));
}
if (chaiPolicy != null) {
for (final String key : chaiPolicy.getKeys()) {
ruleMap.put(key, chaiPolicy.getValue(key));
}
if (!"read".equals(pwmApplication.getConfig().readSettingAsString(PwmSetting.PASSWORD_POLICY_CASE_SENSITIVITY))) {
ruleMap.put(
PwmPasswordRule.CaseSensitive.getKey(),
pwmApplication.getConfig().readSettingAsString(PwmSetting.PASSWORD_POLICY_CASE_SENSITIVITY)
);
}
return PwmPasswordPolicy.createPwmPasswordPolicy(ruleMap, chaiPolicy);
}
} catch (ChaiOperationException e) {
LOGGER.warn("error reading password policy for user " + theUser.getEntryDN() + ", error: " + e.getMessage());
}
return PwmPasswordPolicy.defaultPolicy();
}
public static PasswordCheckInfo checkEnteredPassword(
final PwmApplication pwmApplication,
final Locale locale,
final ChaiUser user,
final UserInfoBean userInfoBean,
final LoginInfoBean loginInfoBean,
final PasswordData password,
final PasswordData confirmPassword
)
throws PwmUnrecoverableException, ChaiUnavailableException
{
if (userInfoBean == null) {
throw new NullPointerException("userInfoBean cannot be null");
}
boolean pass = false;
String userMessage = "";
int errorCode = 0;
final boolean passwordIsCaseSensitive = userInfoBean.getPasswordPolicy() == null || userInfoBean.getPasswordPolicy().getRuleHelper().readBooleanValue(PwmPasswordRule.CaseSensitive);
final CachePolicy cachePolicy;
{
final long cacheLifetimeMS = Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.CACHE_PWRULECHECK_LIFETIME_MS));
cachePolicy = CachePolicy.makePolicyWithExpirationMS(cacheLifetimeMS);
}
if (password == null) {
userMessage = new ErrorInformation(PwmError.PASSWORD_MISSING).toUserStr(locale, pwmApplication.getConfig());
} else {
final CacheService cacheService = pwmApplication.getCacheService();
final CacheKey cacheKey = user != null && userInfoBean.getUserIdentity() != null
? CacheKey.makeCacheKey(
PasswordUtility.class,
userInfoBean.getUserIdentity(),
user.getEntryDN() + ":" + password.hash())
: null;
if (pwmApplication.getConfig().isDevDebugMode()) {
LOGGER.trace("generated cacheKey for password check request: " + cacheKey);
}
try {
if (cacheService != null && cacheKey != null) {
final String cachedValue = cacheService.get(cacheKey);
if (cachedValue != null) {
if (NEGATIVE_CACHE_HIT.equals(cachedValue)) {
pass = true;
} else {
LOGGER.trace("cache hit!");
final ErrorInformation errorInformation = JsonUtil.deserialize(cachedValue, ErrorInformation.class);
throw new PwmDataValidationException(errorInformation);
}
}
}
if (!pass) {
final PwmPasswordRuleValidator pwmPasswordRuleValidator = new PwmPasswordRuleValidator(pwmApplication, userInfoBean.getPasswordPolicy(), locale);
final PasswordData oldPassword = loginInfoBean == null ? null : loginInfoBean.getUserCurrentPassword();
pwmPasswordRuleValidator.testPassword(password, oldPassword, userInfoBean, user);
pass = true;
if (cacheService != null && cacheKey != null) {
cacheService.put(cacheKey, cachePolicy, NEGATIVE_CACHE_HIT);
}
}
} catch (PwmDataValidationException e) {
errorCode = e.getError().getErrorCode();
userMessage = e.getErrorInformation().toUserStr(locale, pwmApplication.getConfig());
pass = false;
if (cacheService != null && cacheKey != null) {
final String jsonPayload = JsonUtil.serialize(e.getErrorInformation());
cacheService.put(cacheKey, cachePolicy, jsonPayload);
}
}
}
final PasswordCheckInfo.MatchStatus matchStatus = figureMatchStatus(passwordIsCaseSensitive ,password, confirmPassword);
if (pass) {
switch (matchStatus) {
case EMPTY:
userMessage = new ErrorInformation(PwmError.PASSWORD_MISSING_CONFIRM).toUserStr(locale, pwmApplication.getConfig());
break;
case MATCH:
userMessage = new ErrorInformation(PwmError.PASSWORD_MEETS_RULES).toUserStr(locale, pwmApplication.getConfig());
break;
case NO_MATCH:
userMessage = new ErrorInformation(PwmError.PASSWORD_DOESNOTMATCH).toUserStr(locale, pwmApplication.getConfig());
break;
default:
userMessage = "";
}
}
final int strength = judgePasswordStrength(password == null ? null : password.getStringValue());
return new PasswordCheckInfo(userMessage, pass, strength, matchStatus, errorCode);
}
public static PasswordCheckInfo.MatchStatus figureMatchStatus(
final boolean caseSensitive,
final PasswordData password1,
final PasswordData password2
) {
final PasswordCheckInfo.MatchStatus matchStatus;
if (password2 == null) {
matchStatus = PasswordCheckInfo.MatchStatus.EMPTY;
} else if (password1 == null) {
matchStatus = PasswordCheckInfo.MatchStatus.NO_MATCH;
} else {
if (caseSensitive) {
matchStatus = password1.equals(password2) ? PasswordCheckInfo.MatchStatus.MATCH : PasswordCheckInfo.MatchStatus.NO_MATCH;
} else {
matchStatus = password1.equalsIgnoreCase(password2) ? PasswordCheckInfo.MatchStatus.MATCH : PasswordCheckInfo.MatchStatus.NO_MATCH;
}
}
return matchStatus;
}
public static class PasswordCheckInfo implements Serializable {
private final String message;
private final boolean passed;
private final int strength;
private final MatchStatus match;
private final int errorCode;
public enum MatchStatus {
MATCH, NO_MATCH, EMPTY
}
public PasswordCheckInfo(final String message, final boolean passed, final int strength, final MatchStatus match, final int errorCode) {
this.message = message;
this.passed = passed;
this.strength = strength;
this.match = match;
this.errorCode = errorCode;
}
public String getMessage() {
return message;
}
public boolean isPassed() {
return passed;
}
public int getStrength() {
return strength;
}
public MatchStatus getMatch() {
return match;
}
public int getErrorCode() {
return errorCode;
}
}
private static void sendChangePasswordHelpdeskEmailNotice(
final PwmSession pwmSession,
final PwmApplication pwmApplication,
final UserInfoBean userInfoBean
)
throws PwmUnrecoverableException
{
final Configuration config = pwmApplication.getConfig();
final Locale locale = pwmSession.getSessionStateBean().getLocale();
final EmailItemBean configuredEmailSetting = config.readSettingAsEmail(PwmSetting.EMAIL_CHANGEPASSWORD_HELPDESK, locale);
if (configuredEmailSetting == null) {
LOGGER.debug(pwmSession, "skipping send change password email for '" + pwmSession.getUserInfoBean().getUserIdentity() + "' no email configured");
return;
}
final MacroMachine macroMachine = userInfoBean == null
? null
: new MacroMachine(
pwmApplication,
pwmSession.getLabel(),
userInfoBean,
null,
LdapUserDataReader.appProxiedReader(
pwmApplication,
userInfoBean.getUserIdentity()
)
);
pwmApplication.getEmailQueue().submitEmail(configuredEmailSetting, userInfoBean, macroMachine);
}
public static Instant determinePwdLastModified(
final PwmApplication pwmApplication,
final SessionLabel sessionLabel,
final UserIdentity userIdentity
)
throws ChaiUnavailableException, PwmUnrecoverableException
{
final ChaiUser theUser = pwmApplication.getProxiedChaiUser(userIdentity);
return determinePwdLastModified(pwmApplication, sessionLabel, theUser, userIdentity);
}
private static Instant determinePwdLastModified(
final PwmApplication pwmApplication,
final SessionLabel sessionLabel,
final ChaiUser theUser,
final UserIdentity userIdentity
)
throws ChaiUnavailableException, PwmUnrecoverableException
{
// fetch last password modification time from pwm last update attribute operation
try {
final Date chaiReadDate = theUser.readPasswordModificationDate();
if (chaiReadDate != null) {
LOGGER.trace(sessionLabel, "read last user password change timestamp (via chai) as: " + JavaHelper.toIsoDate(chaiReadDate));
return chaiReadDate.toInstant();
}
} catch (ChaiOperationException e) {
LOGGER.error(sessionLabel, "unexpected error reading password last modified timestamp: " + e.getMessage());
}
final LdapProfile ldapProfile = pwmApplication.getConfig().getLdapProfiles().get(userIdentity.getLdapProfileID());
final String pwmLastSetAttr = ldapProfile.readSettingAsString(PwmSetting.PASSWORD_LAST_UPDATE_ATTRIBUTE);
if (pwmLastSetAttr != null && pwmLastSetAttr.length() > 0) {
try {
final Date pwmPwdLastModified = theUser.readDateAttribute(pwmLastSetAttr);
LOGGER.trace(sessionLabel, "read pwmPasswordChangeTime as: " + (pwmPwdLastModified == null ? "n/a" : JavaHelper.toIsoDate(pwmPwdLastModified)));
return pwmPwdLastModified == null ? null : pwmPwdLastModified.toInstant();
} catch (ChaiOperationException e) {
LOGGER.error(sessionLabel, "error parsing password last modified PWM password value for user " + theUser.getEntryDN() + "; error: " + e.getMessage());
}
}
LOGGER.debug(sessionLabel, "unable to determine time of user's last password modification");
return null;
}
public static void checkIfPasswordWithinMinimumLifetime(
final ChaiUser chaiUser,
final SessionLabel sessionLabel,
final PwmPasswordPolicy passwordPolicy,
final Instant lastModified,
final PasswordStatus passwordStatus
)
throws PwmOperationalException, PwmUnrecoverableException
{
// for oracle DS; this check is also handled in UserAuthenticator.
try {
if (ChaiProvider.DIRECTORY_VENDOR.ORACLE_DS == chaiUser.getChaiProvider().getDirectoryVendor()) {
final String oracleDS_PrePasswordAllowChangeTime = chaiUser.readStringAttribute("passwordAllowChangeTime");
if (oracleDS_PrePasswordAllowChangeTime != null && !oracleDS_PrePasswordAllowChangeTime.isEmpty()) {
final Date date = OracleDSEntries.convertZuluToDate(oracleDS_PrePasswordAllowChangeTime);
if (new Date().before(date)) {
LOGGER.debug("discovered oracleds allowed change time is set to: " + JavaHelper.toIsoDate(date) + ", won't permit password change");
final String errorMsg = "change not permitted until " + JavaHelper.toIsoDate(date);
final ErrorInformation errorInformation = new ErrorInformation(PwmError.PASSWORD_TOO_SOON, errorMsg);
throw new PwmUnrecoverableException(errorInformation);
}
}
return;
}
} catch(ChaiException e) {
LOGGER.debug(sessionLabel, "unexpected error reading OracleDS password allow modification time: " + e.getMessage());
}
final TimeDuration minimumLifetime;
{
final int minimumLifetimeSeconds = passwordPolicy.getRuleHelper().readIntValue(PwmPasswordRule.MinimumLifetime);
if (minimumLifetimeSeconds < 1) {
return;
}
if (lastModified == null) {
LOGGER.debug(sessionLabel, "skipping minimum lifetime check, password last set time is unknown");
return;
}
minimumLifetime = new TimeDuration(minimumLifetimeSeconds, TimeUnit.SECONDS);
}
final TimeDuration passwordAge = TimeDuration.fromCurrent(lastModified);
LOGGER.trace(sessionLabel, "beginning check for minimum lifetime, lastModified="
+ JavaHelper.toIsoDate(lastModified)
+ ", minimumLifetimeSeconds=" + minimumLifetime.asCompactString()
+ ", passwordAge=" + passwordAge.asCompactString());
if (lastModified.isAfter(Instant.now())) {
LOGGER.debug(sessionLabel, "skipping minimum lifetime check, password lastModified time is in the future");
return;
}
final boolean passwordTooSoon = passwordAge.isShorterThan(minimumLifetime);
if (!passwordTooSoon) {
LOGGER.trace(sessionLabel, "minimum lifetime check passed, password age ");
return;
}
if (passwordStatus.isExpired() || passwordStatus.isPreExpired() || passwordStatus.isWarnPeriod()) {
LOGGER.debug(sessionLabel, "current password is too young, but skipping enforcement of minimum lifetime check because current password is expired");
return;
}
final Instant allowedChangeDate = Instant.ofEpochMilli(lastModified.toEpochMilli() + minimumLifetime.getTotalMilliseconds());
final String errorMsg = "last password change was at "
+ JavaHelper.toIsoDate(lastModified)
+ " and is too recent (" + passwordAge.asCompactString()
+ " ago), password cannot be changed within minimum lifetime of "
+ minimumLifetime.asCompactString()
+ ", next eligible time to change is after " + JavaHelper.toIsoDate(allowedChangeDate);
final ErrorInformation errorInformation = new ErrorInformation(PwmError.PASSWORD_TOO_SOON,errorMsg);
throw new PwmOperationalException(errorInformation);
}
}